Skip to content

Commit 4242cbb

Browse files
authored
Merge pull request #492 from nanotaboada/feat/uuid-primary-key
feat(api): replace integer PK with UUID v4 for Player entity (#66)
2 parents df8ce10 + 2c87e51 commit 4242cbb

File tree

16 files changed

+911
-102
lines changed

16 files changed

+911
-102
lines changed

.coveragerc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ omit =
1010

1111
[report]
1212
exclude_lines =
13-
pragma: no cover # Standard pragma to intentionally skip lines
14-
if __name__ == .__main__.: # Skips CLI bootstrapping code
15-
raise NotImplementedError # Often placeholder stubs not meant to be covered
13+
pragma: no cover
14+
if __name__ == .__main__.:
15+
raise NotImplementedError

.github/copilot-instructions.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ schemas/ — SQLAlchemy ORM models (database schema) [data laye
2626
databases/ — async SQLAlchemy session setup
2727
models/ — Pydantic models for request/response validation
2828
storage/ — SQLite database file (players-sqlite3.db, pre-seeded)
29+
scripts/ — shell scripts for Docker (entrypoint.sh, healthcheck.sh)
30+
tools/ — standalone seed scripts (run manually, not via Alembic)
2931
tests/ — pytest integration tests
3032
```
3133

@@ -37,6 +39,8 @@ tests/ — pytest integration tests
3739
- **Type hints**: Required everywhere — functions, variables, return types
3840
- **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()`)
3941
- **API contract**: camelCase JSON via Pydantic `alias_generator=to_camel`; Python internals stay snake_case
42+
- **Models**: `PlayerRequestModel` (no `id`, used for POST/PUT) and `PlayerResponseModel` (includes `id: UUID`, used for GET/POST responses); never use the removed `PlayerModel`
43+
- **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
4044
- **Caching**: cache key `"players"` (hardcoded); clear on POST/PUT/DELETE; `X-Cache` header (HIT/MISS)
4145
- **Errors**: Catch specific exceptions with rollback in services; Pydantic validation returns 422 (not 400)
4246
- **Logging**: `logging` module only; never `print()`
@@ -90,7 +94,7 @@ Example: `feat(api): add player stats endpoint (#42)`
9094

9195
### Ask before changing
9296

93-
- Database schema (`schemas/player_schema.py` — no Alembic, manual process)
97+
- Database schema (`schemas/player_schema.py` — no Alembic, use tools/ seed scripts manually)
9498
- Dependencies (`requirements*.txt`)
9599
- CI/CD configuration (`.github/workflows/`)
96100
- Docker setup

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ cover/
6161
local_settings.py
6262
db.sqlite3
6363
db.sqlite3-journal
64+
*.db-shm
65+
*.db-wal
66+
*.db.bak.*
6467

6568
# Flask stuff:
6669
instance/

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"**/htmlcov/**",
3737
"**/postman_collections/**",
3838
"**/scripts/**",
39+
"**/tools/**",
3940
"**/storage/**",
4041
"**/__pycache__/**",
4142
"**/tests/test_main.py"

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,39 @@ This project uses famous football coaches as release codenames, following an A-Z
4444

4545
### Added
4646

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

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

5170
### Removed
5271

5372
### Fixed
5473

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

5782
---

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ ignore:
4444
- "^models/.*"
4545
- "^postman_collections/.*"
4646
- "^schemas/.*"
47+
- "^tools/.*"
4748
- "^tests/.*"
4849
- ".*\\.yml$"
4950
- ".*\\.json$"

models/player_model.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
Pydantic models defining the data schema for football players.
33
44
- `MainModel`: Base model with common config for camelCase aliasing.
5-
- `PlayerModel`: Represents a football player with personal and team details.
5+
- `PlayerRequestModel`: Represents player data for Create and Update operations.
6+
- `PlayerResponseModel`: Represents player data including UUID for Retrieve operations.
67
78
These models are used for data validation and serialization in the API.
89
"""
910

1011
from typing import Optional
12+
from uuid import UUID
1113
from pydantic import BaseModel, ConfigDict
1214
from pydantic.alias_generators import to_camel
1315

@@ -27,15 +29,17 @@ class MainModel(BaseModel):
2729
Pydantic models.
2830
"""
2931

30-
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
32+
model_config = ConfigDict(
33+
alias_generator=to_camel, populate_by_name=True, from_attributes=True
34+
)
3135

3236

33-
class PlayerModel(MainModel):
37+
class PlayerRequestModel(MainModel):
3438
"""
35-
Pydantic model representing a football Player.
39+
Pydantic model representing the data required for Create and Update operations
40+
on a football Player.
3641
3742
Attributes:
38-
id (int): The unique identifier for the Player.
3943
first_name (str): The first name of the Player.
4044
middle_name (Optional[str]): The middle name of the Player, if any.
4145
last_name (str): The last name of the Player.
@@ -50,14 +54,47 @@ class PlayerModel(MainModel):
5054
if provided.
5155
"""
5256

53-
id: int
5457
first_name: str
55-
middle_name: Optional[str]
58+
middle_name: Optional[str] = None
5659
last_name: str
57-
date_of_birth: Optional[str]
60+
date_of_birth: Optional[str] = None
5861
squad_number: int
5962
position: str
60-
abbr_position: Optional[str]
61-
team: Optional[str]
62-
league: Optional[str]
63-
starting11: Optional[bool]
63+
abbr_position: Optional[str] = None
64+
team: Optional[str] = None
65+
league: Optional[str] = None
66+
starting11: Optional[bool] = None
67+
68+
69+
class PlayerResponseModel(MainModel):
70+
"""
71+
Pydantic model representing a football Player with a UUID for Retrieve operations.
72+
73+
Attributes:
74+
id (UUID): The unique identifier for the Player (UUID v4 for API-created
75+
records, UUID v5 for migration-seeded records).
76+
first_name (str): The first name of the Player.
77+
middle_name (Optional[str]): The middle name of the Player, if any.
78+
last_name (str): The last name of the Player.
79+
date_of_birth (Optional[str]): The date of birth of the Player, if provided.
80+
squad_number (int): The unique squad number assigned to the Player.
81+
position (str): The playing position of the Player.
82+
abbr_position (Optional[str]): The abbreviated form of the Player's position,
83+
if any.
84+
team (Optional[str]): The team to which the Player belongs, if any.
85+
league (Optional[str]): The league where the team plays, if any.
86+
starting11 (Optional[bool]): Indicates if the Player is in the starting 11,
87+
if provided.
88+
"""
89+
90+
id: UUID
91+
first_name: str
92+
middle_name: Optional[str] = None
93+
last_name: str
94+
date_of_birth: Optional[str] = None
95+
squad_number: int
96+
position: str
97+
abbr_position: Optional[str] = None
98+
team: Optional[str] = None
99+
league: Optional[str] = None
100+
starting11: Optional[bool] = None

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exclude = '''
1313
| htmlcov
1414
| postman_collections
1515
| scripts
16+
| tools
1617
| storage
1718
| __pycache__
1819
| tests/test_main\.py

0 commit comments

Comments
 (0)