RESTful API with Python 3.13 + FastAPI demonstrating modern async patterns. Player registry with CRUD operations, SQLite + SQLAlchemy 2.0 (async), Pydantic validation, containerization. Part of multi-language comparison study (Java, .NET, TypeScript, Python, Go, Rust). Target: 80%+ test coverage.
# Install dependencies
pip install -r requirements.txt
pip install -r requirements-lint.txt
pip install -r requirements-test.txt
# Run development server
uvicorn main:app --reload --port 9000
# Access: http://localhost:9000/docs
# Run tests with coverage
pytest --cov=./ --cov-report=html
# Lint and format
flake8 .
black --check . # or: black . (to auto-format)
# Docker
docker compose up
docker compose down -v # Reset database- Python 3.13.3 (
.python-version- auto-detected by pyenv/asdf/mise) - FastAPI 0.128.6, Uvicorn
- SQLite + SQLAlchemy 2.0 (async) + aiosqlite
- pytest + pytest-cov + httpx
- Flake8 + Black
- aiocache (in-memory, 10min TTL)
Request → Routes → Services → SQLAlchemy → SQLite
(API) (Logic) (Async ORM) (Storage)
↓
Pydantic (Validation)
Key Directories:
routes/- API endpoints (player_route.py, health_route.py)services/- Business logic (player_service.py)models/- Pydantic validation (camelCase JSON API)schemas/- SQLAlchemy ORM modelsdatabases/- Async DB setup, session factorystorage/- SQLite file (pre-seeded, 26 players)tests/- pytest suite (test_main.py, conftest.py)
Config Files:
.flake8- Linter (max-line-length=88, complexity=10)pyproject.toml- Black formatter (line-length=88).coveragerc- Coverage config (80% target)compose.yaml- Docker orchestrationDockerfile- Multi-stage build
All async with AsyncSession injection:
POST /players/→ 201|409|422GET /players/→ 200 (cached 10min)GET /players/{player_id}→ 200|404GET /players/squadnumber/{squad_number}→ 200|404PUT /players/{player_id}→ 200|404|422DELETE /players/{player_id}→ 200|404GET /health→ 200
JSON: camelCase (e.g., squadNumber, firstName)
python-ci.yml (push/PR to master):
- Lint: commitlint →
flake8 .→black --check . - Test:
pytest -v→ coverage - Upload to Codecov
python-cd.yml (tags v*.*.*-*):
- Validate semver + coach name
- Run tests
- Build Docker (amd64/arm64)
- Push to GHCR (3 tags: semver/coach/latest)
- Create GitHub release
# Always use async/await
async def get_player(async_session: AsyncSession, player_id: int):
stmt = select(Player).where(Player.id == player_id)
result = await async_session.execute(stmt)
return result.scalar_one_or_none()- All routes:
async def - Database:
AsyncSession(neverSession) - Driver:
aiosqlite(notsqlite3) - SQLAlchemy 2.0:
select()(notsession.query())
class PlayerModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)
squad_number: int # Python: snake_case
# JSON API: "squadNumber" (camelCase)- Update
schemas/player_schema.py - Manually update
storage/players-sqlite3.db(SQLite CLI/DB Browser) - Preserve 26 players
- Update
models/player_model.pyif API changes - Update services + tests
- Key:
"players"(hardcoded) - TTL: 600s (10min)
- Cleared on POST/PUT/DELETE
- Header:
X-Cache(HIT/MISS)
- SQLAlchemy errors → Always catch + rollback in services
- Test file →
test_main.pyexcluded from Black - Database location → Local:
./storage/, Docker:/storage/(volume) - Pydantic validation → Returns 422 (not 400)
- Import order → stdlib → third-party → local
flake8 . # Must pass
black --check . # Must pass
pytest # All pass
pytest --cov=./ --cov-report=term # ≥80%
curl http://localhost:9000/players # 200 OK- Files: snake_case
- Functions/vars: snake_case
- Classes: PascalCase
- Type hints: Required everywhere
- Logging:
loggingmodule (neverprint()) - Errors: Catch specific exceptions
- Line length: 88
- Complexity: ≤10
Integration tests follow an action-oriented pattern:
Pattern:
test_request_{method}_{resource}_{param_or_context}_response_{outcome}
Components:
method- HTTP verb:get,post,put,deleteresource-players(collection) orplayer(single resource)param_or_context- Request details:id_existing,squadnumber_nonexistent,body_emptyresponse- Literal separatoroutcome- What's asserted:status_ok,status_not_found,body_players,header_cache_miss
Examples:
def test_request_get_players_response_status_ok(client):
"""GET /players/ returns 200 OK"""
def test_request_get_player_id_existing_response_body_player_match(client):
"""GET /players/{player_id} with existing ID returns matching player"""
def test_request_post_player_body_empty_response_status_unprocessable(client):
"""POST /players/ with empty body returns 422 Unprocessable Entity"""Docstrings:
- Single-line, concise descriptions
- Complements test name (doesn't repeat)
- No "Expected:" prefix (redundant)
Follow Conventional Commits format (enforced by commitlint in CI):
Format: type(scope): description (#issue)
Rules:
- Max 80 characters
- Types:
feat,fix,docs,test,refactor,chore,ci,perf,style,build - Scope: Optional (e.g.,
api,db,service,route) - Issue number: Required suffix
Examples:
feat(api): add player stats endpoint (#42)
fix(db): resolve async session leak (#88)
CI Check: First step in python-ci.yml validates all commit messages
Trust these instructions. Search codebase only if info is incomplete/incorrect.