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
REST2JSON/Rest2JSON.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#Объединяет класс парсера и класс клиента
|
|
2
|
+
|
|
3
|
+
#на вход получает конфигурацию - разбивает ее,выбирает стратегию,по возможности - внешняя оценка результата для контроля загрузки
|
|
4
|
+
import os
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from .utils.OASParser import OASParser
|
|
7
|
+
from .utils.utils import has_data
|
|
8
|
+
from .URESTClient import URESTClient
|
|
9
|
+
|
|
10
|
+
class ParserAdapter(OASParser):
|
|
11
|
+
def __init__(self,OpName,endpoint_url,method,spec):
|
|
12
|
+
self._parser = None
|
|
13
|
+
self.spec = spec
|
|
14
|
+
self.OpName = OpName
|
|
15
|
+
self.endpoint_url = endpoint_url
|
|
16
|
+
self.method = method
|
|
17
|
+
|
|
18
|
+
def get_parser(self):
|
|
19
|
+
if self._parser is None:
|
|
20
|
+
self._parser = OASParser(self.OpName,self.endpoint_url,self.method,self.spec)
|
|
21
|
+
return self._parser
|
|
22
|
+
|
|
23
|
+
class ClientAdapter(URESTClient):
|
|
24
|
+
def __init__(self, entity,extra_headers,base_url,timeout):
|
|
25
|
+
super().__init__(entity, extra_headers,base_url,timeout)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# TODO
|
|
30
|
+
# проверка есть ли ключи словаря в спеке
|
|
31
|
+
# Если required один то подставить его к списку значений
|
|
32
|
+
# формировать data, формировать очередь единичных загрузок
|
|
33
|
+
# Текущая реализация непотокобезопасна
|
|
34
|
+
|
|
35
|
+
class REST2JSON:
|
|
36
|
+
def __init__(self,
|
|
37
|
+
config: dict = None):
|
|
38
|
+
#Загружаем конфигурацию
|
|
39
|
+
(self.payload,
|
|
40
|
+
self.base_url,
|
|
41
|
+
self.OpenAPISpecYAML,
|
|
42
|
+
self.OpenAPISpecYAMLURL,
|
|
43
|
+
self.auth_header,
|
|
44
|
+
self.auth_body,
|
|
45
|
+
self.OpName,
|
|
46
|
+
self.retries,
|
|
47
|
+
self.endpoint_url,
|
|
48
|
+
self.method,
|
|
49
|
+
self.timeout,
|
|
50
|
+
self.paginate,
|
|
51
|
+
self.page_param,
|
|
52
|
+
self.type_mapping) = self.__load_configuration(config)
|
|
53
|
+
#Получаем спецификацию
|
|
54
|
+
self.spec = self._load_specification_(self.OpenAPISpecYAML,self.OpenAPISpecYAMLURL)
|
|
55
|
+
#Загрузка адаптера
|
|
56
|
+
|
|
57
|
+
self.__parser_adapter = ParserAdapter(self.OpName,self.endpoint_url,self.method,self.spec).get_parser()
|
|
58
|
+
self.entity_config = self.__parser_adapter.request
|
|
59
|
+
|
|
60
|
+
self.base_url = self.__getbase_url(self.entity_config)
|
|
61
|
+
#self.Tokens = self.Tokens_MOCK(self.TokensFilename,self.base_url)
|
|
62
|
+
self.__client_adapter = ClientAdapter(self.entity_config,self.auth_header,self.base_url,self.timeout)
|
|
63
|
+
self.__in_context = False # доп флаг
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
if self.__in_context:
|
|
67
|
+
raise RuntimeError("Объект уже используется.")
|
|
68
|
+
self.__client_adapter.__enter__()
|
|
69
|
+
self.__in_context = True
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
74
|
+
self.__in_context = False
|
|
75
|
+
self.__client_adapter.__exit__(exc_type, exc_val, exc_tb)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _load_specification_(self,src,url) ->dict:
|
|
79
|
+
import yaml
|
|
80
|
+
if url: #TODO обработка списка ссылок,добавть break,приоритет
|
|
81
|
+
data = self._get_specfromurl(url)
|
|
82
|
+
elif src:
|
|
83
|
+
try:
|
|
84
|
+
data = yaml.safe_load(src)
|
|
85
|
+
except:
|
|
86
|
+
raise ValueError("Невалидный OpenAPI spec")
|
|
87
|
+
if not data:
|
|
88
|
+
raise ValueError("Отсутствуют источники спецификаций.")
|
|
89
|
+
return data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_specfromurl(self,url):
|
|
93
|
+
import requests,yaml,re
|
|
94
|
+
try:
|
|
95
|
+
if re.match(r'^\w+:\/\/\w',url):
|
|
96
|
+
parsed = urlparse(url)
|
|
97
|
+
if parsed.scheme in ('http', 'https'):
|
|
98
|
+
response = requests.get(url)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
response.encoding = response.apparent_encoding or 'utf-8'
|
|
101
|
+
response = response.text
|
|
102
|
+
elif parsed.scheme == 'file':
|
|
103
|
+
path_part = url.replace('file://', '', 1)
|
|
104
|
+
#запрещенка в ссылке на файл,список возможных символов
|
|
105
|
+
forbidden = ['..', '~', '$', ';', '|', '&', '`', '\\']
|
|
106
|
+
if any(x in path_part for x in forbidden):
|
|
107
|
+
raise ValueError("Обнаружены потенциально опасные символы")
|
|
108
|
+
clean_path = os.path.normpath(path_part)
|
|
109
|
+
abs_path = os.path.abspath(clean_path)
|
|
110
|
+
# 4. Открытие файла
|
|
111
|
+
if os.path.isfile(abs_path) and os.access(abs_path, os.R_OK):
|
|
112
|
+
response = open(abs_path, 'r').read()
|
|
113
|
+
else:
|
|
114
|
+
return None
|
|
115
|
+
data = yaml.safe_load(response)
|
|
116
|
+
return data
|
|
117
|
+
return None
|
|
118
|
+
except requests.exceptions.RequestException as e: #Падение. Есть ли смысл ронять программу здесь,когда возможно обработка списка
|
|
119
|
+
return None
|
|
120
|
+
except yaml.YAMLError as e:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def __getbase_url(self,entity_config):
|
|
124
|
+
base_url = entity_config.get("base_url",None)
|
|
125
|
+
if self.base_url:
|
|
126
|
+
return self.base_url
|
|
127
|
+
elif base_url:
|
|
128
|
+
return base_url
|
|
129
|
+
else:
|
|
130
|
+
return ''
|
|
131
|
+
|
|
132
|
+
def __load_configuration(self,config):
|
|
133
|
+
try:
|
|
134
|
+
proc = config.get('proc',{})
|
|
135
|
+
env = config.get('env',{})
|
|
136
|
+
auth = config.get('auth',{})
|
|
137
|
+
src = proc.get('src',{})
|
|
138
|
+
proc_conn_params = src.get('conn_params',{})
|
|
139
|
+
conn_type = src.get('conn_type',{})
|
|
140
|
+
auth_header,auth_body = auth.get('src',{}).get('header',{}),{}
|
|
141
|
+
if not auth_header:
|
|
142
|
+
auth_body = auth.get('src',{}).get('body',{})
|
|
143
|
+
src_data = proc.get('src',{}).get('data',{})
|
|
144
|
+
name = src.get('name',{})
|
|
145
|
+
type_mapping = env.get('json',{}).get('type_mapping',{})
|
|
146
|
+
type_mapping.update(src_data.get('json_mapping_override',{}))
|
|
147
|
+
payload = src_data.get('payload',None)
|
|
148
|
+
if proc_conn_params:
|
|
149
|
+
endpoint_url = proc_conn_params.get('endpoint_url',None)
|
|
150
|
+
method = proc_conn_params.get('method',None)
|
|
151
|
+
timeout = proc_conn_params.get('timeout',None)
|
|
152
|
+
retries = proc_conn_params.get('retries',None)
|
|
153
|
+
pagination = proc_conn_params.get('pagination',{}).get('enabled',None)
|
|
154
|
+
page_param = proc_conn_params.get('pagination',{}).get('page_param',None)
|
|
155
|
+
spec_url = proc_conn_params.get('spec_url',None)
|
|
156
|
+
spec_data = proc_conn_params.get('spec_data',None)
|
|
157
|
+
base_url = proc_conn_params.get('base_url',None)
|
|
158
|
+
if None in (proc,src,name,conn_type,((endpoint_url and base_url) or name),(spec_url or spec_data),pagination,auth):
|
|
159
|
+
raise ('Отсутствует обязазательный параметр конфигурации (один из списка): proc,src,name,conn_type,(endpoint_url and base_url) or name),(spec_url или spec_data),pagination,auth')
|
|
160
|
+
return payload,base_url,spec_data,spec_url,auth_header,auth_body,name,retries,endpoint_url,method,timeout,pagination,page_param,type_mapping
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(e)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def get_schema(self,raw:bool = False):
|
|
166
|
+
if not isinstance(raw,bool):
|
|
167
|
+
raise TypeError('Неподдерживаемый тип данных')
|
|
168
|
+
if self.__parser_adapter is None:
|
|
169
|
+
return None
|
|
170
|
+
if raw:
|
|
171
|
+
return self.__parser_adapter.get_response_map()
|
|
172
|
+
return self.__parser_adapter.getStructTypeSchema(self.type_mapping)
|
|
173
|
+
|
|
174
|
+
def _prepare_payload(self, data):
|
|
175
|
+
#TODO
|
|
176
|
+
#добавить вставку ApiKey в каждый запрос,
|
|
177
|
+
#по условию если нет хидера и есть боди
|
|
178
|
+
payload = []
|
|
179
|
+
required = self.entity_config.get('required',[])
|
|
180
|
+
datatype = type(data)
|
|
181
|
+
if datatype == dict:
|
|
182
|
+
entity_variables = self.entity_config.get('variables',None)
|
|
183
|
+
keys = data.keys()
|
|
184
|
+
if set(entity_variables) & set(keys):
|
|
185
|
+
print('переменная(ые) есть в списке')
|
|
186
|
+
payload = [data]
|
|
187
|
+
elif datatype == list:
|
|
188
|
+
if all(isinstance(item, dict) for item in data):
|
|
189
|
+
payload = data
|
|
190
|
+
else:
|
|
191
|
+
if len(required) == 1:
|
|
192
|
+
payload = [{required[0]: value} for value in data]
|
|
193
|
+
else:
|
|
194
|
+
print('Требуется явно указать параметр(ы) запроса')
|
|
195
|
+
elif data:
|
|
196
|
+
if required:
|
|
197
|
+
payload = [{required[0]: value} for value in [data]]
|
|
198
|
+
else:
|
|
199
|
+
payload = [data]
|
|
200
|
+
else: # Если запрос - это просто обращение по ссылке
|
|
201
|
+
payload = data
|
|
202
|
+
if not self.auth_header and self.auth_body: #Добавляем пароль к сообщению , 1 приоритет - header
|
|
203
|
+
if payload:
|
|
204
|
+
payload = [value.update(self.auth_body) for value in [data]] # на этот момент payload уже список словарей
|
|
205
|
+
else:
|
|
206
|
+
payload = self.auth_body
|
|
207
|
+
return payload
|
|
208
|
+
|
|
209
|
+
def _is_valid_response(self, response=None,entity_schema=None,debug=True):
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
Проверка валидности ответа от API
|
|
213
|
+
добавить оценку если это массив структур,иначе результат ложноположительный
|
|
214
|
+
|
|
215
|
+
стратегия: заинферить схему из ответа и столкнуть с схемой ответа из специ,похожесть
|
|
216
|
+
еще пандас
|
|
217
|
+
Отложено: всегда обрывает пагинацию
|
|
218
|
+
"""
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
def __direct(self, payload):
|
|
222
|
+
"""
|
|
223
|
+
Прямой запрос к API используя _get/_post
|
|
224
|
+
"""
|
|
225
|
+
got_list = False
|
|
226
|
+
result = []
|
|
227
|
+
if isinstance(payload,list):
|
|
228
|
+
data_list = self._prepare_payload(payload)
|
|
229
|
+
got_list = True
|
|
230
|
+
else:
|
|
231
|
+
data_list = self._prepare_payload(payload)
|
|
232
|
+
try:
|
|
233
|
+
http_client = self.__client_adapter.client
|
|
234
|
+
for data in data_list:
|
|
235
|
+
# Определяем метод из конфига
|
|
236
|
+
method = self.__client_adapter.config.get('method', 'GET').upper()
|
|
237
|
+
|
|
238
|
+
# Формируем URL
|
|
239
|
+
url_template = f"{self.base_url}{self.__client_adapter.config.get('url', '')}"
|
|
240
|
+
if data:
|
|
241
|
+
try:
|
|
242
|
+
url = url_template.format(**data)
|
|
243
|
+
except KeyError:
|
|
244
|
+
url = url_template
|
|
245
|
+
else:
|
|
246
|
+
url = url_template
|
|
247
|
+
|
|
248
|
+
# Выполняем прямой запрос
|
|
249
|
+
if method == 'GET':
|
|
250
|
+
response = http_client._get(url, data)
|
|
251
|
+
else: # POST
|
|
252
|
+
response = http_client._post(url, data)
|
|
253
|
+
|
|
254
|
+
# Валидируем ответ
|
|
255
|
+
if not got_list:
|
|
256
|
+
if isinstance(response,dict):
|
|
257
|
+
return [response]
|
|
258
|
+
else:
|
|
259
|
+
return response
|
|
260
|
+
result.append(response)
|
|
261
|
+
return result
|
|
262
|
+
except Exception as e:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def get_data(self,data = None):
|
|
266
|
+
datatype = type(data).__name__ #Явная проверка на наличие аргумента в вызове метода
|
|
267
|
+
if datatype == 'NoneType':
|
|
268
|
+
data = self.payload
|
|
269
|
+
if self.__in_context:
|
|
270
|
+
return self.__direct(data)
|
|
271
|
+
else:
|
|
272
|
+
payload = self._prepare_payload(data)
|
|
273
|
+
self.__enter__()
|
|
274
|
+
try:
|
|
275
|
+
results = self._execute(payload)
|
|
276
|
+
finally:
|
|
277
|
+
self.__exit__(None, None, None)
|
|
278
|
+
|
|
279
|
+
return results
|
|
280
|
+
|
|
281
|
+
def _execute(self,data):
|
|
282
|
+
results = []
|
|
283
|
+
#разделить на два процесса в зависимости от пагинации
|
|
284
|
+
'''
|
|
285
|
+
#Отложено 11/03/2026
|
|
286
|
+
|
|
287
|
+
if self.paginate:
|
|
288
|
+
for item in data:
|
|
289
|
+
page = 1
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
print(f'proccess page {page}')
|
|
293
|
+
item[self.page_param] = page
|
|
294
|
+
response = self.client_adapter.execute(item)
|
|
295
|
+
if self._is_valid_response(response = response,debug=True): #вынести в контроль загрузки
|
|
296
|
+
results.append(response)
|
|
297
|
+
else:
|
|
298
|
+
break
|
|
299
|
+
page += 1
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"Error processing {item}: {e}")
|
|
302
|
+
break
|
|
303
|
+
'''
|
|
304
|
+
if not data:
|
|
305
|
+
try:
|
|
306
|
+
response = self.__client_adapter.execute() # Вызов без данных
|
|
307
|
+
if self._is_valid_response(response = response,debug=True):
|
|
308
|
+
results.append(response)
|
|
309
|
+
return results
|
|
310
|
+
return []
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Error processing empty payload: {e}")
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
else:
|
|
316
|
+
for item in data:
|
|
317
|
+
try:
|
|
318
|
+
response = self.__client_adapter.execute(item)
|
|
319
|
+
if self._is_valid_response(response = response,debug=True) or True: #вынести в контроль загрузки
|
|
320
|
+
results.append(response)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print(f"Error processing {item}: {e}")
|
|
323
|
+
return results
|
|
324
|
+
|
|
325
|
+
def close(self):
|
|
326
|
+
if self.__in_context:
|
|
327
|
+
self.__in_context = False
|
|
328
|
+
self.__client_adapter.close()
|
|
329
|
+
|
|
330
|
+
|
REST2JSON/URESTClient.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
class ClientBase:
|
|
6
|
+
"""Base class for API client"""
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
headers: dict,
|
|
11
|
+
extra_headers: Optional[dict] = None,
|
|
12
|
+
timeout: int = 3,
|
|
13
|
+
):
|
|
14
|
+
self.headers = headers
|
|
15
|
+
self.headers["Accept"] = "application/json" #default
|
|
16
|
+
if extra_headers:
|
|
17
|
+
for key,value in extra_headers.items():
|
|
18
|
+
self.headers[key] = value
|
|
19
|
+
self._client = httpx.Client(headers=headers, timeout=timeout)
|
|
20
|
+
|
|
21
|
+
def __enter__(self) -> "ClientBase":
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def __exit__(self):
|
|
25
|
+
self.close()
|
|
26
|
+
|
|
27
|
+
def close(self):
|
|
28
|
+
"""Close network connections"""
|
|
29
|
+
self._client.close()
|
|
30
|
+
|
|
31
|
+
def _get(self, url, data,headers = None,timeout = None):
|
|
32
|
+
"""GET request to Dadata API"""
|
|
33
|
+
response = self._client.get(url, params=data)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
return response.json()
|
|
36
|
+
|
|
37
|
+
def _post(self, url, data,headers = None,timeout = None):
|
|
38
|
+
"""POST request to Dadata API"""
|
|
39
|
+
response = self._client.post(url, json=data)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
return response.json()
|
|
42
|
+
|
|
43
|
+
class URESTClient():
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: Any, token: Optional[str] = None, base_url: Optional[str] = None,timeout = None):
|
|
46
|
+
"""
|
|
47
|
+
Инициализация клиента API
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config_file: Путь к файлу конфигурации endpoints
|
|
51
|
+
tokens_file: Путь к файлу с токенами (опционально)
|
|
52
|
+
base_url: Базовый URL для относительных путей (опционально)
|
|
53
|
+
"""
|
|
54
|
+
self.config = copy.deepcopy(config)
|
|
55
|
+
self.token = token
|
|
56
|
+
self.base_url = base_url
|
|
57
|
+
self._client_instance = None
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
self._client_owned = False
|
|
60
|
+
|
|
61
|
+
def _prepare_headers(self):
|
|
62
|
+
headers = self.config.get('headers', {})
|
|
63
|
+
if self.token:
|
|
64
|
+
if isinstance(self.token, dict):
|
|
65
|
+
headers.update(self.token)
|
|
66
|
+
return headers
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def execute(self, data=None):
|
|
70
|
+
data = data or {}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
base = self.base_url or ''
|
|
74
|
+
url_template = f"{base}{self.config.get('url', '')}"
|
|
75
|
+
try:
|
|
76
|
+
if isinstance(data, dict):
|
|
77
|
+
url = url_template.format(**data)
|
|
78
|
+
else: #просочилась строка
|
|
79
|
+
import json
|
|
80
|
+
data_dict = json.loads(data)
|
|
81
|
+
url = url_template.format(data_dict)
|
|
82
|
+
except KeyError:
|
|
83
|
+
url = url_template
|
|
84
|
+
|
|
85
|
+
method = self.config.get('method', 'GET').upper()
|
|
86
|
+
|
|
87
|
+
if method == 'GET':
|
|
88
|
+
return self.client._get(url, data)
|
|
89
|
+
elif method == 'POST':
|
|
90
|
+
return self.client._post(url, data)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
#print(f"Error in execute: {e}")
|
|
96
|
+
self.close()
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def client(self):
|
|
102
|
+
if self._client_instance is None:
|
|
103
|
+
self._client_instance = ClientBase(
|
|
104
|
+
headers=self._prepare_headers(),
|
|
105
|
+
timeout=self.timeout
|
|
106
|
+
)
|
|
107
|
+
self._client_owned = True
|
|
108
|
+
return self._client_instance
|
|
109
|
+
|
|
110
|
+
def close(self):
|
|
111
|
+
if self._client_instance and self._client_owned:
|
|
112
|
+
self._client_instance.close()
|
|
113
|
+
self._client_instance = None
|
|
114
|
+
self._client_owned = False
|
|
115
|
+
|
|
116
|
+
def __enter__(self):
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, *args):
|
|
120
|
+
self.close()
|
|
121
|
+
|
|
122
|
+
def _post(self,url:str,data):
|
|
123
|
+
|
|
124
|
+
response = self.client._post(url=url,data=data)
|
|
125
|
+
return response if response else None
|
|
126
|
+
|
|
127
|
+
def _get(self,url:str,data):
|
|
128
|
+
|
|
129
|
+
response = self.client._get(url=url,data=data)
|
|
130
|
+
return response if response else None
|