gengineapi 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (23) hide show
  1. {gengineapi-0.1.0 → gengineapi-0.2.0}/PKG-INFO +82 -75
  2. {gengineapi-0.1.0 → gengineapi-0.2.0}/README.md +80 -72
  3. {gengineapi-0.1.0 → gengineapi-0.2.0}/pyproject.toml +3 -10
  4. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/__init__.py +4 -0
  5. gengineapi-0.2.0/src/gengineapi/farm.py +464 -0
  6. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi.egg-info/PKG-INFO +82 -75
  7. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi.egg-info/SOURCES.txt +1 -0
  8. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi.egg-info/requires.txt +0 -1
  9. {gengineapi-0.1.0 → gengineapi-0.2.0}/setup.cfg +0 -0
  10. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/client.py +0 -0
  11. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/config.py +0 -0
  12. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/exceptions.py +0 -0
  13. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/http.py +0 -0
  14. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/__init__.py +0 -0
  15. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/auth.py +0 -0
  16. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/base.py +0 -0
  17. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/currencies.py +0 -0
  18. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/finances.py +0 -0
  19. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/payments.py +0 -0
  20. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/transactions.py +0 -0
  21. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi/modules/users.py +0 -0
  22. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi.egg-info/dependency_links.txt +0 -0
  23. {gengineapi-0.1.0 → gengineapi-0.2.0}/src/gengineapi.egg-info/top_level.txt +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gengineapi
3
- Version: 0.1.0
4
- Summary: Add your description here
3
+ Version: 0.2.0
4
+ Summary: G-Engine API Client
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: aiohttp>=3.8.0
8
8
  Requires-Dist: typing-extensions>=4.0.0
9
9
  Requires-Dist: aiohttp-socks>=0.7.1
10
- Requires-Dist: uv-publish
11
10
 
12
11
  # G-Engine API Client
13
12
 
@@ -21,6 +20,7 @@ Requires-Dist: uv-publish
21
20
  - Автоматические повторные попытки для временных ошибок
22
21
  - Поддержка JWT-аутентификации
23
22
  - Поддержка прокси, включая SOCKS5
23
+ - Ферма клиентов с балансировкой нагрузки и стратегиями ротации
24
24
  - Подробное логирование
25
25
  - Строгая типизация с помощью аннотаций типов
26
26
  - Класс-конфигурация для централизованной настройки и повторного использования
@@ -40,21 +40,23 @@ pip install .
40
40
  ## Структура проекта
41
41
 
42
42
  ```
43
- gengine_client/
44
- ├── __init__.py # Экспорт основных классов
45
- ├── client.py # Основной класс клиента
46
- ├── http.py # HTTP клиент с поддержкой аутентификации
47
- ├── exceptions.py # Иерархия исключений API
48
- ├── config.py # Класс-конфигурация для настройки параметров
49
- └── modules/ # Папка с модулями API
50
- ├── __init__.py
51
- ├── base.py # Базовый класс для всех модулей
52
- ├── payments.py # Модуль для работы с платежами
53
- ├── finances.py # Модуль для работы с финансами
54
- ├── auth.py # Модуль для аутентификации
55
- ├── users.py # Модуль для работы с пользователями
56
- ├── transactions.py # Модуль для работы с транзакциями
57
- └── currencies.py # Модуль для работы с валютами
43
+ src/
44
+ └── gengineapi/
45
+ ├── __init__.py # Экспорт основных классов
46
+ ├── client.py # Основной класс клиента
47
+ ├── http.py # HTTP клиент с поддержкой аутентификации
48
+ ├── exceptions.py # Иерархия исключений API
49
+ ├── config.py # Класс-конфигурация для настройки параметров
50
+ ├── farm.py # Ферма клиентов для управления множеством клиентов
51
+ └── modules/ # Папка с модулями API
52
+ ├── __init__.py
53
+ ├── base.py # Базовый класс для всех модулей
54
+ ├── payments.py # Модуль для работы с платежами
55
+ ├── finances.py # Модуль для работы с финансами
56
+ ├── auth.py # Модуль для аутентификации
57
+ ├── users.py # Модуль для работы с пользователями
58
+ ├── transactions.py # Модуль для работы с транзакциями
59
+ └── currencies.py # Модуль для работы с валютами
58
60
  ```
59
61
 
60
62
  ## Использование
@@ -63,7 +65,7 @@ gengine_client/
63
65
 
64
66
  ```python
65
67
  import asyncio
66
- from gengine_client import GEngineClient
68
+ from src.gengineapi import GEngineClient
67
69
 
68
70
  async def main():
69
71
  # Создаем клиент с существующим токеном
@@ -84,7 +86,7 @@ asyncio.run(main())
84
86
 
85
87
  ```python
86
88
  import asyncio
87
- from gengine_client import GEngineClient
89
+ from src.gengineapi import GEngineClient
88
90
 
89
91
  async def main():
90
92
  # Создаем клиент без токена
@@ -119,7 +121,7 @@ asyncio.run(main())
119
121
 
120
122
  ```python
121
123
  import asyncio
122
- from gengine_client import GEngineClient
124
+ from src.gengineapi import GEngineClient
123
125
 
124
126
  async def main():
125
127
  # Создаем клиент с использованием SOCKS5 прокси
@@ -149,7 +151,7 @@ asyncio.run(main())
149
151
 
150
152
  ```python
151
153
  import asyncio
152
- from gengine_client import GEngineConfig
154
+ from src.gengineapi import GEngineConfig
153
155
 
154
156
  async def main():
155
157
  # Настройка параметров клиента
@@ -188,80 +190,85 @@ async def main():
188
190
  asyncio.run(main())
189
191
  ```
190
192
 
191
- ### Загрузка конфигурации из переменных окружения
193
+ ### Использование фермы клиентов
194
+
195
+ Ферма клиентов позволяет управлять множеством клиентов с разными конфигурациями и стратегиями ротации:
192
196
 
193
197
  ```python
194
198
  import asyncio
195
- import os
196
- from gengine_client import GEngineConfig
197
-
198
- # Установка переменных окружения
199
- os.environ["GENGINE_BASE_URL"] = "https://api.example.com/api/v2"
200
- os.environ["GENGINE_TOKEN"] = "env_jwt_token"
201
- os.environ["GENGINE_TIMEOUT"] = "45"
202
- os.environ["GENGINE_MAX_RETRIES"] = "5"
203
- os.environ["GENGINE_PROXY"] = "socks5://127.0.0.1:9050" # Опционально - прокси
199
+ from src.gengineapi import ClientFarm
204
200
 
205
201
  async def main():
206
- # Загрузка настроек из переменных окружения
207
- GEngineConfig.load_from_env()
202
+ # Создаем ферму клиентов
203
+ farm = ClientFarm()
208
204
 
209
- # Получение клиента с настройками из переменных окружения
210
- client = await GEngineConfig.get_client()
205
+ # Добавляем несколько конфигураций с разными прокси
206
+ farm.create_config(
207
+ name="client1",
208
+ base_url="https://api.example.com/api/v2",
209
+ jwt_token="token1",
210
+ proxy="socks5://user1:pass1@host1:port1",
211
+ tags=["group1", "production"]
212
+ )
211
213
 
212
- # Использование клиента
213
- # ...
214
+ farm.create_config(
215
+ name="client2",
216
+ base_url="https://api.example.com/api/v2",
217
+ jwt_token="token2",
218
+ proxy="socks5://user2:pass2@host2:port2",
219
+ tags=["group1", "backup"]
220
+ )
214
221
 
215
- # При завершении приложения
216
- await GEngineConfig.reset()
222
+ # Устанавливаем стратегию выбора клиента
223
+ farm.set_selection_strategy("round_robin") # или "random", "least_errors", "least_recently_used"
224
+
225
+ # Получаем клиент по имени
226
+ client1 = await farm.get_client("client1")
227
+ await client1.users.get_me()
228
+
229
+ # Получаем следующий клиент согласно стратегии
230
+ client = await farm.get_next_client()
231
+ await client.currencies.get_rate(source="cb_rf", pair="USD:RUB")
232
+
233
+ # Получаем клиент по тегу
234
+ client = await farm.get_next_client(tag="production")
235
+ await client.transactions.get_transactions(limit=5)
236
+
237
+ # При завершении работы закрываем все клиенты
238
+ await farm.close_all()
217
239
 
218
240
  asyncio.run(main())
219
241
  ```
220
242
 
221
- ### Работа с платежами
243
+ ### Экспорт и импорт конфигураций фермы
222
244
 
223
245
  ```python
224
246
  import asyncio
225
- import uuid
226
- from decimal import Decimal
227
- from gengine_client import GEngineClient
247
+ from src.gengineapi import ClientFarm
228
248
 
229
249
  async def main():
230
- async with GEngineClient(
231
- base_url="https://api.example.com/api/v2",
232
- jwt_token="your-jwt-token"
233
- ) as client:
234
- # Генерируем уникальный идентификатор транзакции
235
- transaction_id = str(uuid.uuid4())
236
-
237
- # Создаем и верифицируем платеж
238
- payment = await client.payments.verify(
239
- transaction_id=transaction_id,
240
- service_id=1,
241
- account="user123",
242
- amount=Decimal("10.99"),
243
- currency="USD"
244
- )
245
- print(f"Создан платеж: {payment['transaction_id']}")
246
-
247
- # Выполняем платеж
248
- result = await client.payments.execute(transaction_id=transaction_id)
249
- print(f"Статус платежа: {result['status_code']}")
250
-
251
- # Получаем статус платежа
252
- status = await client.payments.get_status(transaction_id=transaction_id)
253
- print(f"Текущий статус платежа: {status['status_code']}")
250
+ # Создаем и настраиваем ферму
251
+ farm = ClientFarm()
252
+
253
+ # Добавляем конфигурации
254
+ farm.create_config(name="client1", base_url="...", jwt_token="...", proxy="...")
255
+ farm.create_config(name="client2", base_url="...", jwt_token="...", proxy="...")
256
+
257
+ # Экспортируем конфигурации в файл
258
+ await farm.export_configs("farm_config.json")
259
+
260
+ # Импортируем конфигурации из файла
261
+ new_farm = await ClientFarm.import_configs("farm_config.json")
262
+
263
+ # Используем импортированные конфигурации
264
+ client = await new_farm.get_next_client()
265
+
266
+ # При завершении работы закрываем все клиенты
267
+ await new_farm.close_all()
254
268
 
255
269
  asyncio.run(main())
256
270
  ```
257
271
 
258
- ## Примеры
259
-
260
- В репозитории есть примеры использования клиента с разными сценариями:
261
-
262
- - `examples.py` - полный набор примеров использования, включая работу с прокси
263
- - `old_examples/` - директория с устаревшими примерами
264
-
265
272
  ## Зависимости
266
273
 
267
274
  - Python 3.7+
@@ -10,6 +10,7 @@
10
10
  - Автоматические повторные попытки для временных ошибок
11
11
  - Поддержка JWT-аутентификации
12
12
  - Поддержка прокси, включая SOCKS5
13
+ - Ферма клиентов с балансировкой нагрузки и стратегиями ротации
13
14
  - Подробное логирование
14
15
  - Строгая типизация с помощью аннотаций типов
15
16
  - Класс-конфигурация для централизованной настройки и повторного использования
@@ -29,21 +30,23 @@ pip install .
29
30
  ## Структура проекта
30
31
 
31
32
  ```
32
- gengine_client/
33
- ├── __init__.py # Экспорт основных классов
34
- ├── client.py # Основной класс клиента
35
- ├── http.py # HTTP клиент с поддержкой аутентификации
36
- ├── exceptions.py # Иерархия исключений API
37
- ├── config.py # Класс-конфигурация для настройки параметров
38
- └── modules/ # Папка с модулями API
39
- ├── __init__.py
40
- ├── base.py # Базовый класс для всех модулей
41
- ├── payments.py # Модуль для работы с платежами
42
- ├── finances.py # Модуль для работы с финансами
43
- ├── auth.py # Модуль для аутентификации
44
- ├── users.py # Модуль для работы с пользователями
45
- ├── transactions.py # Модуль для работы с транзакциями
46
- └── currencies.py # Модуль для работы с валютами
33
+ src/
34
+ └── gengineapi/
35
+ ├── __init__.py # Экспорт основных классов
36
+ ├── client.py # Основной класс клиента
37
+ ├── http.py # HTTP клиент с поддержкой аутентификации
38
+ ├── exceptions.py # Иерархия исключений API
39
+ ├── config.py # Класс-конфигурация для настройки параметров
40
+ ├── farm.py # Ферма клиентов для управления множеством клиентов
41
+ └── modules/ # Папка с модулями API
42
+ ├── __init__.py
43
+ ├── base.py # Базовый класс для всех модулей
44
+ ├── payments.py # Модуль для работы с платежами
45
+ ├── finances.py # Модуль для работы с финансами
46
+ ├── auth.py # Модуль для аутентификации
47
+ ├── users.py # Модуль для работы с пользователями
48
+ ├── transactions.py # Модуль для работы с транзакциями
49
+ └── currencies.py # Модуль для работы с валютами
47
50
  ```
48
51
 
49
52
  ## Использование
@@ -52,7 +55,7 @@ gengine_client/
52
55
 
53
56
  ```python
54
57
  import asyncio
55
- from gengine_client import GEngineClient
58
+ from src.gengineapi import GEngineClient
56
59
 
57
60
  async def main():
58
61
  # Создаем клиент с существующим токеном
@@ -73,7 +76,7 @@ asyncio.run(main())
73
76
 
74
77
  ```python
75
78
  import asyncio
76
- from gengine_client import GEngineClient
79
+ from src.gengineapi import GEngineClient
77
80
 
78
81
  async def main():
79
82
  # Создаем клиент без токена
@@ -108,7 +111,7 @@ asyncio.run(main())
108
111
 
109
112
  ```python
110
113
  import asyncio
111
- from gengine_client import GEngineClient
114
+ from src.gengineapi import GEngineClient
112
115
 
113
116
  async def main():
114
117
  # Создаем клиент с использованием SOCKS5 прокси
@@ -138,7 +141,7 @@ asyncio.run(main())
138
141
 
139
142
  ```python
140
143
  import asyncio
141
- from gengine_client import GEngineConfig
144
+ from src.gengineapi import GEngineConfig
142
145
 
143
146
  async def main():
144
147
  # Настройка параметров клиента
@@ -177,80 +180,85 @@ async def main():
177
180
  asyncio.run(main())
178
181
  ```
179
182
 
180
- ### Загрузка конфигурации из переменных окружения
183
+ ### Использование фермы клиентов
184
+
185
+ Ферма клиентов позволяет управлять множеством клиентов с разными конфигурациями и стратегиями ротации:
181
186
 
182
187
  ```python
183
188
  import asyncio
184
- import os
185
- from gengine_client import GEngineConfig
186
-
187
- # Установка переменных окружения
188
- os.environ["GENGINE_BASE_URL"] = "https://api.example.com/api/v2"
189
- os.environ["GENGINE_TOKEN"] = "env_jwt_token"
190
- os.environ["GENGINE_TIMEOUT"] = "45"
191
- os.environ["GENGINE_MAX_RETRIES"] = "5"
192
- os.environ["GENGINE_PROXY"] = "socks5://127.0.0.1:9050" # Опционально - прокси
189
+ from src.gengineapi import ClientFarm
193
190
 
194
191
  async def main():
195
- # Загрузка настроек из переменных окружения
196
- GEngineConfig.load_from_env()
192
+ # Создаем ферму клиентов
193
+ farm = ClientFarm()
197
194
 
198
- # Получение клиента с настройками из переменных окружения
199
- client = await GEngineConfig.get_client()
195
+ # Добавляем несколько конфигураций с разными прокси
196
+ farm.create_config(
197
+ name="client1",
198
+ base_url="https://api.example.com/api/v2",
199
+ jwt_token="token1",
200
+ proxy="socks5://user1:pass1@host1:port1",
201
+ tags=["group1", "production"]
202
+ )
200
203
 
201
- # Использование клиента
202
- # ...
204
+ farm.create_config(
205
+ name="client2",
206
+ base_url="https://api.example.com/api/v2",
207
+ jwt_token="token2",
208
+ proxy="socks5://user2:pass2@host2:port2",
209
+ tags=["group1", "backup"]
210
+ )
203
211
 
204
- # При завершении приложения
205
- await GEngineConfig.reset()
212
+ # Устанавливаем стратегию выбора клиента
213
+ farm.set_selection_strategy("round_robin") # или "random", "least_errors", "least_recently_used"
214
+
215
+ # Получаем клиент по имени
216
+ client1 = await farm.get_client("client1")
217
+ await client1.users.get_me()
218
+
219
+ # Получаем следующий клиент согласно стратегии
220
+ client = await farm.get_next_client()
221
+ await client.currencies.get_rate(source="cb_rf", pair="USD:RUB")
222
+
223
+ # Получаем клиент по тегу
224
+ client = await farm.get_next_client(tag="production")
225
+ await client.transactions.get_transactions(limit=5)
226
+
227
+ # При завершении работы закрываем все клиенты
228
+ await farm.close_all()
206
229
 
207
230
  asyncio.run(main())
208
231
  ```
209
232
 
210
- ### Работа с платежами
233
+ ### Экспорт и импорт конфигураций фермы
211
234
 
212
235
  ```python
213
236
  import asyncio
214
- import uuid
215
- from decimal import Decimal
216
- from gengine_client import GEngineClient
237
+ from src.gengineapi import ClientFarm
217
238
 
218
239
  async def main():
219
- async with GEngineClient(
220
- base_url="https://api.example.com/api/v2",
221
- jwt_token="your-jwt-token"
222
- ) as client:
223
- # Генерируем уникальный идентификатор транзакции
224
- transaction_id = str(uuid.uuid4())
225
-
226
- # Создаем и верифицируем платеж
227
- payment = await client.payments.verify(
228
- transaction_id=transaction_id,
229
- service_id=1,
230
- account="user123",
231
- amount=Decimal("10.99"),
232
- currency="USD"
233
- )
234
- print(f"Создан платеж: {payment['transaction_id']}")
235
-
236
- # Выполняем платеж
237
- result = await client.payments.execute(transaction_id=transaction_id)
238
- print(f"Статус платежа: {result['status_code']}")
239
-
240
- # Получаем статус платежа
241
- status = await client.payments.get_status(transaction_id=transaction_id)
242
- print(f"Текущий статус платежа: {status['status_code']}")
240
+ # Создаем и настраиваем ферму
241
+ farm = ClientFarm()
242
+
243
+ # Добавляем конфигурации
244
+ farm.create_config(name="client1", base_url="...", jwt_token="...", proxy="...")
245
+ farm.create_config(name="client2", base_url="...", jwt_token="...", proxy="...")
246
+
247
+ # Экспортируем конфигурации в файл
248
+ await farm.export_configs("farm_config.json")
249
+
250
+ # Импортируем конфигурации из файла
251
+ new_farm = await ClientFarm.import_configs("farm_config.json")
252
+
253
+ # Используем импортированные конфигурации
254
+ client = await new_farm.get_next_client()
255
+
256
+ # При завершении работы закрываем все клиенты
257
+ await new_farm.close_all()
243
258
 
244
259
  asyncio.run(main())
245
260
  ```
246
261
 
247
- ## Примеры
248
-
249
- В репозитории есть примеры использования клиента с разными сценариями:
250
-
251
- - `examples.py` - полный набор примеров использования, включая работу с прокси
252
- - `old_examples/` - директория с устаревшими примерами
253
-
254
262
  ## Зависимости
255
263
 
256
264
  - Python 3.7+
@@ -1,18 +1,11 @@
1
1
  [project]
2
2
  name = "gengineapi"
3
- version = "0.1.0"
4
- description = "Add your description here"
3
+ version = "0.2.0"
4
+ description = "G-Engine API Client"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
8
  "aiohttp>=3.8.0", # Для асинхронных HTTP-запросов и WebSocket
9
9
  "typing-extensions>=4.0.0", # Для расширенных типов аннотаций
10
10
  "aiohttp-socks>=0.7.1",
11
- "uv-publish",
12
- ]
13
-
14
- [[tool.uv.index]]
15
- name = "testpypi"
16
- url = "https://test.pypi.org/simple/"
17
- publish-url = "https://test.pypi.org/legacy/"
18
- explicit = true
11
+ ]
@@ -19,6 +19,7 @@ from .exceptions import (
19
19
  ApiTimeoutError,
20
20
  ApiValidationError,
21
21
  )
22
+ from .farm import ClientFarm, ClientConfig
22
23
  from .http import AsyncHttpClient
23
24
 
24
25
  __version__ = "1.0.0"
@@ -29,6 +30,9 @@ __all__ = [
29
30
  'GEngineConfig',
30
31
  # HTTP клиент
31
32
  'AsyncHttpClient',
33
+ # Ферма клиентов
34
+ 'ClientFarm',
35
+ 'ClientConfig',
32
36
  # Исключения
33
37
  'ApiError',
34
38
  'ApiConnectionError',
@@ -0,0 +1,464 @@
1
+ """
2
+ Модуль, предоставляющий функциональность для управления фермой клиентов G-Engine API.
3
+
4
+ Позволяет создавать и управлять несколькими независимыми клиентами
5
+ с различными конфигурациями, прокси и токенами аутентификации.
6
+ """
7
+ import asyncio
8
+ import logging
9
+ import uuid
10
+ from typing import Dict, Optional, List, Any
11
+
12
+ from .client import GEngineClient
13
+
14
+
15
+ class ClientConfig:
16
+ """
17
+ Класс конфигурации для отдельного клиента.
18
+
19
+ Позволяет хранить и управлять параметрами для создания клиента.
20
+
21
+ Attributes:
22
+ name: Уникальное имя конфигурации
23
+ base_url: Базовый URL для API
24
+ jwt_token: JWT токен для аутентификации
25
+ timeout: Таймаут для запросов в секундах
26
+ max_retries: Максимальное количество повторных попыток
27
+ logger: Логгер для записи информации
28
+ proxy: Прокси для запросов
29
+ client: Экземпляр клиента, если он был создан
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: str,
35
+ jwt_token: Optional[str] = None,
36
+ timeout: int = 30,
37
+ max_retries: int = 3,
38
+ logger: Optional[logging.Logger] = None,
39
+ proxy: Optional[str] = None,
40
+ name: Optional[str] = None, # Уникальное имя конфига
41
+ tags: Optional[List[str]] = None, # Теги для группировки и фильтрации
42
+ ) -> None:
43
+ """
44
+ Инициализация конфигурации клиента.
45
+
46
+ Args:
47
+ base_url: Базовый URL для API
48
+ jwt_token: JWT токен для аутентификации (опционально)
49
+ timeout: Таймаут для запросов в секундах (по умолчанию 30)
50
+ max_retries: Максимальное количество повторных попыток (по умолчанию 3)
51
+ logger: Логгер для записи информации (опционально)
52
+ proxy: Прокси для запросов в формате 'socks5://user:pass@host:port' (опционально)
53
+ name: Уникальное имя конфигурации (опционально, генерируется автоматически)
54
+ tags: Список тегов для группировки и фильтрации (опционально)
55
+ """
56
+ self.base_url = base_url
57
+ self.jwt_token = jwt_token
58
+ self.timeout = timeout
59
+ self.max_retries = max_retries
60
+ self.logger = logger
61
+ self.proxy = proxy
62
+ self.name = name or str(uuid.uuid4())
63
+ self.tags = tags or []
64
+ self.client: Optional[GEngineClient] = None
65
+ self.last_used: float = 0 # Время последнего использования (для ротации)
66
+ self.error_count: int = 0 # Счетчик ошибок
67
+ self.success_count: int = 0 # Счетчик успешных запросов
68
+ self.is_active: bool = True # Флаг активности конфигурации
69
+
70
+ async def get_client(self) -> GEngineClient:
71
+ """
72
+ Возвращает клиент или создает новый, если он не существует.
73
+
74
+ Returns:
75
+ GEngineClient: Экземпляр клиента API
76
+ """
77
+ if self.client is None:
78
+ self.client = GEngineClient(
79
+ base_url=self.base_url,
80
+ jwt_token=self.jwt_token,
81
+ timeout=self.timeout,
82
+ max_retries=self.max_retries,
83
+ logger=self.logger,
84
+ proxy=self.proxy
85
+ )
86
+
87
+ # Обновляем время последнего использования
88
+ import time
89
+ self.last_used = time.time()
90
+
91
+ return self.client
92
+
93
+ async def close(self) -> None:
94
+ """Закрывает клиент."""
95
+ if self.client:
96
+ await self.client.close()
97
+ self.client = None
98
+
99
+ def update_token(self, jwt_token: str) -> None:
100
+ """
101
+ Обновляет JWT токен в конфигурации и в клиенте, если он существует.
102
+
103
+ Args:
104
+ jwt_token: Новый JWT токен
105
+ """
106
+ self.jwt_token = jwt_token
107
+ if self.client:
108
+ self.client.update_token(jwt_token)
109
+
110
+ def update_proxy(self, proxy: Optional[str] = None) -> None:
111
+ """
112
+ Обновляет настройки прокси в конфигурации и в клиенте, если он существует.
113
+
114
+ Args:
115
+ proxy: Новый прокси или None для отключения прокси
116
+ """
117
+ self.proxy = proxy
118
+ if self.client:
119
+ if hasattr(self.client, 'update_proxy'):
120
+ self.client.update_proxy(proxy)
121
+ else:
122
+ # Если клиент не поддерживает динамическое обновление прокси,
123
+ # закрываем его и создадим новый при следующем запросе
124
+ asyncio.create_task(self.close())
125
+
126
+ def deactivate(self) -> None:
127
+ """Деактивирует конфигурацию."""
128
+ self.is_active = False
129
+
130
+ def activate(self) -> None:
131
+ """Активирует конфигурацию."""
132
+ self.is_active = True
133
+
134
+ def to_dict(self) -> Dict[str, Any]:
135
+ """
136
+ Преобразует конфигурацию в словарь.
137
+
138
+ Returns:
139
+ Dict[str, Any]: Словарь с параметрами конфигурации
140
+ """
141
+ return {
142
+ "name": self.name,
143
+ "base_url": self.base_url,
144
+ "jwt_token": self.jwt_token,
145
+ "timeout": self.timeout,
146
+ "max_retries": self.max_retries,
147
+ "proxy": self.proxy,
148
+ "tags": self.tags,
149
+ "is_active": self.is_active,
150
+ "error_count": self.error_count,
151
+ "success_count": self.success_count,
152
+ "last_used": self.last_used,
153
+ }
154
+
155
+
156
+ class ClientFarm:
157
+ """
158
+ Управляет фермой клиентов с разными конфигурациями.
159
+
160
+ Позволяет создавать, хранить и использовать множество клиентов
161
+ с разными настройками, токенами и прокси.
162
+ """
163
+
164
+ def __init__(self, logger: Optional[logging.Logger] = None) -> None:
165
+ """
166
+ Инициализация фермы клиентов.
167
+
168
+ Args:
169
+ logger: Логгер для записи информации (опционально)
170
+ """
171
+ self.configs: Dict[str, ClientConfig] = {}
172
+ self.logger = logger or logging.getLogger(__name__)
173
+ self._selection_strategy: str = "round_robin" # Стратегия выбора клиента
174
+ self._last_used_index: int = -1 # Индекс последнего использованного клиента
175
+
176
+ def add_config(self, config: ClientConfig) -> None:
177
+ """
178
+ Добавляет конфигурацию в ферму.
179
+
180
+ Args:
181
+ config: Конфигурация клиента
182
+ """
183
+ self.configs[config.name] = config
184
+ self.logger.info(f"Добавлена конфигурация: {config.name}")
185
+
186
+ def create_config(self, **kwargs) -> ClientConfig:
187
+ """
188
+ Создает и добавляет новую конфигурацию.
189
+
190
+ Args:
191
+ **kwargs: Параметры для создания конфигурации
192
+
193
+ Returns:
194
+ ClientConfig: Созданная конфигурация
195
+ """
196
+ config = ClientConfig(**kwargs)
197
+ self.add_config(config)
198
+ return config
199
+
200
+ async def get_client(self, name: str) -> Optional[GEngineClient]:
201
+ """
202
+ Возвращает клиент по имени конфигурации.
203
+
204
+ Args:
205
+ name: Имя конфигурации
206
+
207
+ Returns:
208
+ Optional[GEngineClient]: Экземпляр клиента или None, если конфигурация не найдена
209
+ """
210
+ config = self.configs.get(name)
211
+ if config and config.is_active:
212
+ return await config.get_client()
213
+ return None
214
+
215
+ async def get_next_client(self, tag: Optional[str] = None) -> Optional[GEngineClient]:
216
+ """
217
+ Возвращает следующий клиент согласно выбранной стратегии.
218
+
219
+ Args:
220
+ tag: Тег для фильтрации клиентов (опционально)
221
+
222
+ Returns:
223
+ Optional[GEngineClient]: Экземпляр клиента или None, если нет доступных клиентов
224
+ """
225
+ active_configs = [
226
+ config for config in self.configs.values()
227
+ if config.is_active and (tag is None or tag in config.tags)
228
+ ]
229
+
230
+ if not active_configs:
231
+ self.logger.warning("Нет доступных конфигураций клиентов")
232
+ return None
233
+
234
+ if self._selection_strategy == "round_robin":
235
+ # Стратегия Round Robin
236
+ self._last_used_index = (self._last_used_index + 1) % len(active_configs)
237
+ config = active_configs[self._last_used_index]
238
+ return await config.get_client()
239
+
240
+ elif self._selection_strategy == "random":
241
+ # Случайный выбор
242
+ import random
243
+ config = random.choice(active_configs)
244
+ return await config.get_client()
245
+
246
+ elif self._selection_strategy == "least_errors":
247
+ # Выбираем клиент с наименьшим количеством ошибок
248
+ config = min(active_configs, key=lambda c: c.error_count)
249
+ return await config.get_client()
250
+
251
+ elif self._selection_strategy == "least_recently_used":
252
+ # Выбираем наименее недавно использованный клиент
253
+ config = min(active_configs, key=lambda c: c.last_used)
254
+ return await config.get_client()
255
+
256
+ else:
257
+ # По умолчанию используем Round Robin
258
+ self._last_used_index = (self._last_used_index + 1) % len(active_configs)
259
+ config = active_configs[self._last_used_index]
260
+ return await config.get_client()
261
+
262
+ def set_selection_strategy(self, strategy: str) -> None:
263
+ """
264
+ Устанавливает стратегию выбора клиента.
265
+
266
+ Args:
267
+ strategy: Стратегия выбора ('round_robin', 'random', 'least_errors', 'least_recently_used')
268
+ """
269
+ valid_strategies = ["round_robin", "random", "least_errors", "least_recently_used"]
270
+ if strategy not in valid_strategies:
271
+ raise ValueError(f"Недопустимая стратегия выбора: {strategy}. "
272
+ f"Должна быть одна из: {', '.join(valid_strategies)}")
273
+
274
+ self._selection_strategy = strategy
275
+ self.logger.info(f"Установлена стратегия выбора клиента: {strategy}")
276
+
277
+ async def close_client(self, name: str) -> None:
278
+ """
279
+ Закрывает клиент по имени конфигурации.
280
+
281
+ Args:
282
+ name: Имя конфигурации
283
+ """
284
+ config = self.configs.get(name)
285
+ if config:
286
+ await config.close()
287
+ self.logger.info(f"Клиент закрыт: {name}")
288
+
289
+ async def close_all(self) -> None:
290
+ """Закрывает все клиенты в ферме."""
291
+ for name, config in self.configs.items():
292
+ await config.close()
293
+ self.logger.info(f"Клиент закрыт: {name}")
294
+
295
+ def remove_config(self, name: str) -> None:
296
+ """
297
+ Удаляет конфигурацию из фермы.
298
+
299
+ Args:
300
+ name: Имя конфигурации
301
+ """
302
+ if name in self.configs:
303
+ config = self.configs[name]
304
+ # Запустим задачу на закрытие клиента, если он существует
305
+ if config.client:
306
+ asyncio.create_task(config.close())
307
+ del self.configs[name]
308
+ self.logger.info(f"Конфигурация удалена: {name}")
309
+
310
+ def get_configs_by_tag(self, tag: str) -> List[ClientConfig]:
311
+ """
312
+ Возвращает список конфигураций с указанным тегом.
313
+
314
+ Args:
315
+ tag: Тег для фильтрации
316
+
317
+ Returns:
318
+ List[ClientConfig]: Список конфигураций с указанным тегом
319
+ """
320
+ return [config for config in self.configs.values() if tag in config.tags]
321
+
322
+ def get_active_configs(self) -> List[ClientConfig]:
323
+ """
324
+ Возвращает список активных конфигураций.
325
+
326
+ Returns:
327
+ List[ClientConfig]: Список активных конфигураций
328
+ """
329
+ return [config for config in self.configs.values() if config.is_active]
330
+
331
+ def deactivate_config(self, name: str) -> None:
332
+ """
333
+ Деактивирует конфигурацию.
334
+
335
+ Args:
336
+ name: Имя конфигурации
337
+ """
338
+ if name in self.configs:
339
+ self.configs[name].deactivate()
340
+ self.logger.info(f"Конфигурация деактивирована: {name}")
341
+
342
+ def activate_config(self, name: str) -> None:
343
+ """
344
+ Активирует конфигурацию.
345
+
346
+ Args:
347
+ name: Имя конфигурации
348
+ """
349
+ if name in self.configs:
350
+ self.configs[name].activate()
351
+ self.logger.info(f"Конфигурация активирована: {name}")
352
+
353
+ def register_error(self, name: str) -> None:
354
+ """
355
+ Регистрирует ошибку для указанной конфигурации.
356
+
357
+ Args:
358
+ name: Имя конфигурации
359
+ """
360
+ if name in self.configs:
361
+ self.configs[name].error_count += 1
362
+
363
+ # Если превышен порог ошибок, деактивируем конфигурацию
364
+ if self.configs[name].error_count >= 10: # Пример порога
365
+ self.configs[name].deactivate()
366
+ self.logger.warning(f"Конфигурация деактивирована из-за ошибок: {name}")
367
+
368
+ def register_success(self, name: str) -> None:
369
+ """
370
+ Регистрирует успешное использование указанной конфигурации.
371
+
372
+ Args:
373
+ name: Имя конфигурации
374
+ """
375
+ if name in self.configs:
376
+ self.configs[name].success_count += 1
377
+
378
+ # Если клиент был деактивирован из-за ошибок, но успешно выполнил запрос,
379
+ # сбрасываем счетчик ошибок и активируем его снова
380
+ if not self.configs[name].is_active and self.configs[name].error_count > 0:
381
+ self.configs[name].error_count = 0
382
+ self.configs[name].activate()
383
+ self.logger.info(f"Конфигурация активирована после успешного запроса: {name}")
384
+
385
+ def get_stats(self) -> Dict[str, Dict[str, Any]]:
386
+ """
387
+ Возвращает статистику по всем конфигурациям.
388
+
389
+ Returns:
390
+ Dict[str, Dict[str, Any]]: Словарь со статистикой
391
+ """
392
+ return {name: config.to_dict() for name, config in self.configs.items()}
393
+
394
+ async def export_configs(self, file_path: str) -> None:
395
+ """
396
+ Экспортирует конфигурации в JSON-файл.
397
+
398
+ Args:
399
+ file_path: Путь к файлу для экспорта
400
+ """
401
+ import json
402
+
403
+ # Закрываем все клиенты перед экспортом
404
+ await self.close_all()
405
+
406
+ # Получаем данные конфигураций
407
+ configs_data = {name: config.to_dict() for name, config in self.configs.items()}
408
+
409
+ # Сохраняем в файл
410
+ with open(file_path, "w") as f:
411
+ json.dump(configs_data, f, indent=2)
412
+
413
+ self.logger.info(f"Конфигурации экспортированы в файл: {file_path}")
414
+
415
+ @classmethod
416
+ async def import_configs(cls, file_path: str, logger: Optional[logging.Logger] = None) -> "ClientFarm":
417
+ """
418
+ Импортирует конфигурации из JSON-файла.
419
+
420
+ Args:
421
+ file_path: Путь к файлу с конфигурациями
422
+ logger: Логгер для записи информации (опционально)
423
+
424
+ Returns:
425
+ ClientFarm: Новый экземпляр фермы клиентов с импортированными конфигурациями
426
+ """
427
+ import json
428
+
429
+ farm = cls(logger=logger)
430
+
431
+ try:
432
+ # Загружаем данные из файла
433
+ with open(file_path, "r") as f:
434
+ configs_data = json.load(f)
435
+
436
+ # Создаем конфигурации на основе данных
437
+ for name, data in configs_data.items():
438
+ # Извлекаем только параметры, необходимые для создания конфигурации
439
+ config_params = {
440
+ "name": data.get("name"),
441
+ "base_url": data.get("base_url"),
442
+ "jwt_token": data.get("jwt_token"),
443
+ "timeout": data.get("timeout"),
444
+ "max_retries": data.get("max_retries"),
445
+ "proxy": data.get("proxy"),
446
+ "tags": data.get("tags", []),
447
+ }
448
+
449
+ # Создаем конфигурацию
450
+ config = ClientConfig(**config_params)
451
+
452
+ # Устанавливаем дополнительные параметры
453
+ config.is_active = data.get("is_active", True)
454
+ config.error_count = data.get("error_count", 0)
455
+ config.success_count = data.get("success_count", 0)
456
+
457
+ # Добавляем конфигурацию в ферму
458
+ farm.add_config(config)
459
+ except Exception as e:
460
+ if logger:
461
+ logger.error(f"Ошибка при импорте конфигураций: {e}")
462
+ raise
463
+
464
+ return farm
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gengineapi
3
- Version: 0.1.0
4
- Summary: Add your description here
3
+ Version: 0.2.0
4
+ Summary: G-Engine API Client
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: aiohttp>=3.8.0
8
8
  Requires-Dist: typing-extensions>=4.0.0
9
9
  Requires-Dist: aiohttp-socks>=0.7.1
10
- Requires-Dist: uv-publish
11
10
 
12
11
  # G-Engine API Client
13
12
 
@@ -21,6 +20,7 @@ Requires-Dist: uv-publish
21
20
  - Автоматические повторные попытки для временных ошибок
22
21
  - Поддержка JWT-аутентификации
23
22
  - Поддержка прокси, включая SOCKS5
23
+ - Ферма клиентов с балансировкой нагрузки и стратегиями ротации
24
24
  - Подробное логирование
25
25
  - Строгая типизация с помощью аннотаций типов
26
26
  - Класс-конфигурация для централизованной настройки и повторного использования
@@ -40,21 +40,23 @@ pip install .
40
40
  ## Структура проекта
41
41
 
42
42
  ```
43
- gengine_client/
44
- ├── __init__.py # Экспорт основных классов
45
- ├── client.py # Основной класс клиента
46
- ├── http.py # HTTP клиент с поддержкой аутентификации
47
- ├── exceptions.py # Иерархия исключений API
48
- ├── config.py # Класс-конфигурация для настройки параметров
49
- └── modules/ # Папка с модулями API
50
- ├── __init__.py
51
- ├── base.py # Базовый класс для всех модулей
52
- ├── payments.py # Модуль для работы с платежами
53
- ├── finances.py # Модуль для работы с финансами
54
- ├── auth.py # Модуль для аутентификации
55
- ├── users.py # Модуль для работы с пользователями
56
- ├── transactions.py # Модуль для работы с транзакциями
57
- └── currencies.py # Модуль для работы с валютами
43
+ src/
44
+ └── gengineapi/
45
+ ├── __init__.py # Экспорт основных классов
46
+ ├── client.py # Основной класс клиента
47
+ ├── http.py # HTTP клиент с поддержкой аутентификации
48
+ ├── exceptions.py # Иерархия исключений API
49
+ ├── config.py # Класс-конфигурация для настройки параметров
50
+ ├── farm.py # Ферма клиентов для управления множеством клиентов
51
+ └── modules/ # Папка с модулями API
52
+ ├── __init__.py
53
+ ├── base.py # Базовый класс для всех модулей
54
+ ├── payments.py # Модуль для работы с платежами
55
+ ├── finances.py # Модуль для работы с финансами
56
+ ├── auth.py # Модуль для аутентификации
57
+ ├── users.py # Модуль для работы с пользователями
58
+ ├── transactions.py # Модуль для работы с транзакциями
59
+ └── currencies.py # Модуль для работы с валютами
58
60
  ```
59
61
 
60
62
  ## Использование
@@ -63,7 +65,7 @@ gengine_client/
63
65
 
64
66
  ```python
65
67
  import asyncio
66
- from gengine_client import GEngineClient
68
+ from src.gengineapi import GEngineClient
67
69
 
68
70
  async def main():
69
71
  # Создаем клиент с существующим токеном
@@ -84,7 +86,7 @@ asyncio.run(main())
84
86
 
85
87
  ```python
86
88
  import asyncio
87
- from gengine_client import GEngineClient
89
+ from src.gengineapi import GEngineClient
88
90
 
89
91
  async def main():
90
92
  # Создаем клиент без токена
@@ -119,7 +121,7 @@ asyncio.run(main())
119
121
 
120
122
  ```python
121
123
  import asyncio
122
- from gengine_client import GEngineClient
124
+ from src.gengineapi import GEngineClient
123
125
 
124
126
  async def main():
125
127
  # Создаем клиент с использованием SOCKS5 прокси
@@ -149,7 +151,7 @@ asyncio.run(main())
149
151
 
150
152
  ```python
151
153
  import asyncio
152
- from gengine_client import GEngineConfig
154
+ from src.gengineapi import GEngineConfig
153
155
 
154
156
  async def main():
155
157
  # Настройка параметров клиента
@@ -188,80 +190,85 @@ async def main():
188
190
  asyncio.run(main())
189
191
  ```
190
192
 
191
- ### Загрузка конфигурации из переменных окружения
193
+ ### Использование фермы клиентов
194
+
195
+ Ферма клиентов позволяет управлять множеством клиентов с разными конфигурациями и стратегиями ротации:
192
196
 
193
197
  ```python
194
198
  import asyncio
195
- import os
196
- from gengine_client import GEngineConfig
197
-
198
- # Установка переменных окружения
199
- os.environ["GENGINE_BASE_URL"] = "https://api.example.com/api/v2"
200
- os.environ["GENGINE_TOKEN"] = "env_jwt_token"
201
- os.environ["GENGINE_TIMEOUT"] = "45"
202
- os.environ["GENGINE_MAX_RETRIES"] = "5"
203
- os.environ["GENGINE_PROXY"] = "socks5://127.0.0.1:9050" # Опционально - прокси
199
+ from src.gengineapi import ClientFarm
204
200
 
205
201
  async def main():
206
- # Загрузка настроек из переменных окружения
207
- GEngineConfig.load_from_env()
202
+ # Создаем ферму клиентов
203
+ farm = ClientFarm()
208
204
 
209
- # Получение клиента с настройками из переменных окружения
210
- client = await GEngineConfig.get_client()
205
+ # Добавляем несколько конфигураций с разными прокси
206
+ farm.create_config(
207
+ name="client1",
208
+ base_url="https://api.example.com/api/v2",
209
+ jwt_token="token1",
210
+ proxy="socks5://user1:pass1@host1:port1",
211
+ tags=["group1", "production"]
212
+ )
211
213
 
212
- # Использование клиента
213
- # ...
214
+ farm.create_config(
215
+ name="client2",
216
+ base_url="https://api.example.com/api/v2",
217
+ jwt_token="token2",
218
+ proxy="socks5://user2:pass2@host2:port2",
219
+ tags=["group1", "backup"]
220
+ )
214
221
 
215
- # При завершении приложения
216
- await GEngineConfig.reset()
222
+ # Устанавливаем стратегию выбора клиента
223
+ farm.set_selection_strategy("round_robin") # или "random", "least_errors", "least_recently_used"
224
+
225
+ # Получаем клиент по имени
226
+ client1 = await farm.get_client("client1")
227
+ await client1.users.get_me()
228
+
229
+ # Получаем следующий клиент согласно стратегии
230
+ client = await farm.get_next_client()
231
+ await client.currencies.get_rate(source="cb_rf", pair="USD:RUB")
232
+
233
+ # Получаем клиент по тегу
234
+ client = await farm.get_next_client(tag="production")
235
+ await client.transactions.get_transactions(limit=5)
236
+
237
+ # При завершении работы закрываем все клиенты
238
+ await farm.close_all()
217
239
 
218
240
  asyncio.run(main())
219
241
  ```
220
242
 
221
- ### Работа с платежами
243
+ ### Экспорт и импорт конфигураций фермы
222
244
 
223
245
  ```python
224
246
  import asyncio
225
- import uuid
226
- from decimal import Decimal
227
- from gengine_client import GEngineClient
247
+ from src.gengineapi import ClientFarm
228
248
 
229
249
  async def main():
230
- async with GEngineClient(
231
- base_url="https://api.example.com/api/v2",
232
- jwt_token="your-jwt-token"
233
- ) as client:
234
- # Генерируем уникальный идентификатор транзакции
235
- transaction_id = str(uuid.uuid4())
236
-
237
- # Создаем и верифицируем платеж
238
- payment = await client.payments.verify(
239
- transaction_id=transaction_id,
240
- service_id=1,
241
- account="user123",
242
- amount=Decimal("10.99"),
243
- currency="USD"
244
- )
245
- print(f"Создан платеж: {payment['transaction_id']}")
246
-
247
- # Выполняем платеж
248
- result = await client.payments.execute(transaction_id=transaction_id)
249
- print(f"Статус платежа: {result['status_code']}")
250
-
251
- # Получаем статус платежа
252
- status = await client.payments.get_status(transaction_id=transaction_id)
253
- print(f"Текущий статус платежа: {status['status_code']}")
250
+ # Создаем и настраиваем ферму
251
+ farm = ClientFarm()
252
+
253
+ # Добавляем конфигурации
254
+ farm.create_config(name="client1", base_url="...", jwt_token="...", proxy="...")
255
+ farm.create_config(name="client2", base_url="...", jwt_token="...", proxy="...")
256
+
257
+ # Экспортируем конфигурации в файл
258
+ await farm.export_configs("farm_config.json")
259
+
260
+ # Импортируем конфигурации из файла
261
+ new_farm = await ClientFarm.import_configs("farm_config.json")
262
+
263
+ # Используем импортированные конфигурации
264
+ client = await new_farm.get_next_client()
265
+
266
+ # При завершении работы закрываем все клиенты
267
+ await new_farm.close_all()
254
268
 
255
269
  asyncio.run(main())
256
270
  ```
257
271
 
258
- ## Примеры
259
-
260
- В репозитории есть примеры использования клиента с разными сценариями:
261
-
262
- - `examples.py` - полный набор примеров использования, включая работу с прокси
263
- - `old_examples/` - директория с устаревшими примерами
264
-
265
272
  ## Зависимости
266
273
 
267
274
  - Python 3.7+
@@ -4,6 +4,7 @@ src/gengineapi/__init__.py
4
4
  src/gengineapi/client.py
5
5
  src/gengineapi/config.py
6
6
  src/gengineapi/exceptions.py
7
+ src/gengineapi/farm.py
7
8
  src/gengineapi/http.py
8
9
  src/gengineapi.egg-info/PKG-INFO
9
10
  src/gengineapi.egg-info/SOURCES.txt
@@ -1,4 +1,3 @@
1
1
  aiohttp>=3.8.0
2
2
  typing-extensions>=4.0.0
3
3
  aiohttp-socks>=0.7.1
4
- uv-publish
File without changes