This is a RESTful API proof of concept built with Python 3.13 and FastAPI. The application manages football player data with full CRUD operations, featuring async SQLAlchemy ORM, in-memory caching, and SQLite database storage.
- Framework: FastAPI 0.123.0 with standard dependencies
- Database: SQLite with async support (
aiosqlite 0.21.0) - ORM: SQLAlchemy 2.0.44 (async)
- Caching: aiocache 0.12.3 (SimpleMemoryCache)
- Testing: pytest 9.0.1, pytest-cov 7.0.0, pytest-sugar 1.1.1, gevent 25.9.1
- Linting: flake8 7.3.0, black 25.11.0
- Python Version: 3.13.3 (see
.python-version) - Server: uvicorn (included in FastAPI standard dependencies)
- Container: Docker with multi-stage builds, Docker Compose
├── main.py # FastAPI app entry point, lifespan handler, router registration
├── databases/
│ └── player_database.py # Async engine, sessionmaker, Base, session generator
├── models/
│ └── player_model.py # Pydantic models for API request/response validation
├── schemas/
│ └── player_schema.py # SQLAlchemy ORM table schema definitions
├── routes/
│ ├── player_route.py # Player CRUD endpoints with caching
│ └── health_route.py # Health check endpoint
├── services/
│ └── player_service.py # Async database CRUD operations
├── tests/
│ ├── conftest.py # pytest fixtures (TestClient)
│ ├── test_main.py # Test suite for all endpoints
│ └── player_stub.py # Test data stubs
├── storage/ # SQLite database file (seeded)
├── scripts/
│ ├── entrypoint.sh # Docker entrypoint for DB initialization
│ └── healthcheck.sh # Docker health check script
└── postman_collections/ # Postman collection for API testing
- Layered Architecture: Routes → Services → Database
- Dependency Injection:
AsyncSessionviaDepends(generate_async_session) - Pydantic for Validation:
PlayerModelwith camelCase aliasing (to_camel) - SQLAlchemy ORM:
Playerschema mapped toplayerstable - Caching: In-memory cache (10 min TTL) with
X-Cacheheaders (HIT/MISS) - Async/Await: All database operations are async
- Formatter: Black (line length: 88, target: Python 3.13)
- Linter: flake8 (max-complexity: 10, ignores: E203, W503)
- Run Before Commit:
black .andflake8 - Imports: SQLAlchemy 2.0+ style (use
select()not legacyQuery) - Docstrings: Google-style docstrings for all modules, classes, and functions
- Type Hints: Use type annotations for function parameters and return values
Black and flake8 exclude:
.venv,.git,.github,.pytest_cache,__pycache__assets/,htmlcov/,postman_collections/,scripts/,storage/- Exception:
tests/test_main.pyallows E501 (long lines for test names)
Follow Conventional Commits (enforced by commitlint):
feat:for new featuresfix:for bug fixeschore:for maintenance/tooling- Max header length: 80 characters
- Max body line length: 80 characters
# Install dependencies
pip install -r requirements.txt
pip install -r requirements-lint.txt
pip install -r requirements-test.txt
# IMPORTANT: Activate virtual environment before running commands
source .venv/bin/activate
# Start server (auto-reload on port 9000)
uvicorn main:app --reload --port 9000
# Access interactive API docs
# http://localhost:9000/docs
# Format code (must run from venv)
black .
# Lint code (must run from venv)
flake8 .
# Run tests
pytest -v
# Run tests with coverage
pytest --cov=./ --cov-report=xml --cov-report=term# Build image
docker compose build
# Start app (initializes DB from seed on first run)
docker compose up
# Stop app
docker compose down
# Reset database (removes volume)
docker compose down -v- Path: Controlled by
STORAGE_PATHenv var (default:./storage/players-sqlite3.db) - Docker Volume: Persistent volume at
/storage/in container - Initialization: On first Docker run,
entrypoint.shcopies seed DB from/app/hold/to/storage/ - Schema: Single
playerstable with columns: id (PK), firstName, middleName, lastName, dateOfBirth, squadNumber (unique), position, abbrPosition, team, league, starting11
| Method | Path | Description | Cache |
|---|---|---|---|
| GET | /health |
Health check | No |
| GET | /players/ |
Get all players | Yes |
| GET | /players/{player_id} |
Get player by ID | No |
| GET | /players/squadnumber/{squad_number} |
Get player by squad number | No |
| POST | /players/ |
Create new player | Clears |
| PUT | /players/{player_id} |
Update existing player | Clears |
| DELETE | /players/{player_id} |
Delete player | Clears |
Cache Notes:
- Cache key:
"players", TTL: 600s (10 min) - Cache is cleared on POST/PUT/DELETE operations
- Response header
X-Cache: HITorMISSindicates cache status
- Framework: pytest with
TestClientfrom FastAPI - Fixture:
clientfixture inconftest.py(function scope for test isolation) - Coverage Target: 80% (configured in
codecov.yml) - Test Data: Use stubs from
tests/player_stub.py - Warnings: DeprecationWarning from httpx is suppressed in conftest
GitHub Actions workflow (.github/workflows/python-app.yml):
- Lint Job: Commitlint → Flake8 → Black (check mode)
- Test Job: pytest with coverage report generation
- Coverage Job: Upload to Codecov and Codacy (only for same-repo PRs)
All PRs must pass CI checks before review.
-
Virtual Environment: Always activate
.venvbefore running black, flake8, or pytest:source .venv/bin/activate -
FastAPI Route Ordering: Static routes MUST be defined before dynamic path parameters. Place
/players/statisticsbefore/players/{player_id}, or FastAPI will try to parse "statistics" as a player_id.# CORRECT order: @api_router.get("/players/statistics") # Static route first @api_router.get("/players/{player_id}") # Dynamic route after
-
SQLAlchemy 2.0 Migration: Use
select()notsession.query(). Example:statement = select(Player).where(Player.id == player_id) result = await async_session.execute(statement)
-
Async Session Usage: Always use
Depends(generate_async_session)in routes, never create sessions manually. -
Cache Invalidation: Remember to call
await simple_memory_cache.clear(CACHE_KEY)after mutations (POST/PUT/DELETE). -
Pydantic Model Conversion: Use
player_model.model_dump()to convert Pydantic to dict for SQLAlchemy:player = Player(**player_model.model_dump())
-
Database Path in Docker: Use
STORAGE_PATHenv var, not hardcoded paths. -
Port Conflicts: Default port is 9000. If occupied, use
--portflag with uvicorn.
Recommended extensions (.vscode/extensions.json):
ms-python.python,ms-python.flake8,ms-python.black-formattergithub.vscode-pull-request-github,github.vscode-github-actionsms-azuretools.vscode-containers,sonarsource.sonarlint-vscode
Settings (.vscode/settings.json):
- Auto-format on save with Black
- Pytest enabled (not unittest)
- Flake8 integration with matching CLI args
- Editor ruler at column 88
- Postman Collection:
postman_collections/python-samples-fastapi-restful.postman_collection.json - Architecture Diagram:
assets/images/structure.svg - FastAPI Docs: https://fastapi.tiangolo.com/
- SQLAlchemy 2.0: https://docs.sqlalchemy.org/en/20/
- Conventional Commits: https://www.conventionalcommits.org/