22
33import logging
44from collections import defaultdict
5+ from functools import wraps
56from typing import (
67 TYPE_CHECKING ,
78 Any ,
9+ Awaitable ,
10+ Callable ,
811 Dict ,
912 List ,
1013 Optional ,
1821
1922from fastapi_jsonapi import RoutersJSONAPI
2023from fastapi_jsonapi .atomic .prepared_atomic_operation import LocalIdsType , OperationBase
21- from fastapi_jsonapi .atomic .schemas import AtomicOperationRequest , AtomicResultResponse
24+ from fastapi_jsonapi .atomic .schemas import AtomicOperation , AtomicOperationRequest , AtomicResultResponse
2225from fastapi_jsonapi .utils .dependency_helper import DependencyHelper
2326from fastapi_jsonapi .views .utils import HTTPMethodConfig
2427
2932AtomicResponseDict = TypedDict ("AtomicResponseDict" , {"atomic:results" : List [Any ]})
3033
3134
35+ def catch_exc_on_operation_handle (func : Callable [..., Awaitable ]):
36+ @wraps (func )
37+ async def wrapper (* a , operation : OperationBase , ** kw ):
38+ try :
39+ return await func (* a , operation = operation , ** kw )
40+ except (ValidationError , ValueError ) as ex :
41+ log .exception (
42+ "Validation error on atomic action ref=%s, data=%s" ,
43+ operation .ref ,
44+ operation .data ,
45+ )
46+ errors_details = {
47+ "message" : f"Validation error on operation { operation .op_type } " ,
48+ "ref" : operation .ref ,
49+ "data" : operation .data .dict (),
50+ }
51+ if isinstance (ex , ValidationError ):
52+ errors_details .update (errors = ex .errors ())
53+ elif isinstance (ex , ValueError ):
54+ errors_details .update (error = str (ex ))
55+ else :
56+ raise
57+ # TODO: json:api exception
58+ raise HTTPException (
59+ status_code = status .HTTP_422_UNPROCESSABLE_ENTITY ,
60+ detail = errors_details ,
61+ )
62+
63+ return wrapper
64+
65+
3266class AtomicViewHandler :
3367 def __init__ (
3468 self ,
@@ -37,6 +71,7 @@ def __init__(
3771 ):
3872 self .request = request
3973 self .operations_request = operations_request
74+ self .local_ids_cache : LocalIdsType = defaultdict (dict )
4075
4176 async def handle_view_dependencies (
4277 self ,
@@ -55,68 +90,64 @@ def handle_dependencies(**dep_kwargs):
5590 dependencies_result : Dict [str , Any ] = await DependencyHelper (request = self .request ).run (handle_dependencies )
5691 return dependencies_result
5792
93+ async def prepare_one_operation (self , operation : AtomicOperation ):
94+ """
95+ :param operation:
96+ :return:
97+ """
98+ operation_type = operation .ref and operation .ref .type or operation .data and operation .data .type
99+ assert operation_type
100+ if operation_type not in RoutersJSONAPI .all_jsonapi_routers :
101+ msg = f"Unknown resource type { operation_type !r} . Register it via RoutersJSONAPI"
102+ raise ValueError (msg )
103+ jsonapi = RoutersJSONAPI .all_jsonapi_routers [operation_type ]
104+
105+ dependencies_result : Dict [str , Any ] = await self .handle_view_dependencies (
106+ jsonapi = jsonapi ,
107+ )
108+ one_operation = OperationBase .prepare (
109+ action = operation .op ,
110+ request = self .request ,
111+ jsonapi = jsonapi ,
112+ ref = operation .ref ,
113+ data = operation .data ,
114+ data_layer_view_dependencies = dependencies_result ,
115+ )
116+ return one_operation
117+
58118 async def prepare_operations (self ) -> List [OperationBase ]:
59119 prepared_operations : List [OperationBase ] = []
60120
61121 for operation in self .operations_request .operations :
62- operation_type = operation .ref and operation .ref .type or operation .data and operation .data .type
63- assert operation_type
64- jsonapi = RoutersJSONAPI .all_jsonapi_routers [operation_type ]
65-
66- dependencies_result : Dict [str , Any ] = await self .handle_view_dependencies (
67- jsonapi = jsonapi ,
68- )
69- one_operation = OperationBase .prepare (
70- action = operation .op ,
71- request = self .request ,
72- jsonapi = jsonapi ,
73- ref = operation .ref ,
74- data = operation .data ,
75- data_layer_view_dependencies = dependencies_result ,
76- )
122+ one_operation = await self .prepare_one_operation (operation )
77123 prepared_operations .append (one_operation )
78124
79125 return prepared_operations
80126
127+ @catch_exc_on_operation_handle
128+ async def process_one_operation (
129+ self ,
130+ dl : BaseDataLayer ,
131+ operation : OperationBase ,
132+ ):
133+ operation .update_relationships_with_lid (local_ids = self .local_ids_cache )
134+ return await operation .handle (dl = dl )
135+
81136 async def handle (self ) -> Union [AtomicResponseDict , AtomicResultResponse , None ]:
82137 prepared_operations = await self .prepare_operations ()
83138 results = []
84-
85- # TODO: try/except, catch schema ValidationError
86-
87139 only_empty_responses = True
88- local_ids_cache : LocalIdsType = defaultdict (dict )
89140 success = True
90141 previous_dl : Optional [BaseDataLayer ] = None
91- for idx , operation in enumerate ( prepared_operations , start = 1 ) :
142+ for operation in prepared_operations :
92143 dl : BaseDataLayer = await operation .get_data_layer ()
93144 await dl .atomic_start (previous_dl = previous_dl )
145+ response = await self .process_one_operation (
146+ dl = dl ,
147+ operation = operation ,
148+ )
94149 previous_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- )
150+
120151 # response.data.id
121152 if not response :
122153 # https://jsonapi.org/ext/atomic/#result-objects
@@ -127,7 +158,7 @@ async def handle(self) -> Union[AtomicResponseDict, AtomicResultResponse, None]:
127158 only_empty_responses = False
128159 results .append ({"data" : response .data })
129160 if operation .data .lid and response .data :
130- local_ids_cache [operation .data .type ][operation .data .lid ] = response .data .id
161+ self . local_ids_cache [operation .data .type ][operation .data .lid ] = response .data .id
131162
132163 if previous_dl :
133164 await previous_dl .atomic_end (success = success )
0 commit comments