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,296 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from enum import IntEnum, unique
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import Any, Optional, List, Generic, Type, TypeVar, Union, Dict, NamedTuple
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from ul_api_utils.api_resource.api_response import JsonApiResponsePayload, RootJsonApiResponsePayload
|
|
11
|
+
from ul_api_utils.api_resource.signature_check import set_model
|
|
12
|
+
from ul_api_utils.const import RESPONSE_PROP_DEBUG_STATS, RESPONSE_PROP_PAYLOAD, RESPONSE_PROP_OK, RESPONSE_PROP_ERRORS, RESPONSE_PROP_TOTAL, RESPONSE_PROP_COUNT, MIME__JSON, \
|
|
13
|
+
RESPONSE_HEADER__CONTENT_TYPE
|
|
14
|
+
from ul_api_utils.errors import Server5XXInternalApiError, Client4XXInternalApiError, \
|
|
15
|
+
ResponseStatusAbstractInternalApiError, ResponsePayloadTypeInternalApiError, ResponseJsonSchemaInternalApiError, ResponseJsonInternalApiError
|
|
16
|
+
from ul_api_utils.internal_api.internal_api_check_context import internal_api_check_context_rm_response, internal_api_check_context_add_response
|
|
17
|
+
from ul_api_utils.utils.api_format import ApiFormat
|
|
18
|
+
from ul_api_utils.internal_api.internal_api_error import InternalApiResponseErrorObj
|
|
19
|
+
from ul_api_utils.utils.unwrap_typing import UnwrappedOptionalObjOrListOfObj
|
|
20
|
+
|
|
21
|
+
TPyloadType = TypeVar('TPyloadType', bound=Union[JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]], None])
|
|
22
|
+
TResp = TypeVar('TResp', bound='InternalApiResponse[Any]')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CHECK_TYPE = {'payload_type', 'json', 'std_schema'}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InternalApiResponseSchema(NamedTuple):
|
|
29
|
+
response_ok: bool
|
|
30
|
+
response_status_code: int
|
|
31
|
+
response_payload_raw: Any
|
|
32
|
+
response_errors: List[InternalApiResponseErrorObj]
|
|
33
|
+
response_total_count: Optional[int]
|
|
34
|
+
response_count: Optional[int]
|
|
35
|
+
response_many: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@unique
|
|
39
|
+
class InternalApiResponseCheckLevel(IntEnum):
|
|
40
|
+
STATUS_CODE = 1
|
|
41
|
+
JSON = 2
|
|
42
|
+
STD_SCHEMA = 3
|
|
43
|
+
PAYLOAD_TYPE = 4
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return f'{type(self).__name__}.{self.name}'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InternalApiOverriddenResponse(NamedTuple):
|
|
50
|
+
status_code: int
|
|
51
|
+
text: str
|
|
52
|
+
headers: Dict[str, str]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def content(self) -> bytes:
|
|
56
|
+
return self.text.encode('utf-8')
|
|
57
|
+
|
|
58
|
+
def json(self) -> Any:
|
|
59
|
+
return json.loads(self.text)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InternalApiResponse(Generic[TPyloadType]):
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def id(self) -> uuid.UUID:
|
|
66
|
+
return self._id
|
|
67
|
+
|
|
68
|
+
def __init__(self, info: str, resp: Union[requests.Response, InternalApiOverriddenResponse], std_schema: bool, payload_type: Optional[Type[TPyloadType]]) -> None:
|
|
69
|
+
self._id = uuid.uuid4()
|
|
70
|
+
self._resp = resp
|
|
71
|
+
self._has_std_schema = std_schema
|
|
72
|
+
self._payload_type = None
|
|
73
|
+
|
|
74
|
+
self._parsed_schema: Optional[InternalApiResponseSchema] = None
|
|
75
|
+
self._parsed_json: Any = None
|
|
76
|
+
self._parsed_payload: Union[None, JsonApiResponsePayload, List[JsonApiResponsePayload], RootJsonApiResponsePayload[Any], List[RootJsonApiResponsePayload[Any]]] = None
|
|
77
|
+
|
|
78
|
+
self._status_checked = False
|
|
79
|
+
self._info = info
|
|
80
|
+
|
|
81
|
+
if payload_type is not None:
|
|
82
|
+
self._payload_type = UnwrappedOptionalObjOrListOfObj.parse(payload_type, JsonApiResponsePayload) or UnwrappedOptionalObjOrListOfObj.parse(payload_type, RootJsonApiResponsePayload)
|
|
83
|
+
if self._payload_type is None:
|
|
84
|
+
tn1, tn2 = JsonApiResponsePayload.__name__, RootJsonApiResponsePayload.__name__
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f'payload_typing is invalid. must be Union[Type[{tn1} | {tn2}], Optional[Type[{tn1} | {tn2}]], List[Type[{tn1} | {tn2}]], Optional[List[Type[{tn1} | {tn2}]]]]. {payload_type} was given',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def typed(self, payload_type: Type[TPyloadType]) -> 'InternalApiResponse[TPyloadType]':
|
|
90
|
+
assert payload_type is not None
|
|
91
|
+
internal_api_check_context_rm_response(self._id)
|
|
92
|
+
new_one = InternalApiResponse(self._info, self._resp, self._has_std_schema, payload_type)
|
|
93
|
+
internal_api_check_context_add_response(new_one)
|
|
94
|
+
return new_one
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def has_payload_type(self) -> bool:
|
|
98
|
+
return self._payload_type is not None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def many(self) -> bool:
|
|
102
|
+
if self._payload_type is not None:
|
|
103
|
+
return self._payload_type.many
|
|
104
|
+
return self._schema.response_many
|
|
105
|
+
|
|
106
|
+
# ERROR HANDLING
|
|
107
|
+
|
|
108
|
+
def check(self: TResp, *, level: InternalApiResponseCheckLevel = InternalApiResponseCheckLevel.PAYLOAD_TYPE) -> TResp:
|
|
109
|
+
assert isinstance(level, InternalApiResponseCheckLevel)
|
|
110
|
+
self._raise_for_err(self._status_code_error)
|
|
111
|
+
|
|
112
|
+
if level < InternalApiResponseCheckLevel.JSON:
|
|
113
|
+
return self
|
|
114
|
+
self._raise_for_err(self._result_json_error)
|
|
115
|
+
|
|
116
|
+
if level < InternalApiResponseCheckLevel.STD_SCHEMA:
|
|
117
|
+
return self
|
|
118
|
+
self._raise_for_err(self._result_json_schema_error)
|
|
119
|
+
|
|
120
|
+
if level < InternalApiResponseCheckLevel.PAYLOAD_TYPE:
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
if self._payload_type is None:
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
self._raise_for_err(self._payload_type_error)
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
@cached_property
|
|
130
|
+
def _schema(self) -> InternalApiResponseSchema:
|
|
131
|
+
self._raise_for_err(self._status_code_error, self._status_checked)
|
|
132
|
+
self._raise_for_err(self._result_json_error)
|
|
133
|
+
self._raise_for_err(self._result_json_schema_error)
|
|
134
|
+
assert self._parsed_schema is not None
|
|
135
|
+
return self._parsed_schema
|
|
136
|
+
|
|
137
|
+
@cached_property
|
|
138
|
+
def _payload_type_error(self) -> Optional[ResponsePayloadTypeInternalApiError]:
|
|
139
|
+
if self._payload_type is None:
|
|
140
|
+
return ResponsePayloadTypeInternalApiError('payload type is invalid', ValueError('payload_type for response must be specified'))
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
assert self._parsed_schema is not None
|
|
144
|
+
self._parsed_payload = self._payload_type.apply(self._parsed_schema.response_payload_raw, set_model)
|
|
145
|
+
except Exception as e: # noqa: B902
|
|
146
|
+
return ResponsePayloadTypeInternalApiError('payload schema type is not valid', e)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def _result_json_schema_error(self) -> Optional[ResponseJsonSchemaInternalApiError]:
|
|
151
|
+
ok = self._resp.status_code < 400
|
|
152
|
+
payload_raw = self._parsed_json
|
|
153
|
+
errors = []
|
|
154
|
+
response_many = isinstance(payload_raw, list)
|
|
155
|
+
total_count = len(payload_raw) if response_many else None
|
|
156
|
+
count = len(payload_raw) if response_many else None
|
|
157
|
+
|
|
158
|
+
if self._has_std_schema:
|
|
159
|
+
try:
|
|
160
|
+
assert isinstance(self._parsed_json, dict)
|
|
161
|
+
ok = self._parsed_json.get(RESPONSE_PROP_OK, False)
|
|
162
|
+
total_count = self._parsed_json.get(RESPONSE_PROP_TOTAL, None)
|
|
163
|
+
assert count is None or isinstance(total_count, int), f'total_count must be int. "{type(total_count).__name__}" was given'
|
|
164
|
+
count = self._parsed_json.get(RESPONSE_PROP_COUNT, None)
|
|
165
|
+
assert count is None or isinstance(count, int), f'count must be int. "{type(count).__name__}" was given'
|
|
166
|
+
payload_raw = self._parsed_json.get(RESPONSE_PROP_PAYLOAD, None)
|
|
167
|
+
response_many = isinstance(payload_raw, list)
|
|
168
|
+
|
|
169
|
+
if response_many:
|
|
170
|
+
assert count is not None and count >= 0
|
|
171
|
+
assert total_count is not None and total_count >= 0
|
|
172
|
+
|
|
173
|
+
errors = self._mk_std_errors(self._parsed_json)
|
|
174
|
+
except Exception as e: # noqa: B902
|
|
175
|
+
return ResponseJsonSchemaInternalApiError('invalid schema', e)
|
|
176
|
+
|
|
177
|
+
self._parsed_schema = InternalApiResponseSchema(
|
|
178
|
+
response_ok=ok,
|
|
179
|
+
response_status_code=self._resp.status_code,
|
|
180
|
+
response_payload_raw=payload_raw,
|
|
181
|
+
response_errors=errors,
|
|
182
|
+
response_total_count=total_count,
|
|
183
|
+
response_count=count,
|
|
184
|
+
response_many=response_many,
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def _internal_use__checked_once(self) -> bool:
|
|
190
|
+
return self._status_checked
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def _internal_use__info(self) -> str:
|
|
194
|
+
return self._info
|
|
195
|
+
|
|
196
|
+
@cached_property
|
|
197
|
+
def _result_json_error(self) -> Optional[ResponseJsonInternalApiError]:
|
|
198
|
+
content_mime: str = self._resp.headers.get(RESPONSE_HEADER__CONTENT_TYPE, MIME__JSON)
|
|
199
|
+
api_format = ApiFormat.from_mime(content_mime)
|
|
200
|
+
if api_format is None:
|
|
201
|
+
return ResponseJsonInternalApiError('content is not parsable as json', ValueError(f'content type "{content_mime}" was given'))
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
self._parsed_json = api_format.parse_bytes(self._resp.content)
|
|
205
|
+
except Exception as e: # noqa: B902
|
|
206
|
+
return ResponseJsonInternalApiError('content is not parsable as json', e)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def _mk_std_errors(self, result_json: Dict[str, Any]) -> List[InternalApiResponseErrorObj]:
|
|
210
|
+
if not isinstance(result_json, dict):
|
|
211
|
+
return [] # type: ignore
|
|
212
|
+
return [set_model(InternalApiResponseErrorObj, error) for error in result_json.get(RESPONSE_PROP_ERRORS, [])]
|
|
213
|
+
|
|
214
|
+
@cached_property
|
|
215
|
+
def _status_code_error(self) -> Optional[ResponseStatusAbstractInternalApiError]:
|
|
216
|
+
self._status_checked = True
|
|
217
|
+
code = self._resp.status_code
|
|
218
|
+
|
|
219
|
+
if code >= 400:
|
|
220
|
+
errors = []
|
|
221
|
+
if self._has_std_schema and self._result_json_error is None:
|
|
222
|
+
try:
|
|
223
|
+
errors = self._mk_std_errors(self._parsed_json)
|
|
224
|
+
except ValidationError:
|
|
225
|
+
pass
|
|
226
|
+
return Server5XXInternalApiError(code, errors) if code >= 500 else Client4XXInternalApiError(code, errors)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _raise_for_err(self, err: Optional[Exception], checked: bool = False) -> None:
|
|
230
|
+
if not checked and err is not None:
|
|
231
|
+
raise err
|
|
232
|
+
|
|
233
|
+
# STANDARD PAYLOAD SCHEMA PROPERTIES
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def ok(self) -> bool:
|
|
237
|
+
return self._schema.response_ok
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def total_count(self) -> Optional[int]:
|
|
241
|
+
sch = self._schema
|
|
242
|
+
if not sch.response_many:
|
|
243
|
+
raise OverflowError('count it is not permitted for not array payload')
|
|
244
|
+
return sch.response_total_count
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def count(self) -> Optional[int]:
|
|
248
|
+
sch = self._schema
|
|
249
|
+
if not sch.response_many:
|
|
250
|
+
raise OverflowError('count it is not permitted for not array payload')
|
|
251
|
+
return sch.response_count
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def errors(self) -> List[InternalApiResponseErrorObj]:
|
|
255
|
+
return self._schema.response_errors
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def payload_raw(self) -> Union[Optional[Dict[str, Any]], List[Dict[str, Any]]]: # for backward compatibility
|
|
259
|
+
return self._schema.response_payload_raw
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def payload(self) -> TPyloadType:
|
|
263
|
+
_sch = self._schema # noqa: F841
|
|
264
|
+
self._raise_for_err(self._payload_type_error)
|
|
265
|
+
return self._parsed_payload # type: ignore
|
|
266
|
+
|
|
267
|
+
@cached_property
|
|
268
|
+
def internal_use__debug_stats(self) -> List[List[Any]]:
|
|
269
|
+
if not self._has_std_schema:
|
|
270
|
+
return []
|
|
271
|
+
if self._result_json_error is not None or self._parsed_json is None:
|
|
272
|
+
return []
|
|
273
|
+
return self._parsed_json.get(RESPONSE_PROP_DEBUG_STATS, [])
|
|
274
|
+
|
|
275
|
+
# STANDARD PAYLOAD PROPS
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def status_code(self) -> int:
|
|
279
|
+
self._raise_for_err(self._status_code_error, self._status_checked)
|
|
280
|
+
return self._resp.status_code
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def result_bytes(self) -> bytes:
|
|
284
|
+
self._raise_for_err(self._status_code_error, self._status_checked)
|
|
285
|
+
return self._resp.content
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def result_text(self) -> str:
|
|
289
|
+
self._raise_for_err(self._status_code_error, self._status_checked)
|
|
290
|
+
return self._resp.text
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def result_json(self) -> Any:
|
|
294
|
+
self._raise_for_err(self._status_code_error, self._status_checked)
|
|
295
|
+
self._raise_for_err(self._result_json_error)
|
|
296
|
+
return self._parsed_json
|
ul_api_utils/main.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
|
6
|
+
|
|
7
|
+
from ul_api_utils.commands.cmd_worker_start import CmdWorkerStart
|
|
8
|
+
from ul_py_tool.commands.cmd import Cmd
|
|
9
|
+
from ul_api_utils.commands.cmd_enc_keys import CmdEncKeys
|
|
10
|
+
from ul_api_utils.commands.cmd_gen_new_api_user import CmdGenerateNewApiUser
|
|
11
|
+
from ul_api_utils.commands.cmd_gen_api_user_token import CmdGenerateApiUserToken
|
|
12
|
+
from ul_api_utils.commands.cmd_generate_api_docs import CmdGenApiFunctionDocumentation
|
|
13
|
+
|
|
14
|
+
from ul_api_utils.commands.cmd_start import CmdStart
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
Cmd.main({
|
|
19
|
+
'start': CmdStart,
|
|
20
|
+
'enc_keys': CmdEncKeys,
|
|
21
|
+
'start_worker': CmdWorkerStart,
|
|
22
|
+
'gen_new_api_user': CmdGenerateNewApiUser,
|
|
23
|
+
'gen_api_user_token': CmdGenerateApiUserToken,
|
|
24
|
+
'gen_api_docs': CmdGenApiFunctionDocumentation,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == '__main__':
|
|
29
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime, time
|
|
4
|
+
from typing import List
|
|
5
|
+
from uuid import uuid4, UUID
|
|
6
|
+
|
|
7
|
+
from ul_py_tool.utils.write_stdout import write_stdout
|
|
8
|
+
|
|
9
|
+
from ul_api_utils.conf import APPLICATION_ENV
|
|
10
|
+
from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_score(name: str, compressed: str, uncompressed: str) -> None:
|
|
14
|
+
write_stdout(
|
|
15
|
+
f'\n\n{name} '
|
|
16
|
+
f':: uncompressed={len(uncompressed)} '
|
|
17
|
+
f'({sys.getsizeof(uncompressed)}) '
|
|
18
|
+
f':: compressed={len(compressed)} '
|
|
19
|
+
f'({sys.getsizeof(compressed)}) '
|
|
20
|
+
f':: diff={((len(uncompressed) - len(compressed)) / len(uncompressed)) * 100:0.3f}% '
|
|
21
|
+
f'({((sys.getsizeof(uncompressed) - sys.getsizeof(compressed)) / sys.getsizeof(uncompressed)) * 100:0.3f}%)',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_jwt_compressed() -> None:
|
|
26
|
+
att, _rtt = ApiSdkJwt.create_jwt_pair(
|
|
27
|
+
environment='production',
|
|
28
|
+
user_id=UUID('1870e062-7914-4e56-8fe7-dad7b00a67c3'),
|
|
29
|
+
organization_id=UUID("5dca46d3-8fcb-4ce7-856e-f6c0104f3b72"),
|
|
30
|
+
permissions=[13127, 91701, 91702, 91703, 91704, 91705, 90901, 90905, 91801, 91802, 91803, 91804, 91805, 10007, 90301, 13068, 13014, 13017, 13032, 13033, 13034, 13072, 13040, 13041, 13042, 13043, 13044, 13046, 13047, 13048, 13049, 13050, 13052, 13053, 13054, 13055, 13056, 13057, 13058, 13059, 13060, 13061, 13062, 13064, 91401, 91402, 13065, 13066, 91403, 91404, 91405, 91406, 10001, 13067, 13069, 13076, 13077, 10005, 13070, 13071, 90902, 90903, 90904, 90906, 10013, 90907, 90908, 90909, 90401, 90402, 90403, 90404, 90405, 90406, 90407, 90408, 13083, 13084, 13085, 13091, 10021, 10023, 13086, 10025, 13097, 10034, 13098, 13087, 10029, 10031, 10033, 13105, 13106, 13088, 10036, 10037, 10039, 10040, 13089, 13113, 13112, 13114, 13115, 13090, 13116, 13122, 13118, 13119, 13120, 13121, 13125, 13132, 13124, 13126, 13092, 13128, 13129, 13130, 13131, 13093, 13133, 13134, 10041, 13094, 13095, 13080, 13096, 91501, 91502, 91503, 91504, 13099, 91505, 91506, 13100, 91001, 13081, 13101, 13102, 13103, 90501, 90502, 90503, 90504, 13104, 90505, 90506, 90507, 90508, 90001, 90002, 90003, 13082, 13107, 13108, 13109, 13110, 13111, 13117, 91601, 91602, 91603, 91604, 91605, 90101, 90102, 90103, 90104, 90105, 90106, 90107, 90108, 90109, 90110, 90111], # noqa: E501
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
for algo in ['RS256', 'ES256']:
|
|
34
|
+
priv_k, pub_k_fn = ApiSdkJwt.generate_cert(algo) # type: ignore
|
|
35
|
+
pub_key = pub_k_fn()
|
|
36
|
+
|
|
37
|
+
uncompressed = att.encode(priv_k, algo) # type: ignore
|
|
38
|
+
compressed = att.encode(priv_k, algo, compressed=True) # type: ignore
|
|
39
|
+
|
|
40
|
+
print_score(algo, compressed, uncompressed)
|
|
41
|
+
|
|
42
|
+
t_decoded_uncompressed = ApiSdkJwt.decode(uncompressed, pub_key)._asdict()
|
|
43
|
+
t_decoded_compressed = ApiSdkJwt.decode(compressed, pub_key)._asdict()
|
|
44
|
+
|
|
45
|
+
t_decoded_uncompressed.pop('raw')
|
|
46
|
+
t_decoded_compressed.pop('raw')
|
|
47
|
+
|
|
48
|
+
t_decoded_uncompressed['exp_date'] = datetime.combine(
|
|
49
|
+
t_decoded_uncompressed['exp_date'].date(),
|
|
50
|
+
time(
|
|
51
|
+
hour=t_decoded_uncompressed['exp_date'].time().hour,
|
|
52
|
+
minute=t_decoded_uncompressed['exp_date'].time().minute,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert t_decoded_uncompressed == t_decoded_compressed
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_jwt_cert() -> None:
|
|
60
|
+
for algo in ['RS256', 'ES256']:
|
|
61
|
+
pk1, pkf1 = ApiSdkJwt.generate_cert(algo) # type: ignore
|
|
62
|
+
|
|
63
|
+
pk2, pkf2 = ApiSdkJwt.load_cert(pk1)
|
|
64
|
+
|
|
65
|
+
att, rtt = ApiSdkJwt.create_jwt_pair(
|
|
66
|
+
environment=APPLICATION_ENV,
|
|
67
|
+
user_id=uuid4(),
|
|
68
|
+
organization_id=uuid4(),
|
|
69
|
+
permissions=[],
|
|
70
|
+
)
|
|
71
|
+
at, *_ = att.encode(pk1, algo), rtt.encode(pk1, algo) # type: ignore
|
|
72
|
+
|
|
73
|
+
assert ApiSdkJwt.decode(at, pkf1()) == ApiSdkJwt.decode(at, pkf2())
|
|
74
|
+
|
|
75
|
+
assert pk1 == pk2
|
|
76
|
+
|
|
77
|
+
for algo1, algo2 in [('RS256', 'ES256'), ('ES256', 'RS256')]:
|
|
78
|
+
pk1, _pkf1 = ApiSdkJwt.generate_cert(algo1) # type: ignore
|
|
79
|
+
|
|
80
|
+
att, _rtt = ApiSdkJwt.create_jwt_pair(
|
|
81
|
+
environment=APPLICATION_ENV,
|
|
82
|
+
user_id=uuid4(),
|
|
83
|
+
organization_id=uuid4(),
|
|
84
|
+
permissions=[],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
att.encode(pk1, algo2) # type: ignore
|
|
89
|
+
except Exception: # noqa: B902
|
|
90
|
+
pass
|
|
91
|
+
else:
|
|
92
|
+
raise AssertionError()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_jwt() -> None:
|
|
96
|
+
for algo in ['RS256', 'ES256']:
|
|
97
|
+
private_key, public_key_factory = ApiSdkJwt.generate_cert(algo) # type: ignore
|
|
98
|
+
att, rtt = ApiSdkJwt.create_jwt_pair(
|
|
99
|
+
environment=APPLICATION_ENV,
|
|
100
|
+
user_id=uuid4(),
|
|
101
|
+
organization_id=uuid4(),
|
|
102
|
+
permissions=[13127, 91701, 91702, 91703, 91704, 91705, 90901, 90905, 91801, 91802, 91803, 91804, 91805, 10007, 90301, 13068, 13014, 13017, 13032, 13033, 13034, 13072, 13040, 13041, 13042, 13043, 13044, 13046, 13047, 13048, 13049, 13050, 13052, 13053, 13054, 13055, 13056, 13057, 13058, 13059, 13060, 13061, 13062, 13064, 91401, 91402, 13065, 13066, 91403, 91404, 91405, 91406, 10001, 13067, 13069, 13076, 13077, 10005, 13070, 13071, 90902, 90903, 90904, 90906, 10013, 90907, 90908, 90909, 90401, 90402, 90403, 90404, 90405, 90406, 90407, 90408, 13083, 13084, 13085, 13091, 10021, 10023, 13086, 10025, 13097, 10034, 13098, 13087, 10029, 10031, 10033, 13105, 13106, 13088, 10036, 10037, 10039, 10040, 13089, 13113, 13112, 13114, 13115, 13090, 13116, 13122, 13118, 13119, 13120, 13121, 13125, 13132, 13124, 13126, 13092, 13128, 13129, 13130, 13131, 13093, 13133, 13134, 10041, 13094, 13095, 13080, 13096, 91501, 91502, 91503, 91504, 13099, 91505, 91506, 13100, 91001, 13081, 13101, 13102, 13103, 90501, 90502, 90503, 90504, 13104, 90505, 90506, 90507, 90508, 90001, 90002, 90003, 13082, 13107, 13108, 13109, 13110, 13111, 13117, 91601, 91602, 91603, 91604, 91605, 90101, 90102, 90103, 90104, 90105, 90106, 90107, 90108, 90109, 90110, 90111], # noqa: E501,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
at, rt = att.encode(private_key, algo), rtt.encode(private_key, algo) # type: ignore
|
|
106
|
+
|
|
107
|
+
parsed_at = ApiSdkJwt.decode(at, public_key_factory())
|
|
108
|
+
|
|
109
|
+
assert att.env == parsed_at.env == APPLICATION_ENV
|
|
110
|
+
assert att.id == parsed_at.id
|
|
111
|
+
assert att.user_id == parsed_at.user_id
|
|
112
|
+
assert att.organization_id == parsed_at.organization_id
|
|
113
|
+
assert att.version == parsed_at.version
|
|
114
|
+
assert att.token_type == parsed_at.token_type
|
|
115
|
+
assert att.exp_date == parsed_at.exp_date
|
|
116
|
+
assert att.permissions == parsed_at.permissions
|
|
117
|
+
assert att.additional_data == parsed_at.additional_data
|
|
118
|
+
|
|
119
|
+
new_at = ApiSdkJwt.decode(rt, public_key_factory()).create_access_token().encode(private_key, algo, True) # type: ignore
|
|
120
|
+
|
|
121
|
+
parsed_t = ApiSdkJwt.decode(token=new_at, username=None, certificate=public_key_factory())
|
|
122
|
+
|
|
123
|
+
assert parsed_t.env == rtt.env == APPLICATION_ENV
|
|
124
|
+
assert parsed_t.id == rtt.id
|
|
125
|
+
assert parsed_t.user_id == rtt.user_id
|
|
126
|
+
assert parsed_t.organization_id == rtt.organization_id
|
|
127
|
+
assert parsed_t.version == rtt.version
|
|
128
|
+
assert parsed_t.token_type == 'access'
|
|
129
|
+
assert parsed_t.exp_date <= rtt.exp_date
|
|
130
|
+
assert parsed_t.permissions == rtt.permissions
|
|
131
|
+
assert parsed_t.additional_data == rtt.additional_data
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# opts: [5] => [1111011, 11110000011110011]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_permission_compression() -> None:
|
|
138
|
+
c_permissions = ApiSdkJwt.compress_permissions([1, 2, 5, 3, 4])
|
|
139
|
+
assert c_permissions == '%1'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_permission_decompression() -> None:
|
|
143
|
+
c_permissions = ApiSdkJwt.decompress_permissions('A5')
|
|
144
|
+
assert c_permissions == [1, 2, 3, 4, 5]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_permission_compression_decompression() -> None:
|
|
148
|
+
permissions = [13127, 91701, 91702, 91703, 91704, 91705, 90901, 90905, 91801, 91802, 91803, 91804, 91805, 10007, 90301, 13068, 13014, 13017, 13032, 13033, 13034, 13072, 13040, 13041, 13042, 13043, 13044, 13046, 13047, 13048, 13049, 13050, 13052, 13053, 13054, 13055, 13056, 13057, 13058, 13059, 13060, 13061, 13062, 13064, 91401, 91402, 13065, 13066, 91403, 91404, 91405, 91406, 10001, 13067, 13069, 13076, 13077, 10005, 13070, 13071, 90902, 90903, 90904, 90906, 10013, 90907, 90908, 90909, 90401, 90402, 90403, 90404, 90405, 90406, 90407, 90408, 13083, 13084, 13085, 13091, 10021, 10023, 13086, 10025, 13097, 10034, 13098, 13087, 10029, 10031, 10033, 13105, 13106, 13088, 10036, 10037, 10039, 10040, 13089, 13113, 13112, 13114, 13115, 13090, 13116, 13122, 13118, 13119, 13120, 13121, 13125, 13132, 13124, 13126, 13092, 13128, 13129, 13130, 13131, 13093, 13133, 13134, 10041, 13094, 13095, 13080, 13096, 91501, 91502, 91503, 91504, 13099, 91505, 91506, 13100, 91001, 13081, 13101, 13102, 13103, 90501, 90502, 90503, 90504, 13104, 90505, 90506, 90507, 90508, 90001, 90002, 90003, 13082, 13107, 13108, 13109, 13110, 13111, 13117, 91601, 91602, 91603, 91604, 91605, 90101, 90102, 90103, 90104, 90105, 90106, 90107, 90108, 90109, 90110, 90111] # noqa: E501
|
|
149
|
+
p_permissions = list(sorted(set(permissions)))
|
|
150
|
+
c_permissions = ApiSdkJwt.compress_permissions(permissions)
|
|
151
|
+
assert p_permissions == ApiSdkJwt.decompress_permissions(c_permissions)
|
|
152
|
+
|
|
153
|
+
r_p = json.dumps(p_permissions, separators=(',', ':'))
|
|
154
|
+
r_c = json.dumps(c_permissions, separators=(',', ':'))
|
|
155
|
+
|
|
156
|
+
print_score('', r_c, r_p)
|
|
157
|
+
write_stdout(c_permissions)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_permission_compression_decompression2() -> None:
|
|
161
|
+
compressed_win = 0
|
|
162
|
+
total_len = 0
|
|
163
|
+
for step in range(1, 999):
|
|
164
|
+
for i in range(0, 100):
|
|
165
|
+
permissions = list(range(999, 999 + step * i, step))
|
|
166
|
+
p_permissions = list(sorted(set(permissions)))
|
|
167
|
+
c_permissions = ApiSdkJwt.compress_permissions(permissions)
|
|
168
|
+
|
|
169
|
+
p_l = len(json.dumps(p_permissions, separators=(',', ':')))
|
|
170
|
+
total_len += p_l
|
|
171
|
+
compressed_win += p_l - len(json.dumps(c_permissions, separators=(',', ':')))
|
|
172
|
+
assert p_permissions == ApiSdkJwt.decompress_permissions(c_permissions), f'{i}::{step}::{c_permissions}'
|
|
173
|
+
|
|
174
|
+
write_stdout(f'compressed_win={compressed_win} total_len={total_len} {compressed_win * 100 / total_len:0.3f}%')
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_permission_compression_decompression4() -> None:
|
|
178
|
+
import random
|
|
179
|
+
r = random.Random()
|
|
180
|
+
r.seed("test")
|
|
181
|
+
|
|
182
|
+
permissions: List[int] = []
|
|
183
|
+
for j in range(1, 501, 20):
|
|
184
|
+
for _i in range(10000):
|
|
185
|
+
v = r.randint(1, j)
|
|
186
|
+
permissions.append(v if not permissions else permissions[-1] + v)
|
|
187
|
+
|
|
188
|
+
p_permissions = list(sorted(set(permissions)))
|
|
189
|
+
c_permissions = ApiSdkJwt.compress_permissions(permissions)
|
|
190
|
+
assert p_permissions == ApiSdkJwt.decompress_permissions(c_permissions), c_permissions
|
|
191
|
+
|
|
192
|
+
r_p = json.dumps(p_permissions, separators=(',', ':'))
|
|
193
|
+
r_c = json.dumps(c_permissions, separators=(',', ':'))
|
|
194
|
+
|
|
195
|
+
print_score('', r_c, r_p)
|