Skip to content

Commit cc52512

Browse files
committed
Fix read model id field
+ other fixes + tests
1 parent 1556af3 commit cc52512

File tree

18 files changed

+533
-6
lines changed

18 files changed

+533
-6
lines changed

examples/api_for_sqlalchemy/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from examples.api_for_sqlalchemy.models.age_rating import AgeRating
12
from examples.api_for_sqlalchemy.models.child import Child
23
from examples.api_for_sqlalchemy.models.computer import Computer
4+
from examples.api_for_sqlalchemy.models.movie import Movie
35
from examples.api_for_sqlalchemy.models.parent import Parent
46
from examples.api_for_sqlalchemy.models.parent_to_child_association import ParentToChildAssociation
57
from examples.api_for_sqlalchemy.models.post import Post
@@ -9,8 +11,10 @@
911
from examples.api_for_sqlalchemy.models.workplace import Workplace
1012

1113
__all__ = (
14+
"AgeRating",
1215
"Child",
1316
"Computer",
17+
"Movie",
1418
"Parent",
1519
"ParentToChildAssociation",
1620
"Post",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from typing import TYPE_CHECKING
2+
3+
from sqlalchemy import String, Text
4+
from sqlalchemy.orm import Mapped, mapped_column, relationship
5+
6+
from examples.api_for_sqlalchemy.models.base import BaseMetadata
7+
8+
if TYPE_CHECKING:
9+
from examples.api_for_sqlalchemy.models.movie import Movie
10+
11+
12+
class AgeRating(BaseMetadata):
13+
__tablename__ = "age_rating"
14+
15+
name: Mapped[str] = mapped_column(
16+
String(20),
17+
primary_key=True,
18+
)
19+
description: Mapped[str] = mapped_column(
20+
Text(),
21+
default="",
22+
server_default="",
23+
)
24+
25+
movies: Mapped[list["Movie"]] = relationship(
26+
back_populates="age_rating_obj",
27+
)
28+
29+
def __str__(self) -> str:
30+
return self.name

examples/api_for_sqlalchemy/models/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
44

55

6-
class Base(DeclarativeBase):
6+
class BaseMetadata(DeclarativeBase):
77
__table_args__: ClassVar[dict[str, Any]] = {
88
"extend_existing": True,
99
}
1010

11+
12+
class Base(BaseMetadata):
13+
__abstract__ = True
14+
1115
id: Mapped[int] = mapped_column(primary_key=True)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import TYPE_CHECKING, Optional
2+
3+
from sqlalchemy import (
4+
ForeignKey,
5+
Identity,
6+
Integer,
7+
String,
8+
Text,
9+
)
10+
from sqlalchemy.orm import (
11+
Mapped,
12+
mapped_column,
13+
relationship,
14+
)
15+
16+
from examples.api_for_sqlalchemy.models.base import Base
17+
18+
if TYPE_CHECKING:
19+
from examples.api_for_sqlalchemy.models.age_rating import AgeRating
20+
21+
22+
class Movie(Base):
23+
__tablename__ = "movie"
24+
25+
id: Mapped[int] = mapped_column(
26+
Integer,
27+
Identity(always=True),
28+
primary_key=True,
29+
autoincrement=True,
30+
)
31+
title: Mapped[str] = mapped_column(
32+
String(120),
33+
index=True,
34+
)
35+
description: Mapped[str] = mapped_column(
36+
Text,
37+
default="",
38+
server_default="",
39+
)
40+
age_rating: Mapped[Optional[str]] = mapped_column(
41+
ForeignKey(
42+
"age_rating.name",
43+
ondelete="SET NULL",
44+
),
45+
)
46+
age_rating_obj: Mapped["AgeRating"] = relationship(
47+
back_populates="movies",
48+
)
49+
50+
def __str__(self) -> str:
51+
return self.title
52+
53+
def __repr__(self) -> str:
54+
return f"Movie(id={self.id}, title={self.title!r})"

examples/api_for_sqlalchemy/schemas/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
from .age_rating import (
2+
AgeRatingAttributesSchema,
3+
AgeRatingBaseSchema,
4+
AgeRatingCreateSchema,
5+
AgeRatingSchema,
6+
AgeRatingUpdateSchema,
7+
)
18
from .child import (
29
ChildAttributesSchema,
310
ChildInSchema,
@@ -10,6 +17,13 @@
1017
ComputerPatchSchema,
1118
ComputerSchema,
1219
)
20+
from .movie import (
21+
MovieAttributesSchema,
22+
MovieBaseSchema,
23+
MovieCreateSchema,
24+
MovieSchema,
25+
MovieUpdateSchema,
26+
)
1327
from .parent import (
1428
ParentAttributesSchema,
1529
ParentInSchema,
@@ -51,6 +65,11 @@
5165
)
5266

5367
__all__ = (
68+
"AgeRatingAttributesSchema",
69+
"AgeRatingBaseSchema",
70+
"AgeRatingCreateSchema",
71+
"AgeRatingSchema",
72+
"AgeRatingUpdateSchema",
5473
"ChildAttributesSchema",
5574
"ChildInSchema",
5675
"ChildPatchSchema",
@@ -60,6 +79,11 @@
6079
"ComputerPatchSchema",
6180
"ComputerSchema",
6281
"CustomUserAttributesSchema",
82+
"MovieAttributesSchema",
83+
"MovieBaseSchema",
84+
"MovieCreateSchema",
85+
"MovieSchema",
86+
"MovieUpdateSchema",
6387
"ParentAttributesSchema",
6488
"ParentInSchema",
6589
"ParentPatchSchema",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from typing import TYPE_CHECKING, Annotated, Optional
2+
3+
from annotated_types import MaxLen, MinLen
4+
from pydantic import ConfigDict
5+
6+
from fastapi_jsonapi.schema_base import BaseModel
7+
from fastapi_jsonapi.types_metadata import RelationshipInfo
8+
9+
name_constrained = Annotated[
10+
str,
11+
MinLen(1),
12+
MaxLen(20),
13+
]
14+
15+
if TYPE_CHECKING:
16+
from examples.api_for_sqlalchemy.schemas.movie import MovieSchema
17+
18+
19+
class AgeRatingAttributesSchema(BaseModel):
20+
model_config = ConfigDict(
21+
from_attributes=True,
22+
)
23+
name: str
24+
description: str
25+
26+
27+
class AgeRatingBaseSchema(AgeRatingAttributesSchema):
28+
movies: Annotated[
29+
Optional[list["MovieSchema"]],
30+
RelationshipInfo(
31+
resource_type="movie",
32+
many=True,
33+
),
34+
] = None
35+
36+
37+
class AgeRatingCreateSchema(AgeRatingBaseSchema):
38+
name: name_constrained
39+
40+
41+
class AgeRatingUpdateSchema(AgeRatingBaseSchema):
42+
name: Optional[name_constrained] = None
43+
description: Optional[str] = None
44+
45+
46+
class AgeRatingSchema(AgeRatingBaseSchema):
47+
"""
48+
Age Rating
49+
"""
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from datetime import date
2+
from typing import (
3+
TYPE_CHECKING,
4+
Annotated,
5+
Optional,
6+
)
7+
8+
from annotated_types import MaxLen, MinLen
9+
from pydantic import ConfigDict
10+
11+
from fastapi_jsonapi.schema_base import BaseModel
12+
from fastapi_jsonapi.types_metadata import RelationshipInfo
13+
14+
if TYPE_CHECKING:
15+
from examples.api_for_sqlalchemy.schemas.age_rating import AgeRatingSchema
16+
17+
title_constrained = Annotated[
18+
str,
19+
MinLen(1),
20+
MaxLen(120),
21+
]
22+
23+
24+
class MovieAttributesSchema(BaseModel):
25+
model_config = ConfigDict(
26+
from_attributes=True,
27+
)
28+
title: str
29+
description: str
30+
age_rating: Optional[str] = None
31+
32+
33+
class MovieBaseSchema(MovieAttributesSchema):
34+
age_rating_obj: Annotated[
35+
Optional["AgeRatingSchema"],
36+
RelationshipInfo(
37+
resource_type="age-rating",
38+
resource_id_example="PG-13",
39+
id_field_name="name",
40+
),
41+
] = None
42+
43+
44+
class MovieCreateSchema(MovieBaseSchema):
45+
"""
46+
Create
47+
"""
48+
49+
title: title_constrained
50+
51+
52+
class MovieUpdateSchema(MovieBaseSchema):
53+
title: Optional[title_constrained] = None
54+
description: Optional[str] = None
55+
release_date: Optional[date] = None
56+
duration: Optional[int] = None
57+
58+
59+
class MovieSchema(MovieBaseSchema):
60+
id: int

examples/api_for_sqlalchemy/urls.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
from .api.views_base import ViewBase
99
from .models import (
10+
AgeRating,
1011
Child,
1112
Computer,
13+
Movie,
1214
Parent,
1315
ParentToChildAssociation,
1416
Post,
@@ -17,12 +19,18 @@
1719
Workplace,
1820
)
1921
from .schemas import (
22+
AgeRatingCreateSchema,
23+
AgeRatingSchema,
24+
AgeRatingUpdateSchema,
2025
ChildInSchema,
2126
ChildPatchSchema,
2227
ChildSchema,
2328
ComputerInSchema,
2429
ComputerPatchSchema,
2530
ComputerSchema,
31+
MovieCreateSchema,
32+
MovieSchema,
33+
MovieUpdateSchema,
2634
ParentInSchema,
2735
ParentPatchSchema,
2836
ParentSchema,
@@ -131,6 +139,27 @@ def add_routes(app: FastAPI):
131139
schema_in_patch=WorkplacePatchSchema,
132140
schema_in_post=WorkplaceInSchema,
133141
)
142+
builder.add_resource(
143+
path="/age-ratings",
144+
tags=["Age Ratings"],
145+
resource_type="age-rating",
146+
view=ViewBase,
147+
model=AgeRating,
148+
schema=AgeRatingSchema,
149+
schema_in_post=AgeRatingCreateSchema,
150+
schema_in_patch=AgeRatingUpdateSchema,
151+
model_id_field_name="name",
152+
)
153+
builder.add_resource(
154+
path="/movies",
155+
tags=["Movie"],
156+
resource_type="movie",
157+
view=ViewBase,
158+
model=Movie,
159+
schema=MovieSchema,
160+
schema_in_post=MovieCreateSchema,
161+
schema_in_patch=MovieUpdateSchema,
162+
)
134163
builder.initialize()
135164

136165
atomic = AtomicOperations()

fastapi_jsonapi/api/schemas.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
from typing import Iterable, Optional, Type, Union
22

3-
from pydantic import BaseModel
3+
from fastapi import APIRouter
4+
from pydantic import BaseModel, ConfigDict
45

56
from fastapi_jsonapi.data_typing import TypeModel, TypeSchema
67
from fastapi_jsonapi.views import Operation, ViewBase
78

89

910
class ResourceData(BaseModel):
11+
model_config = ConfigDict(
12+
arbitrary_types_allowed=True,
13+
)
14+
1015
path: Union[str, list[str]]
16+
router: Optional[APIRouter]
1117
tags: list[str]
1218
view: Type[ViewBase]
1319
model: Type[TypeModel]

fastapi_jsonapi/data_layers/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from fastapi_jsonapi.data_typing import TypeModel, TypeSchema
1515
from fastapi_jsonapi.querystring import QueryStringManager
1616
from fastapi_jsonapi.schema import BaseJSONAPIItemInSchema
17+
from fastapi_jsonapi.storages import models_storage
1718
from fastapi_jsonapi.views import RelationshipRequestInfo
1819

1920

@@ -51,6 +52,7 @@ def __init__(
5152
self.disable_collection_count: bool = disable_collection_count
5253
self.default_collection_count: int = default_collection_count
5354
self.is_atomic = False
55+
self.id_column_name = models_storage.get_model_id_field_name(resource_type)
5456

5557
async def atomic_start(self, previous_dl: Optional["BaseDataLayer"] = None):
5658
self.is_atomic = True

0 commit comments

Comments
 (0)