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,122 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Union, Any, Iterable
|
|
3
|
+
|
|
4
|
+
from pydantic import ConfigDict, BaseModel
|
|
5
|
+
|
|
6
|
+
from ul_api_utils.conf import APPLICATION_F
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PermissionDefinition(BaseModel):
|
|
10
|
+
id: int
|
|
11
|
+
key: str
|
|
12
|
+
name: str
|
|
13
|
+
category: str
|
|
14
|
+
flags: str = ''
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(frozen=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PermissionRegistry:
|
|
20
|
+
"""
|
|
21
|
+
Examples:
|
|
22
|
+
reg = PermissionRegistry('some_service_name', 10000, 10100)
|
|
23
|
+
PII_GET_USER = reg.add(PermissionDefinition.new('PII_GET_USER', 1, 'permission get user', 'user'))
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = (
|
|
27
|
+
'_service_name',
|
|
28
|
+
'_index_by_id',
|
|
29
|
+
'_index_by_key',
|
|
30
|
+
'_index_by_category',
|
|
31
|
+
'_permission_ids',
|
|
32
|
+
'_categories',
|
|
33
|
+
'_start_id',
|
|
34
|
+
'_end_id',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def __init__(self, service_name: str, start_id: int, end_id: int) -> None:
|
|
38
|
+
self._service_name = service_name
|
|
39
|
+
self._index_by_id: Dict[int, PermissionDefinition] = dict()
|
|
40
|
+
self._index_by_key: Dict[str, PermissionDefinition] = dict()
|
|
41
|
+
self._index_by_category: Dict[str, List[PermissionDefinition]] = dict()
|
|
42
|
+
self._permission_ids: List[int] = list()
|
|
43
|
+
self._categories: List[str] = list()
|
|
44
|
+
|
|
45
|
+
self._start_id = start_id
|
|
46
|
+
self._end_id = end_id
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get_ids_from_iterable(permissions: Iterable[PermissionDefinition]) -> List[int]:
|
|
50
|
+
return [p.id for p in permissions]
|
|
51
|
+
|
|
52
|
+
def has(self, permission: Union[int, PermissionDefinition]) -> bool:
|
|
53
|
+
return (permission.id if isinstance(permission, PermissionDefinition) else permission) in self._index_by_id
|
|
54
|
+
|
|
55
|
+
def add(self, key: str, id: int, name: str, category: str, flags: str = '') -> PermissionDefinition:
|
|
56
|
+
assert isinstance(id, int), f"id must be int. {type(id)} given"
|
|
57
|
+
assert isinstance(name, str), f"name must be str. {type(name)} given"
|
|
58
|
+
assert isinstance(key, str), f"key must be str. {type(key)} given"
|
|
59
|
+
assert isinstance(category, str), f"category must be str. {type(category)} given"
|
|
60
|
+
|
|
61
|
+
id = self._start_id + id # BECAUSE BASE COULD BE CHANGED
|
|
62
|
+
|
|
63
|
+
assert id not in {GLOBAL_PERMISSION__PRIVATE.id, GLOBAL_PERMISSION__PUBLIC.id, GLOBAL_PERMISSION__PRIVATE_RT.id}
|
|
64
|
+
|
|
65
|
+
if id in self._index_by_id:
|
|
66
|
+
raise ValueError(f'duplicate id={id}')
|
|
67
|
+
if key in self._index_by_key:
|
|
68
|
+
raise ValueError(f'duplicate key={key}')
|
|
69
|
+
if id > self._end_id:
|
|
70
|
+
raise ValueError(f'permission id={id} > {self._end_id}')
|
|
71
|
+
if id < self._start_id:
|
|
72
|
+
raise ValueError(f'permission id={id} < {self._end_id}')
|
|
73
|
+
if not re.match(r'^[A-Z][A-Z0-9_]+[A-Z0-9]$', key):
|
|
74
|
+
raise ValueError('key has invalid template')
|
|
75
|
+
|
|
76
|
+
permission = PermissionDefinition(
|
|
77
|
+
id=id,
|
|
78
|
+
key=key,
|
|
79
|
+
name=name,
|
|
80
|
+
category=category,
|
|
81
|
+
flags=flags,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._permission_ids.append(id)
|
|
85
|
+
|
|
86
|
+
self._index_by_id[id] = permission
|
|
87
|
+
self._index_by_key[key] = permission
|
|
88
|
+
|
|
89
|
+
if category not in self._index_by_category:
|
|
90
|
+
self._index_by_category[category] = list()
|
|
91
|
+
self._categories.append(category)
|
|
92
|
+
self._index_by_category[category].append(permission)
|
|
93
|
+
|
|
94
|
+
return permission
|
|
95
|
+
|
|
96
|
+
def get_categories_with_permissions(self) -> List[Dict[str, Union[str, List[Dict[str, Union[int, str]]]]]]:
|
|
97
|
+
result: List[Dict[str, Any]] = []
|
|
98
|
+
for c in self._categories:
|
|
99
|
+
perms = []
|
|
100
|
+
for p in self._index_by_category[c]:
|
|
101
|
+
if not p.flags or APPLICATION_F.has_or_unset(p.flags):
|
|
102
|
+
perms.append(dict(id=p.id, name=p.name, key=p.key))
|
|
103
|
+
result.append(dict(
|
|
104
|
+
category=c,
|
|
105
|
+
service=self._service_name,
|
|
106
|
+
permissions=perms,
|
|
107
|
+
))
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def get_permissions_ids(self) -> List[int]:
|
|
111
|
+
return self._permission_ids
|
|
112
|
+
|
|
113
|
+
def get_categories(self) -> List[str]:
|
|
114
|
+
return self._categories
|
|
115
|
+
|
|
116
|
+
def get_permissions_ids_of_category(self, category: str) -> List[int]:
|
|
117
|
+
return [p.id for p in self._index_by_category[category]]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
GLOBAL_PERMISSION__PUBLIC = PermissionDefinition(id=0, key='PUBLIC', name='public access', category='')
|
|
121
|
+
GLOBAL_PERMISSION__PRIVATE = PermissionDefinition(id=1, key='PRIVATE_AT', name='user must be logged in', category='')
|
|
122
|
+
GLOBAL_PERMISSION__PRIVATE_RT = PermissionDefinition(id=2, key='PRIVATE_RT', name='user must be logged in and resource must check only refresh token', category='')
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import NamedTuple, Iterable, Any, List, Tuple, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from flask_sqlalchemy.query import Query
|
|
5
|
+
from pydantic import model_validator, BaseModel
|
|
6
|
+
|
|
7
|
+
from ul_api_utils.errors import SimpleValidateApiError
|
|
8
|
+
from ul_api_utils.utils.api_pagination import ApiPagination
|
|
9
|
+
from ul_api_utils.utils.imports import has_already_imported_db
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiRequestQueryPagination(NamedTuple):
|
|
13
|
+
page: int
|
|
14
|
+
limit: int
|
|
15
|
+
offset: int
|
|
16
|
+
per_page: int
|
|
17
|
+
|
|
18
|
+
def mk_item_pagination(self, items: Iterable[Any], total: int) -> ApiPagination:
|
|
19
|
+
return ApiPagination(
|
|
20
|
+
total=total,
|
|
21
|
+
per_page=self.per_page,
|
|
22
|
+
page=self.page,
|
|
23
|
+
items=items,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# TODO: Not sure that is worked. Check it if will be found usages
|
|
27
|
+
def mk_sqlalchemy_pagination(self, items: Iterable[Any], total: int, query: Any = None) -> 'ApiPagination':
|
|
28
|
+
if has_already_imported_db() and query:
|
|
29
|
+
return query.paginate(total=total, query=query, per_page=self.per_page, page=self.page, items=items) # type: ignore
|
|
30
|
+
return self.mk_item_pagination(items, total)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiRequestQuerySortBy(NamedTuple):
|
|
34
|
+
params: List[Tuple[str, str]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApiRequestQueryFilterBy(NamedTuple):
|
|
38
|
+
params: List[Dict[str, Any]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ApiRequestQuery(BaseModel):
|
|
42
|
+
sort: Optional[str] = None
|
|
43
|
+
filter: Optional[str] = None
|
|
44
|
+
limit: Optional[int] = None
|
|
45
|
+
offset: Optional[int] = None
|
|
46
|
+
page: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
@model_validator(mode="before")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_empty_values(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
51
|
+
vals = dict()
|
|
52
|
+
for k, v in values.items():
|
|
53
|
+
vals[k] = v if v != "" else None
|
|
54
|
+
return vals
|
|
55
|
+
|
|
56
|
+
def pagination(self, default_limit: int, max_limit: int) -> ApiRequestQueryPagination:
|
|
57
|
+
try:
|
|
58
|
+
offset = max(int(self.offset or '0'), 0)
|
|
59
|
+
except Exception: # noqa: B902
|
|
60
|
+
offset = 0
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
limit = min(max(int(self.limit or str(default_limit)), 0), max_limit)
|
|
64
|
+
except Exception: # noqa: B902
|
|
65
|
+
limit = default_limit
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
page = max(int(self.page or '1'), 1)
|
|
69
|
+
except Exception: # noqa: B902
|
|
70
|
+
page = 1
|
|
71
|
+
|
|
72
|
+
if self.page is not None:
|
|
73
|
+
offset = limit * (page - 1)
|
|
74
|
+
else:
|
|
75
|
+
page = int(offset / limit) + 1
|
|
76
|
+
|
|
77
|
+
return ApiRequestQueryPagination(
|
|
78
|
+
limit=limit,
|
|
79
|
+
offset=offset,
|
|
80
|
+
page=page,
|
|
81
|
+
per_page=limit,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def filter_by(self, attr: str = "filter") -> List[Dict[str, Any]]:
|
|
85
|
+
filter_value = getattr(self, attr)
|
|
86
|
+
if not filter_value:
|
|
87
|
+
return []
|
|
88
|
+
_filter_by = json.loads(filter_value)
|
|
89
|
+
if not all([isinstance(filter_arg, dict) for filter_arg in _filter_by]):
|
|
90
|
+
raise SimpleValidateApiError('invalid filters format')
|
|
91
|
+
return _filter_by
|
|
92
|
+
|
|
93
|
+
def sort_by(self, attr: str = "sort") -> List[Tuple[str, str]]:
|
|
94
|
+
sort_value = getattr(self, attr)
|
|
95
|
+
_sort_by: List[Tuple[str, str]] = []
|
|
96
|
+
if not sort_value or not sort_value.strip():
|
|
97
|
+
return _sort_by
|
|
98
|
+
for _sort_arg in sort_value.strip().split(' '):
|
|
99
|
+
if not _sort_arg:
|
|
100
|
+
continue
|
|
101
|
+
if _sort_arg.startswith("+") or _sort_arg.startswith("-"):
|
|
102
|
+
_sort_by.append((_sort_arg[0], _sort_arg[1:]))
|
|
103
|
+
else:
|
|
104
|
+
_sort_by.append(("+", _sort_arg))
|
|
105
|
+
return _sort_by
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from tempfile import NamedTemporaryFile
|
|
7
|
+
from typing import List, Dict, Any, Optional, Tuple, Union, BinaryIO, Mapping, TypeVar, Callable, Set, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from flask import render_template, request
|
|
10
|
+
from pydantic_core import PydanticCustomError
|
|
11
|
+
from werkzeug.datastructures import FileStorage
|
|
12
|
+
|
|
13
|
+
from ul_api_utils.access import GLOBAL_PERMISSION__PUBLIC, PermissionDefinition, GLOBAL_PERMISSION__PRIVATE_RT, GLOBAL_PERMISSION__PRIVATE
|
|
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
|
|
17
|
+
from ul_api_utils.api_resource.api_resource_fn_typing import ApiResourceFnTyping
|
|
18
|
+
from ul_api_utils.api_resource.api_resource_type import ApiResourceType
|
|
19
|
+
from ul_api_utils.api_resource.api_response import JsonApiResponse, HtmlApiResponse, FileApiResponse, \
|
|
20
|
+
JsonApiResponsePayload, EmptyJsonApiResponse, \
|
|
21
|
+
RedirectApiResponse, ProxyJsonApiResponse, RootJsonApiResponse, RootJsonApiResponsePayload
|
|
22
|
+
from ul_api_utils.conf import APPLICATION_ENV, APPLICATION_JWT_PUBLIC_KEY, APPLICATION_DEBUG
|
|
23
|
+
from ul_api_utils.const import REQUEST_HEADER__X_FORWARDED_FOR, \
|
|
24
|
+
RESPONSE_HEADER__WWW_AUTH, OOPS, REQUEST_HEADER__USER_AGENT
|
|
25
|
+
from ul_api_utils.errors import ValidationListApiError, AccessApiError, NoResultFoundApiError, PermissionDeniedApiError, \
|
|
26
|
+
SimpleValidateApiError, ValidateApiError, HasAlreadyExistsApiError, AbstractInternalApiError, \
|
|
27
|
+
InvalidContentTypeError
|
|
28
|
+
from ul_api_utils.internal_api.internal_api_response import InternalApiResponse
|
|
29
|
+
from ul_api_utils.modules.api_sdk_config import ApiSdkConfig
|
|
30
|
+
from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt, JWT_VERSION
|
|
31
|
+
from ul_api_utils.utils.api_method import ApiMethod
|
|
32
|
+
from ul_api_utils.utils.api_request_info import ApiRequestInfo
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from ul_api_utils.api_resource.db_types import TPayloadInputUnion
|
|
37
|
+
|
|
38
|
+
TPayload = TypeVar('TPayload')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T = TypeVar('T')
|
|
42
|
+
TResp = TypeVar('TResp', bound=Union[JsonApiResponsePayload, RootJsonApiResponsePayload[Any]])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ApiResource:
|
|
46
|
+
__slots__ = (
|
|
47
|
+
'_debugger_enabled',
|
|
48
|
+
'_token',
|
|
49
|
+
'_token_raw',
|
|
50
|
+
'_type',
|
|
51
|
+
'_config',
|
|
52
|
+
'_api_resource_config',
|
|
53
|
+
'_fn_typing',
|
|
54
|
+
'_logger',
|
|
55
|
+
'_headers',
|
|
56
|
+
'_access',
|
|
57
|
+
'_method',
|
|
58
|
+
'_internal_use__files_to_clean',
|
|
59
|
+
'_now',
|
|
60
|
+
'_limiter_enabled',
|
|
61
|
+
'_db_initialized',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
logger: logging.Logger,
|
|
68
|
+
debugger_enabled: bool,
|
|
69
|
+
type: ApiResourceType,
|
|
70
|
+
config: ApiSdkConfig,
|
|
71
|
+
access: PermissionDefinition,
|
|
72
|
+
headers: Mapping[str, str],
|
|
73
|
+
api_resource_config: Optional[ApiResourceConfig] = None,
|
|
74
|
+
fn_typing: ApiResourceFnTyping,
|
|
75
|
+
limiter_enabled: bool,
|
|
76
|
+
db_initialized: bool,
|
|
77
|
+
) -> None:
|
|
78
|
+
self._debugger_enabled = debugger_enabled
|
|
79
|
+
self._token: Optional[ApiSdkJwt] = None
|
|
80
|
+
self._token_raw: Optional[str] = None
|
|
81
|
+
self._type = type
|
|
82
|
+
self._config = config
|
|
83
|
+
self._api_resource_config = api_resource_config or ApiResourceConfig()
|
|
84
|
+
self._fn_typing = fn_typing
|
|
85
|
+
self._logger = logger
|
|
86
|
+
|
|
87
|
+
self._headers = headers
|
|
88
|
+
self._access = access
|
|
89
|
+
self._method = ApiMethod(str(request.method).strip().upper()) # todo: move it in host function
|
|
90
|
+
self._internal_use__files_to_clean: Set[str] = set()
|
|
91
|
+
self._now = datetime.now()
|
|
92
|
+
self._limiter_enabled = limiter_enabled
|
|
93
|
+
self._db_initialized = db_initialized
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"ApiResource object. Type: {self._type}, Function: {self._fn_typing.fn.__name__}"
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def debugger_enabled(self) -> bool:
|
|
100
|
+
return self._debugger_enabled
|
|
101
|
+
|
|
102
|
+
def mk_tmp_file(
|
|
103
|
+
self,
|
|
104
|
+
suffix: str | None = None,
|
|
105
|
+
prefix: str | None = None,
|
|
106
|
+
) -> str:
|
|
107
|
+
f = NamedTemporaryFile(suffix=suffix, prefix=prefix)
|
|
108
|
+
f.seek(0)
|
|
109
|
+
name = f.name
|
|
110
|
+
self._internal_use__files_to_clean.add(str(name))
|
|
111
|
+
return name
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def logger(self) -> logging.Logger:
|
|
115
|
+
return self._logger
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def default_response_payload(self) -> Optional[List[Any]]:
|
|
119
|
+
ApiResourceType.API.validate(self._type)
|
|
120
|
+
return None if not self._fn_typing.response_payload_many else []
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def method(self) -> ApiMethod:
|
|
124
|
+
return self._method
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def request_files(self) -> Mapping[str, FileStorage]:
|
|
128
|
+
return request.files
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def request_headers(self) -> Mapping[str, str]:
|
|
132
|
+
return self._headers
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def request_info(self) -> ApiRequestInfo:
|
|
136
|
+
x_forwarded_for = self._headers.get(REQUEST_HEADER__X_FORWARDED_FOR)
|
|
137
|
+
return ApiRequestInfo(
|
|
138
|
+
user_agent=request.headers.get(REQUEST_HEADER__USER_AGENT, ''),
|
|
139
|
+
ipv4=x_forwarded_for.split(",")[0] if x_forwarded_for else request.remote_addr or '',
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def auth_token_raw(self) -> str:
|
|
144
|
+
if self._access == GLOBAL_PERMISSION__PUBLIC:
|
|
145
|
+
raise OverflowError('you could not use token in public api method')
|
|
146
|
+
assert self._token_raw is not None
|
|
147
|
+
return self._token_raw
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def auth_token(self) -> ApiSdkJwt:
|
|
151
|
+
if self._access == GLOBAL_PERMISSION__PUBLIC:
|
|
152
|
+
raise OverflowError('you could not use token in public api method')
|
|
153
|
+
assert self._token is not None
|
|
154
|
+
return self._token
|
|
155
|
+
|
|
156
|
+
def _mk_error(self, error_type: str, error_message: str, debug_specific: bool = True) -> Dict[str, str]:
|
|
157
|
+
error_message = error_message if not debug_specific or (self._debugger_enabled or APPLICATION_DEBUG) else OOPS
|
|
158
|
+
return {
|
|
159
|
+
"error_type": error_type,
|
|
160
|
+
"error_message": error_message,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def response_api_error(self, err: Exception) -> JsonApiResponse[JsonApiResponsePayload]:
|
|
164
|
+
if isinstance(err, ValidationListApiError):
|
|
165
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, err.errors)
|
|
166
|
+
if isinstance(err, json.decoder.JSONDecodeError):
|
|
167
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [self._mk_error("validation-error", 'json decode failed', False)])
|
|
168
|
+
if isinstance(err, PermissionDeniedApiError):
|
|
169
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 403, [self._mk_error("permission-error", 'no rights', False)])
|
|
170
|
+
if isinstance(err, AccessApiError):
|
|
171
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 401, [self._mk_error("access-error", str(err), False)])
|
|
172
|
+
if isinstance(err, SimpleValidateApiError):
|
|
173
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [self._mk_error("validation-error", str(err), False)])
|
|
174
|
+
if isinstance(err, (PydanticCustomError, ValidateApiError)):
|
|
175
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [{
|
|
176
|
+
"error_kind": f"value_error.{err.code}", # type: ignore
|
|
177
|
+
"error_location": err.location, # type: ignore
|
|
178
|
+
"error_type": "body-validation-error",
|
|
179
|
+
"error_message": f"{err.msg_template}", # type: ignore
|
|
180
|
+
"error_input": err.input, # type: ignore
|
|
181
|
+
}])
|
|
182
|
+
if isinstance(err, NoResultFoundApiError):
|
|
183
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 404, [
|
|
184
|
+
self._mk_error("validation-error", str(err), False),
|
|
185
|
+
])
|
|
186
|
+
if isinstance(err, HasAlreadyExistsApiError):
|
|
187
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [
|
|
188
|
+
self._mk_error("validation-error", str(err) or "Resource already exist", False),
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
if isinstance(err, InvalidContentTypeError):
|
|
192
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [
|
|
193
|
+
self._mk_error("validation-error", str(err), False),
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
if self._db_initialized:
|
|
197
|
+
from ul_db_utils.errors.db_filter_error import DBFiltersError
|
|
198
|
+
from ul_db_utils.errors.db_sort_error import DBSortError
|
|
199
|
+
from sqlalchemy.exc import NoResultFound as NoResultFoundError
|
|
200
|
+
if isinstance(err, (DBFiltersError, DBSortError)):
|
|
201
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 400, [
|
|
202
|
+
self._mk_error("query-validation-error", str(err), False),
|
|
203
|
+
])
|
|
204
|
+
if isinstance(err, NoResultFoundError):
|
|
205
|
+
e = self._mk_error("validation-error", err._message() if err.args else "Resource not found", False) # type: ignore
|
|
206
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 404, [e])
|
|
207
|
+
|
|
208
|
+
if self._limiter_enabled:
|
|
209
|
+
from flask_limiter import RateLimitExceeded
|
|
210
|
+
if isinstance(err, RateLimitExceeded):
|
|
211
|
+
return JsonApiResponse._internal_use_response_error(
|
|
212
|
+
self._fn_typing.response_payload_many,
|
|
213
|
+
429,
|
|
214
|
+
[self._mk_error("rate-limit-exceeded", f"request outside of rate limit - {err.limit.limit}", False)],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return JsonApiResponse._internal_use_response_error(self._fn_typing.response_payload_many, 500, [self._mk_error("system-error", str(err))])
|
|
218
|
+
|
|
219
|
+
def _generate_web_error_response(
|
|
220
|
+
self,
|
|
221
|
+
happened_exception: Exception,
|
|
222
|
+
default_status_code: int,
|
|
223
|
+
error_message: str,
|
|
224
|
+
apply_auth_headers: bool = False,
|
|
225
|
+
) -> HtmlApiResponse:
|
|
226
|
+
if hasattr(happened_exception, "status_code"):
|
|
227
|
+
default_status_code = happened_exception.status_code
|
|
228
|
+
assert self._config.web_error_template is not None # only for mypy
|
|
229
|
+
response = HtmlApiResponse(
|
|
230
|
+
content=self.render_template(
|
|
231
|
+
self._config.web_error_template,
|
|
232
|
+
{
|
|
233
|
+
"error_traceback": happened_exception,
|
|
234
|
+
"error_message": error_message,
|
|
235
|
+
"status_code": default_status_code,
|
|
236
|
+
},
|
|
237
|
+
),
|
|
238
|
+
ok=False,
|
|
239
|
+
status_code=default_status_code,
|
|
240
|
+
)
|
|
241
|
+
if apply_auth_headers:
|
|
242
|
+
if self.method != ApiMethod.OPTIONS and self._config.http_auth is not None:
|
|
243
|
+
response.headers[RESPONSE_HEADER__WWW_AUTH] = f'{self._config.http_auth.scheme} realm="{self._config.http_auth.realm}"'
|
|
244
|
+
return response
|
|
245
|
+
return response
|
|
246
|
+
|
|
247
|
+
def response_web_error(self, err: Exception) -> HtmlApiResponse:
|
|
248
|
+
if self._config.web_error_template is None:
|
|
249
|
+
return HtmlApiResponse(error=err, content=OOPS, ok=False, status_code=500)
|
|
250
|
+
|
|
251
|
+
if self._db_initialized:
|
|
252
|
+
from sqlalchemy.exc import NoResultFound as NoResultFoundError
|
|
253
|
+
|
|
254
|
+
WEB_EXCEPTION_HANDLING_PARAMS__MAP[NoResultFoundError] = ProcessingExceptionsParams(default_status_code=404, error_message="Not found")
|
|
255
|
+
|
|
256
|
+
error_response_params = WEB_EXCEPTION_HANDLING_PARAMS__MAP.get(type(err), WEB_UNKNOWN_ERROR_PARAMS)
|
|
257
|
+
|
|
258
|
+
return self._generate_web_error_response(
|
|
259
|
+
happened_exception=err,
|
|
260
|
+
default_status_code=error_response_params.default_status_code,
|
|
261
|
+
error_message=error_response_params.error_message,
|
|
262
|
+
apply_auth_headers=error_response_params.apply_auth_headers,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def render_template(self, template_name: str, data: Dict[str, Any]) -> str:
|
|
266
|
+
return render_template(
|
|
267
|
+
template_name_or_list=template_name,
|
|
268
|
+
**data,
|
|
269
|
+
NOW=self._now,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def response_template(self, template_name: str, **kwargs: Any) -> HtmlApiResponse:
|
|
273
|
+
ApiResourceType.WEB.validate(self._type)
|
|
274
|
+
return HtmlApiResponse(
|
|
275
|
+
ok=True,
|
|
276
|
+
status_code=200,
|
|
277
|
+
content=self.render_template(template_name, kwargs),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def response_proxy(self, response: InternalApiResponse[TResp]) -> ProxyJsonApiResponse[TResp]:
|
|
281
|
+
try:
|
|
282
|
+
response.check()
|
|
283
|
+
except AbstractInternalApiError:
|
|
284
|
+
pass
|
|
285
|
+
return ProxyJsonApiResponse(ok=response.ok, status_code=response.status_code, response=response.result_json)
|
|
286
|
+
|
|
287
|
+
def response_redirect(self, location: str) -> RedirectApiResponse:
|
|
288
|
+
return RedirectApiResponse(ok=True, location=location)
|
|
289
|
+
|
|
290
|
+
def response_file_ok(
|
|
291
|
+
self,
|
|
292
|
+
path_or_file: Union[str, BinaryIO, io.BytesIO],
|
|
293
|
+
|
|
294
|
+
mimetype: Optional[str], # it will be auto-detected by extension if mimetype==None
|
|
295
|
+
as_attachment: bool = False,
|
|
296
|
+
download_name: Optional[str] = None,
|
|
297
|
+
conditional: bool = True,
|
|
298
|
+
etag: Union[bool, str] = True,
|
|
299
|
+
last_modified: Optional[Union[datetime, int, float]] = None,
|
|
300
|
+
max_age: Optional[Union[int, Callable[[Optional[str]], Optional[int]]]] = None,
|
|
301
|
+
) -> FileApiResponse:
|
|
302
|
+
ApiResourceType.FILE.validate(self._type)
|
|
303
|
+
if isinstance(path_or_file, str) and not os.path.exists(path_or_file):
|
|
304
|
+
raise NoResultFoundApiError(f'file "{path_or_file}" is not exists')
|
|
305
|
+
return FileApiResponse(
|
|
306
|
+
ok=True,
|
|
307
|
+
status_code=200,
|
|
308
|
+
file_path=path_or_file,
|
|
309
|
+
mimetype=mimetype,
|
|
310
|
+
as_attachment=as_attachment,
|
|
311
|
+
download_name=download_name,
|
|
312
|
+
conditional=conditional,
|
|
313
|
+
etag=etag,
|
|
314
|
+
last_modified=last_modified,
|
|
315
|
+
max_age=max_age,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def response_created_ok(self, payload: 'TPayloadInputUnion', total_count: Optional[int] = None) -> JsonApiResponse[Any]:
|
|
319
|
+
ApiResourceType.API.validate(self._type)
|
|
320
|
+
return JsonApiResponse(
|
|
321
|
+
ok=True,
|
|
322
|
+
total_count=total_count,
|
|
323
|
+
payload=payload,
|
|
324
|
+
status_code=201,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def response_empty_ok(self) -> EmptyJsonApiResponse:
|
|
328
|
+
return EmptyJsonApiResponse(ok=True, status_code=200)
|
|
329
|
+
|
|
330
|
+
def response_root(self, payload: 'TPayloadInputUnion', status_code: int = 200) -> RootJsonApiResponse[Any]:
|
|
331
|
+
ApiResourceType.API.validate(self._type)
|
|
332
|
+
return RootJsonApiResponse(ok=True, status_code=status_code, root=payload)
|
|
333
|
+
|
|
334
|
+
def response_ok(self, payload: 'TPayloadInputUnion', total_count: Optional[int] = None) -> JsonApiResponse[Any]:
|
|
335
|
+
return JsonApiResponse(
|
|
336
|
+
ok=True,
|
|
337
|
+
payload=payload,
|
|
338
|
+
total_count=total_count,
|
|
339
|
+
status_code=200,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def response_deleted_ok(self) -> JsonApiResponse[Any]:
|
|
343
|
+
ApiResourceType.API.validate(self._type)
|
|
344
|
+
|
|
345
|
+
return JsonApiResponse(
|
|
346
|
+
ok=True,
|
|
347
|
+
payload=self.default_response_payload,
|
|
348
|
+
total_count=0 if self._fn_typing.response_payload_many else None,
|
|
349
|
+
status_code=200,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _internal_use__check_access(self, jwt_token: Optional[Tuple[Optional[str], str]]) -> None:
|
|
353
|
+
assert self._token is None
|
|
354
|
+
|
|
355
|
+
if self._access == GLOBAL_PERMISSION__PUBLIC:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
if jwt_token is None:
|
|
359
|
+
raise AccessApiError('empty token')
|
|
360
|
+
|
|
361
|
+
auth_username, auth_token = jwt_token
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
t = ApiSdkJwt.decode(
|
|
365
|
+
token=auth_token,
|
|
366
|
+
username=auth_username,
|
|
367
|
+
certificate=APPLICATION_JWT_PUBLIC_KEY,
|
|
368
|
+
)
|
|
369
|
+
except Exception: # noqa: B902
|
|
370
|
+
raise AccessApiError('invalid token data')
|
|
371
|
+
|
|
372
|
+
if t.version != JWT_VERSION:
|
|
373
|
+
raise AccessApiError('token has invalid version')
|
|
374
|
+
|
|
375
|
+
if self._config.jwt_environment_check_enabled and t.env != APPLICATION_ENV:
|
|
376
|
+
raise AccessApiError('token has invalid environment')
|
|
377
|
+
|
|
378
|
+
if t.is_expired:
|
|
379
|
+
raise AccessApiError('token is expired')
|
|
380
|
+
|
|
381
|
+
if not self._config.permissions_check_enabled:
|
|
382
|
+
self._token = t
|
|
383
|
+
self._token_raw = auth_token
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
if self._config.jwt_validator is not None:
|
|
387
|
+
try:
|
|
388
|
+
jwt_validation_result = self._config.jwt_validator(t)
|
|
389
|
+
if jwt_validation_result is not None and not jwt_validation_result:
|
|
390
|
+
raise AccessApiError('Permission denied')
|
|
391
|
+
except Exception as e: # noqa: B902
|
|
392
|
+
raise AccessApiError(str(e))
|
|
393
|
+
|
|
394
|
+
if self._access == GLOBAL_PERMISSION__PRIVATE_RT:
|
|
395
|
+
if not t.is_refresh_token:
|
|
396
|
+
raise AccessApiError('invalid token type')
|
|
397
|
+
self._token_raw = auth_token
|
|
398
|
+
self._token = t
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
if not t.is_access_token:
|
|
402
|
+
raise AccessApiError('invalid token type')
|
|
403
|
+
|
|
404
|
+
if self._access != GLOBAL_PERMISSION__PRIVATE:
|
|
405
|
+
if self._config.permissions_validator is not None:
|
|
406
|
+
perm_valid_result = self._config.permissions_validator(t, self._access)
|
|
407
|
+
if perm_valid_result is not None and not perm_valid_result:
|
|
408
|
+
raise PermissionDeniedApiError('no rights to make this action')
|
|
409
|
+
else:
|
|
410
|
+
if not t.has_permission(self._access):
|
|
411
|
+
raise PermissionDeniedApiError('no rights to make this action')
|
|
412
|
+
|
|
413
|
+
self._token_raw = auth_token
|
|
414
|
+
self._token = t
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Optional, Callable, Tuple
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict, BaseModel
|
|
4
|
+
|
|
5
|
+
from ul_api_utils.api_resource.api_response import ApiResponse
|
|
6
|
+
from werkzeug import Response as BaseResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiResourceConfig(BaseModel):
|
|
10
|
+
swagger_group: Optional[str] = None
|
|
11
|
+
swagger_disabled: bool = False
|
|
12
|
+
exc_handler_bad_request: Optional[Callable[[Exception], Optional[ApiResponse]]] = None
|
|
13
|
+
exc_handler_access: Optional[Callable[[Exception], Optional[ApiResponse]]] = None
|
|
14
|
+
exc_handler_endpoint: Optional[Callable[[Exception], Optional[ApiResponse]]] = None
|
|
15
|
+
override_flask_response: Optional[Callable[[Tuple[BaseResponse, int]], Tuple[BaseResponse, int]]] = None
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(
|
|
18
|
+
extra="forbid",
|
|
19
|
+
frozen=True,
|
|
20
|
+
)
|
|
@@ -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")
|