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,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