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.
Files changed (156) hide show
  1. example/__init__.py +0 -0
  2. example/conf.py +35 -0
  3. example/main.py +24 -0
  4. example/models/__init__.py +0 -0
  5. example/permissions.py +6 -0
  6. example/pure_flask_example.py +65 -0
  7. example/rate_limit_load.py +10 -0
  8. example/redis_repository.py +22 -0
  9. example/routes/__init__.py +0 -0
  10. example/routes/api_some.py +335 -0
  11. example/sockets/__init__.py +0 -0
  12. example/sockets/on_connect.py +16 -0
  13. example/sockets/on_disconnect.py +14 -0
  14. example/sockets/on_json.py +10 -0
  15. example/sockets/on_message.py +13 -0
  16. example/sockets/on_open.py +16 -0
  17. example/workers/__init__.py +0 -0
  18. example/workers/worker.py +28 -0
  19. ul_api_utils/__init__.py +0 -0
  20. ul_api_utils/access/__init__.py +122 -0
  21. ul_api_utils/api_resource/__init__.py +0 -0
  22. ul_api_utils/api_resource/api_request.py +105 -0
  23. ul_api_utils/api_resource/api_resource.py +414 -0
  24. ul_api_utils/api_resource/api_resource_config.py +20 -0
  25. ul_api_utils/api_resource/api_resource_error_handling.py +21 -0
  26. ul_api_utils/api_resource/api_resource_fn_typing.py +356 -0
  27. ul_api_utils/api_resource/api_resource_type.py +16 -0
  28. ul_api_utils/api_resource/api_response.py +300 -0
  29. ul_api_utils/api_resource/api_response_db.py +26 -0
  30. ul_api_utils/api_resource/api_response_payload_alias.py +25 -0
  31. ul_api_utils/api_resource/db_types.py +9 -0
  32. ul_api_utils/api_resource/signature_check.py +41 -0
  33. ul_api_utils/commands/__init__.py +0 -0
  34. ul_api_utils/commands/cmd_enc_keys.py +172 -0
  35. ul_api_utils/commands/cmd_gen_api_user_token.py +77 -0
  36. ul_api_utils/commands/cmd_gen_new_api_user.py +106 -0
  37. ul_api_utils/commands/cmd_generate_api_docs.py +181 -0
  38. ul_api_utils/commands/cmd_start.py +110 -0
  39. ul_api_utils/commands/cmd_worker_start.py +76 -0
  40. ul_api_utils/commands/start/__init__.py +0 -0
  41. ul_api_utils/commands/start/gunicorn.conf.local.py +0 -0
  42. ul_api_utils/commands/start/gunicorn.conf.py +26 -0
  43. ul_api_utils/commands/start/wsgi.py +22 -0
  44. ul_api_utils/conf/ul-debugger-main.js +1 -0
  45. ul_api_utils/conf/ul-debugger-ui.js +1 -0
  46. ul_api_utils/conf.py +70 -0
  47. ul_api_utils/const.py +78 -0
  48. ul_api_utils/debug/__init__.py +0 -0
  49. ul_api_utils/debug/debugger.py +119 -0
  50. ul_api_utils/debug/malloc.py +93 -0
  51. ul_api_utils/debug/stat.py +444 -0
  52. ul_api_utils/encrypt/__init__.py +0 -0
  53. ul_api_utils/encrypt/encrypt_decrypt_abstract.py +15 -0
  54. ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +59 -0
  55. ul_api_utils/errors.py +200 -0
  56. ul_api_utils/internal_api/__init__.py +0 -0
  57. ul_api_utils/internal_api/__tests__/__init__.py +0 -0
  58. ul_api_utils/internal_api/__tests__/internal_api.py +29 -0
  59. ul_api_utils/internal_api/__tests__/internal_api_content_type.py +22 -0
  60. ul_api_utils/internal_api/internal_api.py +369 -0
  61. ul_api_utils/internal_api/internal_api_check_context.py +42 -0
  62. ul_api_utils/internal_api/internal_api_error.py +17 -0
  63. ul_api_utils/internal_api/internal_api_response.py +296 -0
  64. ul_api_utils/main.py +29 -0
  65. ul_api_utils/modules/__init__.py +0 -0
  66. ul_api_utils/modules/__tests__/__init__.py +0 -0
  67. ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +195 -0
  68. ul_api_utils/modules/api_sdk.py +555 -0
  69. ul_api_utils/modules/api_sdk_config.py +63 -0
  70. ul_api_utils/modules/api_sdk_jwt.py +377 -0
  71. ul_api_utils/modules/intermediate_state.py +34 -0
  72. ul_api_utils/modules/worker_context.py +35 -0
  73. ul_api_utils/modules/worker_sdk.py +109 -0
  74. ul_api_utils/modules/worker_sdk_config.py +13 -0
  75. ul_api_utils/py.typed +0 -0
  76. ul_api_utils/resources/__init__.py +0 -0
  77. ul_api_utils/resources/caching.py +196 -0
  78. ul_api_utils/resources/debugger_scripts.py +97 -0
  79. ul_api_utils/resources/health_check/__init__.py +0 -0
  80. ul_api_utils/resources/health_check/const.py +2 -0
  81. ul_api_utils/resources/health_check/health_check.py +439 -0
  82. ul_api_utils/resources/health_check/health_check_template.py +64 -0
  83. ul_api_utils/resources/health_check/resource.py +97 -0
  84. ul_api_utils/resources/not_implemented.py +25 -0
  85. ul_api_utils/resources/permissions.py +29 -0
  86. ul_api_utils/resources/rate_limitter.py +84 -0
  87. ul_api_utils/resources/socketio.py +55 -0
  88. ul_api_utils/resources/swagger.py +119 -0
  89. ul_api_utils/resources/web_forms/__init__.py +0 -0
  90. ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
  91. ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
  92. ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
  93. ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
  94. ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
  95. ul_api_utils/resources/web_forms/uni_form.py +75 -0
  96. ul_api_utils/sentry.py +52 -0
  97. ul_api_utils/utils/__init__.py +0 -0
  98. ul_api_utils/utils/__tests__/__init__.py +0 -0
  99. ul_api_utils/utils/__tests__/api_path_version.py +16 -0
  100. ul_api_utils/utils/__tests__/unwrap_typing.py +67 -0
  101. ul_api_utils/utils/api_encoding.py +51 -0
  102. ul_api_utils/utils/api_format.py +61 -0
  103. ul_api_utils/utils/api_method.py +55 -0
  104. ul_api_utils/utils/api_pagination.py +58 -0
  105. ul_api_utils/utils/api_path_version.py +60 -0
  106. ul_api_utils/utils/api_request_info.py +6 -0
  107. ul_api_utils/utils/avro.py +131 -0
  108. ul_api_utils/utils/broker_topics_message_count.py +47 -0
  109. ul_api_utils/utils/cached_per_request.py +23 -0
  110. ul_api_utils/utils/colors.py +31 -0
  111. ul_api_utils/utils/constants.py +7 -0
  112. ul_api_utils/utils/decode_base64.py +9 -0
  113. ul_api_utils/utils/deprecated.py +19 -0
  114. ul_api_utils/utils/flags.py +29 -0
  115. ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
  116. ul_api_utils/utils/flask_swagger_generator/conf.py +4 -0
  117. ul_api_utils/utils/flask_swagger_generator/exceptions.py +7 -0
  118. ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
  119. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +57 -0
  120. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +48 -0
  121. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +777 -0
  122. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +40 -0
  123. ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
  124. ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +77 -0
  125. ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +51 -0
  126. ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +18 -0
  127. ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +52 -0
  128. ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +15 -0
  129. ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +39 -0
  130. ul_api_utils/utils/imports.py +16 -0
  131. ul_api_utils/utils/instance_checks.py +16 -0
  132. ul_api_utils/utils/jinja/__init__.py +0 -0
  133. ul_api_utils/utils/jinja/t_url_for.py +19 -0
  134. ul_api_utils/utils/jinja/to_pretty_json.py +11 -0
  135. ul_api_utils/utils/json_encoder.py +126 -0
  136. ul_api_utils/utils/load_modules.py +15 -0
  137. ul_api_utils/utils/memory_db/__init__.py +0 -0
  138. ul_api_utils/utils/memory_db/__tests__/__init__.py +0 -0
  139. ul_api_utils/utils/memory_db/errors.py +8 -0
  140. ul_api_utils/utils/memory_db/repository.py +102 -0
  141. ul_api_utils/utils/token_check.py +14 -0
  142. ul_api_utils/utils/token_check_through_request.py +16 -0
  143. ul_api_utils/utils/unwrap_typing.py +117 -0
  144. ul_api_utils/utils/uuid_converter.py +22 -0
  145. ul_api_utils/validators/__init__.py +0 -0
  146. ul_api_utils/validators/__tests__/__init__.py +0 -0
  147. ul_api_utils/validators/__tests__/test_custom_fields.py +32 -0
  148. ul_api_utils/validators/custom_fields.py +66 -0
  149. ul_api_utils/validators/validate_empty_object.py +10 -0
  150. ul_api_utils/validators/validate_uuid.py +11 -0
  151. ul_api_utils-9.3.0.dist-info/LICENSE +21 -0
  152. ul_api_utils-9.3.0.dist-info/METADATA +279 -0
  153. ul_api_utils-9.3.0.dist-info/RECORD +156 -0
  154. ul_api_utils-9.3.0.dist-info/WHEEL +5 -0
  155. ul_api_utils-9.3.0.dist-info/entry_points.txt +2 -0
  156. 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)