gengineapi 0.1.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.
gengineapi/http.py ADDED
@@ -0,0 +1,540 @@
1
+ """
2
+ Модуль, содержащий асинхронный HTTP-клиент для взаимодействия с API G-Engine.
3
+
4
+ Поддерживает JWT аутентификацию, обработку различных HTTP-методов,
5
+ автоматическое форматирование URL, механизм повторных попыток и подробное логирование.
6
+ Также поддерживает работу через прокси, включая SOCKS5.
7
+ """
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import time
12
+ from typing import Any, Dict, List, Optional, Tuple, Union
13
+
14
+ import aiohttp
15
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout, TCPConnector
16
+
17
+ from .exceptions import (
18
+ ApiConnectionError,
19
+ ApiError,
20
+ ApiParsingError,
21
+ ApiTimeoutError,
22
+ create_api_error,
23
+ )
24
+
25
+
26
+ class AsyncHttpClient:
27
+ """
28
+ Асинхронный HTTP-клиент для выполнения запросов к API.
29
+
30
+ Attributes:
31
+ base_url: Базовый URL для API
32
+ jwt_token: JWT токен для аутентификации
33
+ timeout: Таймаут для запросов в секундах
34
+ max_retries: Максимальное количество повторных попыток при ошибках
35
+ retry_statuses: Список кодов статуса для повторных попыток
36
+ retry_exceptions: Список исключений для повторных попыток
37
+ logger: Логгер для записи информации о запросах и ответах
38
+ proxy: Прокси для запросов (например, 'socks5://user:pass@host:port')
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ base_url: str,
44
+ jwt_token: Optional[str] = None,
45
+ timeout: int = 30,
46
+ max_retries: int = 3,
47
+ retry_statuses: List[int] = None,
48
+ logger: Optional[logging.Logger] = None,
49
+ proxy: Optional[str] = None,
50
+ ) -> None:
51
+ """
52
+ Инициализация HTTP-клиента.
53
+
54
+ Args:
55
+ base_url: Базовый URL для API
56
+ jwt_token: JWT токен для аутентификации (опционально)
57
+ timeout: Таймаут для запросов в секундах (по умолчанию 30)
58
+ max_retries: Максимальное количество повторных попыток (по умолчанию 3)
59
+ retry_statuses: Список кодов статуса для повторных попыток
60
+ (по умолчанию [429, 500, 502, 503, 504])
61
+ logger: Логгер для записи информации о запросах и ответах (опционально)
62
+ proxy: Прокси для запросов в формате 'socks5://user:pass@host:port' (опционально)
63
+ """
64
+ self.base_url = base_url.rstrip("/")
65
+ self.jwt_token = jwt_token
66
+ self.timeout = timeout
67
+ self.max_retries = max_retries
68
+ self.retry_statuses = retry_statuses or [429, 500, 502, 503, 504]
69
+ self.retry_exceptions = (
70
+ aiohttp.ClientError,
71
+ asyncio.TimeoutError,
72
+ )
73
+ self.logger = logger or logging.getLogger(__name__)
74
+ self.proxy = proxy
75
+ self._session: Optional[ClientSession] = None
76
+ self._session_lock = asyncio.Lock()
77
+
78
+ # Проверяем, является ли прокси SOCKS5
79
+ if self.proxy and self.proxy.startswith('socks5://'):
80
+ try:
81
+ # Попытка импортировать aiohttp_socks
82
+ import aiohttp_socks
83
+ self.logger.info(f"Используется SOCKS5 прокси: {self.proxy}")
84
+ except ImportError:
85
+ self.logger.error("Для использования SOCKS5 прокси необходимо установить пакет 'aiohttp-socks'")
86
+ self.logger.error("Установите его с помощью: pip install aiohttp-socks")
87
+ self.proxy = None
88
+
89
+ async def _ensure_session(self) -> ClientSession:
90
+ """
91
+ Убеждается, что сессия aiohttp существует и валидна.
92
+
93
+ Returns:
94
+ ClientSession: Активная aiohttp сессия
95
+ """
96
+ if self._session is None or self._session.closed:
97
+ async with self._session_lock:
98
+ if self._session is None or self._session.closed:
99
+ timeout = ClientTimeout(total=self.timeout)
100
+
101
+ # Создаем соответствующий коннектор в зависимости от настроек прокси
102
+ if self.proxy:
103
+ if self.proxy.startswith('socks5://'):
104
+ try:
105
+ # Импортируем aiohttp_socks для поддержки SOCKS5 прокси
106
+ from aiohttp_socks import ProxyConnector
107
+ connector = ProxyConnector.from_url(self.proxy, ssl=False)
108
+ self.logger.debug(f"Создан SOCKS5 коннектор для прокси: {self.proxy}")
109
+ except ImportError:
110
+ self.logger.error("Не удалось импортировать aiohttp_socks")
111
+ connector = TCPConnector(ssl=False)
112
+ else:
113
+ # Для HTTP/HTTPS прокси используем встроенные возможности aiohttp
114
+ connector = TCPConnector(ssl=False)
115
+ self.logger.debug(f"Будет использован HTTP прокси: {self.proxy}")
116
+ else:
117
+ connector = TCPConnector(ssl=False)
118
+
119
+ # Создаем сессию
120
+ session_kwargs = {
121
+ 'timeout': timeout,
122
+ 'connector': connector,
123
+ 'raise_for_status': False,
124
+ }
125
+
126
+ # Добавляем прокси для HTTP/HTTPS прокси
127
+ if self.proxy and not self.proxy.startswith('socks5://'):
128
+ session_kwargs['proxy'] = self.proxy
129
+
130
+ self._session = ClientSession(**session_kwargs)
131
+
132
+ return self._session
133
+
134
+ def _build_url(self, endpoint: str) -> str:
135
+ """
136
+ Формирует полный URL для запроса.
137
+
138
+ Args:
139
+ endpoint: Конечная точка API
140
+
141
+ Returns:
142
+ str: Полный URL для запроса
143
+ """
144
+ endpoint = endpoint.lstrip("/")
145
+ return f"{self.base_url}/{endpoint}"
146
+
147
+ def _get_headers(self, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
148
+ """
149
+ Формирует заголовки для запроса, включая авторизацию.
150
+
151
+ Args:
152
+ additional_headers: Дополнительные заголовки для запроса
153
+
154
+ Returns:
155
+ Dict[str, str]: Заголовки для запроса
156
+ """
157
+ headers = {
158
+ "Accept": "application/json",
159
+ "Content-Type": "application/json",
160
+ }
161
+
162
+ if self.jwt_token:
163
+ headers["Authorization"] = f"Bearer {self.jwt_token}"
164
+
165
+ if additional_headers:
166
+ headers.update(additional_headers)
167
+
168
+ return headers
169
+
170
+ async def _parse_response(self, response: ClientResponse) -> Dict[str, Any]:
171
+ """
172
+ Парсит ответ от API.
173
+
174
+ Args:
175
+ response: Ответ от API
176
+
177
+ Returns:
178
+ Dict[str, Any]: Данные из ответа API
179
+
180
+ Raises:
181
+ ApiParsingError: Если не удалось распарсить ответ
182
+ """
183
+ try:
184
+ if response.content_type.startswith("application/json"):
185
+ return await response.json()
186
+ else:
187
+ text = await response.text()
188
+ return {"text": text}
189
+ except (json.JSONDecodeError, aiohttp.ContentTypeError) as e:
190
+ status_text = await response.text()
191
+ self.logger.error(f"Ошибка парсинга ответа: {str(e)}, Содержимое: {status_text[:100]}...")
192
+ raise ApiParsingError(f"Не удалось распарсить ответ: {str(e)}")
193
+
194
+ async def _handle_error_response(self, response: ClientResponse) -> None:
195
+ """
196
+ Обрабатывает ответы с ошибками от API.
197
+
198
+ Args:
199
+ response: Ответ от API с ошибкой
200
+
201
+ Raises:
202
+ ApiError: Соответствующее исключение в зависимости от кода ответа
203
+ """
204
+ try:
205
+ response_data = await self._parse_response(response)
206
+ except ApiParsingError:
207
+ response_data = {}
208
+
209
+ # Получаем сообщение об ошибке из ответа, если оно есть
210
+ message = None
211
+ if isinstance(response_data, dict):
212
+ message = response_data.get("message")
213
+ if not message and "detail" in response_data:
214
+ # Обработка ошибок валидации FastAPI
215
+ detail = response_data["detail"]
216
+ if isinstance(detail, list) and detail:
217
+ errors = [f"{e.get('loc', [''])[0]}: {e.get('msg', '')}" for e in detail if isinstance(e, dict)]
218
+ message = ", ".join(errors)
219
+ else:
220
+ message = str(detail)
221
+
222
+ # Создаем соответствующее исключение
223
+ raise create_api_error(
224
+ status_code=response.status,
225
+ message=message,
226
+ response_data=response_data,
227
+ )
228
+
229
+ async def _request(
230
+ self,
231
+ method: str,
232
+ endpoint: str,
233
+ params: Optional[Dict[str, Any]] = None,
234
+ json_data: Optional[Dict[str, Any]] = None,
235
+ headers: Optional[Dict[str, str]] = None,
236
+ timeout: Optional[int] = None,
237
+ ) -> Dict[str, Any]:
238
+ """
239
+ Выполняет HTTP-запрос с повторными попытками.
240
+
241
+ Args:
242
+ method: HTTP-метод (GET, POST, PUT, DELETE, etc.)
243
+ endpoint: Конечная точка API
244
+ params: Параметры запроса (опционально)
245
+ json_data: Данные для отправки в формате JSON (опционально)
246
+ headers: Дополнительные заголовки для запроса (опционально)
247
+ timeout: Таймаут для запроса в секундах (опционально)
248
+
249
+ Returns:
250
+ Dict[str, Any]: Ответ от API
251
+
252
+ Raises:
253
+ ApiConnectionError: При ошибке соединения
254
+ ApiTimeoutError: При превышении времени ожидания
255
+ ApiError: При других ошибках API
256
+ """
257
+ url = self._build_url(endpoint)
258
+ request_headers = self._get_headers(headers)
259
+ request_timeout = ClientTimeout(total=timeout or self.timeout)
260
+
261
+ attempt = 0
262
+ last_error = None
263
+
264
+ while attempt <= self.max_retries:
265
+ try:
266
+ attempt += 1
267
+ session = await self._ensure_session()
268
+
269
+ # Логирование запроса на уровне DEBUG
270
+ self.logger.debug(
271
+ f"Запрос {method} {url} (попытка {attempt}/{self.max_retries + 1}). "
272
+ f"Параметры: {params}, Данные: {json_data}, Заголовки: {request_headers}"
273
+ )
274
+ if self.proxy:
275
+ self.logger.debug(f"Запрос выполняется через прокси: {self.proxy}")
276
+
277
+ start_time = time.time()
278
+ async with session.request(
279
+ method=method,
280
+ url=url,
281
+ params=params,
282
+ json=json_data,
283
+ headers=request_headers,
284
+ timeout=request_timeout,
285
+ ) as response:
286
+ elapsed_time = time.time() - start_time
287
+
288
+ # Логирование ответа на уровне DEBUG
289
+ self.logger.debug(
290
+ f"Ответ на {method} {url}: статус {response.status}, "
291
+ f"заголовки {dict(response.headers)}, время {elapsed_time:.2f}с"
292
+ )
293
+
294
+ # Если успешный ответ
295
+ if 200 <= response.status < 300:
296
+ return await self._parse_response(response)
297
+
298
+ # Если ошибка, но нужно повторить
299
+ if response.status in self.retry_statuses and attempt <= self.max_retries:
300
+ delay = self._calculate_retry_delay(attempt)
301
+ self.logger.warning(
302
+ f"Получен статус {response.status} при запросе {method} {url}. "
303
+ f"Повторная попытка через {delay:.2f}с (попытка {attempt})"
304
+ )
305
+ await asyncio.sleep(delay)
306
+ continue
307
+
308
+ # Если ошибка и больше не повторяем
309
+ await self._handle_error_response(response)
310
+
311
+ except self.retry_exceptions as e:
312
+ last_error = e
313
+ if attempt <= self.max_retries:
314
+ delay = self._calculate_retry_delay(attempt)
315
+ error_type = type(e).__name__
316
+ self.logger.warning(
317
+ f"Ошибка {error_type} при запросе {method} {url}: {str(e)}. "
318
+ f"Повторная попытка через {delay:.2f}с (попытка {attempt})"
319
+ )
320
+ await asyncio.sleep(delay)
321
+ else:
322
+ break
323
+
324
+ # Если все попытки неудачные
325
+ if isinstance(last_error, asyncio.TimeoutError):
326
+ raise ApiTimeoutError(f"Превышено время ожидания при запросе {method} {url} после {attempt} попыток")
327
+ else:
328
+ raise ApiConnectionError(f"Ошибка соединения при запросе {method} {url} после {attempt} попыток: {last_error}")
329
+
330
+ def _calculate_retry_delay(self, attempt: int) -> float:
331
+ """
332
+ Вычисляет задержку перед повторной попыткой с экспоненциальным ростом.
333
+
334
+ Args:
335
+ attempt: Номер попытки (начиная с 1)
336
+
337
+ Returns:
338
+ float: Задержка в секундах
339
+ """
340
+ # Экспоненциальная задержка с небольшим случайным разбросом
341
+ import random
342
+ base_delay = min(2 ** (attempt - 1), 30) # Ограничиваем до 30 секунд
343
+ jitter = random.uniform(0, 0.5 * base_delay) # Добавляем до 50% случайности
344
+ return base_delay + jitter
345
+
346
+ async def get(
347
+ self,
348
+ endpoint: str,
349
+ params: Optional[Dict[str, Any]] = None,
350
+ headers: Optional[Dict[str, str]] = None,
351
+ timeout: Optional[int] = None,
352
+ ) -> Dict[str, Any]:
353
+ """
354
+ Выполняет GET-запрос.
355
+
356
+ Args:
357
+ endpoint: Конечная точка API
358
+ params: Параметры запроса (опционально)
359
+ headers: Дополнительные заголовки для запроса (опционально)
360
+ timeout: Таймаут для запроса в секундах (опционально)
361
+
362
+ Returns:
363
+ Dict[str, Any]: Ответ от API
364
+ """
365
+ return await self._request("GET", endpoint, params=params, headers=headers, timeout=timeout)
366
+
367
+ async def post(
368
+ self,
369
+ endpoint: str,
370
+ json_data: Optional[Dict[str, Any]] = None,
371
+ params: Optional[Dict[str, Any]] = None,
372
+ headers: Optional[Dict[str, str]] = None,
373
+ timeout: Optional[int] = None,
374
+ ) -> Dict[str, Any]:
375
+ """
376
+ Выполняет POST-запрос.
377
+
378
+ Args:
379
+ endpoint: Конечная точка API
380
+ json_data: Данные для отправки в формате JSON (опционально)
381
+ params: Параметры запроса (опционально)
382
+ headers: Дополнительные заголовки для запроса (опционально)
383
+ timeout: Таймаут для запроса в секундах (опционально)
384
+
385
+ Returns:
386
+ Dict[str, Any]: Ответ от API
387
+ """
388
+ return await self._request("POST", endpoint, params=params, json_data=json_data, headers=headers, timeout=timeout)
389
+
390
+ async def put(
391
+ self,
392
+ endpoint: str,
393
+ json_data: Optional[Dict[str, Any]] = None,
394
+ params: Optional[Dict[str, Any]] = None,
395
+ headers: Optional[Dict[str, str]] = None,
396
+ timeout: Optional[int] = None,
397
+ ) -> Dict[str, Any]:
398
+ """
399
+ Выполняет PUT-запрос.
400
+
401
+ Args:
402
+ endpoint: Конечная точка API
403
+ json_data: Данные для отправки в формате JSON (опционально)
404
+ params: Параметры запроса (опционально)
405
+ headers: Дополнительные заголовки для запроса (опционально)
406
+ timeout: Таймаут для запроса в секундах (опционально)
407
+
408
+ Returns:
409
+ Dict[str, Any]: Ответ от API
410
+ """
411
+ return await self._request("PUT", endpoint, params=params, json_data=json_data, headers=headers, timeout=timeout)
412
+
413
+ async def delete(
414
+ self,
415
+ endpoint: str,
416
+ params: Optional[Dict[str, Any]] = None,
417
+ json_data: Optional[Dict[str, Any]] = None,
418
+ headers: Optional[Dict[str, str]] = None,
419
+ timeout: Optional[int] = None,
420
+ ) -> Dict[str, Any]:
421
+ """
422
+ Выполняет DELETE-запрос.
423
+
424
+ Args:
425
+ endpoint: Конечная точка API
426
+ params: Параметры запроса (опционально)
427
+ json_data: Данные для отправки в формате JSON (опционально)
428
+ headers: Дополнительные заголовки для запроса (опционально)
429
+ timeout: Таймаут для запроса в секундах (опционально)
430
+
431
+ Returns:
432
+ Dict[str, Any]: Ответ от API
433
+ """
434
+ return await self._request("DELETE", endpoint, params=params, json_data=json_data, headers=headers, timeout=timeout)
435
+
436
+ async def patch(
437
+ self,
438
+ endpoint: str,
439
+ json_data: Optional[Dict[str, Any]] = None,
440
+ params: Optional[Dict[str, Any]] = None,
441
+ headers: Optional[Dict[str, str]] = None,
442
+ timeout: Optional[int] = None,
443
+ ) -> Dict[str, Any]:
444
+ """
445
+ Выполняет PATCH-запрос.
446
+
447
+ Args:
448
+ endpoint: Конечная точка API
449
+ json_data: Данные для отправки в формате JSON (опционально)
450
+ params: Параметры запроса (опционально)
451
+ headers: Дополнительные заголовки для запроса (опционально)
452
+ timeout: Таймаут для запроса в секундах (опционально)
453
+
454
+ Returns:
455
+ Dict[str, Any]: Ответ от API
456
+ """
457
+ return await self._request("PATCH", endpoint, params=params, json_data=json_data, headers=headers, timeout=timeout)
458
+
459
+ async def head(
460
+ self,
461
+ endpoint: str,
462
+ params: Optional[Dict[str, Any]] = None,
463
+ headers: Optional[Dict[str, str]] = None,
464
+ timeout: Optional[int] = None,
465
+ ) -> Dict[str, Any]:
466
+ """
467
+ Выполняет HEAD-запрос.
468
+
469
+ Args:
470
+ endpoint: Конечная точка API
471
+ params: Параметры запроса (опционально)
472
+ headers: Дополнительные заголовки для запроса (опционально)
473
+ timeout: Таймаут для запроса в секундах (опционально)
474
+
475
+ Returns:
476
+ Dict[str, Any]: Ответ от API
477
+ """
478
+ return await self._request("HEAD", endpoint, params=params, headers=headers, timeout=timeout)
479
+
480
+ async def options(
481
+ self,
482
+ endpoint: str,
483
+ params: Optional[Dict[str, Any]] = None,
484
+ headers: Optional[Dict[str, str]] = None,
485
+ timeout: Optional[int] = None,
486
+ ) -> Dict[str, Any]:
487
+ """
488
+ Выполняет OPTIONS-запрос.
489
+
490
+ Args:
491
+ endpoint: Конечная точка API
492
+ params: Параметры запроса (опционально)
493
+ headers: Дополнительные заголовки для запроса (опционально)
494
+ timeout: Таймаут для запроса в секундах (опционально)
495
+
496
+ Returns:
497
+ Dict[str, Any]: Ответ от API
498
+ """
499
+ return await self._request("OPTIONS", endpoint, params=params, headers=headers, timeout=timeout)
500
+
501
+ async def close(self) -> None:
502
+ """Закрывает сессию aiohttp."""
503
+ if self._session and not self._session.closed:
504
+ await self._session.close()
505
+ self._session = None
506
+
507
+ def update_token(self, jwt_token: str) -> None:
508
+ """
509
+ Обновляет JWT токен для аутентификации.
510
+
511
+ Args:
512
+ jwt_token: Новый JWT токен
513
+ """
514
+ self.jwt_token = jwt_token
515
+
516
+ def update_proxy(self, proxy: Optional[str] = None) -> None:
517
+ """
518
+ Обновляет настройки прокси.
519
+
520
+ Args:
521
+ proxy: Новый прокси в формате 'socks5://user:pass@host:port' или None для отключения прокси
522
+ """
523
+ # Если прокси изменился, нужно закрыть существующую сессию
524
+ if self.proxy != proxy:
525
+ self.proxy = proxy
526
+
527
+ # Закрываем сессию, чтобы при следующем запросе она была пересоздана с новыми настройками
528
+ if self._session and not self._session.closed:
529
+ asyncio.create_task(self._session.close())
530
+ self._session = None
531
+
532
+ # Проверяем, если указан SOCKS5 прокси, нужен ли aiohttp_socks
533
+ if self.proxy and self.proxy.startswith('socks5://'):
534
+ try:
535
+ import aiohttp_socks
536
+ self.logger.info(f"Используется SOCKS5 прокси: {self.proxy}")
537
+ except ImportError:
538
+ self.logger.error("Для использования SOCKS5 прокси необходимо установить пакет 'aiohttp-socks'")
539
+ self.logger.error("Установите его с помощью: pip install aiohttp-socks")
540
+ self.proxy = None
@@ -0,0 +1,23 @@
1
+ """
2
+ Пакет с модулями API клиента G-Engine.
3
+
4
+ Экспортирует классы модулей для различных функциональных областей API.
5
+ """
6
+
7
+ from .auth import AuthModule
8
+ from .base import BaseApiModule
9
+ from .currencies import CurrenciesModule
10
+ from .finances import FinancesModule
11
+ from .payments import PaymentsModule
12
+ from .transactions import TransactionsModule
13
+ from .users import UsersModule
14
+
15
+ __all__ = [
16
+ 'AuthModule',
17
+ 'BaseApiModule',
18
+ 'CurrenciesModule',
19
+ 'FinancesModule',
20
+ 'PaymentsModule',
21
+ 'TransactionsModule',
22
+ 'UsersModule',
23
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Модуль для работы с аутентификацией в API G-Engine.
3
+
4
+ Предоставляет методы для получения токенов доступа.
5
+ """
6
+ from typing import Any, Dict
7
+
8
+ from .base import BaseApiModule
9
+
10
+
11
+ class AuthModule(BaseApiModule):
12
+ """
13
+ Модуль для работы с аутентификацией.
14
+
15
+ Предоставляет методы для аутентификации пользователя и получения JWT-токена.
16
+ """
17
+
18
+ async def login(self, login: str, password: str) -> Dict[str, Any]:
19
+ """
20
+ Получает токен доступа на основе логина и пароля.
21
+
22
+ Args:
23
+ login: Логин пользователя
24
+ password: Пароль пользователя
25
+
26
+ Returns:
27
+ Dict[str, Any]: Информация о токене доступа
28
+
29
+ Examples:
30
+ >>> async with Client(base_url="https://api.example.com") as client:
31
+ >>> token_info = await client.auth.login(
32
+ >>> login="user@example.com",
33
+ >>> password="password123"
34
+ >>> )
35
+ >>> print(f"Получен токен: {token_info['access_token']}")
36
+ >>>
37
+ >>> # Обновляем токен в клиенте
38
+ >>> client.update_token(token_info['access_token'])
39
+ """
40
+ data = {
41
+ "login": login,
42
+ "password": password,
43
+ }
44
+
45
+ self.logger.info(f"Аутентификация пользователя: {login}")
46
+ return await self._post("auth/token", data=data)