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,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Tuple, Any, Optional
|
|
4
|
+
|
|
5
|
+
import msgpack
|
|
6
|
+
|
|
7
|
+
from ul_api_utils.const import MIME__JSON, MIME__MSGPCK
|
|
8
|
+
from ul_api_utils.utils.json_encoder import CustomJSONEncoder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiFormat(Enum):
|
|
12
|
+
JSON = 'JSON'
|
|
13
|
+
MESSAGE_PACK = 'MESSAGE_PACK'
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str:
|
|
16
|
+
return f'{type(self).__name__}.{self.name}'
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def accept_mimes(force: Optional['ApiFormat'] = None) -> Tuple[str, ...]:
|
|
20
|
+
if force is ApiFormat.MESSAGE_PACK:
|
|
21
|
+
return MIME__MSGPCK, # noqa: C818
|
|
22
|
+
if force is ApiFormat.JSON:
|
|
23
|
+
return MIME__JSON, # noqa: C818
|
|
24
|
+
return MIME__MSGPCK, MIME__JSON
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def mime(self) -> str:
|
|
28
|
+
if self is ApiFormat.MESSAGE_PACK:
|
|
29
|
+
return MIME__MSGPCK
|
|
30
|
+
if self is ApiFormat.JSON:
|
|
31
|
+
return MIME__JSON
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def from_mime(mime: str) -> Optional['ApiFormat']:
|
|
36
|
+
if mime == MIME__MSGPCK:
|
|
37
|
+
return ApiFormat.MESSAGE_PACK
|
|
38
|
+
if mime == MIME__JSON or (mime.startswith("application/") and mime.endswith("+json")):
|
|
39
|
+
return ApiFormat.JSON
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def serialize_bytes(self, data: Any) -> bytes:
|
|
43
|
+
if self is ApiFormat.MESSAGE_PACK:
|
|
44
|
+
return msgpack.dumps(data, default=CustomJSONEncoder().default)
|
|
45
|
+
if self is ApiFormat.JSON:
|
|
46
|
+
return json.dumps(data, cls=CustomJSONEncoder).encode('utf-8')
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
def parse_bytes(self, data: bytes) -> Any:
|
|
50
|
+
if self is ApiFormat.MESSAGE_PACK:
|
|
51
|
+
return msgpack.unpackb(data, use_list=True)
|
|
52
|
+
if self is ApiFormat.JSON:
|
|
53
|
+
return json.loads(data)
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
def parse_text(self, data: str) -> Any:
|
|
57
|
+
if self is ApiFormat.MESSAGE_PACK:
|
|
58
|
+
return msgpack.unpackb(data.encode('utf-8'), use_list=True)
|
|
59
|
+
if self is ApiFormat.JSON:
|
|
60
|
+
return json.loads(data)
|
|
61
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from enum import Enum, unique
|
|
2
|
+
from typing import Union, List, Tuple, Any, Literal
|
|
3
|
+
|
|
4
|
+
from ul_api_utils.const import REQUEST_METHOD__PUT, REQUEST_METHOD__GET, REQUEST_METHOD__POST, \
|
|
5
|
+
REQUEST_METHOD__PATCH, REQUEST_METHOD__DELETE, REQUEST_METHOD__OPTIONS, REQUEST_METHOD__QUERY
|
|
6
|
+
|
|
7
|
+
TMethodStr = Union[Literal['GET'], Literal['POST'], Literal['PUT'], Literal['PATCH'], Literal['DELETE'], Literal['OPTIONS']]
|
|
8
|
+
TMethod = Union[TMethodStr, 'ApiMethod', List[TMethodStr], Tuple[TMethodStr, ...], List['ApiMethod'], Tuple['ApiMethod', ...]]
|
|
9
|
+
TMethodShort = Union[TMethodStr, 'ApiMethod']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@unique
|
|
13
|
+
class ApiMethod(Enum):
|
|
14
|
+
PUT = REQUEST_METHOD__PUT
|
|
15
|
+
GET = REQUEST_METHOD__GET
|
|
16
|
+
POST = REQUEST_METHOD__POST
|
|
17
|
+
QUERY = REQUEST_METHOD__QUERY
|
|
18
|
+
PATCH = REQUEST_METHOD__PATCH
|
|
19
|
+
DELETE = REQUEST_METHOD__DELETE
|
|
20
|
+
OPTIONS = REQUEST_METHOD__OPTIONS
|
|
21
|
+
|
|
22
|
+
def __repr__(self) -> str:
|
|
23
|
+
return f'{type(self).__name__}.{self.name}'
|
|
24
|
+
|
|
25
|
+
def __hash__(self) -> int:
|
|
26
|
+
return hash(self.value)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return self.value
|
|
30
|
+
|
|
31
|
+
def __eq__(self, other: Any) -> bool:
|
|
32
|
+
if isinstance(other, str):
|
|
33
|
+
try:
|
|
34
|
+
other = type(self)(other.upper())
|
|
35
|
+
except Exception: # noqa: B902
|
|
36
|
+
return False
|
|
37
|
+
return super(ApiMethod, self).__eq__(other)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def compile_methods(methods: TMethod) -> Tuple[List[str], List['ApiMethod']]:
|
|
41
|
+
str_res_methods = []
|
|
42
|
+
enum_res_methods = []
|
|
43
|
+
for m in (list(methods) if isinstance(methods, (list, tuple)) else [methods]):
|
|
44
|
+
if isinstance(m, ApiMethod):
|
|
45
|
+
str_res_methods.append(m.value)
|
|
46
|
+
enum_res_methods.append(m)
|
|
47
|
+
else:
|
|
48
|
+
assert isinstance(m, str)
|
|
49
|
+
m = m.strip().upper() # type: ignore
|
|
50
|
+
enum_res_methods.append(ApiMethod(m))
|
|
51
|
+
str_res_methods.append(m)
|
|
52
|
+
return str_res_methods, enum_res_methods
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
NO_REQUEST_BODY_METHODS = {ApiMethod.GET, ApiMethod.OPTIONS}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Iterable, Any, Optional, Iterator
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiPagination:
|
|
7
|
+
def __init__(self, page: int, per_page: int, total: int, items: Iterable[Any]) -> None:
|
|
8
|
+
self.query = None
|
|
9
|
+
self.page = page
|
|
10
|
+
self.per_page = per_page
|
|
11
|
+
self.total = total
|
|
12
|
+
self.items = items
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def pages(self) -> int:
|
|
16
|
+
"""The total number of pages"""
|
|
17
|
+
if self.per_page == 0:
|
|
18
|
+
pages = 0
|
|
19
|
+
else:
|
|
20
|
+
pages = int(math.ceil(self.total / float(self.per_page)))
|
|
21
|
+
return pages
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def prev_num(self) -> Optional[int]:
|
|
25
|
+
"""Number of the previous page."""
|
|
26
|
+
if not self.has_prev:
|
|
27
|
+
return None
|
|
28
|
+
return self.page - 1
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def has_prev(self) -> bool:
|
|
32
|
+
"""True if a previous page exists"""
|
|
33
|
+
return self.page > 1
|
|
34
|
+
|
|
35
|
+
def prev(self, error_out: bool = False) -> None:
|
|
36
|
+
raise AssertionError('a query object is required for this method to work')
|
|
37
|
+
|
|
38
|
+
def next(self, error_out: bool = False) -> None:
|
|
39
|
+
raise AssertionError('a query object is required for this method to work')
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def has_next(self) -> bool:
|
|
43
|
+
return self.page < self.pages
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def next_num(self) -> Optional[int]:
|
|
47
|
+
if not self.has_next:
|
|
48
|
+
return None
|
|
49
|
+
return self.page + 1
|
|
50
|
+
|
|
51
|
+
def iter_pages(self, left_edge: int = 2, left_current: int = 2, right_current: int = 5, right_edge: int = 2) -> Iterator[Optional[int]]:
|
|
52
|
+
last = 0
|
|
53
|
+
for num in range(1, self.pages + 1):
|
|
54
|
+
if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) or num > self.pages - right_edge:
|
|
55
|
+
if last + 1 != num:
|
|
56
|
+
yield None
|
|
57
|
+
yield num
|
|
58
|
+
last = num
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional, Mapping, Union, Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ApiPathVersion(Enum):
|
|
6
|
+
V01 = 'v1'
|
|
7
|
+
V02 = 'v2'
|
|
8
|
+
V03 = 'v3'
|
|
9
|
+
V04 = 'v4'
|
|
10
|
+
V05 = 'v5'
|
|
11
|
+
V06 = 'v6'
|
|
12
|
+
V07 = 'v7'
|
|
13
|
+
V08 = 'v8'
|
|
14
|
+
V09 = 'v9'
|
|
15
|
+
V10 = 'v10'
|
|
16
|
+
|
|
17
|
+
NO_VERSION = "no-version"
|
|
18
|
+
NO_PREFIX = 'no-prefix'
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return f'{type(self).__name__}.{self.name}'
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def cleanup_q(q: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
25
|
+
if q is None:
|
|
26
|
+
return None
|
|
27
|
+
res = {}
|
|
28
|
+
for k, v in q.items():
|
|
29
|
+
if v is not None:
|
|
30
|
+
res[k] = v
|
|
31
|
+
return res
|
|
32
|
+
|
|
33
|
+
def compile_path(self, path: str, prefix: str = "", *, q: Optional[Mapping[str, Union[int, str]]] = None) -> str:
|
|
34
|
+
assert '?' not in path, f'restricted symbol "?" was found in url.path="{path}"'
|
|
35
|
+
assert '&' not in path, f'restricted symbol "&" was found in url.path="{path}"'
|
|
36
|
+
assert '?' not in prefix, f'restricted symbol "?" was found in url.prefix="{prefix}"'
|
|
37
|
+
assert '&' not in prefix, f'restricted symbol "&" was found in url.prefix="{prefix}"'
|
|
38
|
+
|
|
39
|
+
qr = ''
|
|
40
|
+
if q:
|
|
41
|
+
for k, kv in q.items():
|
|
42
|
+
if kv is None:
|
|
43
|
+
continue # type: ignore
|
|
44
|
+
qr += ('?' if len(qr) == 0 else '&') + f'{k}={kv}'
|
|
45
|
+
|
|
46
|
+
if self is ApiPathVersion.NO_PREFIX:
|
|
47
|
+
if not path:
|
|
48
|
+
return qr
|
|
49
|
+
return f'/{path.lstrip("/")}{qr}'
|
|
50
|
+
|
|
51
|
+
if self is ApiPathVersion.NO_VERSION:
|
|
52
|
+
if not path:
|
|
53
|
+
return f'{"/" if prefix else ""}{prefix.lstrip("/")}{qr}'
|
|
54
|
+
return f'{"/" if prefix else ""}{prefix.strip("/")}/{path.lstrip("/")}{qr}'
|
|
55
|
+
|
|
56
|
+
assert prefix, f'prefix must not be empty. {prefix} was given'
|
|
57
|
+
|
|
58
|
+
if not path:
|
|
59
|
+
return f'/{prefix.strip("/")}/{self.value}{qr}'
|
|
60
|
+
return f'/{prefix.strip("/")}/{self.value}/{path.lstrip("/")}{qr}'
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from typing import Type, Dict, Any, Optional, List, Set
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
# CODE FROM https://github.com/godatadriven/pydantic-avro/blob/main/src/pydantic_avro/base.py
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_avro_schema(model: Type[BaseModel], by_alias: bool = True, namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
9
|
+
schema = model.schema(by_alias=by_alias)
|
|
10
|
+
return {
|
|
11
|
+
"type": "record",
|
|
12
|
+
"namespace": schema["title"] if namespace is None else namespace,
|
|
13
|
+
"name": schema["title"],
|
|
14
|
+
"fields": _get_fields(schema, set()),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_type(schema: Dict[str, Any], value: Dict[str, Any], classes_seen: Set[str]) -> Dict[str, Any]:
|
|
19
|
+
"""Returns a type of single field"""
|
|
20
|
+
t = value.get("type")
|
|
21
|
+
f = value.get("format")
|
|
22
|
+
r = value.get("$ref")
|
|
23
|
+
a = value.get("additionalProperties")
|
|
24
|
+
avro_type_dict: Dict[str, Any] = {}
|
|
25
|
+
if "default" in value:
|
|
26
|
+
avro_type_dict["default"] = value.get("default")
|
|
27
|
+
if "description" in value:
|
|
28
|
+
avro_type_dict["doc"] = value.get("description")
|
|
29
|
+
if "allOf" in value and len(value["allOf"]) == 1:
|
|
30
|
+
r = value["allOf"][0]["$ref"]
|
|
31
|
+
if r is not None:
|
|
32
|
+
class_name = r.replace("#/definitions/", "")
|
|
33
|
+
if class_name in classes_seen:
|
|
34
|
+
avro_type_dict["type"] = class_name
|
|
35
|
+
else:
|
|
36
|
+
d = _get_definition(r, schema)
|
|
37
|
+
if "enum" in d:
|
|
38
|
+
avro_type_dict["type"] = {
|
|
39
|
+
"type": "enum",
|
|
40
|
+
"symbols": [str(v) for v in d["enum"]],
|
|
41
|
+
"name": d["title"],
|
|
42
|
+
}
|
|
43
|
+
else:
|
|
44
|
+
avro_type_dict["type"] = {
|
|
45
|
+
"type": "record",
|
|
46
|
+
"fields": _get_fields(d, classes_seen),
|
|
47
|
+
# Name of the struct should be unique true the complete schema
|
|
48
|
+
# Because of this the path in the schema is tracked and used as name for a nested struct/array
|
|
49
|
+
"name": class_name,
|
|
50
|
+
}
|
|
51
|
+
classes_seen.add(class_name)
|
|
52
|
+
elif t == "array":
|
|
53
|
+
items: Dict[str, Any] = value.get("items") # type: ignore
|
|
54
|
+
tn: Dict[str, Any] = _get_type(schema, items, classes_seen)
|
|
55
|
+
# If items in array are a object:
|
|
56
|
+
if "$ref" in items:
|
|
57
|
+
tn = tn["type"]
|
|
58
|
+
# If items in array are a logicalType
|
|
59
|
+
if (
|
|
60
|
+
isinstance(tn, dict)
|
|
61
|
+
and isinstance(tn.get("type", {}), dict)
|
|
62
|
+
and tn.get("type", {}).get("logicalType") is not None
|
|
63
|
+
):
|
|
64
|
+
tn = tn["type"]
|
|
65
|
+
avro_type_dict["type"] = {"type": "array", "items": tn}
|
|
66
|
+
elif t == "string" and f == "date-time":
|
|
67
|
+
avro_type_dict["type"] = {
|
|
68
|
+
"type": "long",
|
|
69
|
+
"logicalType": "timestamp-micros",
|
|
70
|
+
}
|
|
71
|
+
elif t == "string" and f == "date":
|
|
72
|
+
avro_type_dict["type"] = {
|
|
73
|
+
"type": "int",
|
|
74
|
+
"logicalType": "date",
|
|
75
|
+
}
|
|
76
|
+
elif t == "string" and f == "time":
|
|
77
|
+
avro_type_dict["type"] = {
|
|
78
|
+
"type": "long",
|
|
79
|
+
"logicalType": "time-micros",
|
|
80
|
+
}
|
|
81
|
+
elif t == "string" and f == "uuid":
|
|
82
|
+
avro_type_dict["type"] = {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"logicalType": "uuid",
|
|
85
|
+
}
|
|
86
|
+
elif t == "string":
|
|
87
|
+
avro_type_dict["type"] = "string"
|
|
88
|
+
elif t == "number":
|
|
89
|
+
avro_type_dict["type"] = "double"
|
|
90
|
+
elif t == "integer":
|
|
91
|
+
# integer in python can be a long
|
|
92
|
+
avro_type_dict["type"] = "long"
|
|
93
|
+
elif t == "boolean":
|
|
94
|
+
avro_type_dict["type"] = "boolean"
|
|
95
|
+
elif t == "object":
|
|
96
|
+
if a is None:
|
|
97
|
+
value_type = "string"
|
|
98
|
+
else:
|
|
99
|
+
value_type = _get_type(schema, a, classes_seen) # type: ignore
|
|
100
|
+
if isinstance(value_type, dict) and len(value_type) == 1: # type: ignore
|
|
101
|
+
value_type = value_type.get("type") # type: ignore
|
|
102
|
+
avro_type_dict["type"] = {"type": "map", "values": value_type}
|
|
103
|
+
else:
|
|
104
|
+
raise NotImplementedError(f"Type '{t}' not support yet, please report this at https://github.com/godatadriven/pydantic-avro/issues")
|
|
105
|
+
return avro_type_dict
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_definition(ref: str, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
109
|
+
"""Reading definition of base schema for nested structs"""
|
|
110
|
+
id = ref.replace("#/definitions/", "")
|
|
111
|
+
d = schema.get("definitions", {}).get(id)
|
|
112
|
+
if d is None:
|
|
113
|
+
raise RuntimeError(f"Definition {id} does not exist")
|
|
114
|
+
return d
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_fields(schema: Dict[str, Any], classes_seen: Set[str]) -> List[Dict[str, Any]]:
|
|
118
|
+
"""Return a list of fields of a struct"""
|
|
119
|
+
fields = []
|
|
120
|
+
required = schema.get("required", [])
|
|
121
|
+
for key, value in schema.get("properties", {}).items():
|
|
122
|
+
avro_type_dict = _get_type(schema, value, classes_seen)
|
|
123
|
+
avro_type_dict["name"] = key
|
|
124
|
+
|
|
125
|
+
if key not in required:
|
|
126
|
+
if avro_type_dict.get("default") is None:
|
|
127
|
+
avro_type_dict["type"] = ["null", avro_type_dict["type"]]
|
|
128
|
+
avro_type_dict["default"] = None
|
|
129
|
+
|
|
130
|
+
fields.append(avro_type_dict)
|
|
131
|
+
return fields
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import NamedTuple, List, Optional
|
|
2
|
+
|
|
3
|
+
from ul_unipipeline.errors import UniError
|
|
4
|
+
from ul_unipipeline.modules.uni import Uni
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataStreamStats(NamedTuple):
|
|
8
|
+
messages_count: int
|
|
9
|
+
queue_name: str
|
|
10
|
+
error_queue: bool
|
|
11
|
+
error: Optional[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_data_streams_stats(uni: Uni) -> List[DataStreamStats]:
|
|
15
|
+
stats = []
|
|
16
|
+
error__error_messages_count = error__expect_messages_count = None
|
|
17
|
+
for wd in uni.config.workers.values():
|
|
18
|
+
try:
|
|
19
|
+
broker = uni._mediator.get_broker(wd.broker.name)
|
|
20
|
+
except UniError:
|
|
21
|
+
pass
|
|
22
|
+
else:
|
|
23
|
+
try:
|
|
24
|
+
expect_messages_count = broker.get_topic_approximate_messages_count(wd.topic)
|
|
25
|
+
except UniError:
|
|
26
|
+
expect_messages_count = -1
|
|
27
|
+
error__expect_messages_count = "inactive"
|
|
28
|
+
try:
|
|
29
|
+
error_messages_count = broker.get_topic_approximate_messages_count(wd.error_topic)
|
|
30
|
+
except UniError:
|
|
31
|
+
error_messages_count = -1
|
|
32
|
+
error__error_messages_count = "inactive"
|
|
33
|
+
|
|
34
|
+
stats.append(DataStreamStats(
|
|
35
|
+
messages_count=expect_messages_count,
|
|
36
|
+
queue_name=wd.topic,
|
|
37
|
+
error_queue=False,
|
|
38
|
+
error=error__expect_messages_count,
|
|
39
|
+
))
|
|
40
|
+
stats.append(DataStreamStats(
|
|
41
|
+
messages_count=error_messages_count,
|
|
42
|
+
queue_name=wd.error_topic,
|
|
43
|
+
error_queue=True,
|
|
44
|
+
error=error__error_messages_count,
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
return stats
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Callable, TypeVar, Any
|
|
3
|
+
|
|
4
|
+
from flask import g
|
|
5
|
+
|
|
6
|
+
TFn = TypeVar("TFn", bound=Callable[..., Any])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_OBJ = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cached_per_request(key: str) -> Callable[[TFn], TFn]:
|
|
13
|
+
def wrapper(fn: Callable[[...], Any]) -> Any: # type: ignore
|
|
14
|
+
@functools.wraps(fn)
|
|
15
|
+
def wr(*args: Any, **kwargs: Any) -> Any:
|
|
16
|
+
cached_res = getattr(g, key, DEFAULT_OBJ)
|
|
17
|
+
if cached_res is not DEFAULT_OBJ:
|
|
18
|
+
return cached_res
|
|
19
|
+
res = fn(*args, **kwargs)
|
|
20
|
+
setattr(g, key, res)
|
|
21
|
+
return res
|
|
22
|
+
return wr
|
|
23
|
+
return wrapper
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
C_NC = "[0m"
|
|
4
|
+
C_FG_RED = "[38;5;1m"
|
|
5
|
+
C_FG_GREEN = "[38;5;2m"
|
|
6
|
+
C_FG_YELLOW = "[38;5;3m"
|
|
7
|
+
C_FG_GRAY = "[38;5;8m"
|
|
8
|
+
C_FG_BLUE = "[38;5;4m"
|
|
9
|
+
|
|
10
|
+
COLORS_MAP__TERMINAL: Dict[str, str] = {
|
|
11
|
+
C_NC: C_NC,
|
|
12
|
+
C_FG_RED: C_FG_RED,
|
|
13
|
+
C_FG_GREEN: C_FG_GREEN,
|
|
14
|
+
C_FG_YELLOW: C_FG_YELLOW,
|
|
15
|
+
C_FG_GRAY: C_FG_GRAY,
|
|
16
|
+
C_FG_BLUE: C_FG_BLUE,
|
|
17
|
+
"": "",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
COLORS_MAP__HTML = {
|
|
22
|
+
C_NC: '</span>',
|
|
23
|
+
C_FG_GRAY: '<span style="color: gray;">',
|
|
24
|
+
C_FG_RED: '<span style="color: red;">',
|
|
25
|
+
C_FG_YELLOW: '<span style="color: yellow;">',
|
|
26
|
+
C_FG_GREEN: '<span style="color: green;">',
|
|
27
|
+
C_FG_BLUE: '<span style="color: blue;">',
|
|
28
|
+
"": "",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert set(COLORS_MAP__TERMINAL.keys()) == set(COLORS_MAP__HTML.keys())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def decode_base64_to_string(encoded_str: str) -> str:
|
|
5
|
+
assert isinstance(encoded_str, str) and len(encoded_str) > 0, f'String with item required. Object {encoded_str} with type {type(encoded_str)} was given'
|
|
6
|
+
encoded_bytes = encoded_str.encode('utf-8')
|
|
7
|
+
decoded_bytes = base64.b64decode(encoded_bytes)
|
|
8
|
+
decoded_str = decoded_bytes.decode('utf-8')
|
|
9
|
+
return decoded_str
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import warnings
|
|
3
|
+
from typing import TypeVar, Callable, cast
|
|
4
|
+
|
|
5
|
+
TFunc = TypeVar('TFunc', bound=Callable) # type: ignore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
|
|
9
|
+
def deprecated(func: TFunc) -> TFunc:
|
|
10
|
+
"""This is a decorator which can be used to mark functions
|
|
11
|
+
as deprecated. It will result in a warning being emitted
|
|
12
|
+
when the function is used."""
|
|
13
|
+
@functools.wraps(func)
|
|
14
|
+
def new_func(*args, **kwargs): # type: ignore
|
|
15
|
+
warnings.simplefilter('always', DeprecationWarning) # turn off filter
|
|
16
|
+
warnings.warn("Call to deprecated function {}.".format(func.__name__), category=DeprecationWarning, stacklevel=2)
|
|
17
|
+
warnings.simplefilter('default', DeprecationWarning) # reset filter
|
|
18
|
+
return func(*args, **kwargs)
|
|
19
|
+
return cast(TFunc, new_func)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Flagged:
|
|
6
|
+
def __init__(self, flags: str) -> None:
|
|
7
|
+
self._flsgs = set(_feature.strip() for _feature in flags.strip().upper().split(',') if len(_feature.strip()))
|
|
8
|
+
|
|
9
|
+
@functools.cache # noqa: B019
|
|
10
|
+
def has_or_unset(self, flag: str) -> bool:
|
|
11
|
+
flags = set(_feature.strip() for _feature in flag.strip().upper().split(',') if len(_feature.strip()))
|
|
12
|
+
if not len(flags):
|
|
13
|
+
return True
|
|
14
|
+
for f in flags:
|
|
15
|
+
if f in self._flsgs:
|
|
16
|
+
return True
|
|
17
|
+
fh = hashlib.sha1()
|
|
18
|
+
fh.update(f.encode())
|
|
19
|
+
if fh.digest().hex().upper() in self._flsgs:
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def gen_hashed(self) -> str:
|
|
24
|
+
res_flags = []
|
|
25
|
+
for f in self._flsgs:
|
|
26
|
+
fh = hashlib.sha1()
|
|
27
|
+
fh.update(f.encode())
|
|
28
|
+
res_flags.append(fh.digest().hex().upper())
|
|
29
|
+
return ",".join(res_flags)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from random import randint
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SwaggerModel(ABC):
|
|
6
|
+
TAB = " "
|
|
7
|
+
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self.swagger_models = [] # type: ignore
|
|
10
|
+
|
|
11
|
+
def add_swagger_model(self, child_swagger_model): # type: ignore
|
|
12
|
+
self.swagger_models.append(child_swagger_model)
|
|
13
|
+
|
|
14
|
+
def add_swagger_models(self, child_swagger_models): # type: ignore
|
|
15
|
+
self.swagger_models += child_swagger_models
|
|
16
|
+
|
|
17
|
+
def has_swagger_model_child_type(self, child_swagger_model_type) -> bool: # type: ignore
|
|
18
|
+
for swagger_model in self.swagger_models:
|
|
19
|
+
|
|
20
|
+
if isinstance(swagger_model, child_swagger_model_type):
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def get_swagger_child_models_of_type(self, swagger_model_type): # type: ignore
|
|
26
|
+
selection = []
|
|
27
|
+
|
|
28
|
+
for swagger_model in self.swagger_models:
|
|
29
|
+
|
|
30
|
+
if isinstance(swagger_model, swagger_model_type):
|
|
31
|
+
selection.append(swagger_model)
|
|
32
|
+
|
|
33
|
+
return selection
|
|
34
|
+
|
|
35
|
+
def write(self, file) -> None: # type: ignore
|
|
36
|
+
self.perform_write(file)
|
|
37
|
+
|
|
38
|
+
for swagger_model in self.swagger_models:
|
|
39
|
+
swagger_model.write(file)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def indent(string: str, prefix: str) -> str:
|
|
43
|
+
return ''.join(prefix + line for line in string.splitlines(True))
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def perform_write(self, file) -> None: # type: ignore
|
|
47
|
+
raise NotImplementedError()
|
|
48
|
+
|
|
49
|
+
def random_id(self) -> int:
|
|
50
|
+
"""
|
|
51
|
+
Function to create a random ID.
|
|
52
|
+
Returns: random integer that can be used as an ID
|
|
53
|
+
"""
|
|
54
|
+
minimal = 100
|
|
55
|
+
maximal = 1000000000
|
|
56
|
+
rand = randint(minimal, maximal)
|
|
57
|
+
return rand
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from ul_api_utils.utils.flask_swagger_generator.utils.request_type import RequestType
|
|
5
|
+
from ul_api_utils.utils.flask_swagger_generator.utils.security_type import SecurityType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SwaggerSpecifier(ABC):
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.application_name: Optional[str] = None
|
|
12
|
+
self.application_version: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def add_response(self, function_name: str, status_code: int, schema, description: str = ""): # type: ignore
|
|
16
|
+
raise NotImplementedError()
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def add_endpoint(
|
|
20
|
+
self,
|
|
21
|
+
function_name: str,
|
|
22
|
+
path: str,
|
|
23
|
+
request_types: List[RequestType],
|
|
24
|
+
group: Optional[str] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
raise NotImplementedError()
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def add_query_parameters(self) -> None:
|
|
30
|
+
raise NotImplementedError()
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def add_request_body(self, function_name: str, schema) -> None: # type: ignore
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def add_security(self, function_name: str, security_type: SecurityType) -> None:
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
def set_application_name(self, application_name: str) -> None:
|
|
41
|
+
self.application_name = application_name
|
|
42
|
+
|
|
43
|
+
def set_application_version(self, application_version: str) -> None:
|
|
44
|
+
self.application_version = application_version
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def clean(self) -> None:
|
|
48
|
+
pass
|