Skip to content

Commit 1e1fa3b

Browse files
committed
atomic operations fixes, coverage for many-to-many
1 parent 13af972 commit 1e1fa3b

File tree

8 files changed

+261
-61
lines changed

8 files changed

+261
-61
lines changed

examples/api_for_sqlalchemy/models/schemas/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class Enum:
4545
),
4646
)
4747

48-
computers: Optional["ComputerSchema"] = Field(
48+
computers: Optional[List["ComputerSchema"]] = Field(
4949
relationship=RelationshipInfo(
5050
resource_type="computer",
5151
many=True,

fastapi_jsonapi/atomic/atomic_handler.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
from collections import defaultdict
45
from typing import (
56
TYPE_CHECKING,
@@ -11,6 +12,8 @@
1112
Union,
1213
)
1314

15+
from fastapi import HTTPException, status
16+
from pydantic import ValidationError
1417
from starlette.requests import Request
1518

1619
from fastapi_jsonapi import RoutersJSONAPI
@@ -22,6 +25,7 @@
2225
if TYPE_CHECKING:
2326
from fastapi_jsonapi.data_layers.base import BaseDataLayer
2427

28+
log = logging.getLogger(__name__)
2529
AtomicResponseDict = TypedDict("AtomicResponseDict", {"atomic:results": List[Any]})
2630

2731

@@ -84,12 +88,35 @@ async def handle(self) -> Union[AtomicResponseDict, AtomicResultResponse, None]:
8488
local_ids_cache: LocalIdsType = defaultdict(dict)
8589
success = True
8690
previous_dl: Optional[BaseDataLayer] = None
87-
for operation in prepared_operations:
91+
for idx, operation in enumerate(prepared_operations, start=1):
8892
dl: BaseDataLayer = await operation.get_data_layer()
8993
await dl.atomic_start(previous_dl=previous_dl)
9094
previous_dl = dl
91-
operation.update_relationships_with_lid(local_ids=local_ids_cache)
92-
response = await operation.handle(dl=dl)
95+
try:
96+
operation.update_relationships_with_lid(local_ids=local_ids_cache)
97+
response = await operation.handle(dl=dl)
98+
except (ValidationError, ValueError) as ex:
99+
log.exception(
100+
"Validation error on atomic action ref=%s, data=%s",
101+
operation.ref,
102+
operation.data,
103+
)
104+
errors_details = {
105+
"message": f"Validation error on operation #{idx}",
106+
"ref": operation.ref,
107+
"data": operation.data.dict(),
108+
}
109+
if isinstance(ex, ValidationError):
110+
errors_details.update(errors=ex.errors())
111+
elif isinstance(ex, ValueError):
112+
errors_details.update(error=str(ex))
113+
else:
114+
raise
115+
# TODO: json:api exception
116+
raise HTTPException(
117+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
118+
detail=errors_details,
119+
)
93120
# response.data.id
94121
if not response:
95122
# https://jsonapi.org/ext/atomic/#result-objects

fastapi_jsonapi/atomic/prepared_atomic_operation.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,15 @@ def upd_one_relationship_with_local_id(cls, relationship_info: dict, local_ids:
8181
resource_type = relationship_info["type"]
8282
if resource_type not in local_ids:
8383
msg = (
84-
f"Resource {resource_type} not found in previous operations,"
85-
f" no lid {lid} defined yet, cannot create {relationship_info}"
86-
)
87-
raise ValueError(
88-
msg,
84+
f"Resource {resource_type!r} not found in previous operations,"
85+
f" no lid {lid!r} defined yet, cannot create {relationship_info}"
8986
)
87+
raise ValueError(msg)
9088

9189
lids_for_resource = local_ids[resource_type]
9290
if lid not in lids_for_resource:
9391
msg = (
94-
f"lid {lid} for {resource_type} not found in previous operations,"
92+
f"lid {lid!r} for {resource_type!r} not found in previous operations,"
9593
f" cannot process {relationship_info}"
9694
)
9795
raise ValueError(msg)

fastapi_jsonapi/schema_builder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ def _get_info_from_schema_for_building(
252252
field=field,
253253
relationship_info=relationship,
254254
)
255-
relationship_field = ... if (non_optional_relationships and field.required) else None
255+
# TODO: xxx
256+
# is there a way to read that the field type is Optional? (r.n. it's ForwardRef)
257+
# consider field is not required until is marked required explicitly (`default=...` means required)
258+
field_marked_required = field.required is True
259+
relationship_field = ... if (non_optional_relationships and field_marked_required) else None
256260
if relationship_field is not None:
257261
has_required_relationship = True
258262
relationships_schema_fields[name] = (relationship_schema, relationship_field)

tests/fixtures/app.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Child,
1515
Computer,
1616
Parent,
17+
ParentToChildAssociation,
1718
Post,
1819
PostComment,
1920
User,
@@ -28,6 +29,7 @@
2829
ComputerSchema,
2930
ParentPatchSchema,
3031
ParentSchema,
32+
ParentToChildAssociationSchema,
3133
PostCommentSchema,
3234
PostInSchema,
3335
PostPatchSchema,
@@ -133,6 +135,17 @@ def add_routers(app_plain: FastAPI):
133135
model=Child,
134136
)
135137

138+
RoutersJSONAPI(
139+
router=router,
140+
path="/parent-to-child-association",
141+
tags=["Parent To Child Association"],
142+
class_detail=DetailViewBaseGeneric,
143+
class_list=ListViewBaseGeneric,
144+
schema=ParentToChildAssociationSchema,
145+
resource_type="parent-to-child-association",
146+
model=ParentToChildAssociation,
147+
)
148+
136149
RoutersJSONAPI(
137150
router=router,
138151
path="/computers",

tests/fixtures/db_connection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ async def async_session_dependency():
2828

2929
@async_fixture(scope="class")
3030
async def async_engine():
31-
engine = create_async_engine(url=make_url(sqla_uri()))
31+
engine = create_async_engine(
32+
url=make_url(sqla_uri()),
33+
echo=False,
34+
# echo=True,
35+
)
3236
async with engine.begin() as conn:
3337
await conn.run_sync(Base.metadata.drop_all)
3438
await conn.run_sync(Base.metadata.create_all)

tests/schemas.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,14 @@ class PostCommentSchema(PostCommentBaseSchema):
176176
# Association Schemas ⬇️
177177

178178

179-
class ParentToChildAssociationSchema(BaseModel):
180-
id: int
179+
class ParentToChildAssociationAttributesSchema(BaseModel):
181180
extra_data: str
181+
182+
class Config:
183+
orm_mode = True
184+
185+
186+
class ParentToChildAssociationSchema(ParentToChildAssociationAttributesSchema):
182187
parent: "ParentSchema" = Field(
183188
relationship=RelationshipInfo(
184189
resource_type="parent",
@@ -195,17 +200,20 @@ class ParentToChildAssociationSchema(BaseModel):
195200
# Parent Schemas ⬇️
196201

197202

198-
class ParentBaseSchema(BaseModel):
199-
"""Parent base schema."""
203+
class ParentAttributesSchema(BaseModel):
204+
name: str
200205

201206
class Config:
202207
"""Pydantic schema config."""
203208

204209
orm_mode = True
205210

206-
name: str
211+
212+
class ParentBaseSchema(ParentAttributesSchema):
213+
"""Parent base schema."""
207214

208215
children: List["ParentToChildAssociationSchema"] = Field(
216+
default=None,
209217
relationship=RelationshipInfo(
210218
resource_type="parent_child_association",
211219
many=True,
@@ -230,17 +238,20 @@ class ParentSchema(ParentInSchema):
230238
# Child Schemas ⬇️
231239

232240

233-
class ChildBaseSchema(BaseModel):
234-
"""Child base schema."""
241+
class ChildAttributesSchema(BaseModel):
242+
name: str
235243

236244
class Config:
237245
"""Pydantic schema config."""
238246

239247
orm_mode = True
240248

241-
name: str
249+
250+
class ChildBaseSchema(ChildAttributesSchema):
251+
"""Child base schema."""
242252

243253
parents: List["ParentToChildAssociationSchema"] = Field(
254+
default=None,
244255
relationship=RelationshipInfo(
245256
resource_type="parent_child_association",
246257
many=True,

0 commit comments

Comments
 (0)