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,444 @@
1
+ import contextlib
2
+ import time
3
+ import traceback
4
+ import uuid
5
+ from datetime import datetime
6
+ from enum import Enum, unique
7
+ from typing import List, Optional, NamedTuple, Dict, Any, Generator, Tuple
8
+
9
+ from flask import g
10
+
11
+ from ul_api_utils.conf import APPLICATION_DEBUGGER_PIN
12
+ from ul_api_utils.const import REQUEST_HEADER__DEBUGGER
13
+ from ul_api_utils.utils.api_method import ApiMethod
14
+ from ul_api_utils.utils.colors import COLORS_MAP__TERMINAL, C_NC, C_FG_GRAY, C_FG_GREEN, C_FG_RED, C_FG_YELLOW
15
+
16
+ IND = ' '
17
+ INDENT = ' '
18
+
19
+
20
+ def time_now() -> float:
21
+ return time.perf_counter()
22
+
23
+
24
+ def mark_request_started() -> None:
25
+ g.debug_api_utils_request_started_at = time_now() # type: ignore
26
+
27
+
28
+ def get_request_started_at() -> float:
29
+ return getattr(g, 'debug_api_utils_request_started_at', 0.)
30
+
31
+
32
+ def add_request_stat(stat: 'ApiUtilCallStat') -> None:
33
+ try:
34
+ stats = g.debug_api_utils_request_stat # type: ignore
35
+ except AttributeError:
36
+ stats = g.debug_api_utils_request_stat = [] # type: ignore
37
+ stats.append(stat)
38
+
39
+
40
+ def get_request_stat() -> List['ApiUtilCallStat']:
41
+ return getattr(g, 'debug_api_utils_request_stat', [])
42
+
43
+
44
+ @unique
45
+ class ApiUtilCallStatType(Enum):
46
+ HTTP_REQUEST = 'http'
47
+ SQL_QUERY = 'sql'
48
+ CODE = 'code'
49
+ FILE = 'file'
50
+
51
+ def __repr__(self) -> str:
52
+ return f'{type(self).__name__}.{self.name}'
53
+
54
+
55
+ STAT_TYPE_COLORS: Dict[ApiUtilCallStatType, str] = {
56
+ ApiUtilCallStatType.HTTP_REQUEST: "",
57
+ ApiUtilCallStatType.SQL_QUERY: "",
58
+ ApiUtilCallStatType.CODE: C_FG_GRAY,
59
+ ApiUtilCallStatType.FILE: "",
60
+ }
61
+
62
+ assert set(STAT_TYPE_COLORS.keys()) == set(ApiUtilCallStatType)
63
+
64
+
65
+ MAX_LEN_OF_TYPE = max(len(v.value) for v in ApiUtilCallStatType)
66
+
67
+ prog_blocks = (
68
+ ('▉', 0.85),
69
+ ('▊', 0.75),
70
+ ('▋', 0.6),
71
+ ('▌', 0.5),
72
+ ('▍', 0.4),
73
+ ('▎', 0.2),
74
+ ('▏', 0.1),
75
+ ('', 0.0),
76
+ )
77
+
78
+
79
+ def term_chart(length: int, normalized_value: float) -> str:
80
+ val = round(normalized_value * length, 1)
81
+ suf = ''
82
+ res = val - float(int(val))
83
+ for b, v in prog_blocks:
84
+ if res >= v:
85
+ suf = b
86
+ break
87
+ return f"{'█' * int(val)}{suf}".ljust(length, '_')
88
+
89
+
90
+ def col_duration(
91
+ started_at: float,
92
+ ended_at: Optional[float],
93
+ *,
94
+ max_len: int = 5,
95
+ max_duration: Optional[float] = None,
96
+ chart_size: int = 10,
97
+ cm: Dict[str, str] = COLORS_MAP__TERMINAL,
98
+ ) -> str:
99
+ if ended_at is None:
100
+ return str('N/A ')[:max_len + 1]
101
+ duration = ended_at - started_at
102
+ dur_fg = cm[C_FG_RED] if duration > 0.3 else (cm[C_FG_YELLOW] if duration > 0.1 else cm[C_NC])
103
+ durations = f'{duration:.10f}'
104
+ chart = f'▕{dur_fg}{term_chart(chart_size, duration / max_duration)}{cm[C_NC]}▏' if max_duration is not None and max_duration > 0. else ''
105
+ return f'{dur_fg}{durations[:max_len]}s{cm[C_NC]}{chart}'
106
+
107
+
108
+ class ApiUtilCallStat(NamedTuple):
109
+ type: ApiUtilCallStatType
110
+ started_at: float
111
+ ended_at: Optional[float]
112
+ text: str
113
+ status: str
114
+ ok: bool
115
+ lvl: int
116
+ http_params: Optional[Tuple[Optional[str], str]] = None
117
+
118
+ def unwrap(self) -> Tuple[str, float, Optional[float], str, str, bool, int, Optional[Tuple[Optional[str], str]]]:
119
+ return (
120
+ self.type.value,
121
+ self.started_at,
122
+ self.ended_at,
123
+ self.text,
124
+ self.status,
125
+ self.ok,
126
+ self.lvl,
127
+ self.http_params,
128
+ )
129
+
130
+ @staticmethod
131
+ def wrap(add_lvl: int, *args: Any) -> Optional['ApiUtilCallStat']:
132
+ if len(args) not in {7, 8}:
133
+ return None
134
+ try:
135
+ return ApiUtilCallStat(
136
+ type=ApiUtilCallStatType(args[0]),
137
+ started_at=args[1],
138
+ ended_at=args[2],
139
+ text=args[3],
140
+ status=args[4],
141
+ ok=args[5],
142
+ lvl=args[6] + add_lvl,
143
+ http_params=args[7] if len(args) == 8 else None,
144
+ )
145
+ except Exception: # noqa: B902
146
+ return None
147
+
148
+ @property
149
+ def duration_s(self) -> Optional[float]:
150
+ return self.ended_at - self.started_at if self.ended_at is not None else None
151
+
152
+
153
+ def collecting_enabled() -> bool:
154
+ return getattr(g, 'debug_api_utils_collect_stat', False)
155
+
156
+
157
+ def collecting_enable(enabled: bool) -> None:
158
+ g.debug_api_utils_collect_stat = enabled # type: ignore
159
+
160
+
161
+ def get_stats_request_headers() -> Dict[str, str]:
162
+ if not collecting_enabled():
163
+ return {}
164
+ return {
165
+ REQUEST_HEADER__DEBUGGER: APPLICATION_DEBUGGER_PIN,
166
+ }
167
+
168
+
169
+ def add_http_request_stat(
170
+ *,
171
+ started_at: float,
172
+ method: ApiMethod,
173
+ url: str,
174
+ status_code: Optional[int],
175
+ internal_stats: List[List[Any]],
176
+ request: Optional[str],
177
+ response: str,
178
+ ) -> None:
179
+ now = time_now()
180
+ add_request_stat(ApiUtilCallStat(
181
+ text=f'{method.value.ljust(6)} {url}',
182
+ status=f'{status_code or "UNKNOWN"}',
183
+ started_at=started_at,
184
+ ended_at=now,
185
+ ok=(status_code is not None) and status_code < 400,
186
+ type=ApiUtilCallStatType.HTTP_REQUEST,
187
+ lvl=0,
188
+ http_params=(request, response),
189
+ ))
190
+
191
+ if not isinstance(internal_stats, (list, tuple)):
192
+ return # type: ignore
193
+
194
+ for s in internal_stats:
195
+ st = ApiUtilCallStat.wrap(1, *s)
196
+ if st is None:
197
+ continue
198
+ add_request_stat(st)
199
+
200
+
201
+ def get_stat(*, started_at: float, ended_at: float, code_spans: bool = True, span_threshold: float = 0.01) -> List[ApiUtilCallStat]:
202
+ if not collecting_enabled():
203
+ return []
204
+
205
+ stats: List[ApiUtilCallStat] = list(get_request_stat())
206
+
207
+ from flask_sqlalchemy.record_queries import get_recorded_queries
208
+
209
+ for q in get_recorded_queries():
210
+ ok = q.end_time is not None
211
+
212
+ try:
213
+ text = str(q.statement % q.parameters) if len(q.parameters) else q.statement
214
+ except Exception: # noqa: B902
215
+ text = q.statement
216
+ pass
217
+
218
+ stats.append(ApiUtilCallStat(
219
+ type=ApiUtilCallStatType.SQL_QUERY,
220
+ text=text,
221
+ ok=ok,
222
+ started_at=q.start_time,
223
+ ended_at=q.end_time,
224
+ lvl=0,
225
+ status='OK' if ok else 'ERROR',
226
+ ))
227
+
228
+ stats = sorted(stats, key=lambda obj: obj.started_at)
229
+
230
+ if code_spans:
231
+ if not len(stats):
232
+ code_span = ApiUtilCallStat(
233
+ text='',
234
+ status='OK',
235
+ started_at=started_at,
236
+ ended_at=ended_at,
237
+ ok=True,
238
+ type=ApiUtilCallStatType.CODE,
239
+ lvl=0,
240
+ )
241
+ stats.append(code_span)
242
+ else:
243
+ prev_s: Optional[ApiUtilCallStat] = None
244
+ stats_with_spans = []
245
+ max_dur = 0.
246
+ for s in stats:
247
+ if s.duration_s is not None:
248
+ max_dur = max(max_dur, s.duration_s)
249
+ first_code_span = ApiUtilCallStat(
250
+ text='',
251
+ status='OK',
252
+ started_at=started_at,
253
+ ended_at=stats[0].started_at,
254
+ ok=True,
255
+ type=ApiUtilCallStatType.CODE,
256
+ lvl=0,
257
+ )
258
+ last_code_span = ApiUtilCallStat(
259
+ text='',
260
+ status='OK',
261
+ started_at=stats[-1].ended_at or 0.,
262
+ ended_at=ended_at,
263
+ ok=True,
264
+ type=ApiUtilCallStatType.CODE,
265
+ lvl=0,
266
+ )
267
+ stats_with_spans.append(first_code_span)
268
+ for s in stats:
269
+ if prev_s is not None:
270
+ p_ended_at = prev_s.ended_at or 0.
271
+ span = s.started_at - p_ended_at
272
+ if round(span, 3) >= span_threshold:
273
+ stats_with_spans.append(ApiUtilCallStat(
274
+ text='',
275
+ status='OK',
276
+ started_at=p_ended_at,
277
+ ended_at=s.started_at,
278
+ ok=True,
279
+ type=ApiUtilCallStatType.CODE,
280
+ lvl=prev_s.lvl,
281
+ ))
282
+ stats_with_spans.append(s)
283
+ prev_s = s
284
+ stats_with_spans.append(last_code_span)
285
+ stats = stats_with_spans
286
+
287
+ return stats
288
+
289
+
290
+ class StatMeasure:
291
+
292
+ __slots__ = (
293
+ '_title',
294
+ '_started_at',
295
+ '_ok',
296
+ '_errors',
297
+ '_ended_at',
298
+ )
299
+
300
+ def __init__(self, title: str) -> None:
301
+ self._title = title
302
+ self._started_at = time_now()
303
+ self._ok = False
304
+ self._errors: List[str] = []
305
+ self._ended_at: Optional[float] = None
306
+
307
+ def _internal_use_end(self) -> None:
308
+ assert self._ended_at is None
309
+ self._ended_at = time_now()
310
+
311
+ def format(self) -> str:
312
+ txt = self._title.strip()
313
+ if txt:
314
+ txt += '\n'
315
+ for err_string in self._errors:
316
+ txt += f'{err_string.strip()}\n'
317
+ return txt
318
+
319
+ @property
320
+ def started_at(self) -> float:
321
+ return self._started_at
322
+
323
+ @property
324
+ def ended_at(self) -> Optional[float]:
325
+ return self._ended_at
326
+
327
+ def add_error(self, err_string: str = '') -> None:
328
+ assert isinstance(err_string, str), f'err_string must be str. "{type(err_string).__name__}" was given'
329
+ self._ok = False
330
+ assert self._ended_at is None
331
+ self._errors.append(err_string)
332
+
333
+ @property
334
+ def ok(self) -> bool:
335
+ return self._ok and len(self._errors) == 0
336
+
337
+ @ok.setter
338
+ def ok(self, value: bool) -> None:
339
+ assert self._ended_at is None
340
+ assert isinstance(value, bool), f'value must be bool. "{type(value).__name__}" was given'
341
+ self._ok = value
342
+
343
+
344
+ @contextlib.contextmanager
345
+ def measure(title: str, type: ApiUtilCallStatType = ApiUtilCallStatType.CODE) -> Generator[StatMeasure, None, None]:
346
+ mes = StatMeasure(title)
347
+ try:
348
+ yield mes
349
+ mes.ok = True
350
+ mes._internal_use_end()
351
+ except Exception: # noqa: B902
352
+ mes.add_error(traceback.format_exc())
353
+ mes.ok = False
354
+ mes._internal_use_end()
355
+ raise
356
+ finally:
357
+ add_request_stat(ApiUtilCallStat(
358
+ text=mes.format(),
359
+ status='OK' if mes.ok else 'ERROR',
360
+ started_at=mes.started_at,
361
+ ended_at=mes.ended_at,
362
+ ok=mes.ok,
363
+ type=type,
364
+ lvl=0,
365
+ ))
366
+
367
+
368
+ def mk_stat_string(
369
+ name: str,
370
+ stats: List[ApiUtilCallStat],
371
+ started_at: float,
372
+ ended_at: float,
373
+ truncate_request_len: Optional[int] = None,
374
+ trim_txt_new_line: bool = False,
375
+ cm: Dict[str, str] = COLORS_MAP__TERMINAL,
376
+ ) -> str:
377
+ max_lvl, max_duration = 0, 0.
378
+ max_len_of_status = 0
379
+ unknown_span = ended_at - started_at
380
+ duration_by_categories = {t.value: 0. for t in ApiUtilCallStatType}
381
+ max_duration_by_category = 0.
382
+ for s in stats:
383
+ if s.duration_s is not None:
384
+ duration_by_categories[s.type.value] += s.duration_s
385
+ max_duration_by_category = max(max_duration_by_category, duration_by_categories[s.type.value])
386
+ if s.lvl == 0 and s.duration_s is not None:
387
+ unknown_span -= s.duration_s
388
+ max_lvl = max(max_lvl, s.lvl)
389
+ max_duration = max(max_duration, s.duration_s or 0.)
390
+ max_len_of_status = max(max_len_of_status, len(str(s.status)))
391
+
392
+ total_str = ''
393
+ for cat_type, cat_dur in sorted(duration_by_categories.items(), key=lambda v: v[0]):
394
+ if cat_type == ApiUtilCallStatType.HTTP_REQUEST.value:
395
+ cat_type = f'{cm[C_FG_GRAY]}{cat_type}?{cm[C_NC]}'
396
+ total_str += f'{cat_type}={col_duration(0., cat_dur, max_duration=max_duration_by_category, chart_size=5, cm=cm)}{INDENT}'
397
+
398
+ if unknown_span > 0.001:
399
+ total_str += f'unknown={col_duration(0, unknown_span, max_duration=max_duration_by_category, chart_size=5, cm=cm)}'
400
+
401
+ result_s = ''
402
+ tree_str_size = 3
403
+ i_size = len(str(len(stats)))
404
+ for i, s in enumerate(stats):
405
+ next_s: Optional[ApiUtilCallStat] = None
406
+ has_next_s_with_same_lvl = False
407
+ if len(stats) >= (i + 2):
408
+ if next_s is None:
409
+ next_s = stats[i + 1]
410
+ for ns in stats[i + 1:]:
411
+ if ns.lvl == s.lvl:
412
+ has_next_s_with_same_lvl = True
413
+ break
414
+ if ns.lvl < s.lvl:
415
+ break
416
+ lvl_pref = ('┃' + ' ' * (len(INDENT) - 1)) * s.lvl if next_s is not None else INDENT * s.lvl
417
+ lvl_suf = INDENT * (max_lvl - s.lvl)
418
+ ok_fg = cm[C_FG_RED] if not s.ok else cm[C_FG_GREEN]
419
+ duration_str = col_duration(s.started_at, s.ended_at, max_duration=max_duration, cm=cm)
420
+
421
+ tree_str = '┗'
422
+ if next_s is not None and s.lvl == next_s.lvl:
423
+ tree_str = '┣'
424
+ elif has_next_s_with_same_lvl:
425
+ tree_str = '┣'
426
+
427
+ txt = s.text[:truncate_request_len] if truncate_request_len is not None else s.text
428
+ if trim_txt_new_line:
429
+ txt = txt.replace('\n', ' ').replace('\r', ' ')
430
+ result_s += (
431
+ f'{IND}{cm[C_FG_GRAY]}#{i + 1:0>{i_size}}/{len(stats):0>{i_size}}{IND}{lvl_pref}{tree_str:━<{tree_str_size}}{cm[C_NC]}'
432
+ f' {cm[STAT_TYPE_COLORS[s.type]]}{s.type.value: <{MAX_LEN_OF_TYPE}}{cm[C_NC]} '
433
+ f'{IND}{lvl_suf}{duration_str}'
434
+ f'{IND}{ok_fg}{s.status: <{max_len_of_status}}{cm[C_NC]}'
435
+ f'{IND}{cm[C_FG_GRAY]}::{cm[C_NC]}'
436
+ f'{IND}{txt}\n'
437
+ )
438
+
439
+ header = f'{IND}{" " * (i_size * 2 + 2)}{IND}Request{" " * MAX_LEN_OF_TYPE}{INDENT * max_lvl}{col_duration(started_at, ended_at, cm=cm)}{INDENT}{INDENT}{total_str}'
440
+
441
+ short_identifier = uuid.uuid4().hex[:8]
442
+ return f'▀▀▀▀▀ {name} {cm[C_FG_GRAY]}{short_identifier}{cm[C_NC]}{IND}{"▀" * 55}' \
443
+ f'\n{header}\n' \
444
+ f'{result_s}▄▄▄▄▄ {name} {cm[C_FG_GRAY]}{short_identifier}{cm[C_NC]} {"▄" * 96}'
File without changes
@@ -0,0 +1,15 @@
1
+ import abc
2
+
3
+
4
+ class EncryptDecryptAbstract(abc.ABC):
5
+
6
+ def __init__(self) -> None:
7
+ pass
8
+
9
+ @abc.abstractmethod
10
+ def encrypt(self, decrypted_key: str) -> str:
11
+ raise NotImplementedError('Error')
12
+
13
+ @abc.abstractmethod
14
+ def decrypt(self, encrypted_key: str) -> str:
15
+ raise NotImplementedError('Error')
@@ -0,0 +1,59 @@
1
+ import base64
2
+
3
+ from Crypto.Cipher import AES
4
+ from Crypto.Cipher._mode_gcm import GcmMode
5
+ from Crypto.Random import get_random_bytes
6
+
7
+ from ul_api_utils.encrypt.encrypt_decrypt_abstract import EncryptDecryptAbstract
8
+ from ul_api_utils.errors import SimpleValidateApiError
9
+
10
+
11
+ class EncryptDecryptAESXTEA(EncryptDecryptAbstract):
12
+
13
+ def __init__(self, aes_key: bytes) -> None:
14
+ self._aes_key = aes_key
15
+ super().__init__()
16
+
17
+ @property
18
+ def aes_key(self) -> bytes:
19
+ return self._aes_key
20
+
21
+ def encrypt(self, decrypted_key: str) -> str:
22
+ data = base64.b64decode(decrypted_key)
23
+ if len(data) != 32:
24
+ raise SimpleValidateApiError(f'invalid len of key. must be 32, {len(data)} was given')
25
+ nonce = get_random_bytes(12)
26
+ cipher = AES.new(self.aes_key, AES.MODE_GCM, nonce=nonce)
27
+ assert isinstance(cipher, GcmMode), f'GcmMode required, {type(cipher)} was given'
28
+ cipher_text, tag = cipher.encrypt_and_digest(data)
29
+ decoded_key_encrypted = nonce + cipher_text + tag
30
+ if len(decoded_key_encrypted) != 60:
31
+ raise SimpleValidateApiError('len key_encrypted != 60')
32
+ try:
33
+ encrypted_key = base64.standard_b64encode(decoded_key_encrypted)
34
+ except Exception: # noqa: B902
35
+ raise SimpleValidateApiError('cant encode')
36
+ return encrypted_key.decode('utf-8')
37
+
38
+ def decrypt(self, encrypted_key: str) -> str:
39
+
40
+ if encrypted_key is None:
41
+ raise SimpleValidateApiError('no key')
42
+ try:
43
+ decoded_key_encrypted = base64.standard_b64decode(encrypted_key.encode('utf-8'))
44
+ except Exception: # noqa: B902
45
+ raise SimpleValidateApiError('cant encode')
46
+
47
+ if len(decoded_key_encrypted) != 60:
48
+ raise SimpleValidateApiError('len key_encrypted != 60')
49
+
50
+ nonce = decoded_key_encrypted[:12]
51
+ data = decoded_key_encrypted[12:-16]
52
+ tag = decoded_key_encrypted[-16:]
53
+ cipher = AES.new(self.aes_key, AES.MODE_GCM, nonce=nonce)
54
+ result_key: bytes = cipher.decrypt_and_verify(data, tag) # type: ignore
55
+
56
+ if len(result_key) != 32:
57
+ raise SimpleValidateApiError(f"invalid len of key. must be 32, {len(result_key)} was given")
58
+ decrypted_key = base64.b64encode(result_key)
59
+ return decrypted_key.decode('utf-8')