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.
@@ -0,0 +1,8 @@
1
+ from .OASParser import OASParser
2
+ from .loggerdec import log_this
3
+ from .utils import OpenAPIToSparkConverter
4
+ __all__ = [
5
+ 'OASParser',
6
+ 'log_this',
7
+ 'OpenAPIToSparkConverter',
8
+ ]
@@ -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
@@ -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)