REST2JSON 0.1.3__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.
- REST2JSON/Rest2JSON.py +330 -0
- REST2JSON/URESTClient.py +130 -0
- REST2JSON/__init__.py +5 -0
- REST2JSON/utils/OASParser.py +774 -0
- REST2JSON/utils/__init__.py +8 -0
- REST2JSON/utils/loggerdec.py +67 -0
- REST2JSON/utils/utils.py +305 -0
- rest2json-0.1.3.dist-info/METADATA +348 -0
- rest2json-0.1.3.dist-info/RECORD +11 -0
- rest2json-0.1.3.dist-info/WHEEL +5 -0
- rest2json-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional, Callable
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
|
9
|
+
|
|
10
|
+
# Настройка базового логгера
|
|
11
|
+
def setup_logger(name: str = "app", log_file: str = None, level=logging.CRITICAL):
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(name)
|
|
14
|
+
|
|
15
|
+
if logger.handlers:
|
|
16
|
+
return logger
|
|
17
|
+
|
|
18
|
+
logger.setLevel(level)
|
|
19
|
+
|
|
20
|
+
formatter = logging.Formatter(
|
|
21
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
22
|
+
datefmt='%Y-%m-%d %H:%M:%S')
|
|
23
|
+
|
|
24
|
+
log_path = Path(log_file)
|
|
25
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
27
|
+
file_handler.setFormatter(formatter)
|
|
28
|
+
logger.addHandler(file_handler)
|
|
29
|
+
|
|
30
|
+
return logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log_this(logger=None, log_args: bool = True, log_result: bool = False, log_exceptions: bool = True):
|
|
34
|
+
if logger is None:
|
|
35
|
+
logger = setup_logger(name="URESTParser", log_file= os.path.join(LOG_DIR, "URESTParser.log"), level=logging.DEBUG)
|
|
36
|
+
|
|
37
|
+
def log(func: Callable):
|
|
38
|
+
@functools.wraps(func)
|
|
39
|
+
def wrapper(*args, **kwargs):
|
|
40
|
+
func_name = func.__qualname__
|
|
41
|
+
start_time = time.time()
|
|
42
|
+
|
|
43
|
+
if log_args:
|
|
44
|
+
args_repr = [repr(a) for a in args]
|
|
45
|
+
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
46
|
+
signature = ", ".join(args_repr + kwargs_repr)
|
|
47
|
+
logger.debug(f"Вызов {func_name} с аргументами: {signature}")
|
|
48
|
+
else:
|
|
49
|
+
logger.debug(f"Вызов {func_name}")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = func(*args, **kwargs)
|
|
53
|
+
elapsed = time.time() - start_time
|
|
54
|
+
|
|
55
|
+
if log_result:
|
|
56
|
+
logger.debug(f"{func_name} вернула {result!r} за {elapsed:.3f} сек")
|
|
57
|
+
else:
|
|
58
|
+
logger.debug(f"{func_name} выполнена за {elapsed:.3f} сек")
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
except Exception as e:
|
|
62
|
+
elapsed = time.time() - start_time
|
|
63
|
+
if log_exceptions:
|
|
64
|
+
logger.exception(f"Исключение в {func_name} через {elapsed:.3f} сек: {e}")
|
|
65
|
+
raise
|
|
66
|
+
return wrapper
|
|
67
|
+
return log
|
REST2JSON/utils/utils.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from typing import Any, List, Dict, Optional,Tuple,Union
|
|
2
|
+
|
|
3
|
+
class OpenAPIToSparkConverter:
|
|
4
|
+
"""Конвертер OpenAPI схем в Spark StructType JSON формат без зависимости от PySpark"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __init__(self,mapping_type: Dict[str, Any]):
|
|
9
|
+
self.type_mapping = mapping_type or {}
|
|
10
|
+
|
|
11
|
+
def convert(self,schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
12
|
+
"""
|
|
13
|
+
Конвертирует OpenAPI схему в Spark StructType JSON формат
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
schema: JSON схема из OpenAPI спецификации
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Схема в формате Spark StructType JSON
|
|
20
|
+
"""
|
|
21
|
+
return self._convert_node(schema, "$")
|
|
22
|
+
|
|
23
|
+
def _convert_node(self, node: Any, path: str) -> Any:
|
|
24
|
+
"""Рекурсивно конвертирует узел схемы"""
|
|
25
|
+
|
|
26
|
+
# Обработка null/None
|
|
27
|
+
if node is None:
|
|
28
|
+
return "null"
|
|
29
|
+
|
|
30
|
+
# Если узел - строка, это может быть тип или ссылка
|
|
31
|
+
if isinstance(node, str):
|
|
32
|
+
if node.startswith("#/"): # Это ссылка
|
|
33
|
+
return "string" # Заглушка для ссылок
|
|
34
|
+
return self._map_type(node)
|
|
35
|
+
|
|
36
|
+
# Если узел - список (enum или anyOf/oneOf)
|
|
37
|
+
if isinstance(node, list):
|
|
38
|
+
return self._handle_array_node(node, path)
|
|
39
|
+
|
|
40
|
+
# Если узел - словарь
|
|
41
|
+
if isinstance(node, dict):
|
|
42
|
+
return self._handle_dict_node(node, path)
|
|
43
|
+
|
|
44
|
+
# Для неизвестных типов
|
|
45
|
+
return "string"
|
|
46
|
+
|
|
47
|
+
def _map_type(self, json_type: str) -> str:
|
|
48
|
+
"""Маппинг JSON типов в Spark типы"""
|
|
49
|
+
return self.type_mapping.get(json_type, "string")
|
|
50
|
+
|
|
51
|
+
def _handle_array_node(self, node: List, path: str) -> str:
|
|
52
|
+
"""Обрабатывает узлы-массивы"""
|
|
53
|
+
# Для enum или anyOf/oneOf берем первый тип
|
|
54
|
+
if node and isinstance(node[0], dict):
|
|
55
|
+
return self._convert_node(node[0], f"{path}[0]")
|
|
56
|
+
return "string"
|
|
57
|
+
|
|
58
|
+
def _handle_dict_node(self, node: Dict, path: str) -> Any:
|
|
59
|
+
"""Обрабатывает узлы-словари"""
|
|
60
|
+
|
|
61
|
+
# Получаем тип узла
|
|
62
|
+
node_type = node.get("type")
|
|
63
|
+
|
|
64
|
+
# Обработка ссылок
|
|
65
|
+
if "$ref" in node:
|
|
66
|
+
return "string" # В реальном проекте нужно резолвить ссылки
|
|
67
|
+
|
|
68
|
+
# Обработка комбинированных схем
|
|
69
|
+
for combo in ["allOf", "anyOf", "oneOf"]:
|
|
70
|
+
if combo in node and node[combo]:
|
|
71
|
+
return self._convert_node(node[combo][0], f"{path}.{combo}[0]")
|
|
72
|
+
|
|
73
|
+
# Обработка в зависимости от типа
|
|
74
|
+
if node_type == "array" or "items" in node:
|
|
75
|
+
return self._handle_array_type(node, path)
|
|
76
|
+
elif node_type == "object" or "properties" in node:
|
|
77
|
+
return self._handle_object_type(node, path)
|
|
78
|
+
else:
|
|
79
|
+
# Примитивный тип
|
|
80
|
+
return self._handle_primitive_type(node, path)
|
|
81
|
+
|
|
82
|
+
def _handle_array_type(self, node: Dict, path: str) -> Dict[str, Any]:
|
|
83
|
+
"""Обрабатывает массив"""
|
|
84
|
+
items = node.get("items", {})
|
|
85
|
+
|
|
86
|
+
# Определяем тип элементов
|
|
87
|
+
if isinstance(items, list):
|
|
88
|
+
element_result = "string"
|
|
89
|
+
else:
|
|
90
|
+
element_result = self._convert_node(items, f"{path}[]")
|
|
91
|
+
|
|
92
|
+
result = {
|
|
93
|
+
"type": "array",
|
|
94
|
+
"containsNull": node.get("nullable", False),
|
|
95
|
+
"elementType": element_result # element_result уже содержит правильную структуру
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
def _handle_object_type(self, node: Dict, path: str) -> Dict[str, Any]:
|
|
101
|
+
"""Обрабатывает объект"""
|
|
102
|
+
properties = node.get("properties", {})
|
|
103
|
+
required = node.get("required", [])
|
|
104
|
+
|
|
105
|
+
fields = []
|
|
106
|
+
for prop_name, prop_schema in properties.items():
|
|
107
|
+
# Определяем nullable
|
|
108
|
+
nullable = prop_name not in required
|
|
109
|
+
|
|
110
|
+
# Конвертируем тип поля
|
|
111
|
+
field_result = self._convert_node(prop_schema, f"{path}.{prop_name}")
|
|
112
|
+
|
|
113
|
+
# Обрабатываем результат в зависимости от его типа
|
|
114
|
+
field_type = field_result
|
|
115
|
+
field_metadata = None
|
|
116
|
+
# Если результат - словарь и содержит metadata с format
|
|
117
|
+
if isinstance(field_result, dict) and "metadata" in field_result:
|
|
118
|
+
field_type = field_result["type"]
|
|
119
|
+
field_metadata = field_result.get("metadata",None)
|
|
120
|
+
# Если результат - строка, просто используем её как тип
|
|
121
|
+
|
|
122
|
+
field = {
|
|
123
|
+
"name": prop_name,
|
|
124
|
+
"nullable": nullable,
|
|
125
|
+
"type": field_type,
|
|
126
|
+
"metadata": {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if field_metadata:
|
|
130
|
+
field["metadata"] = field_metadata
|
|
131
|
+
|
|
132
|
+
fields.append(field)
|
|
133
|
+
|
|
134
|
+
# Сортируем поля для консистентности
|
|
135
|
+
fields.sort(key=lambda x: x["name"])
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"type": "struct",
|
|
139
|
+
"fields": fields
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def _handle_primitive_type(self, node: Dict, path: str) -> Any:
|
|
143
|
+
"""Обрабатывает примитивный тип"""
|
|
144
|
+
node_type = node.get("type", "string")
|
|
145
|
+
node_format = node.get("format")
|
|
146
|
+
|
|
147
|
+
spark_type = self._map_type(node_type)
|
|
148
|
+
|
|
149
|
+
# Собираем все остальные поля в metadata
|
|
150
|
+
metadata = {}
|
|
151
|
+
for key, value in node.items():
|
|
152
|
+
if key not in ["type", "nullable","name"]: # Исключаем уже обработанные поля
|
|
153
|
+
if value is not None:
|
|
154
|
+
metadata[key] = value
|
|
155
|
+
|
|
156
|
+
# Если есть метаданные или формат, возвращаем словарь
|
|
157
|
+
if metadata or node_format:
|
|
158
|
+
return {
|
|
159
|
+
"type": spark_type,
|
|
160
|
+
"metadata": metadata
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return spark_type
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def extract_response_schema(openapi_spec: Dict[str, Any],
|
|
167
|
+
path: str,
|
|
168
|
+
method: str,
|
|
169
|
+
status_code: str = "200") -> Optional[Dict[str, Any]]:
|
|
170
|
+
"""
|
|
171
|
+
Извлекает схему ответа из OpenAPI спецификации
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
openapi_spec: OpenAPI спецификация
|
|
175
|
+
path: путь эндпоинта
|
|
176
|
+
method: HTTP метод
|
|
177
|
+
status_code: код ответа
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
JSON схема ответа или None
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
operation = openapi_spec.get("paths", {}).get(path, {}).get(method.lower(), {})
|
|
184
|
+
responses = operation.get("responses", {})
|
|
185
|
+
response = responses.get(status_code, {})
|
|
186
|
+
content = response.get("content", {})
|
|
187
|
+
|
|
188
|
+
# Ищем application/json или первый попавшийся content type
|
|
189
|
+
for content_type, content_schema in content.items():
|
|
190
|
+
if "application/json" in content_type or content_type.startswith("application/"):
|
|
191
|
+
schema = content_schema.get("schema", {})
|
|
192
|
+
return schema
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"Ошибка при извлечении схемы: {e}")
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
#Отложено
|
|
202
|
+
def skeletonize(obj, is_schema=False):
|
|
203
|
+
if obj is None:
|
|
204
|
+
return "null"
|
|
205
|
+
|
|
206
|
+
if isinstance(obj, dict):
|
|
207
|
+
is_collection = False
|
|
208
|
+
if not is_schema and len(obj) > 1:
|
|
209
|
+
dict_values = [v for v in obj.values() if isinstance(v, dict)]
|
|
210
|
+
if len(dict_values) > len(obj.values()) / 2:
|
|
211
|
+
is_collection = True
|
|
212
|
+
|
|
213
|
+
if is_collection:
|
|
214
|
+
first_key = next(iter(obj))
|
|
215
|
+
return {"collection": skeletonize(obj[first_key], is_schema)}
|
|
216
|
+
|
|
217
|
+
result = {}
|
|
218
|
+
for key, value in obj.items():
|
|
219
|
+
if key in ['facets', 'geo_regions'] and isinstance(value, dict) and not value:
|
|
220
|
+
continue
|
|
221
|
+
result[key] = skeletonize(value, is_schema)
|
|
222
|
+
return result if result else "object"
|
|
223
|
+
|
|
224
|
+
elif isinstance(obj, list):
|
|
225
|
+
if len(obj) > 0:
|
|
226
|
+
return {"array": skeletonize(obj[0], is_schema)}
|
|
227
|
+
else:
|
|
228
|
+
return {"array": "empty"}
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
if isinstance(obj, str):
|
|
232
|
+
return "string"
|
|
233
|
+
elif isinstance(obj, (int, float)):
|
|
234
|
+
return "number"
|
|
235
|
+
elif isinstance(obj, bool):
|
|
236
|
+
return "boolean"
|
|
237
|
+
else:
|
|
238
|
+
return type(obj).__name__
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def compare_skeletons(skel1, skel2, path=""):
|
|
242
|
+
if skel1 is None and skel2 is None:
|
|
243
|
+
return 1.0
|
|
244
|
+
if skel1 is None or skel2 is None:
|
|
245
|
+
return 0.0
|
|
246
|
+
|
|
247
|
+
if isinstance(skel1, str) and isinstance(skel2, str):
|
|
248
|
+
if skel1 == skel2:
|
|
249
|
+
return 1.0
|
|
250
|
+
if skel1 in ("int", "float", "number") and skel2 in ("int", "float", "number"):
|
|
251
|
+
return 1.0
|
|
252
|
+
if skel1 == "null" or skel2 == "null":
|
|
253
|
+
return 0.5
|
|
254
|
+
return 0.0
|
|
255
|
+
|
|
256
|
+
if isinstance(skel1, str) or isinstance(skel2, str):
|
|
257
|
+
return 0.3
|
|
258
|
+
|
|
259
|
+
if isinstance(skel1, dict) and isinstance(skel2, dict):
|
|
260
|
+
special_keys = {"array", "collection", "object"}
|
|
261
|
+
|
|
262
|
+
keys1 = set(skel1.keys())
|
|
263
|
+
keys2 = set(skel2.keys())
|
|
264
|
+
|
|
265
|
+
for key in special_keys:
|
|
266
|
+
if key in keys1 and key in keys2:
|
|
267
|
+
return compare_skeletons(skel1[key], skel2[key])
|
|
268
|
+
|
|
269
|
+
all_keys = keys1 | keys2
|
|
270
|
+
|
|
271
|
+
if not all_keys:
|
|
272
|
+
return 1.0
|
|
273
|
+
|
|
274
|
+
total_score = 0.0
|
|
275
|
+
matched_keys = 0
|
|
276
|
+
|
|
277
|
+
for key in all_keys:
|
|
278
|
+
if key in keys1 and key in keys2:
|
|
279
|
+
score = compare_skeletons(skel1[key], skel2[key])
|
|
280
|
+
total_score += score
|
|
281
|
+
matched_keys += 1
|
|
282
|
+
else:
|
|
283
|
+
total_score += 0.2
|
|
284
|
+
matched_keys += 1
|
|
285
|
+
|
|
286
|
+
return total_score / matched_keys if matched_keys > 0 else 0.0
|
|
287
|
+
|
|
288
|
+
return 0.0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def has_data(response, schema, threshold=0.6):
|
|
292
|
+
data_skeleton = skeletonize(response)
|
|
293
|
+
schema_skeleton = skeletonize(schema, is_schema=True)
|
|
294
|
+
similarity = compare_skeletons(data_skeleton, schema_skeleton)
|
|
295
|
+
|
|
296
|
+
def has_container(skel):
|
|
297
|
+
if isinstance(skel, dict):
|
|
298
|
+
if "array" in skel or "collection" in skel:
|
|
299
|
+
return True
|
|
300
|
+
for value in skel.values():
|
|
301
|
+
if has_container(value):
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
return similarity >= threshold and has_container(data_skeleton)
|