Skip to content

Commit 9ab5ffb

Browse files
committed
feat: update hero model to use UUID7 and adjust related functions
1 parent 5d38f7f commit 9ab5ffb

7 files changed

Lines changed: 84 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ The code `dc` can be found in [docker_compose_cli.py](src/utils/docker_compose_c
114114

115115
> [!NOTE]
116116
> To run the service with PostgreSQL backed database set `DATABASE_URL=postgresql://develop:develop_secret@db:5432/develop`
117-
> in .𝐞𝐧𝐯, setting db instead of localhost otherwise the service-api can't reach the database container.
117+
> in .𝐞𝐧𝐯, setting db instead of localhost otherwise the service-api and migration can't reach the database container.
118118
119119
The `dc` CLI utility to run Docker Compose with specific profiles is available to start the application.
120120

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"sqlmodel~=0.0.27",
1212
"alembic>=1.17.2,<2.0.0",
1313
"psycopg2-binary>=2.9.10,<3.0.0",
14+
"uuid-utils>=0.12.0"
1415
]
1516

1617
[dependency-groups]

src/alembic/versions/2025_12_17_1007-7b56ff295e99_init.py renamed to src/alembic/versions/2025_12_18_1212-438f1c13aa3b_init.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""init
22
3-
Revision ID: 7b56ff295e99
3+
Revision ID: 438f1c13aa3b
44
Revises:
5-
Create Date: 2025-12-17 10:07:08.280928
5+
Create Date: 2025-12-18 12:12:05.956211
66
77
"""
88

@@ -13,7 +13,7 @@
1313
from alembic import op
1414

1515
# revision identifiers, used by Alembic.
16-
revision: str = "7b56ff295e99"
16+
revision: str = "438f1c13aa3b"
1717
down_revision: Union[str, Sequence[str], None] = None
1818
branch_labels: Union[str, Sequence[str], None] = None
1919
depends_on: Union[str, Sequence[str], None] = None
@@ -26,7 +26,7 @@ def upgrade() -> None:
2626
"hero",
2727
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
2828
sa.Column("age", sa.Integer(), nullable=True),
29-
sa.Column("id", sa.Integer(), nullable=False),
29+
sa.Column("id", sa.Uuid(), nullable=False),
3030
sa.Column("secret_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
3131
sa.PrimaryKeyConstraint("id"),
3232
)

src/api/endpoints/heroes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from api.deps import SessionDep, get_db_session
44
from excepts import DatabaseEntryNotFound, get_error_content
55
from fastapi import APIRouter, Depends, HTTPException, Query, status
6-
from models import Hero, HeroCreate, HeroPublic, HeroUpdate
6+
from models import UUID7, Hero, HeroCreate, HeroPublic, HeroUpdate
77
from sqlmodel import select
88
from utils.log import get_logger
99

@@ -52,7 +52,7 @@ def read_heroes(
5252
response_model=HeroPublic,
5353
dependencies=[Depends(get_db_session)],
5454
)
55-
def read_hero(hero_id: int, session: SessionDep):
55+
def read_hero(hero_id: UUID7, session: SessionDep):
5656
try:
5757
hero = session.get(Hero, hero_id)
5858
if not hero:
@@ -83,7 +83,7 @@ def read_hero(hero_id: int, session: SessionDep):
8383
response_model=HeroPublic,
8484
dependencies=[Depends(get_db_session)],
8585
)
86-
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):
86+
def update_hero(hero_id: UUID7, hero: HeroUpdate, session: SessionDep):
8787
try:
8888
hero_db = session.get(Hero, hero_id)
8989
if not hero_db:
@@ -118,7 +118,7 @@ def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):
118118
response_description="Indicates whether the hero was successfully deleted",
119119
dependencies=[Depends(get_db_session)],
120120
)
121-
def delete_hero(hero_id: int, session: SessionDep):
121+
def delete_hero(hero_id: UUID7, session: SessionDep):
122122
try:
123123
hero = session.get(Hero, hero_id)
124124
if not hero:

src/models.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,47 @@
1+
from typing import Annotated, Literal
2+
3+
import uuid_utils.compat as uuid
4+
from pydantic import GetPydanticSchema
5+
from pydantic_core import core_schema
16
from sqlmodel import Field, SQLModel
7+
from uuid_utils.compat import UUID
8+
9+
10+
# TODO: Remove this function when Pydantic adds native support for UUID7
11+
# https://github.com/pydantic/pydantic-extra-types/issues/204
12+
def validate_uuid(val, version: Literal[1, 3, 4, 5, 6, 7, 8]) -> UUID:
13+
# Convert string to UUID if needed
14+
if isinstance(val, str):
15+
try:
16+
val = UUID(val)
17+
except (ValueError, AttributeError):
18+
raise ValueError(f"Invalid UUID format: {val}")
19+
if not isinstance(val, UUID):
20+
raise ValueError(f"Expected a UUID, got {type(val)}")
21+
if val.version != version:
22+
raise ValueError(f"Expected a UUID{version}, got UUID{val.version}")
23+
return val
24+
25+
26+
UUID7 = Annotated[
27+
UUID,
28+
GetPydanticSchema(
29+
get_pydantic_core_schema=lambda _,
30+
handler: core_schema.with_info_plain_validator_function(
31+
lambda val, info: validate_uuid(
32+
UUID(val) if info.mode == "json" else val, version=7
33+
),
34+
serialization=core_schema.plain_serializer_function_ser_schema(
35+
lambda val, info: str(val) if info.mode == "json" else val,
36+
info_arg=True,
37+
),
38+
),
39+
get_pydantic_json_schema=lambda _, handler: {
40+
**handler(core_schema.str_schema()),
41+
"format": "uuid7",
42+
},
43+
),
44+
]
245

346

447
class HeroBase(SQLModel):
@@ -13,8 +56,8 @@ class HeroBase(SQLModel):
1356
class Hero(HeroBase, table=True):
1457
"""Database model for Hero with the extra fields that are not always in the other models."""
1558

16-
id: int | None = Field(
17-
default=None,
59+
id: UUID7 = Field(
60+
default_factory=uuid.uuid7,
1861
title="Hero ID",
1962
description="The unique identifier for the hero.",
2063
primary_key=True,
@@ -27,7 +70,7 @@ class Hero(HeroBase, table=True):
2770
class HeroPublic(HeroBase):
2871
"""Public model for Hero without sensitive information."""
2972

30-
id: int
73+
id: UUID7
3174

3275

3376
class HeroCreate(HeroBase):

tests/api/endpoints/test_heroes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ def test_read_heroes(session: Session, client_with_db: TestClient):
5151
assert data[0]["name"] == hero_1.name
5252
assert "secret_name" not in data[0].keys()
5353
assert data[0]["age"] == hero_1.age
54-
assert data[0]["id"] == hero_1.id
54+
assert data[0]["id"] == str(hero_1.id)
5555
assert data[1]["name"] == hero_2.name
5656
assert "secret_name" not in data[1].keys()
5757
assert data[1]["age"] == hero_2.age
58-
assert data[1]["id"] == hero_2.id
58+
assert data[1]["id"] == str(hero_2.id)
5959

6060

6161
def test_read_hero(session: Session, client_with_db: TestClient):
@@ -70,7 +70,7 @@ def test_read_hero(session: Session, client_with_db: TestClient):
7070
assert data["name"] == hero_1.name
7171
assert "secret_name" not in data.keys()
7272
assert data["age"] == hero_1.age
73-
assert data["id"] == hero_1.id
73+
assert data["id"] == str(hero_1.id)
7474

7575

7676
def test_update_hero(session: Session, client_with_db: TestClient):
@@ -85,7 +85,7 @@ def test_update_hero(session: Session, client_with_db: TestClient):
8585
assert data["name"] == "Deadpuddle"
8686
assert "secret_name" not in data.keys()
8787
assert data["age"] is None
88-
assert data["id"] == hero_1.id
88+
assert data["id"] == str(hero_1.id)
8989

9090

9191
def test_delete_hero(session: Session, client_with_db: TestClient):

uv.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)