ul-api-utils 9.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- example/__init__.py +0 -0
- example/conf.py +35 -0
- example/main.py +24 -0
- example/models/__init__.py +0 -0
- example/permissions.py +6 -0
- example/pure_flask_example.py +65 -0
- example/rate_limit_load.py +10 -0
- example/redis_repository.py +22 -0
- example/routes/__init__.py +0 -0
- example/routes/api_some.py +335 -0
- example/sockets/__init__.py +0 -0
- example/sockets/on_connect.py +16 -0
- example/sockets/on_disconnect.py +14 -0
- example/sockets/on_json.py +10 -0
- example/sockets/on_message.py +13 -0
- example/sockets/on_open.py +16 -0
- example/workers/__init__.py +0 -0
- example/workers/worker.py +28 -0
- ul_api_utils/__init__.py +0 -0
- ul_api_utils/access/__init__.py +122 -0
- ul_api_utils/api_resource/__init__.py +0 -0
- ul_api_utils/api_resource/api_request.py +105 -0
- ul_api_utils/api_resource/api_resource.py +414 -0
- ul_api_utils/api_resource/api_resource_config.py +20 -0
- ul_api_utils/api_resource/api_resource_error_handling.py +21 -0
- ul_api_utils/api_resource/api_resource_fn_typing.py +356 -0
- ul_api_utils/api_resource/api_resource_type.py +16 -0
- ul_api_utils/api_resource/api_response.py +300 -0
- ul_api_utils/api_resource/api_response_db.py +26 -0
- ul_api_utils/api_resource/api_response_payload_alias.py +25 -0
- ul_api_utils/api_resource/db_types.py +9 -0
- ul_api_utils/api_resource/signature_check.py +41 -0
- ul_api_utils/commands/__init__.py +0 -0
- ul_api_utils/commands/cmd_enc_keys.py +172 -0
- ul_api_utils/commands/cmd_gen_api_user_token.py +77 -0
- ul_api_utils/commands/cmd_gen_new_api_user.py +106 -0
- ul_api_utils/commands/cmd_generate_api_docs.py +181 -0
- ul_api_utils/commands/cmd_start.py +110 -0
- ul_api_utils/commands/cmd_worker_start.py +76 -0
- ul_api_utils/commands/start/__init__.py +0 -0
- ul_api_utils/commands/start/gunicorn.conf.local.py +0 -0
- ul_api_utils/commands/start/gunicorn.conf.py +26 -0
- ul_api_utils/commands/start/wsgi.py +22 -0
- ul_api_utils/conf/ul-debugger-main.js +1 -0
- ul_api_utils/conf/ul-debugger-ui.js +1 -0
- ul_api_utils/conf.py +70 -0
- ul_api_utils/const.py +78 -0
- ul_api_utils/debug/__init__.py +0 -0
- ul_api_utils/debug/debugger.py +119 -0
- ul_api_utils/debug/malloc.py +93 -0
- ul_api_utils/debug/stat.py +444 -0
- ul_api_utils/encrypt/__init__.py +0 -0
- ul_api_utils/encrypt/encrypt_decrypt_abstract.py +15 -0
- ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +59 -0
- ul_api_utils/errors.py +200 -0
- ul_api_utils/internal_api/__init__.py +0 -0
- ul_api_utils/internal_api/__tests__/__init__.py +0 -0
- ul_api_utils/internal_api/__tests__/internal_api.py +29 -0
- ul_api_utils/internal_api/__tests__/internal_api_content_type.py +22 -0
- ul_api_utils/internal_api/internal_api.py +369 -0
- ul_api_utils/internal_api/internal_api_check_context.py +42 -0
- ul_api_utils/internal_api/internal_api_error.py +17 -0
- ul_api_utils/internal_api/internal_api_response.py +296 -0
- ul_api_utils/main.py +29 -0
- ul_api_utils/modules/__init__.py +0 -0
- ul_api_utils/modules/__tests__/__init__.py +0 -0
- ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +195 -0
- ul_api_utils/modules/api_sdk.py +555 -0
- ul_api_utils/modules/api_sdk_config.py +63 -0
- ul_api_utils/modules/api_sdk_jwt.py +377 -0
- ul_api_utils/modules/intermediate_state.py +34 -0
- ul_api_utils/modules/worker_context.py +35 -0
- ul_api_utils/modules/worker_sdk.py +109 -0
- ul_api_utils/modules/worker_sdk_config.py +13 -0
- ul_api_utils/py.typed +0 -0
- ul_api_utils/resources/__init__.py +0 -0
- ul_api_utils/resources/caching.py +196 -0
- ul_api_utils/resources/debugger_scripts.py +97 -0
- ul_api_utils/resources/health_check/__init__.py +0 -0
- ul_api_utils/resources/health_check/const.py +2 -0
- ul_api_utils/resources/health_check/health_check.py +439 -0
- ul_api_utils/resources/health_check/health_check_template.py +64 -0
- ul_api_utils/resources/health_check/resource.py +97 -0
- ul_api_utils/resources/not_implemented.py +25 -0
- ul_api_utils/resources/permissions.py +29 -0
- ul_api_utils/resources/rate_limitter.py +84 -0
- ul_api_utils/resources/socketio.py +55 -0
- ul_api_utils/resources/swagger.py +119 -0
- ul_api_utils/resources/web_forms/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
- ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
- ul_api_utils/resources/web_forms/uni_form.py +75 -0
- ul_api_utils/sentry.py +52 -0
- ul_api_utils/utils/__init__.py +0 -0
- ul_api_utils/utils/__tests__/__init__.py +0 -0
- ul_api_utils/utils/__tests__/api_path_version.py +16 -0
- ul_api_utils/utils/__tests__/unwrap_typing.py +67 -0
- ul_api_utils/utils/api_encoding.py +51 -0
- ul_api_utils/utils/api_format.py +61 -0
- ul_api_utils/utils/api_method.py +55 -0
- ul_api_utils/utils/api_pagination.py +58 -0
- ul_api_utils/utils/api_path_version.py +60 -0
- ul_api_utils/utils/api_request_info.py +6 -0
- ul_api_utils/utils/avro.py +131 -0
- ul_api_utils/utils/broker_topics_message_count.py +47 -0
- ul_api_utils/utils/cached_per_request.py +23 -0
- ul_api_utils/utils/colors.py +31 -0
- ul_api_utils/utils/constants.py +7 -0
- ul_api_utils/utils/decode_base64.py +9 -0
- ul_api_utils/utils/deprecated.py +19 -0
- ul_api_utils/utils/flags.py +29 -0
- ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/conf.py +4 -0
- ul_api_utils/utils/flask_swagger_generator/exceptions.py +7 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +57 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +48 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +777 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +40 -0
- ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +77 -0
- ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +51 -0
- ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +18 -0
- ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +52 -0
- ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +15 -0
- ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +39 -0
- ul_api_utils/utils/imports.py +16 -0
- ul_api_utils/utils/instance_checks.py +16 -0
- ul_api_utils/utils/jinja/__init__.py +0 -0
- ul_api_utils/utils/jinja/t_url_for.py +19 -0
- ul_api_utils/utils/jinja/to_pretty_json.py +11 -0
- ul_api_utils/utils/json_encoder.py +126 -0
- ul_api_utils/utils/load_modules.py +15 -0
- ul_api_utils/utils/memory_db/__init__.py +0 -0
- ul_api_utils/utils/memory_db/__tests__/__init__.py +0 -0
- ul_api_utils/utils/memory_db/errors.py +8 -0
- ul_api_utils/utils/memory_db/repository.py +102 -0
- ul_api_utils/utils/token_check.py +14 -0
- ul_api_utils/utils/token_check_through_request.py +16 -0
- ul_api_utils/utils/unwrap_typing.py +117 -0
- ul_api_utils/utils/uuid_converter.py +22 -0
- ul_api_utils/validators/__init__.py +0 -0
- ul_api_utils/validators/__tests__/__init__.py +0 -0
- ul_api_utils/validators/__tests__/test_custom_fields.py +32 -0
- ul_api_utils/validators/custom_fields.py +66 -0
- ul_api_utils/validators/validate_empty_object.py +10 -0
- ul_api_utils/validators/validate_uuid.py +11 -0
- ul_api_utils-9.3.0.dist-info/LICENSE +21 -0
- ul_api_utils-9.3.0.dist-info/METADATA +279 -0
- ul_api_utils-9.3.0.dist-info/RECORD +156 -0
- ul_api_utils-9.3.0.dist-info/WHEEL +5 -0
- ul_api_utils-9.3.0.dist-info/entry_points.txt +2 -0
- ul_api_utils-9.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import NamedTuple, Any, Callable, Optional, List, Dict, Type, Tuple, TYPE_CHECKING, Union, get_origin, get_args
|
|
3
|
+
|
|
4
|
+
from flask import request
|
|
5
|
+
from pydantic import BaseModel, ValidationError, validate_call, TypeAdapter, RootModel
|
|
6
|
+
from pydantic.v1.utils import deep_update
|
|
7
|
+
from pydantic_core import ErrorDetails
|
|
8
|
+
|
|
9
|
+
from ul_api_utils.api_resource.api_request import ApiRequestQuery
|
|
10
|
+
from ul_api_utils.api_resource.api_resource_type import ApiResourceType
|
|
11
|
+
from ul_api_utils.api_resource.api_response import HtmlApiResponse, JsonApiResponse, FileApiResponse, \
|
|
12
|
+
RedirectApiResponse, ApiResponse, \
|
|
13
|
+
JsonApiResponsePayload, RootJsonApiResponsePayload, ProxyJsonApiResponse, AnyJsonApiResponse, \
|
|
14
|
+
EmptyJsonApiResponse, TPayloadTotalUnion, RootJsonApiResponse
|
|
15
|
+
from ul_api_utils.api_resource.signature_check import get_typing, set_model_dictable, set_model
|
|
16
|
+
from ul_api_utils.errors import ValidationListApiError, ResourceRuntimeApiError, InvalidContentTypeError
|
|
17
|
+
from ul_api_utils.utils.api_format import ApiFormat
|
|
18
|
+
from ul_api_utils.utils.api_method import ApiMethod
|
|
19
|
+
from ul_api_utils.utils.json_encoder import to_dict
|
|
20
|
+
from ul_api_utils.utils.unwrap_typing import UnwrappedOptionalObjOrListOfObj
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ul_api_utils.api_resource.api_resource import ApiResource
|
|
24
|
+
|
|
25
|
+
FN_SYSTEM_PROPS = {"api_resource", "query", "body", "return", "body_validation_error", "query_validation_error"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_complex_type(annotation: Any) -> bool:
|
|
29
|
+
origin = get_origin(annotation)
|
|
30
|
+
|
|
31
|
+
# Optional[type_] is typing.Union
|
|
32
|
+
if origin is Union:
|
|
33
|
+
return any(_is_complex_type(arg) for arg in get_args(annotation))
|
|
34
|
+
|
|
35
|
+
return origin in (list, dict, tuple, set)
|
|
36
|
+
|
|
37
|
+
def _patch_errors(dest_errors: List[Dict[str, str]], errors: List[ErrorDetails], kind: str) -> List[Dict[str, str]]:
|
|
38
|
+
for error in errors:
|
|
39
|
+
dest_errors.append({
|
|
40
|
+
"error_type": kind,
|
|
41
|
+
"error_message": error['msg'],
|
|
42
|
+
"error_location": error["loc"], # type: ignore
|
|
43
|
+
"error_kind": error["type"],
|
|
44
|
+
"error_input": error["input"],
|
|
45
|
+
})
|
|
46
|
+
return dest_errors
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@validate_call()
|
|
50
|
+
def _body_list(root: List[Dict[str, Any]]) -> List[Any]: # only for pydantic validation
|
|
51
|
+
return root
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ApiResourceFnTyping(NamedTuple):
|
|
55
|
+
fn: Callable[..., ApiResponse]
|
|
56
|
+
|
|
57
|
+
request_body_many: bool
|
|
58
|
+
request_body_optional: bool
|
|
59
|
+
response_payload_many: bool
|
|
60
|
+
|
|
61
|
+
api_resource_type: ApiResourceType
|
|
62
|
+
signatures_typing: List[Tuple[str, Type[Any]]]
|
|
63
|
+
|
|
64
|
+
body_typing: Optional[Type[BaseModel]] # none if it is not specified
|
|
65
|
+
has_body_validation_error: bool
|
|
66
|
+
query_typing: Optional[Type[ApiRequestQuery]] # none if it is not specified
|
|
67
|
+
has_query_validation_error: bool
|
|
68
|
+
return_typing: Optional[Type[ApiResponse]] # NOT NONE for api
|
|
69
|
+
return_payload_typing: Optional[Type[JsonApiResponsePayload] | Type[RootJsonApiResponsePayload[Any]]] # NOT NONE for api
|
|
70
|
+
|
|
71
|
+
def get_return_schema(self) -> Type[BaseModel]:
|
|
72
|
+
inner_type = self.return_payload_typing
|
|
73
|
+
if self.response_payload_many:
|
|
74
|
+
inner_type = List[inner_type] # type: ignore
|
|
75
|
+
return self.return_typing._internal_use__mk_schema(inner_type) # type: ignore
|
|
76
|
+
|
|
77
|
+
def get_body_schema(self) -> Optional[Type[BaseModel]]:
|
|
78
|
+
if self.body_typing is None:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
body_typing = self.body_typing
|
|
82
|
+
|
|
83
|
+
if self.request_body_optional:
|
|
84
|
+
if self.request_body_many:
|
|
85
|
+
class BodyTypingOptList(RootModel[List[body_typing] | None]): # type: ignore
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return BodyTypingOptList
|
|
89
|
+
|
|
90
|
+
class BodyTypingOpt(RootModel[Optional[body_typing]]): # type: ignore
|
|
91
|
+
pass
|
|
92
|
+
return BodyTypingOpt
|
|
93
|
+
|
|
94
|
+
if self.request_body_many:
|
|
95
|
+
class BodyTypingList(RootModel[List[body_typing]]): # type: ignore
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return BodyTypingList
|
|
99
|
+
return body_typing
|
|
100
|
+
|
|
101
|
+
def runtime_validate_api_proxy_payload(self, response: Dict[str, Any], *, quick: bool) -> None:
|
|
102
|
+
assert isinstance(response, dict)
|
|
103
|
+
|
|
104
|
+
def runtime_validate_api_response_payload(self, payload: Any, total_count: Optional[int], *, quick: bool) -> TPayloadTotalUnion: # type: ignore
|
|
105
|
+
quick = quick or self.return_payload_typing is None
|
|
106
|
+
|
|
107
|
+
if self.return_typing == AnyJsonApiResponse:
|
|
108
|
+
return payload, total_count # type: ignore
|
|
109
|
+
|
|
110
|
+
if self.response_payload_many:
|
|
111
|
+
if not (isinstance(total_count, int) and total_count >= 0):
|
|
112
|
+
raise ResourceRuntimeApiError(f'total_count must be int >= 0. {type(total_count).__name__} was given. Error value: {total_count}')
|
|
113
|
+
new_payload = []
|
|
114
|
+
for o in payload:
|
|
115
|
+
r = to_dict(o) if quick else set_model_dictable(self.return_payload_typing, o) # type: ignore
|
|
116
|
+
if r is None:
|
|
117
|
+
raise ResourceRuntimeApiError(f'invalid type of object. {type(o).__name__} was given')
|
|
118
|
+
new_payload.append(r)
|
|
119
|
+
return new_payload, total_count
|
|
120
|
+
|
|
121
|
+
if payload is None: # only for case when payload must be single object
|
|
122
|
+
return None, None
|
|
123
|
+
|
|
124
|
+
new_payload = to_dict(payload) if quick else set_model_dictable(self.return_payload_typing, payload) # type: ignore
|
|
125
|
+
if new_payload is None:
|
|
126
|
+
raise ResourceRuntimeApiError(f'invalid type of object. {type(payload).__name__} was given')
|
|
127
|
+
return new_payload, None
|
|
128
|
+
|
|
129
|
+
def _get_body(self) -> Optional[Any]:
|
|
130
|
+
if self.api_resource_type == ApiResourceType.WEB:
|
|
131
|
+
res_dict: Dict[str, Any] = {}
|
|
132
|
+
for key, value in request.form.to_dict(flat=False).items():
|
|
133
|
+
if '[]' in key:
|
|
134
|
+
key = key.replace('[]', '')
|
|
135
|
+
res_dict[key] = value
|
|
136
|
+
elif '[' in key:
|
|
137
|
+
key, *dict_keys = key.split('[')
|
|
138
|
+
dict_keys = [dict_key[:-1] for dict_key in dict_keys]
|
|
139
|
+
tree_dict: Dict[str, Any] = {}
|
|
140
|
+
for iter, dict_key in enumerate(reversed(dict_keys)):
|
|
141
|
+
if iter == 0:
|
|
142
|
+
tree_dict = {dict_key: value[0]}
|
|
143
|
+
else:
|
|
144
|
+
tree_dict = {dict_key: tree_dict}
|
|
145
|
+
res_dict.setdefault(key, dict())
|
|
146
|
+
res_dict[key] = deep_update(res_dict[key], tree_dict)
|
|
147
|
+
else:
|
|
148
|
+
res_dict[key] = value[0]
|
|
149
|
+
return res_dict
|
|
150
|
+
if self.api_resource_type not in (ApiResourceType.API, ApiResourceType.FILE):
|
|
151
|
+
return None
|
|
152
|
+
if not request.is_json:
|
|
153
|
+
body_data: Optional[bytes] = request.get_data()
|
|
154
|
+
api_format = ApiFormat.from_mime(request.mimetype)
|
|
155
|
+
if api_format is None:
|
|
156
|
+
raise InvalidContentTypeError(f"Failed to decode JSON object: invalid content type '{request.mimetype}'")
|
|
157
|
+
return api_format.parse_bytes(body_data) if body_data else None
|
|
158
|
+
else:
|
|
159
|
+
return request.json # TODO: make this from ApiFormat
|
|
160
|
+
|
|
161
|
+
def _runtime_validate_body(self, method: ApiMethod, kwargs: Dict[str, Any], errors: List[Dict[str, str]]) -> Tuple[Dict[str, Any], List[Dict[str, str]]]:
|
|
162
|
+
if self.request_body_optional:
|
|
163
|
+
kwargs['body'] = None
|
|
164
|
+
if method in {ApiMethod.GET, ApiMethod.OPTIONS} or self.body_typing is None:
|
|
165
|
+
return kwargs, errors
|
|
166
|
+
body = self._get_body()
|
|
167
|
+
try:
|
|
168
|
+
expected_typing: Type[BaseModel | List[BaseModel]] = List[self.body_typing] if self.request_body_many else self.body_typing # type: ignore
|
|
169
|
+
kwargs['body'] = TypeAdapter(expected_typing).validate_python(body)
|
|
170
|
+
except ValidationError as ve:
|
|
171
|
+
if self.has_body_validation_error:
|
|
172
|
+
kwargs['body_validation_error'] = ve
|
|
173
|
+
else:
|
|
174
|
+
_patch_errors(errors, ve.errors(), "body-validation-error")
|
|
175
|
+
return kwargs, errors
|
|
176
|
+
|
|
177
|
+
def _runtime_validate_query(self, kwargs: Dict[str, Any], errors: List[Dict[str, str]]) -> Tuple[Dict[str, Any], List[Dict[str, str]]]:
|
|
178
|
+
if self.query_typing is None:
|
|
179
|
+
return kwargs, errors
|
|
180
|
+
try:
|
|
181
|
+
kwargs["query"] = set_model(self.query_typing, {
|
|
182
|
+
**request.args.to_dict(),
|
|
183
|
+
**{
|
|
184
|
+
key: value for key, value in request.args.to_dict(flat=False).items()
|
|
185
|
+
if key in self.query_typing.model_fields
|
|
186
|
+
and _is_complex_type(self.query_typing.model_fields[key].annotation)
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
except ValidationError as ve:
|
|
190
|
+
if self.has_query_validation_error:
|
|
191
|
+
kwargs['query_validation_error'] = ve
|
|
192
|
+
else:
|
|
193
|
+
_patch_errors(errors, ve.errors(), "query-validation-error")
|
|
194
|
+
return kwargs, errors
|
|
195
|
+
|
|
196
|
+
def _runtime_validate_signature(self, kwargs: Dict[str, Any], errors: List[Dict[str, str]]) -> Tuple[Dict[str, Any], List[Dict[str, str]]]:
|
|
197
|
+
loc_err = []
|
|
198
|
+
for name, type_ in self.signatures_typing:
|
|
199
|
+
try:
|
|
200
|
+
kwargs[name] = TypeAdapter(type_).validate_python(kwargs.get(name))
|
|
201
|
+
except ValidationError as e:
|
|
202
|
+
err = e.errors()[0]
|
|
203
|
+
err["loc"] = [name] # type: ignore
|
|
204
|
+
loc_err.append(err)
|
|
205
|
+
_patch_errors(errors, loc_err, "path-params-validation-error")
|
|
206
|
+
return kwargs, errors
|
|
207
|
+
|
|
208
|
+
def runtime_validate_request_input(self, method: ApiMethod, fn_kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
209
|
+
errors: List[Dict[str, str]] = []
|
|
210
|
+
fn_kwargs, errors = self._runtime_validate_signature(fn_kwargs, errors)
|
|
211
|
+
fn_kwargs, errors = self._runtime_validate_query(fn_kwargs, errors)
|
|
212
|
+
fn_kwargs, errors = self._runtime_validate_body(method, fn_kwargs, errors)
|
|
213
|
+
if len(errors) > 0:
|
|
214
|
+
raise ValidationListApiError(errors)
|
|
215
|
+
return fn_kwargs
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def _parse_body_typing(cls, fn: Callable[['ApiResource'], ApiResponse]) -> Tuple[bool, bool, bool, Optional[Type[BaseModel]]]:
|
|
219
|
+
request_many = False
|
|
220
|
+
body_typing = fn.__annotations__.get('body', None)
|
|
221
|
+
body_validation_error_typing = fn.__annotations__.get('body_validation_error', None)
|
|
222
|
+
body_is_optional = False
|
|
223
|
+
|
|
224
|
+
if body_typing is not None:
|
|
225
|
+
body_typing_parsed = UnwrappedOptionalObjOrListOfObj.parse(body_typing, BaseModel)
|
|
226
|
+
tn = BaseModel.__name__
|
|
227
|
+
assert body_typing_parsed is not None, \
|
|
228
|
+
f'body_typing is invalid. must be Union[Type[{tn}], Optional[Type[{tn}]], List[Type[{tn}]], Optional[List[Type[{tn}]]]], {body_typing} was given'
|
|
229
|
+
body_is_optional = body_typing_parsed.optional
|
|
230
|
+
request_many = body_typing_parsed.many
|
|
231
|
+
body_typing = body_typing_parsed.value_type
|
|
232
|
+
|
|
233
|
+
if body_validation_error_typing:
|
|
234
|
+
assert body_typing is not None
|
|
235
|
+
assert body_is_optional, 'query typing must be optional'
|
|
236
|
+
body_validation_error_typing_parsed = UnwrappedOptionalObjOrListOfObj.parse(body_validation_error_typing, ValidationError)
|
|
237
|
+
assert body_validation_error_typing_parsed is not None and body_validation_error_typing_parsed.optional and not body_validation_error_typing_parsed.many, \
|
|
238
|
+
f'query_typing is invalid. must be Optional[ValidationError], {body_validation_error_typing} was given'
|
|
239
|
+
return request_many, body_is_optional, body_validation_error_typing is not None, body_typing
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def _parse_return_typing(
|
|
243
|
+
cls,
|
|
244
|
+
api_resource_type: 'ApiResourceType',
|
|
245
|
+
fn: Callable[['ApiResource'], ApiResponse],
|
|
246
|
+
) -> Tuple[bool, Optional[Type[JsonApiResponsePayload] | Type[RootJsonApiResponsePayload[Any]]], Optional[Type[ApiResponse]]]:
|
|
247
|
+
response_many = False
|
|
248
|
+
return_typing = fn.__annotations__.get('return', None)
|
|
249
|
+
ret = get_typing(return_typing)
|
|
250
|
+
if len(ret) > 1:
|
|
251
|
+
assert len(ret) == 2, f'invalid generic arguments. must be 1. {len(ret) - 1} was given'
|
|
252
|
+
return_typing, return_payload_typing = ret
|
|
253
|
+
else:
|
|
254
|
+
return_typing, return_payload_typing = ret[0], None
|
|
255
|
+
|
|
256
|
+
assert inspect.isclass(return_typing), f'{fn.__name__} :: invalid response typing. {return_typing} was given'
|
|
257
|
+
|
|
258
|
+
if return_typing != RedirectApiResponse:
|
|
259
|
+
if api_resource_type == ApiResourceType.API:
|
|
260
|
+
if return_payload_typing is not None:
|
|
261
|
+
ret_payload_res = get_typing(return_payload_typing)
|
|
262
|
+
if len(ret_payload_res) > 1:
|
|
263
|
+
return_payload_typing = ret_payload_res[1]
|
|
264
|
+
if ret_payload_res[0] != list: # noqa: E721
|
|
265
|
+
raise TypeError(f'{fn.__name__} :: invalid response payload type wrapper. only List is supported')
|
|
266
|
+
response_many = True
|
|
267
|
+
|
|
268
|
+
assert return_typing is not None, \
|
|
269
|
+
f'{fn.__name__} :: invalid response typing. it must be not None. {return_typing}[{return_payload_typing}] was given'
|
|
270
|
+
|
|
271
|
+
if return_typing is EmptyJsonApiResponse:
|
|
272
|
+
assert return_payload_typing is None, f'{fn.__name__} :: invalid response payload typing. payload must be None. {return_payload_typing.__name__} was given'
|
|
273
|
+
|
|
274
|
+
elif return_typing is RootJsonApiResponse:
|
|
275
|
+
assert return_payload_typing is not None and issubclass(return_payload_typing, (JsonApiResponsePayload, RootJsonApiResponsePayload)), \
|
|
276
|
+
f'{fn.__name__} :: invalid response payload typing. payload must be subclass of (JsonApiResponsePayload, RootJsonApiResponsePayload). ' \
|
|
277
|
+
f'{return_payload_typing.__name__ if return_payload_typing is not None else "None"} was given'
|
|
278
|
+
|
|
279
|
+
elif return_typing is AnyJsonApiResponse:
|
|
280
|
+
assert return_payload_typing is None, f'{fn.__name__} :: invalid response payload typing. payload must be None. {return_payload_typing.__name__} was given'
|
|
281
|
+
|
|
282
|
+
elif issubclass(return_typing, JsonApiResponse):
|
|
283
|
+
assert return_payload_typing is not None and issubclass(return_payload_typing, (JsonApiResponsePayload, RootJsonApiResponsePayload)), \
|
|
284
|
+
f'{fn.__name__} :: invalid response payload typing. payload must be subclass of (JsonApiResponsePayload, RootJsonApiResponsePayload). ' \
|
|
285
|
+
f'{return_payload_typing.__name__ if return_payload_typing is not None else "None"} was given'
|
|
286
|
+
|
|
287
|
+
elif issubclass(return_typing, ProxyJsonApiResponse):
|
|
288
|
+
assert return_payload_typing is not None and issubclass(return_payload_typing, (JsonApiResponsePayload, RootJsonApiResponsePayload)), \
|
|
289
|
+
f'{fn.__name__} :: invalid response payload typing. payload must be subclass of (JsonApiResponsePayload, RootJsonApiResponsePayload). ' \
|
|
290
|
+
f'{return_payload_typing.__name__ if return_payload_typing is not None else "None"} was given'
|
|
291
|
+
|
|
292
|
+
else:
|
|
293
|
+
raise TypeError(f'{fn.__name__} :: invalid response typing. {return_typing.__name__} was given')
|
|
294
|
+
|
|
295
|
+
elif api_resource_type is ApiResourceType.FILE:
|
|
296
|
+
assert return_typing is not None and issubclass(return_typing, FileApiResponse), \
|
|
297
|
+
f'{fn.__name__} :: invalid response typing. it must be subclass of HtmlApiResponse. {return_typing.__name__} was given'
|
|
298
|
+
|
|
299
|
+
elif api_resource_type is ApiResourceType.WEB:
|
|
300
|
+
assert return_typing is not None and issubclass(return_typing, HtmlApiResponse), \
|
|
301
|
+
f'{fn.__name__} :: invalid response typing. it must be subclass of HtmlApiResponse. {return_typing.__name__} was given'
|
|
302
|
+
|
|
303
|
+
return response_many, return_payload_typing, return_typing
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def _parse_query_typing(cls, fn: Callable[['ApiResource'], ApiResponse]) -> Tuple[bool, Optional[Type[ApiRequestQuery]]]:
|
|
307
|
+
query_typing = fn.__annotations__.get('query', None)
|
|
308
|
+
query_validation_error_typing = fn.__annotations__.get('query_validation_error', None)
|
|
309
|
+
query_is_optional = False
|
|
310
|
+
if query_typing is not None:
|
|
311
|
+
query_typing_parsed = UnwrappedOptionalObjOrListOfObj.parse(query_typing, ApiRequestQuery)
|
|
312
|
+
tn = ApiRequestQuery.__name__
|
|
313
|
+
assert query_typing_parsed is not None and not query_typing_parsed.many, \
|
|
314
|
+
f'query_typing is invalid. must be Union[Type[{tn}], Optional[Type[{tn}]]], {query_typing} was given'
|
|
315
|
+
query_is_optional = query_typing_parsed.optional
|
|
316
|
+
|
|
317
|
+
if query_validation_error_typing:
|
|
318
|
+
assert query_typing is not None
|
|
319
|
+
assert query_is_optional, 'query typing must be optional'
|
|
320
|
+
query_err_typing_parsed = UnwrappedOptionalObjOrListOfObj.parse(query_validation_error_typing, ValidationError)
|
|
321
|
+
assert query_err_typing_parsed is not None and query_err_typing_parsed.optional and not query_err_typing_parsed.many, \
|
|
322
|
+
f'query_typing is invalid. must be Optional[ValidationError], {query_validation_error_typing} was given'
|
|
323
|
+
return query_validation_error_typing is not None, query_typing
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def parse_fn(cls, api_resource_type: 'ApiResourceType', methods: List[ApiMethod], fn: Callable[['ApiResource'], ApiResponse]) -> 'ApiResourceFnTyping':
|
|
327
|
+
api_resource_typing = fn.__annotations__.get('api_resource', None)
|
|
328
|
+
if api_resource_typing is None or api_resource_typing.__name__ != 'ApiResource':
|
|
329
|
+
raise TypeError(f'{fn.__name__} :: invalid api_resource typing. must be ApiResource. {api_resource_typing} was given')
|
|
330
|
+
|
|
331
|
+
request_many, body_is_optional, has_body_validation_error, body_typing = cls._parse_body_typing(fn)
|
|
332
|
+
has_query_validation_error, query_typing = cls._parse_query_typing(fn)
|
|
333
|
+
response_many, return_payload_typing, return_typing = cls._parse_return_typing(api_resource_type, fn)
|
|
334
|
+
|
|
335
|
+
for method in methods:
|
|
336
|
+
if method in {ApiMethod.GET, ApiMethod.OPTIONS} and not body_is_optional:
|
|
337
|
+
assert body_typing is None, f'body must be empty for method {method.value}. "{body_typing.__name__}" was given'
|
|
338
|
+
|
|
339
|
+
return ApiResourceFnTyping(
|
|
340
|
+
fn=fn,
|
|
341
|
+
api_resource_type=api_resource_type,
|
|
342
|
+
|
|
343
|
+
signatures_typing=[(k, v) for k, v in fn.__annotations__.items() if k not in FN_SYSTEM_PROPS],
|
|
344
|
+
|
|
345
|
+
body_typing=body_typing,
|
|
346
|
+
has_body_validation_error=has_body_validation_error,
|
|
347
|
+
request_body_many=request_many,
|
|
348
|
+
request_body_optional=body_is_optional,
|
|
349
|
+
|
|
350
|
+
query_typing=query_typing,
|
|
351
|
+
has_query_validation_error=has_query_validation_error,
|
|
352
|
+
|
|
353
|
+
return_typing=return_typing,
|
|
354
|
+
return_payload_typing=return_payload_typing,
|
|
355
|
+
response_payload_many=response_many,
|
|
356
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ul_api_utils.errors import ResourceRuntimeApiError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiResourceType(Enum):
|
|
7
|
+
WEB = 'web'
|
|
8
|
+
API = 'api'
|
|
9
|
+
FILE = 'file'
|
|
10
|
+
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
return f'{type(self).__name__}.{self.name}'
|
|
13
|
+
|
|
14
|
+
def validate(self, type: 'ApiResourceType') -> None:
|
|
15
|
+
if type is not self:
|
|
16
|
+
raise ResourceRuntimeApiError(f'invalid usage method for api with type {type.value}. only {self.value} are permitted')
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TypeVar, Generic, List, Optional, Dict, Any, Callable, Union, BinaryIO, Tuple, Type
|
|
4
|
+
|
|
5
|
+
import msgpack
|
|
6
|
+
from flask import jsonify, send_file, redirect, Response, request
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic import ConfigDict, RootModel
|
|
9
|
+
from werkzeug import Response as BaseResponse
|
|
10
|
+
|
|
11
|
+
from ul_api_utils.api_resource.db_types import TPayloadInputUnion
|
|
12
|
+
from ul_api_utils.const import RESPONSE_PROP_OK, RESPONSE_PROP_PAYLOAD, RESPONSE_PROP_COUNT, RESPONSE_PROP_TOTAL, \
|
|
13
|
+
RESPONSE_PROP_ERRORS, MIME__JSON, MIME__MSGPCK, \
|
|
14
|
+
REQUEST_HEADER__ACCEPT
|
|
15
|
+
from ul_api_utils.debug.debugger import Debugger
|
|
16
|
+
from ul_api_utils.utils.json_encoder import CustomJSONEncoder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def msgpackify(response: Any) -> Response:
|
|
20
|
+
flask_response = jsonify()
|
|
21
|
+
enc = CustomJSONEncoder()
|
|
22
|
+
flask_response.data = msgpack.packb(response, default=enc.default)
|
|
23
|
+
flask_response.content_type = MIME__MSGPCK
|
|
24
|
+
return flask_response
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_generic_params(cls):
|
|
28
|
+
"""
|
|
29
|
+
Wrapper for saving generic params in class
|
|
30
|
+
(to avoid GenericBeforeBaseModelWarning when change order class inheritance)
|
|
31
|
+
"""
|
|
32
|
+
original_class_getitem = cls.__class_getitem__
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def new_class_getitem(cls, params: Any) -> Any:
|
|
36
|
+
result = original_class_getitem(params)
|
|
37
|
+
result._generic_params = params if isinstance(params, tuple) else (params,)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
cls.__class_getitem__ = new_class_getitem
|
|
41
|
+
cls._generic_params = None
|
|
42
|
+
return cls
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ApiResponse(BaseModel):
|
|
46
|
+
ok: bool
|
|
47
|
+
status_code: int
|
|
48
|
+
headers: Dict[str, str] = {}
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(
|
|
51
|
+
extra="forbid",
|
|
52
|
+
arbitrary_types_allowed=True
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
57
|
+
raise NotImplementedError()
|
|
58
|
+
|
|
59
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
60
|
+
raise NotImplementedError()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RedirectApiResponse(ApiResponse):
|
|
64
|
+
location: str
|
|
65
|
+
status_code: int = 302
|
|
66
|
+
|
|
67
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
68
|
+
resp = redirect(
|
|
69
|
+
self.location,
|
|
70
|
+
code=self.status_code,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
resp.headers.update(self.headers)
|
|
74
|
+
|
|
75
|
+
return resp
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class HtmlApiResponse(ApiResponse):
|
|
79
|
+
content: str
|
|
80
|
+
error: Optional[Exception] = None
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
84
|
+
class _ResponseNoneType(RootModel[None]):
|
|
85
|
+
pass
|
|
86
|
+
return _ResponseNoneType
|
|
87
|
+
|
|
88
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
89
|
+
content = self.content
|
|
90
|
+
debugger_content = debugger.render_html(self.status_code) if debugger is not None else ''
|
|
91
|
+
|
|
92
|
+
if debugger_content:
|
|
93
|
+
prev_len = len(content)
|
|
94
|
+
content = content.replace('</body>', f'{debugger_content}</body>', 1)
|
|
95
|
+
if len(content) == prev_len:
|
|
96
|
+
content += debugger_content
|
|
97
|
+
|
|
98
|
+
resp = Response(
|
|
99
|
+
content,
|
|
100
|
+
status=self.status_code,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
resp.headers.update(self.headers)
|
|
104
|
+
|
|
105
|
+
return resp
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class FileApiResponse(ApiResponse):
|
|
109
|
+
file_path: Union[str, BinaryIO, io.BytesIO]
|
|
110
|
+
mimetype: Optional[str] = None # it will be auto-detected by extension if mimetype==None
|
|
111
|
+
as_attachment: bool = False
|
|
112
|
+
download_name: Optional[str] = None
|
|
113
|
+
conditional: bool = True
|
|
114
|
+
etag: Union[bool, str] = True
|
|
115
|
+
last_modified: Optional[Union[datetime, int, float]] = None
|
|
116
|
+
max_age: Optional[Union[int, Callable[[Optional[str]], Optional[int]]]] = None
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
120
|
+
class _ResponseNoneType(RootModel[None]):
|
|
121
|
+
pass
|
|
122
|
+
return _ResponseNoneType
|
|
123
|
+
|
|
124
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
125
|
+
resp = send_file(
|
|
126
|
+
path_or_file=self.file_path,
|
|
127
|
+
mimetype=self.mimetype,
|
|
128
|
+
as_attachment=self.as_attachment,
|
|
129
|
+
download_name=self.download_name,
|
|
130
|
+
conditional=self.conditional,
|
|
131
|
+
etag=self.etag,
|
|
132
|
+
last_modified=self.last_modified,
|
|
133
|
+
max_age=self.max_age,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
resp.headers.update(self.headers)
|
|
137
|
+
|
|
138
|
+
return resp
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class EmptyJsonApiResponse(ApiResponse):
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
145
|
+
class _ResponseNoneType(RootModel[None]):
|
|
146
|
+
pass
|
|
147
|
+
return _ResponseNoneType
|
|
148
|
+
|
|
149
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
150
|
+
resp = Response(response=None, status=self.status_code, mimetype=MIME__JSON)
|
|
151
|
+
resp.headers.update(self.headers)
|
|
152
|
+
return resp
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
T = TypeVar("T")
|
|
156
|
+
|
|
157
|
+
class JsonApiResponsePayload(BaseModel):
|
|
158
|
+
model_config = ConfigDict(extra="ignore")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RootJsonApiResponsePayload(RootModel[T]):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
TRootJsonApiResponsePayload = TypeVar('TRootJsonApiResponsePayload')
|
|
165
|
+
|
|
166
|
+
TResultPayloadUnion = Union[None, Dict[str, Any], JsonApiResponsePayload, TRootJsonApiResponsePayload, List[JsonApiResponsePayload], List[TRootJsonApiResponsePayload], List[Dict[str, Any]]]
|
|
167
|
+
TPayloadTotalUnion = Union[
|
|
168
|
+
Tuple[None, None],
|
|
169
|
+
Tuple[Dict[str, Any], None],
|
|
170
|
+
Tuple[JsonApiResponsePayload, None],
|
|
171
|
+
Tuple[TRootJsonApiResponsePayload, None],
|
|
172
|
+
Tuple[List[JsonApiResponsePayload], int],
|
|
173
|
+
Tuple[List[TRootJsonApiResponsePayload], int],
|
|
174
|
+
Tuple[List[Dict[str, Any]], int],
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class DictJsonApiResponsePayload(RootJsonApiResponsePayload[Dict[str, Any]]):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
TProxyPayload = TypeVar('TProxyPayload', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]], None])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@save_generic_params
|
|
186
|
+
class ProxyJsonApiResponse(EmptyJsonApiResponse, Generic[TProxyPayload]):
|
|
187
|
+
response: Dict[str, Any]
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
191
|
+
class _ResponseStd(BaseModel):
|
|
192
|
+
ok: bool
|
|
193
|
+
payload: inner_type # type: ignore
|
|
194
|
+
errors: List[Dict[str, Any]]
|
|
195
|
+
total_count: Optional[int] = None
|
|
196
|
+
count: Optional[int] = None
|
|
197
|
+
return _ResponseStd
|
|
198
|
+
|
|
199
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
200
|
+
resp = jsonify(self.response)
|
|
201
|
+
resp.status_code = self.status_code
|
|
202
|
+
resp.headers.update(self.headers)
|
|
203
|
+
return resp
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
TJsonObjApiResponsePayload = TypeVar('TJsonObjApiResponsePayload')
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@save_generic_params
|
|
210
|
+
class RootJsonApiResponse(EmptyJsonApiResponse, Generic[TJsonObjApiResponsePayload]):
|
|
211
|
+
root: TJsonObjApiResponsePayload
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
215
|
+
class _ResponseStd(RootModel[inner_type]): # type: ignore
|
|
216
|
+
pass
|
|
217
|
+
return _ResponseStd
|
|
218
|
+
|
|
219
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
220
|
+
resp = jsonify(self.root)
|
|
221
|
+
resp.status_code = self.status_code
|
|
222
|
+
resp.headers.update(self.headers)
|
|
223
|
+
return resp
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class AnyJsonApiResponse(ApiResponse):
|
|
227
|
+
payload: TPayloadInputUnion = Field(union_mode="left_to_right")
|
|
228
|
+
total_count: Optional[int] = None
|
|
229
|
+
errors: List[Dict[str, Any]] = []
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
233
|
+
class _ResponseStd(BaseModel):
|
|
234
|
+
ok: bool
|
|
235
|
+
payload: Any = None
|
|
236
|
+
errors: List[Dict[str, Any]]
|
|
237
|
+
total_count: Optional[int] = None
|
|
238
|
+
count: Optional[int] = None
|
|
239
|
+
return _ResponseStd
|
|
240
|
+
|
|
241
|
+
def to_flask_response(self, debugger: Optional[Debugger] = None) -> BaseResponse:
|
|
242
|
+
list_props: Dict[str, Any] = {}
|
|
243
|
+
if self.total_count is not None and isinstance(self.payload, (tuple, list)):
|
|
244
|
+
list_props = {
|
|
245
|
+
RESPONSE_PROP_COUNT: len(self.payload),
|
|
246
|
+
RESPONSE_PROP_TOTAL: self.total_count,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
data = {
|
|
250
|
+
RESPONSE_PROP_OK: self.ok,
|
|
251
|
+
RESPONSE_PROP_PAYLOAD: self.payload,
|
|
252
|
+
RESPONSE_PROP_ERRORS: self.errors,
|
|
253
|
+
**list_props,
|
|
254
|
+
**(debugger.render_dict(self.status_code) if debugger is not None else {}),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if self.ok and len(self.errors) == 0 and MIME__MSGPCK in request.headers.get(REQUEST_HEADER__ACCEPT, MIME__JSON):
|
|
258
|
+
resp = msgpackify(data)
|
|
259
|
+
else:
|
|
260
|
+
resp = jsonify(data)
|
|
261
|
+
|
|
262
|
+
resp.status_code = self.status_code
|
|
263
|
+
|
|
264
|
+
resp.headers.update(self.headers)
|
|
265
|
+
|
|
266
|
+
return resp
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _internal_use_response_error(many: bool, status_code: int, errors: List[Dict[str, str]]) -> 'JsonApiResponse[TJsonObjApiResponsePayload]':
|
|
270
|
+
# TODO
|
|
271
|
+
# exc -> 0 -> error_location
|
|
272
|
+
# str type expected (type=type_error.str)
|
|
273
|
+
|
|
274
|
+
# pydantic.error_wrappers.ValidationError: 8 validation exc for JsonApiResponse
|
|
275
|
+
#
|
|
276
|
+
# ul_api_utils.exc.api_list_error.ApiValidationListError:
|
|
277
|
+
# validation exc: [
|
|
278
|
+
# {'error_type': 'body-validation-error', 'error_message': 'field required', 'error_location': ('id',), 'error_kind': 'value_error.missing'},
|
|
279
|
+
# {'error_type': 'body-validation-error', 'error_message': 'field required', 'error_location': ('date_created',), 'error_kind': 'value_error.missing'}
|
|
280
|
+
|
|
281
|
+
# exc can by Dict[str, str | Tuple[str]]
|
|
282
|
+
|
|
283
|
+
if many:
|
|
284
|
+
return JsonApiResponse(ok=False, total_count=0, payload={}, status_code=status_code, errors=errors)
|
|
285
|
+
return JsonApiResponse(ok=False, total_count=0, payload={}, status_code=status_code, errors=errors)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@save_generic_params
|
|
289
|
+
class JsonApiResponse(AnyJsonApiResponse, Generic[TJsonObjApiResponsePayload]):
|
|
290
|
+
_generic_params = None
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def _internal_use__mk_schema(cls, inner_type: Optional[Type[BaseModel]]) -> Type[BaseModel]:
|
|
294
|
+
class _ResponseStd(BaseModel):
|
|
295
|
+
ok: bool
|
|
296
|
+
payload: inner_type # type: ignore
|
|
297
|
+
errors: List[Dict[str, Any]]
|
|
298
|
+
total_count: Optional[int] = None
|
|
299
|
+
count: Optional[int] = None
|
|
300
|
+
return _ResponseStd
|