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