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,377 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Set, Dict, Any, NamedTuple, List, Union, Optional, Literal, Tuple, Callable, Iterable
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
|
|
9
|
+
from ul_api_utils.access import PermissionDefinition
|
|
10
|
+
from ul_api_utils.errors import AccessApiError, PermissionDeniedApiError
|
|
11
|
+
from ul_api_utils.utils.json_encoder import CustomJSONEncoder
|
|
12
|
+
|
|
13
|
+
TAlgo = Union[Literal['RS256'], Literal['ES256']]
|
|
14
|
+
ALGORITHM__RS256: TAlgo = 'RS256'
|
|
15
|
+
ALGORITHM__ES256: TAlgo = 'ES256'
|
|
16
|
+
ALGORITHMS: List[str] = [ALGORITHM__ES256, ALGORITHM__RS256]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
JWT_VERSION: str = '1'
|
|
20
|
+
JWT_ACCESS_TOKEN_TTL: timedelta = timedelta(hours=2)
|
|
21
|
+
JWT_REFRESH_TOKEN_TTL: timedelta = timedelta(days=2)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
JWT_TYPE__REFRESH = 'refresh'
|
|
25
|
+
JTW_TYPE__ACCESS = 'access'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
JWT_TYPE__COMPRESSED = {
|
|
29
|
+
JWT_TYPE__REFRESH: 'r',
|
|
30
|
+
JTW_TYPE__ACCESS: 'a',
|
|
31
|
+
}
|
|
32
|
+
JWT_TYPE__UNCOMPRESSED = {
|
|
33
|
+
'r': JWT_TYPE__REFRESH,
|
|
34
|
+
'a': JTW_TYPE__ACCESS,
|
|
35
|
+
}
|
|
36
|
+
RE_COMPRESSED_PROP = re.compile(r'^[ar](\d+)$')
|
|
37
|
+
JWT_EXP_DATE_TIMESTAMP_BASIS = datetime(2022, 1, 1).timestamp()
|
|
38
|
+
|
|
39
|
+
# JWT_SYMBOL_MAP_FOR_CNT = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" # "\-
|
|
40
|
+
# JWT_SYMBOL_MAP_FOR_INC = "!#$%&'()*+,./:;<=>?@[]^_`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ"
|
|
41
|
+
|
|
42
|
+
# JWT_SYMBOL_MAP_FOR_CNT = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" # "\-
|
|
43
|
+
# JWT_SYMBOL_MAP_FOR_INC = "!#$%&'()*+,./:;<=>?@[]^_`{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ"
|
|
44
|
+
|
|
45
|
+
# JWT_SYMBOL_MAP_FOR_CNT = " !#$%&'()*+,./:;<=>?@[]^_`{|}~" # "\-
|
|
46
|
+
# JWT_SYMBOL_MAP_FOR_INC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
47
|
+
|
|
48
|
+
JWT_SYMBOL_MAP_FOR_CNT = " !#$%&'()*+,./:;<=>?@[]^_`{|}~" # "\-
|
|
49
|
+
JWT_SYMBOL_MAP_FOR_INC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
50
|
+
assert len(JWT_SYMBOL_MAP_FOR_INC) == len(set(JWT_SYMBOL_MAP_FOR_INC))
|
|
51
|
+
assert len(JWT_SYMBOL_MAP_FOR_CNT) == len(set(JWT_SYMBOL_MAP_FOR_CNT))
|
|
52
|
+
assert not set(JWT_SYMBOL_MAP_FOR_CNT).intersection(set(JWT_SYMBOL_MAP_FOR_INC))
|
|
53
|
+
assert not set(JWT_SYMBOL_MAP_FOR_INC).intersection(set(JWT_SYMBOL_MAP_FOR_CNT))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ApiSdkJwt(NamedTuple):
|
|
57
|
+
id: UUID
|
|
58
|
+
user_id: UUID
|
|
59
|
+
organization_id: Optional[UUID]
|
|
60
|
+
version: str
|
|
61
|
+
token_type: str
|
|
62
|
+
exp_date: datetime
|
|
63
|
+
env: str
|
|
64
|
+
permissions: Set[int]
|
|
65
|
+
additional_data: Dict[str, Any]
|
|
66
|
+
is_superuser: Optional[bool] = False
|
|
67
|
+
raw: Optional[str] = None
|
|
68
|
+
username: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def load_cert(certificate: str) -> Tuple[str, Callable[[], str]]:
|
|
72
|
+
from cryptography.hazmat.primitives import serialization
|
|
73
|
+
|
|
74
|
+
private_key = serialization.load_pem_private_key(
|
|
75
|
+
certificate.encode('utf-8'),
|
|
76
|
+
password=None,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def pub_key_factory() -> str:
|
|
80
|
+
public_key = private_key.public_key()
|
|
81
|
+
serialized_public = public_key.public_bytes(
|
|
82
|
+
encoding=serialization.Encoding.PEM,
|
|
83
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
84
|
+
)
|
|
85
|
+
return serialized_public.decode('utf-8')
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
certificate,
|
|
89
|
+
pub_key_factory,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def generate_cert(algorithm: TAlgo) -> Tuple[str, Callable[[], str]]:
|
|
94
|
+
from cryptography.hazmat.primitives import serialization
|
|
95
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
96
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
97
|
+
|
|
98
|
+
assert algorithm in ALGORITHMS, f'algorithm {algorithm} is not supported'
|
|
99
|
+
if algorithm == ALGORITHM__ES256:
|
|
100
|
+
private_key = ec.generate_private_key(ec.SECP384R1())
|
|
101
|
+
elif algorithm == ALGORITHM__RS256:
|
|
102
|
+
private_key = rsa.generate_private_key(65537, 2048) # type: ignore
|
|
103
|
+
|
|
104
|
+
def pub_key_factory() -> str:
|
|
105
|
+
public_key = private_key.public_key()
|
|
106
|
+
serialized_public = public_key.public_bytes(
|
|
107
|
+
encoding=serialization.Encoding.PEM,
|
|
108
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
109
|
+
)
|
|
110
|
+
return serialized_public.decode('utf-8')
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
private_key.private_bytes(
|
|
114
|
+
encoding=serialization.Encoding.PEM,
|
|
115
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
116
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
117
|
+
).decode('utf-8'),
|
|
118
|
+
pub_key_factory,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def encode(self, certificate: str, algorithm: TAlgo, compressed: bool = False) -> str:
|
|
122
|
+
assert algorithm in ALGORITHMS
|
|
123
|
+
if not isinstance(certificate, str):
|
|
124
|
+
raise TypeError(f'invalid type of config.jwt_private_key. must be str. {type(certificate).__name__} was given')
|
|
125
|
+
|
|
126
|
+
if compressed:
|
|
127
|
+
data: Dict[str, Any] = {
|
|
128
|
+
f'{JWT_TYPE__COMPRESSED[self.token_type]}{self.version}': [
|
|
129
|
+
self.env, # env
|
|
130
|
+
self.compress_uuid(self.id), # id
|
|
131
|
+
self.compress_uuid(self.user_id), # user_id
|
|
132
|
+
self.compress_uuid(self.organization_id) if self.organization_id else '', # organization_id
|
|
133
|
+
self.is_superuser,
|
|
134
|
+
int((self.exp_date.timestamp() - JWT_EXP_DATE_TIMESTAMP_BASIS) / 60), # exp_date IN MINUTES
|
|
135
|
+
self.compress_permissions(self.permissions), # permissions
|
|
136
|
+
],
|
|
137
|
+
**self.additional_data,
|
|
138
|
+
}
|
|
139
|
+
else:
|
|
140
|
+
data = dict(
|
|
141
|
+
id=str(self.id),
|
|
142
|
+
user_id=str(self.user_id),
|
|
143
|
+
organization_id=str(self.organization_id) if self.organization_id is not None else None,
|
|
144
|
+
is_superuser=self.is_superuser,
|
|
145
|
+
version=str(self.version),
|
|
146
|
+
token_type=self.token_type,
|
|
147
|
+
exp_date=self.exp_date.isoformat(),
|
|
148
|
+
env=self.env,
|
|
149
|
+
permissions=list(self.permissions),
|
|
150
|
+
**self.additional_data,
|
|
151
|
+
)
|
|
152
|
+
return jwt.encode(data, certificate, algorithm=algorithm, json_encoder=CustomJSONEncoder)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def decode(cls, token: str, certificate: str, username: Optional[str] = None) -> 'ApiSdkJwt':
|
|
156
|
+
data = jwt.decode(token, certificate, algorithms=ALGORITHMS)
|
|
157
|
+
compressed_props = [k for k in data.keys() if RE_COMPRESSED_PROP.match(k) is not None]
|
|
158
|
+
|
|
159
|
+
if len(compressed_props) == 1: # COMPRESSED
|
|
160
|
+
env, _id, _user_id, _organization_id, _is_superuser, _exp_date, _permissions = data.pop(compressed_props[0])
|
|
161
|
+
token_type = JWT_TYPE__UNCOMPRESSED[compressed_props[0][0]]
|
|
162
|
+
version = int(compressed_props[0][1:])
|
|
163
|
+
|
|
164
|
+
# print('2>>>', json.dumps(_permissions, separators=(',', ':')))
|
|
165
|
+
id = cls.decompress_uuid(_id)
|
|
166
|
+
user_id = cls.decompress_uuid(_user_id)
|
|
167
|
+
organization_id = cls.decompress_uuid(_organization_id) if len(_organization_id) > 0 else None
|
|
168
|
+
is_superuser = bool(_is_superuser)
|
|
169
|
+
exp_date = datetime.fromtimestamp(JWT_EXP_DATE_TIMESTAMP_BASIS + _exp_date * 60)
|
|
170
|
+
permissions = set(sorted(cls.decompress_permissions(_permissions)))
|
|
171
|
+
else:
|
|
172
|
+
# print('1>>>', json.dumps(list(sorted(data['permissions'])), separators=(',', ':')))
|
|
173
|
+
id = UUID(data.pop('id'))
|
|
174
|
+
env = data.pop('env')
|
|
175
|
+
exp_date = datetime.fromisoformat(data.pop('exp_date'))
|
|
176
|
+
version = int(data.pop('version'))
|
|
177
|
+
token_type = data.pop('token_type')
|
|
178
|
+
user_id = UUID(data.pop('user_id'))
|
|
179
|
+
organization_id = UUID(data.pop('organization_id')) if data.get('organization_id', None) is not None else None
|
|
180
|
+
is_superuser = bool(data.pop('is_superuser')) if data.get('is_superuser') else False
|
|
181
|
+
permissions = set(sorted(data.pop('permissions')))
|
|
182
|
+
|
|
183
|
+
if not isinstance(env, str):
|
|
184
|
+
raise TypeError('invalid type of env')
|
|
185
|
+
|
|
186
|
+
if not isinstance(token_type, str):
|
|
187
|
+
raise TypeError('invalid type of token_type')
|
|
188
|
+
|
|
189
|
+
return ApiSdkJwt(
|
|
190
|
+
id=id,
|
|
191
|
+
env=env,
|
|
192
|
+
token_type=token_type,
|
|
193
|
+
exp_date=exp_date,
|
|
194
|
+
version=str(version),
|
|
195
|
+
user_id=user_id,
|
|
196
|
+
organization_id=organization_id,
|
|
197
|
+
is_superuser=is_superuser,
|
|
198
|
+
permissions=permissions,
|
|
199
|
+
additional_data=data,
|
|
200
|
+
username=username,
|
|
201
|
+
raw=token,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def ensure_organization_id(self) -> UUID:
|
|
205
|
+
if self.organization_id is None:
|
|
206
|
+
raise PermissionDeniedApiError('you must be logged in some organisation')
|
|
207
|
+
return self.organization_id
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def is_expired(self) -> bool:
|
|
211
|
+
return self.exp_date < datetime.now()
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def is_refresh_token(self) -> bool:
|
|
215
|
+
return self.token_type == JWT_TYPE__REFRESH
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def is_access_token(self) -> bool:
|
|
219
|
+
return self.token_type == JTW_TYPE__ACCESS
|
|
220
|
+
|
|
221
|
+
def has_permission(self, permission: Union[PermissionDefinition, int]) -> bool:
|
|
222
|
+
if isinstance(permission, int):
|
|
223
|
+
return permission in self.permissions
|
|
224
|
+
|
|
225
|
+
if isinstance(permission, PermissionDefinition):
|
|
226
|
+
return permission.id in self.permissions
|
|
227
|
+
|
|
228
|
+
raise TypeError('invalid permission type')
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def create_jwt_pair(
|
|
232
|
+
*,
|
|
233
|
+
environment: str,
|
|
234
|
+
user_id: Union[str, UUID],
|
|
235
|
+
organization_id: Optional[Union[str, UUID]],
|
|
236
|
+
permissions: List[Union[int, PermissionDefinition]],
|
|
237
|
+
is_superuser: Optional[bool] = False,
|
|
238
|
+
access_expiration_date: Optional[datetime] = None,
|
|
239
|
+
refresh_expiration_date: Optional[datetime] = None,
|
|
240
|
+
additional_data: Optional[Dict[str, Any]] = None,
|
|
241
|
+
) -> Tuple['ApiSdkJwt', 'ApiSdkJwt']:
|
|
242
|
+
if additional_data is None:
|
|
243
|
+
additional_data = dict()
|
|
244
|
+
id = uuid4()
|
|
245
|
+
now = datetime.now()
|
|
246
|
+
|
|
247
|
+
user_id = user_id if isinstance(user_id, UUID) else UUID(user_id)
|
|
248
|
+
if organization_id is not None:
|
|
249
|
+
organization_id = organization_id if isinstance(organization_id, UUID) else UUID(organization_id)
|
|
250
|
+
|
|
251
|
+
at = ApiSdkJwt(
|
|
252
|
+
id=id,
|
|
253
|
+
env=str(environment),
|
|
254
|
+
version=JWT_VERSION,
|
|
255
|
+
user_id=user_id,
|
|
256
|
+
organization_id=organization_id,
|
|
257
|
+
is_superuser=is_superuser,
|
|
258
|
+
permissions={(p if isinstance(p, int) else p.id) for p in permissions},
|
|
259
|
+
additional_data=additional_data,
|
|
260
|
+
token_type=JTW_TYPE__ACCESS,
|
|
261
|
+
exp_date=access_expiration_date or (now + JWT_ACCESS_TOKEN_TTL),
|
|
262
|
+
)
|
|
263
|
+
rt = ApiSdkJwt(
|
|
264
|
+
id=id,
|
|
265
|
+
env=str(environment),
|
|
266
|
+
version=JWT_VERSION,
|
|
267
|
+
user_id=user_id,
|
|
268
|
+
organization_id=organization_id,
|
|
269
|
+
is_superuser=is_superuser,
|
|
270
|
+
permissions={(p if isinstance(p, int) else p.id) for p in permissions},
|
|
271
|
+
additional_data=additional_data,
|
|
272
|
+
token_type=JWT_TYPE__REFRESH,
|
|
273
|
+
exp_date=refresh_expiration_date or (now + JWT_REFRESH_TOKEN_TTL),
|
|
274
|
+
)
|
|
275
|
+
return at, rt
|
|
276
|
+
|
|
277
|
+
def create_access_token(self, expiration_date: Optional[datetime] = None) -> 'ApiSdkJwt':
|
|
278
|
+
if not self.is_refresh_token:
|
|
279
|
+
raise AccessApiError('invalid token type')
|
|
280
|
+
|
|
281
|
+
exp_date = expiration_date if expiration_date is not None else min(datetime.now() + JWT_ACCESS_TOKEN_TTL, self.exp_date)
|
|
282
|
+
|
|
283
|
+
if exp_date > self.exp_date:
|
|
284
|
+
exp_date = self.exp_date
|
|
285
|
+
|
|
286
|
+
return ApiSdkJwt(
|
|
287
|
+
id=self.id,
|
|
288
|
+
env=self.env,
|
|
289
|
+
version=self.version,
|
|
290
|
+
user_id=self.user_id,
|
|
291
|
+
organization_id=self.organization_id,
|
|
292
|
+
is_superuser=self.is_superuser,
|
|
293
|
+
permissions=self.permissions,
|
|
294
|
+
token_type=JTW_TYPE__ACCESS,
|
|
295
|
+
exp_date=exp_date,
|
|
296
|
+
additional_data=self.additional_data,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def compress_uuid(cls, id: UUID) -> str:
|
|
301
|
+
# return "".join(chr(i+10) for i in id.bytes)
|
|
302
|
+
# return str(id.int)
|
|
303
|
+
return base64.b85encode(id.bytes).decode('utf-8')
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def decompress_uuid(cls, id: str) -> UUID:
|
|
307
|
+
return UUID(bytes=base64.b85decode(id))
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def compress_permissions(cls, permissions: Iterable[int]) -> str:
|
|
311
|
+
permissions = list(sorted(set(permissions)))
|
|
312
|
+
if len(permissions) == 0:
|
|
313
|
+
return ''
|
|
314
|
+
if len(permissions) == 1:
|
|
315
|
+
return str(permissions[0])
|
|
316
|
+
res_permissions: List[Tuple[int, int]] = []
|
|
317
|
+
prev_p: Optional[int] = None
|
|
318
|
+
for p in permissions:
|
|
319
|
+
if prev_p is None:
|
|
320
|
+
res_permissions.append((p, 1))
|
|
321
|
+
else:
|
|
322
|
+
prev_inc, prev_cnt = res_permissions[-1]
|
|
323
|
+
cur_inc = p - prev_p
|
|
324
|
+
if prev_inc == cur_inc:
|
|
325
|
+
res_permissions[-1] = prev_inc, prev_cnt + 1
|
|
326
|
+
else:
|
|
327
|
+
res_permissions.append((cur_inc, 1))
|
|
328
|
+
prev_p = p
|
|
329
|
+
res = ''
|
|
330
|
+
for v, c in res_permissions:
|
|
331
|
+
res += sorted((i for i in (cls._compress_v1(v, c), cls._compress_v2(v, c)) if len(i)), key=len)[0]
|
|
332
|
+
res = res.strip()
|
|
333
|
+
return res
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def _compress_v1(cls, v: int, c: int) -> str:
|
|
337
|
+
l_cnt = len(JWT_SYMBOL_MAP_FOR_CNT)
|
|
338
|
+
# return f'{JWT_SYMBOL_MAP_FOR_CNT[-1] * (c // l_cnt)}{JWT_SYMBOL_MAP_FOR_CNT[(c % l_cnt) - 1] if c % l_cnt != 0 else ""}{v}'
|
|
339
|
+
return (
|
|
340
|
+
f'{JWT_SYMBOL_MAP_FOR_CNT[-1] * (c // l_cnt)}{JWT_SYMBOL_MAP_FOR_CNT[(c % l_cnt) - 1] if c % l_cnt != 0 else ""}'
|
|
341
|
+
f'{v if v < 10 or (v - 9 > len(JWT_SYMBOL_MAP_FOR_INC)) else JWT_SYMBOL_MAP_FOR_INC[v -1 - 9]}'
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def _compress_v2(cls, v: int, c: int) -> str:
|
|
346
|
+
if v > len(JWT_SYMBOL_MAP_FOR_INC):
|
|
347
|
+
return ''
|
|
348
|
+
return f"{JWT_SYMBOL_MAP_FOR_INC[v - 1]}{c if c != 1 else ''}"
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def decompress_permissions(cls, permissions: str) -> List[int]:
|
|
352
|
+
if not permissions:
|
|
353
|
+
return []
|
|
354
|
+
res_permissions: List[int] = []
|
|
355
|
+
for c, v in re.compile(r'(\D+)(\d*)').findall(f' {permissions}' if permissions[0] in '123456789' else permissions):
|
|
356
|
+
res: List[Tuple[int, int]] = []
|
|
357
|
+
cur_grp = ''
|
|
358
|
+
for i, ci in enumerate(c):
|
|
359
|
+
i_last = i == (len(c) - 1)
|
|
360
|
+
if ci in JWT_SYMBOL_MAP_FOR_CNT:
|
|
361
|
+
cur_grp += ci
|
|
362
|
+
else:
|
|
363
|
+
if len(cur_grp):
|
|
364
|
+
cii_v = JWT_SYMBOL_MAP_FOR_INC.find(ci) + 1 + 9
|
|
365
|
+
for cii in cur_grp:
|
|
366
|
+
res.append((cii_v, JWT_SYMBOL_MAP_FOR_CNT.find(cii) + 1))
|
|
367
|
+
cur_grp = ''
|
|
368
|
+
else:
|
|
369
|
+
res.append((JWT_SYMBOL_MAP_FOR_INC.find(ci) + 1, 1 if (len(v) == 0) or not i_last else int(v)))
|
|
370
|
+
if i_last and len(cur_grp):
|
|
371
|
+
for cii in cur_grp:
|
|
372
|
+
res.append((1 if len(v) == 0 else int(v), JWT_SYMBOL_MAP_FOR_CNT.find(cii) + 1))
|
|
373
|
+
cur_grp = ''
|
|
374
|
+
for val, cnt in res:
|
|
375
|
+
for _i in range(cnt):
|
|
376
|
+
res_permissions.append(res_permissions[-1] + val if res_permissions else val)
|
|
377
|
+
return res_permissions
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Optional, TYPE_CHECKING, Union
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from ul_api_utils.modules.api_sdk import ApiSdk
|
|
5
|
+
from ul_api_utils.modules.worker_sdk import WorkerSdk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_configured_sdk: Optional[str] = None
|
|
9
|
+
_initialized_sdk: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def try_configure(obj: Union['ApiSdk', 'WorkerSdk']) -> None:
|
|
13
|
+
global _configured_sdk
|
|
14
|
+
if _configured_sdk is not None:
|
|
15
|
+
raise OverflowError(f'configured ApiSdk/WorkerSdk must be only one! {_configured_sdk} has already configured. Please check your isolation of imports.')
|
|
16
|
+
_configured_sdk = type(obj).__name__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def try_init(obj: Union['ApiSdk', 'WorkerSdk'], app_name: str) -> str:
|
|
20
|
+
if _configured_sdk is None:
|
|
21
|
+
raise OverflowError(f'{type(obj).__name__} was not configured')
|
|
22
|
+
|
|
23
|
+
assert isinstance(app_name, str) and len(app_name.strip()) > 0, f'app_name must be NOT EMPTY str. "{type(app_name).__name__}" was given'
|
|
24
|
+
|
|
25
|
+
global _initialized_sdk
|
|
26
|
+
if _initialized_sdk is not None:
|
|
27
|
+
raise OverflowError(
|
|
28
|
+
'initialized ApiSdk/WorkerSdk must be only one! '
|
|
29
|
+
f'{_configured_sdk} with name="{app_name}" has already initialized. '
|
|
30
|
+
'Please check your isolation of imports.',
|
|
31
|
+
)
|
|
32
|
+
_initialized_sdk = app_name
|
|
33
|
+
|
|
34
|
+
return app_name.strip()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from logging import Logger
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ul_api_utils.sentry import sentry
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sentry_sdk import Scope
|
|
9
|
+
import flask_sqlalchemy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkerContext:
|
|
13
|
+
|
|
14
|
+
__slots__ = (
|
|
15
|
+
'_logger',
|
|
16
|
+
'_sentry_scope',
|
|
17
|
+
'_db',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, logger: Logger, sentry_scope: 'Scope', db: Optional['flask_sqlalchemy.SQLAlchemy']) -> None:
|
|
21
|
+
self._logger = logger
|
|
22
|
+
self._sentry_scope = sentry_scope
|
|
23
|
+
self._db = db
|
|
24
|
+
|
|
25
|
+
def sentry_capture(self, e: Exception) -> None:
|
|
26
|
+
sentry.capture_exception(e)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def logger(self) -> Logger:
|
|
30
|
+
return self._logger
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def db(self) -> 'flask_sqlalchemy.SQLAlchemy':
|
|
34
|
+
assert self._db is not None
|
|
35
|
+
return self._db
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TypeVar, Callable, Any, Union, Dict, Optional, Tuple, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from flask import Flask
|
|
7
|
+
from ul_unipipeline.message.uni_message import UniMessage
|
|
8
|
+
from ul_unipipeline.worker.uni_worker import UniWorker
|
|
9
|
+
from ul_unipipeline.worker.uni_worker_consumer_message import UniWorkerConsumerMessage
|
|
10
|
+
|
|
11
|
+
from ul_api_utils.conf import APPLICATION_START_DT
|
|
12
|
+
from ul_api_utils.modules.intermediate_state import try_init, try_configure
|
|
13
|
+
from ul_api_utils.modules.worker_context import WorkerContext
|
|
14
|
+
from ul_api_utils.modules.worker_sdk_config import WorkerSdkConfig
|
|
15
|
+
from ul_api_utils.resources.socketio import init_socket_io
|
|
16
|
+
from ul_api_utils.sentry import sentry
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import flask_socketio # type: ignore # lib without mypy stubs
|
|
20
|
+
from ul_db_utils.modules.postgres_modules.db import DbConfig
|
|
21
|
+
|
|
22
|
+
TI = TypeVar('TI', bound=UniMessage)
|
|
23
|
+
TO = TypeVar('TO', bound=UniMessage)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WorkerSdk:
|
|
27
|
+
__slots__ = (
|
|
28
|
+
'_initialized_flask_name',
|
|
29
|
+
'_config',
|
|
30
|
+
'_db_initialized',
|
|
31
|
+
'_flask_app_cache',
|
|
32
|
+
'_sio',
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: WorkerSdkConfig) -> None:
|
|
36
|
+
try_configure(self)
|
|
37
|
+
self._initialized_flask_name: Optional[str] = None
|
|
38
|
+
self._config = config
|
|
39
|
+
self._db_initialized = False
|
|
40
|
+
self._flask_app_cache: Optional[Flask] = None
|
|
41
|
+
self._sio: Optional['flask_socketio.SocketIO'] = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def socket(self) -> 'flask_socketio.SocketIO':
|
|
45
|
+
assert self._sio is not None, "SocketIO client is not configured, try adding SocketIOConfig to your WorkerSdk first."
|
|
46
|
+
return self._sio
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def _flask_app(self) -> Flask:
|
|
50
|
+
if self._flask_app_cache is not None:
|
|
51
|
+
return self._flask_app_cache
|
|
52
|
+
|
|
53
|
+
if not self._initialized_flask_name:
|
|
54
|
+
raise OverflowError('app was not initialized')
|
|
55
|
+
|
|
56
|
+
self._flask_app_cache = Flask(import_name=self._initialized_flask_name)
|
|
57
|
+
|
|
58
|
+
return self._flask_app_cache
|
|
59
|
+
|
|
60
|
+
def init(self, app_name: str, *, db_config: Optional['DbConfig'] = None) -> 'WorkerSdk':
|
|
61
|
+
self._initialized_flask_name = try_init(self, app_name)
|
|
62
|
+
self._sio = init_socket_io(config=self._config.socket_config)
|
|
63
|
+
if db_config is not None:
|
|
64
|
+
from ul_db_utils.utils.waiting_for_postgres import waiting_for_postgres
|
|
65
|
+
self._db_initialized = True
|
|
66
|
+
db_config._init_from_sdk_with_flask(self)
|
|
67
|
+
waiting_for_postgres(db_config.uri)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def init_with_flask(self, app_name: str, *, db_config: Optional['DbConfig'] = None) -> Tuple['WorkerSdk', Flask]:
|
|
71
|
+
self.init(app_name, db_config=db_config)
|
|
72
|
+
return self, self._flask_app
|
|
73
|
+
|
|
74
|
+
def handle_message(self, log_edges: bool = True) -> Callable[[Callable[[UniWorker[TI, Optional[TO]], WorkerContext, UniWorkerConsumerMessage[TI]], Optional[Union[Optional[TO], Dict[str, Any]]]]], Callable[[UniWorker[TI, Optional[TO]], UniWorkerConsumerMessage[TI]], Optional[Union[Optional[TO], Dict[str, Any]]]]]: # noqa: E501 # type: ignore
|
|
75
|
+
assert self._initialized_flask_name is not None
|
|
76
|
+
|
|
77
|
+
def wrapper(fn: Callable[[UniWorker[TI, Optional[TO]], WorkerContext, UniWorkerConsumerMessage[TI]], Optional[Union[Optional[TO], Dict[str, Any]]]]) -> Callable[[UniWorker[TI, Optional[TO]], UniWorkerConsumerMessage[TI]], Optional[Union[Optional[TO], Dict[str, Any]]]]: # noqa: E501
|
|
78
|
+
mdl = fn.__module__
|
|
79
|
+
logger = logging.getLogger(mdl)
|
|
80
|
+
|
|
81
|
+
@functools.wraps(fn)
|
|
82
|
+
def wr_handle_message(wrk: UniWorker[TI, Optional[TO]], message: UniWorkerConsumerMessage[TI]) -> Optional[Union[Optional[TO], Dict[str, Any]]]:
|
|
83
|
+
worker_name = type(wrk).__name__
|
|
84
|
+
if log_edges:
|
|
85
|
+
logger.info(f'worker "{worker_name}" handle message :: START :: {message._meta.payload}')
|
|
86
|
+
with self._flask_app.app_context(), sentry.configure_scope() as sentry_scope:
|
|
87
|
+
sentry_scope.set_tag('app_name', self._initialized_flask_name)
|
|
88
|
+
sentry_scope.set_tag('app_type', 'worker')
|
|
89
|
+
sentry_scope.set_tag('app_uptime', f'{(datetime.now() - APPLICATION_START_DT).seconds // 60}s')
|
|
90
|
+
sentry_scope.set_tag('app_worker_name', type(wrk).__name__)
|
|
91
|
+
if message.worker_creator:
|
|
92
|
+
sentry_scope.set_tag('app_worker_creator', message.worker_creator)
|
|
93
|
+
|
|
94
|
+
db_instance = None
|
|
95
|
+
if self._db_initialized:
|
|
96
|
+
from ul_db_utils.modules.postgres_modules import db
|
|
97
|
+
db_instance = db.db
|
|
98
|
+
|
|
99
|
+
ctx = WorkerContext(
|
|
100
|
+
logger=logger,
|
|
101
|
+
sentry_scope=sentry_scope, # type: ignore
|
|
102
|
+
db=db_instance,
|
|
103
|
+
)
|
|
104
|
+
res = fn(wrk, ctx, message)
|
|
105
|
+
if log_edges:
|
|
106
|
+
logger.info(f'worker "{worker_name}" handle message :: END :: {res}')
|
|
107
|
+
return res
|
|
108
|
+
return wr_handle_message
|
|
109
|
+
return wrapper
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import ConfigDict, BaseModel
|
|
2
|
+
|
|
3
|
+
from ul_api_utils.resources.socketio import SocketIOConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WorkerSdkConfig(BaseModel):
|
|
7
|
+
socket_config: SocketIOConfig | None = None
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
extra="forbid",
|
|
11
|
+
frozen=True,
|
|
12
|
+
arbitrary_types_allowed=True,
|
|
13
|
+
)
|
ul_api_utils/py.typed
ADDED
|
File without changes
|
|
File without changes
|