|
1 | | -# Copilot Instructions |
| 1 | +# GitHub Copilot Instructions |
2 | 2 |
|
3 | | -## Project Summary |
| 3 | +## Overview |
4 | 4 |
|
5 | | -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. |
| 5 | +REST API for managing football players built with Python and FastAPI. Implements async CRUD operations with SQLAlchemy 2.0 (async), SQLite, Pydantic validation, and in-memory caching. Part of a cross-language comparison study (.NET, Go, Java, Rust, TypeScript). |
6 | 6 |
|
7 | | -## Quick Start |
| 7 | +## Tech Stack |
8 | 8 |
|
9 | | -```bash |
10 | | -# Install dependencies |
11 | | -pip install -r requirements.txt |
12 | | -pip install -r requirements-lint.txt |
13 | | -pip install -r requirements-test.txt |
14 | | - |
15 | | -# Run development server |
16 | | -uvicorn main:app --reload --port 9000 |
17 | | -# Access: http://localhost:9000/docs |
18 | | - |
19 | | -# Run tests with coverage |
20 | | -pytest --cov=./ --cov-report=html |
21 | | - |
22 | | -# Lint and format |
23 | | -flake8 . |
24 | | -black --check . # or: black . (to auto-format) |
25 | | - |
26 | | -# Docker |
27 | | -docker compose up |
28 | | -docker compose down -v # Reset database |
29 | | -``` |
| 9 | +- **Language**: Python 3.13 |
| 10 | +- **Framework**: FastAPI + Uvicorn |
| 11 | +- **ORM**: SQLAlchemy 2.0 (async) + aiosqlite |
| 12 | +- **Database**: SQLite |
| 13 | +- **Validation**: Pydantic |
| 14 | +- **Caching**: aiocache (in-memory, 10-minute TTL) |
| 15 | +- **Testing**: pytest + pytest-cov + httpx |
| 16 | +- **Linting/Formatting**: Flake8 + Black |
| 17 | +- **Containerization**: Docker |
30 | 18 |
|
31 | | -## Stack |
32 | | - |
33 | | -- Python 3.13.3 (`.python-version` - auto-detected by pyenv/asdf/mise) |
34 | | -- FastAPI 0.128.6, Uvicorn |
35 | | -- SQLite + SQLAlchemy 2.0 (async) + aiosqlite |
36 | | -- pytest + pytest-cov + httpx |
37 | | -- Flake8 + Black |
38 | | -- aiocache (in-memory, 10min TTL) |
39 | | - |
40 | | -## Architecture |
| 19 | +## Structure |
41 | 20 |
|
42 | 21 | ```text |
43 | | -Request → Routes → Services → SQLAlchemy → SQLite |
44 | | - (API) (Logic) (Async ORM) (Storage) |
45 | | - ↓ |
46 | | - Pydantic (Validation) |
| 22 | +main.py — application entry point: FastAPI setup, router registration |
| 23 | +routes/ — HTTP route definitions + dependency injection [HTTP layer] |
| 24 | +services/ — async business logic + cache management [business layer] |
| 25 | +schemas/ — SQLAlchemy ORM models (database schema) [data layer] |
| 26 | +databases/ — async SQLAlchemy session setup |
| 27 | +models/ — Pydantic models for request/response validation |
| 28 | +storage/ — SQLite database file (players-sqlite3.db, pre-seeded) |
| 29 | +tests/ — pytest integration tests |
47 | 30 | ``` |
48 | 31 |
|
49 | | -**Key Directories:** |
50 | | - |
51 | | -- `routes/` - API endpoints (player_route.py, health_route.py) |
52 | | -- `services/` - Business logic (player_service.py) |
53 | | -- `models/` - Pydantic validation (camelCase JSON API) |
54 | | -- `schemas/` - SQLAlchemy ORM models |
55 | | -- `databases/` - Async DB setup, session factory |
56 | | -- `storage/` - SQLite file (pre-seeded, 26 players) |
57 | | -- `tests/` - pytest suite (test_main.py, conftest.py) |
| 32 | +**Layer rule**: `Routes → Services → SQLAlchemy → SQLite`. Routes handle HTTP concerns only; business logic belongs in services. |
58 | 33 |
|
59 | | -**Config Files:** |
| 34 | +## Coding Guidelines |
60 | 35 |
|
61 | | -- `.flake8` - Linter (max-line-length=88, complexity=10) |
62 | | -- `pyproject.toml` - Black formatter (line-length=88) |
63 | | -- `.coveragerc` - Coverage config (80% target) |
64 | | -- `compose.yaml` - Docker orchestration |
65 | | -- `Dockerfile` - Multi-stage build |
66 | | - |
67 | | -## API Endpoints |
68 | | - |
69 | | -All async with `AsyncSession` injection: |
70 | | - |
71 | | -- `POST /players/` → 201|409|422 |
72 | | -- `GET /players/` → 200 (cached 10min) |
73 | | -- `GET /players/{player_id}` → 200|404 |
74 | | -- `GET /players/squadnumber/{squad_number}` → 200|404 |
75 | | -- `PUT /players/{player_id}` → 200|404|422 |
76 | | -- `DELETE /players/{player_id}` → 200|404 |
77 | | -- `GET /health` → 200 |
78 | | - |
79 | | -JSON: camelCase (e.g., `squadNumber`, `firstName`) |
80 | | - |
81 | | -## CI/CD |
82 | | - |
83 | | -**python-ci.yml** (push/PR to master): |
84 | | - |
85 | | -1. Lint: commitlint → `flake8 .` → `black --check .` |
86 | | -2. Test: `pytest -v` → coverage |
87 | | -3. Upload to Codecov |
88 | | - |
89 | | -**python-cd.yml** (tags `v*.*.*-*`): |
90 | | - |
91 | | -1. Validate semver + coach name |
92 | | -2. Run tests |
93 | | -3. Build Docker (amd64/arm64) |
94 | | -4. Push to GHCR (3 tags: semver/coach/latest) |
95 | | -5. Create GitHub release |
96 | | - |
97 | | -## Critical Patterns |
98 | | - |
99 | | -### Async Everywhere |
100 | | - |
101 | | -```python |
102 | | -# Always use async/await |
103 | | -async def get_player(async_session: AsyncSession, player_id: int): |
104 | | - stmt = select(Player).where(Player.id == player_id) |
105 | | - result = await async_session.execute(stmt) |
106 | | - return result.scalar_one_or_none() |
107 | | -``` |
| 36 | +- **Naming**: snake_case (files, functions, variables), PascalCase (classes) |
| 37 | +- **Type hints**: Required everywhere — functions, variables, return types |
| 38 | +- **Async**: All routes and service functions must be `async def`; use `AsyncSession` (never `Session`); use `aiosqlite` (never `sqlite3`); use SQLAlchemy 2.0 `select()` (never `session.query()`) |
| 39 | +- **API contract**: camelCase JSON via Pydantic `alias_generator=to_camel`; Python internals stay snake_case |
| 40 | +- **Caching**: cache key `"players"` (hardcoded); clear on POST/PUT/DELETE; `X-Cache` header (HIT/MISS) |
| 41 | +- **Errors**: Catch specific exceptions with rollback in services; Pydantic validation returns 422 (not 400) |
| 42 | +- **Logging**: `logging` module only; never `print()` |
| 43 | +- **Line length**: 88; complexity ≤ 10 |
| 44 | +- **Import order**: stdlib → third-party → local |
| 45 | +- **Tests**: naming pattern `test_request_{method}_{resource}_{context}_response_{outcome}`; docstrings single-line, concise; `tests/player_stub.py` for test data; `tests/test_main.py` excluded from Black |
| 46 | +- **Avoid**: sync DB access, mixing sync/async, `print()`, missing type hints, unhandled exceptions |
108 | 47 |
|
109 | | -- All routes: `async def` |
110 | | -- Database: `AsyncSession` (never `Session`) |
111 | | -- Driver: `aiosqlite` (not `sqlite3`) |
112 | | -- SQLAlchemy 2.0: `select()` (not `session.query()`) |
| 48 | +## Commands |
113 | 49 |
|
114 | | -### camelCase API Contract |
115 | | - |
116 | | -```python |
117 | | -class PlayerModel(BaseModel): |
118 | | - model_config = ConfigDict(alias_generator=to_camel) |
119 | | - squad_number: int # Python: snake_case |
120 | | - # JSON API: "squadNumber" (camelCase) |
121 | | -``` |
122 | | - |
123 | | -### Database Schema Changes |
124 | | - |
125 | | -⚠️ No Alembic yet - manual process: |
126 | | - |
127 | | -1. Update `schemas/player_schema.py` |
128 | | -2. Manually update `storage/players-sqlite3.db` (SQLite CLI/DB Browser) |
129 | | -3. Preserve 26 players |
130 | | -4. Update `models/player_model.py` if API changes |
131 | | -5. Update services + tests |
132 | | - |
133 | | -### Caching |
134 | | - |
135 | | -- Key: `"players"` (hardcoded) |
136 | | -- TTL: 600s (10min) |
137 | | -- Cleared on POST/PUT/DELETE |
138 | | -- Header: `X-Cache` (HIT/MISS) |
139 | | - |
140 | | -## Common Issues |
141 | | - |
142 | | -1. **SQLAlchemy errors** → Always catch + rollback in services |
143 | | -2. **Test file** → `test_main.py` excluded from Black |
144 | | -3. **Database location** → Local: `./storage/`, Docker: `/storage/` (volume) |
145 | | -4. **Pydantic validation** → Returns 422 (not 400) |
146 | | -5. **Import order** → stdlib → third-party → local |
147 | | - |
148 | | -## Validation Checklist |
| 50 | +### Quick Start |
149 | 51 |
|
150 | 52 | ```bash |
151 | | -flake8 . # Must pass |
152 | | -black --check . # Must pass |
153 | | -pytest # All pass |
154 | | -pytest --cov=./ --cov-report=term # ≥80% |
155 | | -curl http://localhost:9000/players # 200 OK |
| 53 | +pip install -r requirements.txt |
| 54 | +pip install -r requirements-test.txt |
| 55 | +pip install -r requirements-lint.txt |
| 56 | +uvicorn main:app --reload --port 9000 # http://localhost:9000/docs |
| 57 | +pytest # run tests |
| 58 | +pytest --cov=./ --cov-report=term # with coverage (target >=80%) |
| 59 | +flake8 . |
| 60 | +black --check . |
| 61 | +docker compose up |
| 62 | +docker compose down -v |
156 | 63 | ``` |
157 | 64 |
|
158 | | -## Code Conventions |
| 65 | +### Pre-commit Checks |
159 | 66 |
|
160 | | -- Files: snake_case |
161 | | -- Functions/vars: snake_case |
162 | | -- Classes: PascalCase |
163 | | -- Type hints: Required everywhere |
164 | | -- Logging: `logging` module (never `print()`) |
165 | | -- Errors: Catch specific exceptions |
166 | | -- Line length: 88 |
167 | | -- Complexity: ≤10 |
| 67 | +1. Update `CHANGELOG.md` `[Unreleased]` section (Added / Changed / Fixed / Removed) |
| 68 | +2. `flake8 .` — must pass |
| 69 | +3. `black --check .` — must pass |
| 70 | +4. `pytest` — all tests must pass |
| 71 | +5. `pytest --cov=./ --cov-report=term` — coverage must be >=80% |
| 72 | +6. Commit message follows Conventional Commits format (enforced by commitlint) |
168 | 73 |
|
169 | | -## Test Naming Convention |
| 74 | +### Commits |
170 | 75 |
|
171 | | -Integration tests follow an action-oriented pattern: |
| 76 | +Format: `type(scope): description (#issue)` — max 80 chars |
| 77 | +Types: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf` |
| 78 | +Example: `feat(api): add player stats endpoint (#42)` |
172 | 79 |
|
173 | | -**Pattern:** |
| 80 | +## Agent Mode |
174 | 81 |
|
175 | | -```text |
176 | | -test_request_{method}_{resource}_{param_or_context}_response_{outcome} |
177 | | -``` |
| 82 | +### Proceed freely |
178 | 83 |
|
179 | | -**Components:** |
| 84 | +- Add/modify routes and endpoints |
| 85 | +- Service layer logic and cache management |
| 86 | +- Tests (maintain async patterns and naming convention) |
| 87 | +- Documentation and docstring updates |
| 88 | +- Lint/format fixes |
| 89 | +- Refactoring within existing architectural patterns |
180 | 90 |
|
181 | | -- `method` - HTTP verb: `get`, `post`, `put`, `delete` |
182 | | -- `resource` - `players` (collection) or `player` (single resource) |
183 | | -- `param_or_context` - Request details: `id_existing`, `squadnumber_nonexistent`, `body_empty` |
184 | | -- `response` - Literal separator |
185 | | -- `outcome` - What's asserted: `status_ok`, `status_not_found`, `body_players`, `header_cache_miss` |
| 91 | +### Ask before changing |
186 | 92 |
|
187 | | -**Examples:** |
| 93 | +- Database schema (`schemas/player_schema.py` — no Alembic, manual process) |
| 94 | +- Dependencies (`requirements*.txt`) |
| 95 | +- CI/CD configuration (`.github/workflows/`) |
| 96 | +- Docker setup |
| 97 | +- API contracts (breaking Pydantic model changes) |
| 98 | +- Global error handling |
188 | 99 |
|
189 | | -```python |
190 | | -def test_request_get_players_response_status_ok(client): |
191 | | - """GET /players/ returns 200 OK""" |
192 | | - |
193 | | -def test_request_get_player_id_existing_response_body_player_match(client): |
194 | | - """GET /players/{player_id} with existing ID returns matching player""" |
195 | | - |
196 | | -def test_request_post_player_body_empty_response_status_unprocessable(client): |
197 | | - """POST /players/ with empty body returns 422 Unprocessable Entity""" |
198 | | -``` |
| 100 | +### Never modify |
199 | 101 |
|
200 | | -**Docstrings:** |
| 102 | +- `.env` files (secrets) |
| 103 | +- Production configurations |
| 104 | +- Async/await patterns (mandatory throughout) |
| 105 | +- Type hints (mandatory throughout) |
| 106 | +- Core layered architecture |
201 | 107 |
|
202 | | -- Single-line, concise descriptions |
203 | | -- Complements test name (doesn't repeat) |
204 | | -- No "Expected:" prefix (redundant) |
| 108 | +### Key workflows |
205 | 109 |
|
206 | | -## Commit Messages |
| 110 | +**Add an endpoint**: Add Pydantic model in `models/` → add async service method in `services/` with error handling → add route in `routes/` with `Depends(generate_async_session)` → add tests following naming pattern → run pre-commit checks. |
207 | 111 |
|
208 | | -Follow Conventional Commits format (enforced by commitlint in CI): |
| 112 | +**Modify schema**: Update `schemas/player_schema.py` → manually update `storage/players-sqlite3.db` (preserve 26 players) → update `models/player_model.py` if API changes → update services and tests → run `pytest`. |
209 | 113 |
|
210 | | -**Format:** `type(scope): description (#issue)` |
211 | | - |
212 | | -**Rules:** |
213 | | - |
214 | | -- Max 80 characters |
215 | | -- Types: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `ci`, `perf`, `style`, `build` |
216 | | -- Scope: Optional (e.g., `api`, `db`, `service`, `route`) |
217 | | -- Issue number: Required suffix |
218 | | - |
219 | | -**Examples:** |
| 114 | +**After completing work**: Suggest a branch name (e.g. `feat/add-player-stats`) and a commit message following Conventional Commits including co-author line: |
220 | 115 |
|
221 | 116 | ```text |
222 | | -feat(api): add player stats endpoint (#42) |
223 | | -fix(db): resolve async session leak (#88) |
224 | | -``` |
225 | | - |
226 | | -**CI Check:** First step in python-ci.yml validates all commit messages |
| 117 | +feat(scope): description (#issue) |
227 | 118 |
|
228 | | -Trust these instructions. Search codebase only if info is incomplete/incorrect. |
| 119 | +Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
| 120 | +``` |
0 commit comments