ul-api-utils 7.8.0__py3-none-any.whl → 7.8.3__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.

Potentially problematic release.


This version of ul-api-utils might be problematic. Click here for more details.

example/conf.py CHANGED
@@ -16,6 +16,7 @@ sdk = ApiSdk(ApiSdkConfig(
16
16
  flask_debugging_plugins=ApiSdkFlaskDebuggingPluginsEnabled(
17
17
  flask_monitoring_dashboard=True,
18
18
  ),
19
+ web_error_template='error.html.jinja2',
19
20
  ))
20
21
 
21
22
 
@@ -12,7 +12,8 @@ from example.permissions import SOME_PERMISSION, SOME_PERMISSION2
12
12
  from ul_api_utils.api_resource.api_request import ApiRequestQuery
13
13
  from ul_api_utils.api_resource.api_resource import ApiResource
14
14
  from ul_api_utils.api_resource.api_resource_config import ApiResourceConfig
15
- from ul_api_utils.api_resource.api_response import FileApiResponse, JsonApiResponsePayload, JsonApiResponse, AnyJsonApiResponse, ProxyJsonApiResponse, HtmlApiResponse
15
+ from ul_api_utils.api_resource.api_response import FileApiResponse, JsonApiResponsePayload, JsonApiResponse, \
16
+ AnyJsonApiResponse, ProxyJsonApiResponse, HtmlApiResponse, EmptyJsonApiResponse
16
17
  from ul_api_utils.errors import Server5XXInternalApiError, NoResultFoundApiError, Client4XXInternalApiError
17
18
  from ul_api_utils.internal_api.internal_api import InternalApi
18
19
  from ul_api_utils.utils.api_encoding import ApiEncoding
@@ -224,6 +225,17 @@ def override(api_resource: ApiResource) -> JsonApiResponse[RespObject]:
224
225
  return api_resource.response_ok(RespObject(now=datetime.now()))
225
226
 
226
227
 
228
+ @sdk.rest_api('GET', '/example-resource-empty', access=sdk.ACCESS_PUBLIC)
229
+ def empty_resp_test(api_resource: ApiResource) -> EmptyJsonApiResponse:
230
+ return api_resource.response_empty_ok()
231
+
232
+
233
+ @sdk.rest_api('POST', '/example-resource-empty-check', access=sdk.ACCESS_PUBLIC)
234
+ def empty_resp_req_test(api_resource: ApiResource) -> EmptyJsonApiResponse:
235
+ internal_api.request_get('/example-resource-empty').check()
236
+ return api_resource.response_empty_ok()
237
+
238
+
227
239
  @sdk.rest_api('POST', '/example-resource-empty-gzip', access=sdk.ACCESS_PUBLIC)
228
240
  def some12(api_resource: ApiResource, body: List[SomeBody]) -> JsonApiResponse[RespObject]:
229
241
  sess = db.session()
@@ -12,6 +12,8 @@ from werkzeug.datastructures import FileStorage
12
12
 
13
13
  from ul_api_utils.access import GLOBAL_PERMISSION__PUBLIC, PermissionDefinition, GLOBAL_PERMISSION__PRIVATE_RT, GLOBAL_PERMISSION__PRIVATE
14
14
  from ul_api_utils.api_resource.api_resource_config import ApiResourceConfig
15
+ from ul_api_utils.api_resource.api_resource_error_handling import WEB_EXCEPTION_HANDLING_PARAMS__MAP, \
16
+ ProcessingExceptionsParams, WEB_UNKNOWN_ERROR_PARAMS
15
17
  from ul_api_utils.api_resource.api_resource_fn_typing import ApiResourceFnTyping
16
18
  from ul_api_utils.api_resource.api_resource_type import ApiResourceType
17
19
  from ul_api_utils.api_resource.api_response import JsonApiResponse, HtmlApiResponse, FileApiResponse, JsonApiResponsePayload, EmptyJsonApiResponse, \
@@ -20,7 +22,8 @@ from ul_api_utils.conf import APPLICATION_ENV, APPLICATION_JWT_PUBLIC_KEY, APPLI
20
22
  from ul_api_utils.const import REQUEST_HEADER__X_FORWARDED_FOR, \
21
23
  RESPONSE_HEADER__WWW_AUTH, OOPS, REQUEST_HEADER__USER_AGENT
22
24
  from ul_api_utils.errors import ValidationListApiError, AccessApiError, NoResultFoundApiError, PermissionDeniedApiError, \
23
- SimpleValidateApiError, ValidateApiError, HasAlreadyExistsApiError, AbstractInternalApiError, InvalidContentTypeError
25
+ SimpleValidateApiError, ValidateApiError, HasAlreadyExistsApiError, AbstractInternalApiError, \
26
+ InvalidContentTypeError
24
27
  from ul_api_utils.internal_api.internal_api_response import InternalApiResponse
25
28
  from ul_api_utils.modules.api_sdk_config import ApiSdkConfig
26
29
  from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt, JWT_VERSION
@@ -88,6 +91,9 @@ class ApiResource:
88
91
  self._limiter_enabled = limiter_enabled
89
92
  self._db_initialized = db_initialized
90
93
 
94
+ def __repr__(self) -> str:
95
+ return f"ApiResource object. Type: {self._type}, Function: {self._fn_typing.fn.__name__}"
96
+
91
97
  @property
92
98
  def debugger_enabled(self) -> bool:
93
99
  return self._debugger_enabled
@@ -208,30 +214,51 @@ class ApiResource:
208
214
 
209
215
  return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 500, [self._mk_error("system-error", str(err))])
210
216
 
211
- def response_web_error(self, err: Exception) -> HtmlApiResponse:
212
- if isinstance(err, PermissionDeniedApiError):
213
- resp = HtmlApiResponse(error=err, content=f"403 Access Not Permitted. {str(err)}", ok=False, status_code=403)
214
- if self.method != ApiMethod.OPTIONS and self._config.http_auth is not None:
215
- resp.headers[RESPONSE_HEADER__WWW_AUTH] = f'{self._config.http_auth.scheme} realm="{self._config.http_auth.realm}"'
216
- return resp
217
- if isinstance(err, AccessApiError):
218
- resp = HtmlApiResponse(error=err, content=f"401 Invalid Token. {str(err)}", ok=False, status_code=401)
217
+ def _generate_web_error_response(
218
+ self,
219
+ happened_exception: Exception,
220
+ default_status_code: int,
221
+ error_message: str,
222
+ apply_auth_headers: bool = False,
223
+ ) -> HtmlApiResponse:
224
+ if hasattr(happened_exception, "status_code"):
225
+ default_status_code = happened_exception.status_code # type: ignore
226
+ assert self._config.web_error_template is not None # only for mypy
227
+ response = HtmlApiResponse(
228
+ content=self.render_template(
229
+ self._config.web_error_template,
230
+ {
231
+ "error_traceback": happened_exception,
232
+ "error_message": error_message,
233
+ "status_code": default_status_code,
234
+ },
235
+ ),
236
+ ok=False,
237
+ status_code=default_status_code,
238
+ )
239
+ if apply_auth_headers:
219
240
  if self.method != ApiMethod.OPTIONS and self._config.http_auth is not None:
220
- resp.headers[RESPONSE_HEADER__WWW_AUTH] = f'{self._config.http_auth.scheme} realm="{self._config.http_auth.realm}"'
221
- return resp
222
- if isinstance(err, ValidateApiError):
223
- return HtmlApiResponse(error=err, content="400 Invalid Request\n Request validation error", ok=False, status_code=400)
241
+ response.headers[RESPONSE_HEADER__WWW_AUTH] = f'{self._config.http_auth.scheme} realm="{self._config.http_auth.realm}"'
242
+ return response
243
+ return response
244
+
245
+ def response_web_error(self, err: Exception) -> HtmlApiResponse:
246
+ if self._config.web_error_template is None:
247
+ return HtmlApiResponse(error=err, content=OOPS, ok=False, status_code=500)
224
248
 
225
249
  if self._db_initialized:
226
250
  from sqlalchemy.exc import NoResultFound as NoResultFoundError
227
251
 
228
- if isinstance(err, NoResultFoundError):
229
- if self._config.not_found_template is None:
230
- return HtmlApiResponse(error=err, content="404 Not Found", ok=False, status_code=404)
231
- return HtmlApiResponse(error=err, content=self.render_template(self._config.not_found_template, {}), ok=False, status_code=404)
232
- if self._config.not_found_template is None:
233
- return HtmlApiResponse(error=err, content=OOPS, ok=False, status_code=500)
234
- return HtmlApiResponse(error=err, content=OOPS, ok=False, status_code=500)
252
+ WEB_EXCEPTION_HANDLING_PARAMS__MAP[NoResultFoundError] = ProcessingExceptionsParams(default_status_code=404, error_message="Not found")
253
+
254
+ error_response_params = WEB_EXCEPTION_HANDLING_PARAMS__MAP.get(type(err), WEB_UNKNOWN_ERROR_PARAMS)
255
+
256
+ return self._generate_web_error_response(
257
+ happened_exception=err,
258
+ default_status_code=error_response_params.default_status_code,
259
+ error_message=error_response_params.error_message,
260
+ apply_auth_headers=error_response_params.apply_auth_headers,
261
+ )
235
262
 
236
263
  def render_template(self, template_name: str, data: Dict[str, Any]) -> str:
237
264
  return render_template(
@@ -0,0 +1,21 @@
1
+ from typing import NamedTuple, Dict, Type
2
+
3
+ from ul_api_utils.errors import PermissionDeniedApiError, AccessApiError, ValidateApiError, \
4
+ Server5XXInternalApiError, Client4XXInternalApiError
5
+
6
+
7
+ class ProcessingExceptionsParams(NamedTuple):
8
+ default_status_code: int
9
+ error_message: str
10
+ apply_auth_headers: bool = False
11
+
12
+
13
+ WEB_EXCEPTION_HANDLING_PARAMS__MAP: Dict[Type[Exception], ProcessingExceptionsParams] = {
14
+ PermissionDeniedApiError: ProcessingExceptionsParams(default_status_code=403, error_message="Access not permitted", apply_auth_headers=True),
15
+ AccessApiError: ProcessingExceptionsParams(default_status_code=401, error_message="Invalid token", apply_auth_headers=True),
16
+ ValidateApiError: ProcessingExceptionsParams(default_status_code=400, error_message="Request validation error"),
17
+ Server5XXInternalApiError: ProcessingExceptionsParams(default_status_code=500, error_message="Server error"),
18
+ Client4XXInternalApiError: ProcessingExceptionsParams(default_status_code=400, error_message="Request validation error"),
19
+ }
20
+
21
+ WEB_UNKNOWN_ERROR_PARAMS = ProcessingExceptionsParams(default_status_code=500, error_message="Unknown error")
@@ -154,7 +154,8 @@ class ApiResourceFnTyping(NamedTuple):
154
154
  return kwargs, errors
155
155
  body = self._get_body()
156
156
  try:
157
- kwargs['body'] = [set_model(self.body_typing, fields) for fields in _body_list(body)] if self.request_body_many else set_model(self.body_typing, body) # type: ignore
157
+ expected_typing: Type[BaseModel | List[BaseModel]] = List[self.body_typing] if self.request_body_many else self.body_typing # type: ignore
158
+ kwargs['body'] = parse_obj_as(expected_typing, body)
158
159
  except ValidationError as ve:
159
160
  if self.has_body_validation_error:
160
161
  kwargs['body_validation_error'] = ve
@@ -42,7 +42,7 @@ class ApiSdkConfig(BaseModel):
42
42
 
43
43
  static_url_path: Optional[str] = None
44
44
 
45
- not_found_template: Optional[str] = None
45
+ web_error_template: Optional[str] = None
46
46
 
47
47
  rate_limit: Union[str, List[str]] = '100/minute' # [count (int)] [per|/] [second|minute|hour|day|month|year][s]
48
48
  rate_limit_storage_uri: str = '' # supports url of redis, memcached, mongodb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ul-api-utils
3
- Version: 7.8.0
3
+ Version: 7.8.3
4
4
  Summary: Python api utils
5
5
  Author: Unic-lab
6
6
  Author-email:
@@ -41,6 +41,8 @@ Requires-Dist: sentry-sdk[flask] (==1.9.2)
41
41
  Requires-Dist: types-requests (==2.28.8)
42
42
  Requires-Dist: types-jinja2 (==2.11.9)
43
43
  Requires-Dist: werkzeug (==2.3.7)
44
+ Requires-Dist: wtforms (==3.0.1)
45
+ Requires-Dist: wtforms-alchemy (==0.18.0)
44
46
 
45
47
  # Generic library api-utils
46
48
 
@@ -1,12 +1,12 @@
1
1
  example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- example/conf.py,sha256=WPMSjEdxkxp7Vnwc-zBJrX3dE2KfbSMegsIiHktbY-M,844
2
+ example/conf.py,sha256=QrffEnJInUeL38I4hzg2ft-Lczzq4yCLVBqWEeZ4MLs,888
3
3
  example/main.py,sha256=8jOO1VliFr92UoCLDInzSDPRLluzPuO-MMkzHc5Xd2w,419
4
4
  example/permissions.py,sha256=i8_zOOPdra3oMXZfyTspewRYNdn21PCqOD1ATG69Itk,277
5
5
  example/pure_flask_example.py,sha256=A7cbcjTr28FS1sVNAsQbj1N9EgEFIXDB4aRwOV6_tbU,1329
6
6
  example/rate_limit_load.py,sha256=U2Bgp8UztT4TNKdv9NVioxWfE68aCsC7uKz7xPCy6XM,225
7
7
  example/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  example/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- example/routes/api_some.py,sha256=d_X3828IZFs7EAKcP_lX9hHRZaaOQw1XoehDU3ZHcnI,12232
9
+ example/routes/api_some.py,sha256=Lo4lXeLuXwZ_XJP6PnZ5QQCU8OzpyyDP_NU5zY1pbPM,12719
10
10
  example/workers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  example/workers/worker.py,sha256=flMYq50OhLtNSaA2qyDJSMeXSNXIqhdBIsaxcmO5-xQ,681
12
12
  ul_api_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -19,9 +19,10 @@ ul_api_utils/sentry.py,sha256=UH_SwZCAoKH-Nw5B9CVQMoF-b1BJOp-ZTzwqUZ3Oq84,1801
19
19
  ul_api_utils/access/__init__.py,sha256=OD2UnvAi8ax40B_5JlaW2IAcOcZzk2Y15h8Xi_wEa0g,4526
20
20
  ul_api_utils/api_resource/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  ul_api_utils/api_resource/api_request.py,sha256=psVfLJqapV4PBL3YF0Bj22T3PDcCWQME87chtGBH3pw,3279
22
- ul_api_utils/api_resource/api_resource.py,sha256=YPVkj2PMGdTEoY4GYKsS0CFrImTyY9IQuTGTs6SxwrU,17185
22
+ ul_api_utils/api_resource/api_resource.py,sha256=mUaFWxYBfHemcM2SaBQBOtTb4T4fbvZ4CNmE2GwPtOg,17766
23
23
  ul_api_utils/api_resource/api_resource_config.py,sha256=_B7DNe5eLbRw0Spy1y5CatXjlz5M3wqgYIjnJemlY44,760
24
- ul_api_utils/api_resource/api_resource_fn_typing.py,sha256=_cIr115JuRBi9kmFmiTm4jP9sHJ0bNbUlaRzWODcruI,18182
24
+ ul_api_utils/api_resource/api_resource_error_handling.py,sha256=E0SWpjFSIP-4SumbgzrHtFuFiGe9q38WsvLROt0YcPE,1168
25
+ ul_api_utils/api_resource/api_resource_fn_typing.py,sha256=OrmVFSuvqER5FwlhA6N6iaO3N99iPfTapBv_X_6xcbo,18224
25
26
  ul_api_utils/api_resource/api_resource_type.py,sha256=mgjSQI3swGpgpLI6y35LYtFrdN-kXyV5cQorwGW7h6g,462
26
27
  ul_api_utils/api_resource/api_response.py,sha256=jb5mKjouupDmctzPb2b7Z9_dpWVuxUHxVSevz-8sODI,9676
27
28
  ul_api_utils/api_resource/api_response_db.py,sha256=ucY6ANPlHZml7JAbvq-PL85z0bvERTjEJKvz-REPyok,888
@@ -57,7 +58,7 @@ ul_api_utils/internal_api/__tests__/internal_api.py,sha256=X2iopeso6vryszeeA__lc
57
58
  ul_api_utils/internal_api/__tests__/internal_api_content_type.py,sha256=mfiYPkzKtfZKFpi4RSnWAoCd6mRijr6sFsa2TF-s5t8,749
58
59
  ul_api_utils/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
60
  ul_api_utils/modules/api_sdk.py,sha256=mnb3wOuM0xHyOiG3WGgrNYp0sLDWVft6vLOlp5FGdWo,25074
60
- ul_api_utils/modules/api_sdk_config.py,sha256=cKQhIuEaLp4o9UVA4fmnR4QuEZPO9pqEjs_Xqgk8Mlw,2042
61
+ ul_api_utils/modules/api_sdk_config.py,sha256=ANxsbzVHYoZPoeBM5oKsYwnvxpl9VVocmPkoL_mwyK4,2042
61
62
  ul_api_utils/modules/api_sdk_jwt.py,sha256=2XRfb0LxHUnldSL67S60v1uyoDpVPNaq4zofUtkeg88,15112
62
63
  ul_api_utils/modules/intermediate_state.py,sha256=7ZZ3Sypbb8LaSfrVhaXaWRDnj8oyy26NUbmFK7vr-y4,1270
63
64
  ul_api_utils/modules/worker_context.py,sha256=jGjopeuYuTtIDmsrqK7TcbTD-E81t8OWvWS1JpTC6b0,802
@@ -133,9 +134,9 @@ ul_api_utils/validators/validate_empty_object.py,sha256=3Ck_iwyJE_M5e7l6s1i88aqb
133
134
  ul_api_utils/validators/validate_uuid.py,sha256=EfvlRirv2EW0Z6w3s8E8rUa9GaI8qXZkBWhnPs8NFrA,257
134
135
  ul_api_utils/validators/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
136
  ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=QLZ7DFta01Z7DOK9Z5Iq4uf_CmvDkVReis-GAl_QN48,1447
136
- ul_api_utils-7.8.0.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
137
- ul_api_utils-7.8.0.dist-info/METADATA,sha256=ye4_rHtf-C06Ltam7yrnIsVqoJ5IqMFZxpIQEh4xOfg,14454
138
- ul_api_utils-7.8.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
139
- ul_api_utils-7.8.0.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
140
- ul_api_utils-7.8.0.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
141
- ul_api_utils-7.8.0.dist-info/RECORD,,
137
+ ul_api_utils-7.8.3.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
138
+ ul_api_utils-7.8.3.dist-info/METADATA,sha256=94zpzsKBdLQsFDwdf77UW9v42c5dRHjS4CfMWfguxzc,14529
139
+ ul_api_utils-7.8.3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
140
+ ul_api_utils-7.8.3.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
141
+ ul_api_utils-7.8.3.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
142
+ ul_api_utils-7.8.3.dist-info/RECORD,,