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
6 changes: 3 additions & 3 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ omit =

[report]
exclude_lines =
pragma: no cover # Standard pragma to intentionally skip lines
if __name__ == .__main__.: # Skips CLI bootstrapping code
raise NotImplementedError # Often placeholder stubs not meant to be covered
pragma: no cover
if __name__ == .__main__.:
raise NotImplementedError
6 changes: 5 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ schemas/ — SQLAlchemy ORM models (database schema) [data laye
databases/ — async SQLAlchemy session setup
models/ — Pydantic models for request/response validation
storage/ — SQLite database file (players-sqlite3.db, pre-seeded)
scripts/ — shell scripts for Docker (entrypoint.sh, healthcheck.sh)
tools/ — standalone seed scripts (run manually, not via Alembic)
tests/ — pytest integration tests
```

Expand All @@ -37,6 +39,8 @@ tests/ — pytest integration tests
- **Type hints**: Required everywhere — functions, variables, return types
- **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()`)
- **API contract**: camelCase JSON via Pydantic `alias_generator=to_camel`; Python internals stay snake_case
- **Models**: `PlayerRequestModel` (no `id`, used for POST/PUT) and `PlayerResponseModel` (includes `id: UUID`, used for GET/POST responses); never use the removed `PlayerModel`
- **Primary key**: UUID surrogate key (`id`) — opaque, internal, used for all CRUD operations. UUID v4 for API-created records; UUID v5 (deterministic) for migration-seeded records. `squad_number` is the natural key — human-readable, domain-meaningful, preferred lookup for external consumers
- **Caching**: cache key `"players"` (hardcoded); clear on POST/PUT/DELETE; `X-Cache` header (HIT/MISS)
- **Errors**: Catch specific exceptions with rollback in services; Pydantic validation returns 422 (not 400)
- **Logging**: `logging` module only; never `print()`
Expand Down Expand Up @@ -90,7 +94,7 @@ Example: `feat(api): add player stats endpoint (#42)`

### Ask before changing

- Database schema (`schemas/player_schema.py` — no Alembic, manual process)
- Database schema (`schemas/player_schema.py` — no Alembic, use tools/ seed scripts manually)
- Dependencies (`requirements*.txt`)
- CI/CD configuration (`.github/workflows/`)
- Docker setup
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ cover/
local_settings.py
db.sqlite3
db.sqlite3-journal
*.db-shm
*.db-wal
*.db.bak.*

# Flask stuff:
instance/
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"**/htmlcov/**",
"**/postman_collections/**",
"**/scripts/**",
"**/tools/**",
"**/storage/**",
"**/__pycache__/**",
"**/tests/test_main.py"
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,39 @@ This project uses famous football coaches as release codenames, following an A-Z

### Added

- UUID v4 primary key for the `players` table, replacing the previous integer PK (#66)
- `PlayerRequestModel` Pydantic model for POST/PUT request bodies (no `id` field) (#66)
- `PlayerResponseModel` Pydantic model for GET/POST response bodies (includes `id: UUID`) (#66)
- `tools/seed_001_starting_eleven.py`: standalone seed script populating 11 starting-eleven players with deterministic UUID v5 PKs (#66)
- `tools/seed_002_substitutes.py`: standalone seed script populating 14 substitute players with deterministic UUID v5 PKs (#66)
- `HyphenatedUUID` custom `TypeDecorator` in `schemas/player_schema.py` storing UUIDs as hyphenated `CHAR(36)` strings in SQLite, returning `uuid.UUID` objects in Python (#66)

### Changed

- `PlayerModel` split into `PlayerRequestModel` and `PlayerResponseModel` in `models/player_model.py` (#66)
- All route path parameters and service function signatures updated from `int` to `uuid.UUID` (#66)
- POST conflict detection changed from ID lookup to `squad_number` uniqueness check (#66)
- `tests/player_stub.py` updated with UUID-based test fixtures (#66)
- `tests/test_main.py` updated to assert UUID presence and format in API responses (#66)
- `PlayerResponseModel` redeclared with `id` as first field to control JSON serialization order (#66)
- `HyphenatedUUID` methods now have full type annotations and Google-style docstrings; unused `dialect` params renamed to `_dialect` (#66)
- Service logger changed from `getLogger("uvicorn")` to `getLogger("uvicorn.error")`, aligned with `main.py` (#66)
- `logger.error(f"...")` replaced with `logger.exception("...: %s", error)` in all `SQLAlchemyError` handlers (#66)
- EN dashes replaced with ASCII hyphens in `seed_002` log and argparse strings (#66)
- `logger.error` replaced with `logger.exception` in `sqlite3.Error` handlers in `seed_001` and `seed_002` (#66)

### Deprecated

### Removed

### Fixed

- POST/PUT/DELETE routes now raise `HTTP 500` on DB failure instead of silently returning success (#66)
- Cache cleared only after confirmed successful create, update, or delete (#66)
- DELETE test is now self-contained; no longer depends on POST test having run first (#66)
- UUID assertion in GET all test replaced with explicit `_is_valid_uuid()` validator (#66)
- Emiliano Martínez `middleName` corrected from `""` to `None` in `seed_001` (#66)

### Security

---
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ignore:
- "^models/.*"
- "^postman_collections/.*"
- "^schemas/.*"
- "^tools/.*"
- "^tests/.*"
- ".*\\.yml$"
- ".*\\.json$"
Expand Down
61 changes: 49 additions & 12 deletions models/player_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
Pydantic models defining the data schema for football players.

- `MainModel`: Base model with common config for camelCase aliasing.
- `PlayerModel`: Represents a football player with personal and team details.
- `PlayerRequestModel`: Represents player data for Create and Update operations.
- `PlayerResponseModel`: Represents player data including UUID for Retrieve operations.

These models are used for data validation and serialization in the API.
"""

from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

Expand All @@ -27,15 +29,17 @@ class MainModel(BaseModel):
Pydantic models.
"""

model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
model_config = ConfigDict(
alias_generator=to_camel, populate_by_name=True, from_attributes=True
)


class PlayerModel(MainModel):
class PlayerRequestModel(MainModel):
"""
Pydantic model representing a football Player.
Pydantic model representing the data required for Create and Update operations
on a football Player.

Attributes:
id (int): The unique identifier for the Player.
first_name (str): The first name of the Player.
middle_name (Optional[str]): The middle name of the Player, if any.
last_name (str): The last name of the Player.
Expand All @@ -50,14 +54,47 @@ class PlayerModel(MainModel):
if provided.
"""

id: int
first_name: str
middle_name: Optional[str]
middle_name: Optional[str] = None
last_name: str
date_of_birth: Optional[str]
date_of_birth: Optional[str] = None
squad_number: int
position: str
abbr_position: Optional[str]
team: Optional[str]
league: Optional[str]
starting11: Optional[bool]
abbr_position: Optional[str] = None
team: Optional[str] = None
league: Optional[str] = None
starting11: Optional[bool] = None


class PlayerResponseModel(MainModel):
"""
Pydantic model representing a football Player with a UUID for Retrieve operations.

Attributes:
id (UUID): The unique identifier for the Player (UUID v4 for API-created
records, UUID v5 for migration-seeded records).
first_name (str): The first name of the Player.
middle_name (Optional[str]): The middle name of the Player, if any.
last_name (str): The last name of the Player.
date_of_birth (Optional[str]): The date of birth of the Player, if provided.
squad_number (int): The unique squad number assigned to the Player.
position (str): The playing position of the Player.
abbr_position (Optional[str]): The abbreviated form of the Player's position,
if any.
team (Optional[str]): The team to which the Player belongs, if any.
league (Optional[str]): The league where the team plays, if any.
starting11 (Optional[bool]): Indicates if the Player is in the starting 11,
if provided.
"""

id: UUID
first_name: str
middle_name: Optional[str] = None
last_name: str
date_of_birth: Optional[str] = None
squad_number: int
position: str
abbr_position: Optional[str] = None
team: Optional[str] = None
league: Optional[str] = None
starting11: Optional[bool] = None
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exclude = '''
| htmlcov
| postman_collections
| scripts
| tools
| storage
| __pycache__
| tests/test_main\.py
Expand Down
Loading