Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .claudeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# IDE and editor files
.idea/
.vscode/
*.iws
*.iml

# Python cache and bytecode
__pycache__/
*.py[cod]
*$py.class
.mypy_cache/
.dmypy.json
.pytest_cache/
.hypothesis/

# Virtual environments
.venv/
venv/
env/
ENV/

# Build and distribution
build/
dist/
*.egg-info/
*.egg
.eggs/

# Coverage reports
htmlcov/
.coverage
.coverage.*
coverage.xml

# Documentation build
docs/_build/
/site

# Database files
*.db
db.sqlite3

# Credentials and secrets
credentials.env
.env

# Docker volumes
volumes/

# OS files
.DS_Store
Thumbs.db

# Lock files (large, not useful for code understanding)
uv.lock

# Quality tools cache
.qlty
.tox/
.nox/
2 changes: 1 addition & 1 deletion .github/workflows/github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-code-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- uses: actions/checkout@v6
- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- uses: actions/checkout@v6
- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/python-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- uses: actions/checkout@v6
- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
Expand All @@ -29,7 +29,3 @@ jobs:
run: make dev-dependencies
- name: Run coverage
run: make ci-coverage
- uses: qltysh/qlty-action/coverage@v2
with:
token: ${{secrets.QLTY_COVERAGE_TOKEN}}
files: ${{github.workspace}}/coverage.lcov
13 changes: 3 additions & 10 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,17 @@ on:

jobs:
test:
strategy:
matrix:
version: ["3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.version }}
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "${{ matrix.version }}"
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: make dev-dependencies
- name: Test with pytest
run: |
make ci-test
- name: Check typing
run: |
make typing
2 changes: 1 addition & 1 deletion .github/workflows/python-typing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- uses: actions/checkout@v6
- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
Expand Down
2 changes: 1 addition & 1 deletion .idea/bootstrap-python-fastapi.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build and Development Commands

**Package Management (uv):**
- `make dev-dependencies` - Install all dependencies including dev
- `make install-dependencies` - Install production dependencies only
- `make update-dependencies` - Update and sync dependencies

**Running Applications:**
- `make dev-http` - Run HTTP application with hot reload (Docker)
- `make dev-socketio` - Run Socket.io application with hot reload (Docker)
- `docker compose up dramatiq-worker` - Run Dramatiq worker

**Testing:**
- `make test` - Run full test suite with coverage (parallel execution)
- `uv run pytest tests/path/to/test_file.py` - Run a single test file
- `uv run pytest tests/path/to/test_file.py::test_function` - Run a specific test
- `uv run pytest -k "pattern"` - Run tests matching pattern

**Code Quality:**
- `make check` - Run all checks (lint, format, typing, test)
- `make fix` - Auto-fix linting and formatting issues
- `make typing` - Run mypy type checking
- `make lint` - Run ruff linter
- `make format` - Check code formatting

**Database:**
- `make migrate` - Run database migrations
- `make autogenerate-migration` - Generate new migration file

**Documentation:**
- `make docs` - Serve documentation locally

## Architecture Overview

This is a Clean Architecture Python application with multiple entry points (HTTP/FastAPI, WebSocket/Socket.io, async tasks/Dramatiq).

### Layer Structure (`src/`)

```
domains/ → Business logic (services, models, DTOs, events)
gateways/ → External system interfaces (event gateways)
http_app/ → FastAPI application and routes
socketio_app/ → Socket.io application and namespaces
dramatiq_worker/ → Async task worker
common/ → Shared infrastructure (config, DI, storage, telemetry)
migrations/ → Alembic database migrations
```

### Dependency Injection Pattern

Uses `dependency-injector` library. Interface mappings are defined in `src/common/di_container.py`:

```python
# Container maps interfaces to implementations
BookRepositoryInterface: Factory[BookRepositoryInterface] = Factory(
SQLAlchemyAsyncRepository,
bind=SQLAlchemyBindManager.provided.get_bind.call(),
model_class=BookModel,
)
```

Services use `@inject` decorator with `Provide[]`:

```python
@inject
def __init__(
self,
book_repository: BookRepositoryInterface = Provide[BookRepositoryInterface.__name__],
)
```

### Application Bootstrap

All applications share common initialization via `application_init()` in `src/common/bootstrap.py`:
- Configures DI container
- Initializes logging (structlog)
- Sets up SQLAlchemy storage
- Configures Dramatiq
- Instruments OpenTelemetry

### Domain Structure (example: `domains/books/`)

- `_models.py` - SQLAlchemy models (imperative mapping)
- `_service.py` - Business logic services
- `_gateway_interfaces.py` - Repository/gateway protocols
- `_tasks.py` - Dramatiq async tasks
- `dto.py` - Pydantic DTOs for API layer
- `events.py` - CloudEvents definitions
- `interfaces.py` - Public domain interfaces

### Testing

Tests mirror source structure in `src/tests/`. Key patterns:
- Integration tests use `TestClient` with in-memory SQLite
- Unit tests mock dependencies via `AsyncMock`/`MagicMock`
- 100% coverage required (`fail_under = 100` in pyproject.toml)
- Storage tests in `tests/storage/` use isolated database fixtures

### Configuration

Environment-based configuration via Pydantic Settings in `src/common/config.py`. Nested configs use `__` delimiter (e.g., `DRAMATIQ__BROKER_URL`).
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PYTHON_VERSION=3.13
ARG PYTHON_VERSION=3.14
FROM python:$PYTHON_VERSION-slim AS base
ARG UID=2000
ARG GID=2000
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This template provides out of the box some commonly used functionalities:
* Authentication and Identity Provider using [ORY Zero Trust architecture](https://www.ory.sh/docs/kratos/guides/zero-trust-iap-proxy-identity-access-proxy)
* Full observability setup using [OpenTelemetry](https://opentelemetry.io/) (Metrics, Traces and Logs)
* Example CI/CD deployment pipeline for GitLab (The focus for this repository is still GitHub but, in case you want to use GitLab 🤷)
* (experimental) Boilerplate config files for [Claude Code](https://claude.ai/docs)
* [TODO] Producer and consumer to emit and consume events using [CloudEvents](https://cloudevents.io/) format using HTTP, to be used with [Knative Eventing](https://knative.dev/docs/eventing/)

## Documentation
Expand Down
8 changes: 2 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
authors = [
{name = "Federico Busetti", email = "729029+febus982@users.noreply.github.com"},
]
requires-python = "<3.14,>=3.10"
requires-python = "==3.14.*"
name = "bootstrap-fastapi-service"
version = "0.1.0"
description = ""
Expand Down Expand Up @@ -107,10 +107,7 @@ exclude_also = [
[tool.mypy]
files = ["src", "tests"]
exclude = ["migrations"]
# Pydantic plugin causes some issues: https://github.com/pydantic/pydantic-settings/issues/403
#plugins = "pydantic.mypy,strawberry.ext.mypy_plugin"
plugins = "strawberry.ext.mypy_plugin"
python_version = "3.10"
plugins = "pydantic.mypy,strawberry.ext.mypy_plugin"

[[tool.mypy.overrides]]
module = [
Expand All @@ -132,7 +129,6 @@ testpaths = [
]

[tool.ruff]
target-version = "py39"
line-length = 120
# The `src` settings makes sure that imports are correctly
# evaluated during formatting when using nested `pyproject.toml`
Expand Down
4 changes: 2 additions & 2 deletions src/common/telemetry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from functools import wraps
from inspect import iscoroutinefunction

from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
Expand Down Expand Up @@ -72,7 +72,7 @@ def sync_wrapper(*args, **kwargs):
span.set_status(trace.status.Status(trace.status.StatusCode.ERROR))
raise

return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
return async_wrapper if iscoroutinefunction(func) else sync_wrapper

return decorator

Expand Down
4 changes: 2 additions & 2 deletions tests/common/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import asyncio
from inspect import iscoroutinefunction

import pytest

Expand Down Expand Up @@ -32,7 +32,7 @@ async def async_wrapper(*args, **kwargs):
result = await func(*args, **kwargs)
return result + 10

return wrapper if not asyncio.iscoroutinefunction(func) else async_wrapper
return wrapper if not iscoroutinefunction(func) else async_wrapper

@apply_decorator_to_methods(
decorator=add_ten_decorator,
Expand Down
4 changes: 2 additions & 2 deletions tests/http_app/routes/test_hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ async def test_hello_renders_what_returned_by_decoder(
assert '"token": "some_token"' in response.text


async def test_hello_returns_403_without_token(testapp: FastAPI):
async def test_hello_returns_401_without_token(testapp: FastAPI):
testapp.dependency_overrides[decode_jwt] = _fake_decode_jwt
ac = TestClient(app=testapp, base_url="http://test")
response = ac.get("/hello/")
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.status_code == status.HTTP_401_UNAUTHORIZED
4 changes: 2 additions & 2 deletions tests/socketio_app/web_routes/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@


# Test model
class TestModel(BaseModel):
class SomeModel(BaseModel):
name: str
value: int


# Fixtures
@pytest.fixture
def test_model():
return TestModel(name="test", value=42)
return SomeModel(name="test", value=42)


@pytest.fixture
Expand Down
1 change: 1 addition & 0 deletions tests/storage/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ async def test_sa_manager() -> AsyncIterator[SQLAlchemyBindManager]:

yield sa_manager
clear_mappers()
sa_manager.dispose_engines()
Loading