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 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
+
@@ -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
REST2JSON/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .URESTClient import URESTClient
2
+ from .Rest2JSON import REST2JSON
3
+
4
+ __all__ = ['URESTClient','REST2JSON']
5
+