Skip to content

Commit 0cd7ef1

Browse files
committed
create atomic operations base
1 parent 430aeca commit 0cd7ef1

File tree

4 files changed

+188
-5
lines changed

4 files changed

+188
-5
lines changed

examples/api_for_sqlalchemy/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
UserBio,
2121
)
2222
from fastapi_jsonapi import RoutersJSONAPI
23+
from fastapi_jsonapi.atomic import AtomicOperations
2324

2425
from .api.views_base import DetailViewBase, ListViewBase
2526
from .models.schemas import (
@@ -134,5 +135,8 @@ def add_routes(app: FastAPI) -> List[Dict[str, Any]]:
134135
schema_in_post=ComputerInSchema,
135136
)
136137

138+
atomic = AtomicOperations()
139+
137140
app.include_router(router, prefix="")
141+
app.include_router(atomic.router, prefix="")
138142
return tags

fastapi_jsonapi/api.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@
4040

4141

4242
class RoutersJSONAPI:
43-
"""API Router interface for JSON API endpoints in web-services."""
43+
"""
44+
API Router interface for JSON API endpoints in web-services.
45+
"""
46+
47+
# xxx: store in app, not in routers!
48+
all_jsonapi_routers: Dict[str, "RoutersJSONAPI"] = {}
4449

4550
def __init__(
4651
self,
@@ -96,6 +101,11 @@ def __init__(
96101
self.model: Type[TypeModel] = model
97102
self.schema_detail = schema
98103

104+
if self._type in self.all_jsonapi_routers:
105+
msg = f"Resource type {self._type!r} already registered"
106+
raise ValueError(msg)
107+
self.all_jsonapi_routers[self._type] = self
108+
99109
self.pagination_default_size: Optional[int] = pagination_default_size
100110
self.pagination_default_number: Optional[int] = pagination_default_number
101111
self.pagination_default_offset: Optional[int] = pagination_default_offset
@@ -107,8 +117,8 @@ def __init__(
107117
schema_in_post=schema_in_post,
108118
schema_in_patch=schema_in_patch,
109119
)
110-
self._schema_in_post = dto.schema_in_post
111-
self._schema_in_patch = dto.schema_in_patch
120+
self.schema_in_post = dto.schema_in_post
121+
self.schema_in_patch = dto.schema_in_patch
112122
self.detail_response_schema = dto.detail_response_schema
113123
self.list_response_schema = dto.list_response_schema
114124

@@ -394,6 +404,30 @@ def _update_method_config_and_get_dependency_params(
394404

395405
return self._create_dependency_params_from_pydantic_model(method_config.dependencies)
396406

407+
def get_method_config_for_create(self):
408+
method_config = self._update_method_config(
409+
view=self.list_view_resource,
410+
method=HTTPMethod.POST,
411+
)
412+
return method_config
413+
414+
def prepare_dependencies_handler_signature(
415+
self,
416+
custom_handler: Callable[..., Any],
417+
method_config: HTTPMethodConfig,
418+
) -> Signature:
419+
sig = signature(custom_handler)
420+
421+
additional_dependency_params = []
422+
if method_config.dependencies is not None:
423+
additional_dependency_params = self._create_dependency_params_from_pydantic_model(
424+
model_class=method_config.dependencies,
425+
)
426+
427+
params, tail_params = self._get_separated_params(sig)
428+
429+
return sig.replace(parameters=params + list(additional_dependency_params) + tail_params)
430+
397431
def _create_get_resource_list_view(self):
398432
"""
399433
Create wrapper for GET list (get objects list)
@@ -425,7 +459,7 @@ def _create_post_resource_list_view(self):
425459
426460
:return:
427461
"""
428-
schema_in = self._schema_in_post
462+
schema_in = self.schema_in_post
429463

430464
async def wrapper(request: Request, data_create: schema_in, **extra_view_deps):
431465
resource = self.list_view_resource(
@@ -515,7 +549,7 @@ def _create_patch_resource_detail_view(self):
515549
516550
:return:
517551
"""
518-
schema_in = self._schema_in_patch
552+
schema_in = self.schema_in_patch
519553

520554
async def wrapper(
521555
request: Request,

fastapi_jsonapi/atomic/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = ("AtomicOperations",)
2+
3+
from .atomic import AtomicOperations

fastapi_jsonapi/atomic/atomic.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from dataclasses import dataclass
2+
from typing import (
3+
TYPE_CHECKING,
4+
Any,
5+
Dict,
6+
List,
7+
Literal,
8+
Optional,
9+
Type,
10+
Union,
11+
)
12+
13+
from fastapi import APIRouter, Request
14+
15+
from fastapi_jsonapi import RoutersJSONAPI
16+
from fastapi_jsonapi.atomic.schemas import (
17+
AtomicOperationRequest,
18+
AtomicResultResponse,
19+
OperationItemInSchema,
20+
OperationRelationshipSchema,
21+
)
22+
from fastapi_jsonapi.utils.dependency_helper import DependencyHelper
23+
from fastapi_jsonapi.views.utils import HTTPMethodConfig
24+
from fastapi_jsonapi.views.view_base import ViewBase
25+
26+
if TYPE_CHECKING:
27+
from fastapi_jsonapi.data_layers.base import BaseDataLayer
28+
from fastapi_jsonapi.views.list_view import ListViewBase
29+
30+
31+
@dataclass
32+
class PreparedOperation:
33+
action: Literal["add", "update", "remove"]
34+
data_layer: "BaseDataLayer"
35+
view: "ViewBase"
36+
jsonapi: RoutersJSONAPI
37+
data: Union[
38+
# from biggest to smallest!
39+
# any object creation
40+
OperationItemInSchema,
41+
# to-many relationship
42+
List[OperationRelationshipSchema],
43+
# to-one relationship
44+
OperationRelationshipSchema,
45+
# not required
46+
None,
47+
] = None
48+
49+
50+
class AtomicOperations:
51+
def __init__(
52+
self,
53+
url_path: str = "/operations",
54+
router: Optional[APIRouter] = None,
55+
):
56+
self.router = router or APIRouter(tags=["Atomic Operations"])
57+
self.url_path = url_path
58+
self._register_view()
59+
60+
async def handle_view_dependencies(
61+
self,
62+
request: Request,
63+
jsonapi: RoutersJSONAPI,
64+
) -> Dict[str, Any]:
65+
method_config: HTTPMethodConfig = jsonapi.get_method_config_for_create()
66+
67+
def handle_dependencies(**dep_kwargs):
68+
return dep_kwargs
69+
70+
handle_dependencies.__signature__ = jsonapi.prepare_dependencies_handler_signature(
71+
custom_handler=handle_dependencies,
72+
method_config=method_config,
73+
)
74+
75+
dependencies_result: Dict[str, Any] = await DependencyHelper(request=request).run(handle_dependencies)
76+
return dependencies_result
77+
78+
async def view_atomic(
79+
self,
80+
request: Request,
81+
operations_request: AtomicOperationRequest,
82+
):
83+
prepared_operations: List[PreparedOperation] = []
84+
85+
for operation in operations_request.operations:
86+
jsonapi = RoutersJSONAPI.all_jsonapi_routers[operation.data.type]
87+
view_cls: Type["ViewBase"] = jsonapi.detail_view_resource
88+
if operation.op == "add":
89+
view_cls = jsonapi.list_view_resource
90+
view = view_cls(request=request, jsonapi=jsonapi)
91+
dependencies_result: Dict[str, Any] = await self.handle_view_dependencies(
92+
request=request,
93+
jsonapi=jsonapi,
94+
)
95+
dl: "BaseDataLayer" = await view.get_data_layer(dependencies_result)
96+
97+
one_operation = PreparedOperation(
98+
action=operation.op,
99+
data_layer=dl,
100+
view=view,
101+
jsonapi=jsonapi,
102+
data=operation.data,
103+
)
104+
prepared_operations.append(one_operation)
105+
106+
results = []
107+
108+
for operation in prepared_operations:
109+
dl = operation.data_layer
110+
if operation.action == "add":
111+
data = operation.jsonapi.schema_in_post(data=operation.data)
112+
created_object = await dl.create_object(
113+
data_create=data.data,
114+
view_kwargs={},
115+
)
116+
# assert isinstance(operation.view, ListViewBase)
117+
view: "ListViewBase" = operation.view
118+
response = await view.response_for_created_object(
119+
dl=operation.data_layer,
120+
created_object=created_object,
121+
)
122+
results.append({"data": response.data})
123+
elif operation.action == "update":
124+
# TODO
125+
data = operation.jsonapi.schema_in_patch(data=operation.data)
126+
elif operation.action == "remove":
127+
pass
128+
else:
129+
msg = f"unknown action {operation.action!r}"
130+
raise ValueError(msg)
131+
132+
return {"atomic:results": results}
133+
134+
def _register_view(self):
135+
self.router.add_api_route(
136+
path=self.url_path,
137+
endpoint=self.view_atomic,
138+
response_model=AtomicResultResponse,
139+
methods=["Post"],
140+
summary="Atomic operations",
141+
description="""[https://jsonapi.org/ext/atomic/](https://jsonapi.org/ext/atomic/)""",
142+
)

0 commit comments

Comments
 (0)