xync-client 0.0.141__py3-none-any.whl → 0.0.156.dev18__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.
Files changed (40) hide show
  1. xync_client/Abc/AdLoader.py +5 -0
  2. xync_client/Abc/Agent.py +354 -8
  3. xync_client/Abc/Ex.py +432 -25
  4. xync_client/Abc/HasAbotUid.py +10 -0
  5. xync_client/Abc/InAgent.py +0 -11
  6. xync_client/Abc/PmAgent.py +34 -26
  7. xync_client/Abc/xtype.py +57 -3
  8. xync_client/Bybit/InAgent.py +233 -409
  9. xync_client/Bybit/agent.py +844 -777
  10. xync_client/Bybit/etype/__init__.py +0 -0
  11. xync_client/Bybit/etype/ad.py +54 -86
  12. xync_client/Bybit/etype/cred.py +29 -9
  13. xync_client/Bybit/etype/order.py +75 -103
  14. xync_client/Bybit/ex.py +35 -48
  15. xync_client/Gmail/__init__.py +119 -98
  16. xync_client/Htx/agent.py +213 -40
  17. xync_client/Htx/etype/ad.py +40 -16
  18. xync_client/Htx/etype/order.py +194 -0
  19. xync_client/Htx/ex.py +17 -19
  20. xync_client/Mexc/agent.py +268 -0
  21. xync_client/Mexc/api.py +1255 -0
  22. xync_client/Mexc/etype/ad.py +52 -1
  23. xync_client/Mexc/etype/order.py +354 -0
  24. xync_client/Mexc/ex.py +34 -22
  25. xync_client/Okx/1.py +14 -0
  26. xync_client/Okx/agent.py +39 -0
  27. xync_client/Okx/ex.py +8 -8
  28. xync_client/Pms/Payeer/agent.py +396 -0
  29. xync_client/Pms/Payeer/login.py +1 -59
  30. xync_client/Pms/Payeer/trade.py +58 -0
  31. xync_client/Pms/Volet/__init__.py +82 -63
  32. xync_client/Pms/Volet/api.py +5 -4
  33. xync_client/loader.py +2 -0
  34. xync_client/pm_unifier.py +1 -1
  35. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/METADATA +5 -1
  36. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/RECORD +38 -29
  37. xync_client/Pms/Payeer/__init__.py +0 -253
  38. xync_client/Pms/Payeer/api.py +0 -25
  39. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/WHEEL +0 -0
  40. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1255 @@
1
+ """
2
+ MEXC P2P OpenAPI v1.2 Async Client
3
+ """
4
+
5
+ import hmac
6
+ import hashlib
7
+ import json
8
+ import time
9
+ from typing import Optional, Literal, Callable
10
+ from decimal import Decimal
11
+ from urllib.parse import urlencode
12
+
13
+ import aiohttp
14
+ from pydantic import BaseModel
15
+ from xync_schema import models
16
+ from xync_schema.enums import AgentStatus, UserStatus
17
+
18
+ from xync_client.Mexc.etype.order import (
19
+ CreateUpdateAdRequest,
20
+ CreateAdResponse,
21
+ AdListResponse,
22
+ MarketAdListResponse,
23
+ CreateOrderRequest,
24
+ CreateOrderResponse,
25
+ OrderListResponse,
26
+ ConfirmPaidRequest,
27
+ BaseResponse,
28
+ ReleaseCoinRequest,
29
+ OrderDetailResponse,
30
+ ServiceSwitchRequest,
31
+ ListenKeyResponse,
32
+ ConversationResponse,
33
+ ChatMessagesResponse,
34
+ UploadFileResponse,
35
+ ReceivedChatMessage,
36
+ WSRequest,
37
+ WSMethod,
38
+ SendTextMessage,
39
+ SendImageMessage,
40
+ SendVideoMessage,
41
+ SendFileMessage,
42
+ ChatMessageType,
43
+ )
44
+
45
+
46
+ # ============ Client ============
47
+ class MEXCP2PApiClient:
48
+ """Асинхронный клиент для MEXC P2P API v1.2"""
49
+
50
+ BASE_URL = "https://api.mexc.com"
51
+
52
+ def __init__(self, api_key: str, api_secret: str):
53
+ self.api_key = api_key
54
+ self.api_secret = api_secret
55
+ self.session: Optional[aiohttp.ClientSession] = aiohttp.ClientSession()
56
+
57
+ def _generate_signature(self, query_string: str) -> str:
58
+ """Генерация HMAC SHA256 подписи"""
59
+ return hmac.new(self.api_secret.encode(), query_string.encode(), hashlib.sha256).hexdigest()
60
+
61
+ async def _request(
62
+ self, method: str, endpoint: str, params: Optional[dict] = None, data: Optional[BaseModel] = None
63
+ ) -> dict:
64
+ """Базовый метод для HTTP запросов"""
65
+ if not self.session:
66
+ raise RuntimeError("Client not initialized. Use async context manager.")
67
+
68
+ params = params or {}
69
+ # Формирование query string для подписи
70
+ params["recvWindow"] = 5000
71
+ params["timestamp"] = int(time.time() * 1000)
72
+ params = {k: v for k, v in sorted(params.items())}
73
+
74
+ query_string = urlencode(params, doseq=True).replace("+", "%20")
75
+ signature = self._generate_signature(query_string)
76
+
77
+ params["signature"] = signature
78
+
79
+ headers = {"X-MEXC-APIKEY": self.api_key}
80
+ if method in ("POST", "PUT", "PATCH"):
81
+ headers["Content-Type"] = "application/json"
82
+
83
+ url = f"{self.BASE_URL}{endpoint}"
84
+
85
+ json_data = data.model_dump(exclude_none=True) if data else None
86
+
87
+ async with self.session.request(method, url, params=params, json=json_data, headers=headers) as response:
88
+ return await response.json()
89
+
90
+ # ============ Advertisement Methods ============
91
+
92
+ async def create_or_update_ad(self, request: CreateUpdateAdRequest) -> CreateAdResponse:
93
+ """Создание или обновление объявления"""
94
+ result = await self._request("POST", "/api/v3/fiat/merchant/ads/save_or_update", data=request)
95
+ return CreateAdResponse(**result)
96
+
97
+ async def get_my_ads(
98
+ self,
99
+ coin_id: Optional[str] = None,
100
+ adv_status: Optional[str] = None,
101
+ merchant_id: Optional[str] = None,
102
+ fiat_unit: Optional[str] = None,
103
+ side: Optional[str] = None,
104
+ kyc_level: Optional[str] = None,
105
+ start_time: Optional[int] = None,
106
+ end_time: Optional[int] = None,
107
+ page: int = 1,
108
+ limit: int = 10,
109
+ ) -> AdListResponse:
110
+ """Получение списка моих объявлений с пагинацией"""
111
+ params = {"page": page, "limit": limit}
112
+
113
+ if coin_id:
114
+ params["coinId"] = coin_id
115
+ if adv_status:
116
+ params["advStatus"] = adv_status
117
+ if merchant_id:
118
+ params["merchantId"] = merchant_id
119
+ if fiat_unit:
120
+ params["fiatUnit"] = fiat_unit
121
+ if side:
122
+ params["side"] = side
123
+ if kyc_level:
124
+ params["kycLevel"] = kyc_level
125
+ if start_time:
126
+ params["startTime"] = start_time
127
+ if end_time:
128
+ params["endTime"] = end_time
129
+
130
+ result = await self._request("GET", "/api/v3/fiat/merchant/ads/pagination", params=params)
131
+ return AdListResponse(**result)
132
+
133
+ async def get_market_ads(
134
+ self,
135
+ fiat_unit: str,
136
+ coin_id: str,
137
+ country_code: Optional[str] = None,
138
+ side: Optional[str] = None,
139
+ amount: Optional[Decimal] = None,
140
+ quantity: Optional[Decimal] = None,
141
+ pay_method: Optional[str] = None,
142
+ block_trade: Optional[bool] = None,
143
+ allow_trade: Optional[bool] = None,
144
+ have_trade: Optional[bool] = None,
145
+ follow: Optional[bool] = None,
146
+ page: int = 1,
147
+ ) -> MarketAdListResponse:
148
+ """Получение рыночных объявлений"""
149
+ params = {"fiatUnit": fiat_unit, "coinId": coin_id, "page": page}
150
+
151
+ if country_code:
152
+ params["countryCode"] = country_code
153
+ if side:
154
+ params["side"] = side
155
+ if amount:
156
+ params["amount"] = str(amount)
157
+ if quantity:
158
+ params["quantity"] = str(quantity)
159
+ if pay_method:
160
+ params["payMethod"] = pay_method
161
+ if block_trade is not None:
162
+ params["blockTrade"] = block_trade
163
+ if allow_trade is not None:
164
+ params["allowTrade"] = allow_trade
165
+ if have_trade is not None:
166
+ params["haveTrade"] = have_trade
167
+ if follow is not None:
168
+ params["follow"] = follow
169
+
170
+ result = await self._request("GET", "/api/v3/fiat/market/ads/pagination", params=params)
171
+ return not result["code"] and MarketAdListResponse(**result)
172
+
173
+ # ============ Order Methods ============
174
+ async def create_order(self, request: CreateOrderRequest) -> CreateOrderResponse:
175
+ """Создание ордера (захват объявления)"""
176
+ result = await self._request("POST", "/api/v3/fiat/merchant/order/deal", data=request)
177
+ return CreateOrderResponse(**result)
178
+
179
+ async def get_my_orders(
180
+ self,
181
+ start_time: int,
182
+ end_time: int,
183
+ coin_id: Optional[str] = None,
184
+ adv_order_no: Optional[str] = None,
185
+ side: Optional[str] = None,
186
+ order_deal_state: Optional[str] = None,
187
+ page: int = 1,
188
+ limit: int = 10,
189
+ ) -> OrderListResponse:
190
+ """Получение моих ордеров (только как maker)"""
191
+ params = {"startTime": start_time, "endTime": end_time, "page": page, "limit": limit}
192
+
193
+ if coin_id:
194
+ params["coinId"] = coin_id
195
+ if adv_order_no:
196
+ params["advOrderNo"] = adv_order_no
197
+ if side:
198
+ params["side"] = side
199
+ if order_deal_state:
200
+ params["orderDealState"] = order_deal_state
201
+
202
+ result = await self._request("GET", "/api/v3/fiat/merchant/order/pagination", params=params)
203
+ return OrderListResponse(**result)
204
+
205
+ async def get_market_orders(
206
+ self,
207
+ coin_id: Optional[str] = None,
208
+ adv_order_no: Optional[str] = None,
209
+ side: Optional[str] = None,
210
+ order_deal_state: Optional[str] = None,
211
+ start_time: Optional[int] = None,
212
+ end_time: Optional[int] = None,
213
+ page: int = 1,
214
+ limit: int = 10,
215
+ ) -> OrderListResponse:
216
+ """Получение всех ордеров (как maker и taker)"""
217
+ params = {"page": page, "limit": limit}
218
+
219
+ if coin_id:
220
+ params["coinId"] = coin_id
221
+ if adv_order_no:
222
+ params["advOrderNo"] = adv_order_no
223
+ if side:
224
+ params["side"] = side
225
+ if order_deal_state:
226
+ params["orderDealState"] = order_deal_state
227
+ if start_time:
228
+ params["startTime"] = start_time
229
+ if end_time:
230
+ params["endTime"] = end_time
231
+
232
+ result = await self._request("GET", "/api/v3/fiat/market/order/pagination", params=params)
233
+ return OrderListResponse(**result)
234
+
235
+ async def confirm_paid(self, request: ConfirmPaidRequest) -> BaseResponse:
236
+ """Подтверждение оплаты"""
237
+ result = await self._request("POST", "/api/v3/fiat/confirm_paid", data=request)
238
+ return BaseResponse(**result)
239
+
240
+ async def release_coin(self, request: ReleaseCoinRequest) -> BaseResponse:
241
+ """Релиз криптовалюты"""
242
+ result = await self._request("POST", "/api/v3/fiat/release_coin", data=request)
243
+ return BaseResponse(**result)
244
+
245
+ async def get_order_detail(self, adv_order_no: str) -> OrderDetailResponse:
246
+ """Получение деталей ордера"""
247
+ params = {"advOrderNo": adv_order_no}
248
+
249
+ result = await self._request("GET", "/api/v3/fiat/order/detail", params=params)
250
+ return OrderDetailResponse(**result)
251
+
252
+ # ============ Service Methods ============
253
+ async def switch_service(self, request: ServiceSwitchRequest) -> BaseResponse:
254
+ """Открытие/закрытие торговли"""
255
+ result = await self._request("POST", "/api/v3/fiat/merchant/service/switch", data=request)
256
+ return BaseResponse(**result)
257
+
258
+ # ============ WebSocket Methods ============
259
+ async def generate_listen_key(self) -> ListenKeyResponse:
260
+ """Генерация listenKey для WebSocket"""
261
+ result = await self._request("POST", "/api/v3/userDataStream")
262
+ return ListenKeyResponse(**result)
263
+
264
+ async def get_listen_key(self) -> ListenKeyResponse:
265
+ """Получение существующего listenKey"""
266
+ result = await self._request("GET", "/api/v3/userDataStream")
267
+ return ListenKeyResponse(**result)
268
+
269
+ # ============ Chat Methods ============
270
+ async def get_chat_conversation(self, order_no: str) -> ConversationResponse:
271
+ """Получение ID чат-сессии для ордера"""
272
+ params = {"orderNo": order_no}
273
+
274
+ result = await self._request("GET", "/api/v3/fiat/retrieveChatConversation", params=params)
275
+ return ConversationResponse(**result)
276
+
277
+ async def get_chat_messages(
278
+ self,
279
+ conversation_id: int,
280
+ page: int = 1,
281
+ limit: int = 20,
282
+ chat_message_type: Optional[str] = None,
283
+ message_id: Optional[int] = None,
284
+ sort: Literal["DESC", "ASC"] = "DESC",
285
+ ) -> ChatMessagesResponse:
286
+ """Получение истории чата с пагинацией"""
287
+ params = {"conversationId": conversation_id, "page": page, "limit": limit, "sort": sort}
288
+
289
+ if chat_message_type:
290
+ params["chatMessageType"] = chat_message_type
291
+ if message_id:
292
+ params["id"] = message_id
293
+
294
+ result = await self._request("GET", "/api/v3/fiat/retrieveChatMessageWithPagination", params=params)
295
+ return ChatMessagesResponse(**result)
296
+
297
+ async def upload_file(self, file_data: bytes, filename: str) -> UploadFileResponse:
298
+ """Загрузка файла"""
299
+ if not self.session:
300
+ raise RuntimeError("Client not initialized.")
301
+
302
+ timestamp = self._get_timestamp()
303
+ query_string = f"timestamp={timestamp}"
304
+ signature = self._generate_signature(query_string)
305
+
306
+ url = f"{self.BASE_URL}/api/v3/fiat/uploadFile"
307
+ params = {"timestamp": timestamp, "signature": signature}
308
+
309
+ headers = {"X-MEXC-APIKEY": self.api_key}
310
+
311
+ form = aiohttp.FormData()
312
+ form.add_field("file", file_data, filename=filename)
313
+
314
+ async with self.session.post(url, params=params, data=form, headers=headers) as response:
315
+ result = await response.json()
316
+
317
+ return UploadFileResponse(**result)
318
+
319
+ async def download_file(self, file_id: str) -> dict:
320
+ """Скачивание файла"""
321
+ params = {"fileId": file_id}
322
+
323
+ result = await self._request("GET", "/api/v3/fiat/downloadFile", params=params)
324
+ return result
325
+
326
+
327
+ """
328
+ MEXC P2P WebSocket Client для чата
329
+ """
330
+
331
+
332
+ class MEXCWebSocketClient:
333
+ """
334
+ Асинхронный WebSocket клиент для MEXC P2P
335
+ Поддерживает:
336
+ - Автоматический heartbeat (PING/PONG)
337
+ - Переподключение при разрыве соединения
338
+ """
339
+
340
+ WS_URL = "wss://wbs.mexc.com/ws"
341
+ PING_INTERVAL = 5 # секунды
342
+ PING_TIMEOUT = 60 # если нет PONG 60 сек - разрыв
343
+
344
+ def __init__(
345
+ self,
346
+ ws_token: str,
347
+ on_message: Optional[Callable[[ReceivedChatMessage], None]] = None,
348
+ on_error: Optional[Callable[[Exception], None]] = None,
349
+ on_close: Optional[Callable[[], None]] = None,
350
+ auto_reconnect: bool = True,
351
+ ):
352
+ """
353
+ Args:
354
+ ws_token: Ключ авторизации
355
+ on_message: Callback для входящих сообщений
356
+ on_error: Callback для ошибок
357
+ on_close: Callback при закрытии соединения
358
+ auto_reconnect: Автоматическое переподключение
359
+ """
360
+ self.wsToken = ws_token
361
+ self.on_message = on_message
362
+ self.on_error = on_error
363
+ self.on_close = on_close
364
+ self.auto_reconnect = auto_reconnect
365
+
366
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
367
+ self._session: Optional[aiohttp.ClientSession] = None
368
+ self._running = False
369
+ self._ping_task: Optional[asyncio.Task] = None
370
+ self._receive_task: Optional[asyncio.Task] = None
371
+ self._last_pong_time = 0
372
+
373
+ @property
374
+ def is_connected(self) -> bool:
375
+ """Проверка активного соединения"""
376
+ return self._ws is not None and not self._ws.closed
377
+
378
+ async def connect(self):
379
+ """Установка WebSocket соединения"""
380
+ if self.is_connected:
381
+ return
382
+
383
+ url = f"{self.WS_URL}?wsToken={self.wsToken}&platform=web"
384
+
385
+ self._session = aiohttp.ClientSession()
386
+
387
+ try:
388
+ self._ws = await self._session.ws_connect(url)
389
+ self._running = True
390
+ self._last_pong_time = asyncio.get_event_loop().time()
391
+
392
+ # Запуск фоновых задач
393
+ self._ping_task = asyncio.create_task(self._heartbeat_loop())
394
+ self._receive_task = asyncio.create_task(self._receive_loop())
395
+
396
+ print("✓ WebSocket connected")
397
+
398
+ except Exception as e:
399
+ await self._cleanup()
400
+ raise Exception(f"Failed to connect WebSocket: {e}")
401
+
402
+ async def disconnect(self):
403
+ """Закрытие WebSocket соединения"""
404
+ self._running = False
405
+
406
+ if self._ping_task:
407
+ self._ping_task.cancel()
408
+ if self._receive_task:
409
+ self._receive_task.cancel()
410
+
411
+ await self._cleanup()
412
+
413
+ if self.on_close:
414
+ self.on_close()
415
+
416
+ print("✓ WebSocket disconnected")
417
+
418
+ async def _cleanup(self):
419
+ """Очистка ресурсов"""
420
+ if self._ws and not self._ws.closed:
421
+ await self._ws.close()
422
+
423
+ if self._session and not self._session.closed:
424
+ await self._session.close()
425
+
426
+ self._ws = None
427
+ self._session = None
428
+
429
+ async def _send_raw(self, request: WSRequest):
430
+ """Отправка сырого WebSocket сообщения"""
431
+ if not self.is_connected:
432
+ raise ConnectionError("WebSocket not connected")
433
+
434
+ message = request.model_dump_json()
435
+ await self._ws.send_str(message)
436
+
437
+ async def _heartbeat_loop(self):
438
+ """Фоновая задача для PING/PONG"""
439
+ try:
440
+ while self._running and self.is_connected:
441
+ await asyncio.sleep(self.PING_INTERVAL)
442
+
443
+ # Проверка таймаута PONG
444
+ current_time = asyncio.get_event_loop().time()
445
+ if current_time - self._last_pong_time > self.PING_TIMEOUT:
446
+ print("⚠ PING timeout, reconnecting...")
447
+ if self.auto_reconnect:
448
+ await self._reconnect()
449
+ else:
450
+ await self.disconnect()
451
+ break
452
+
453
+ # Отправка PING
454
+ await self._send_ping()
455
+
456
+ except asyncio.CancelledError:
457
+ pass
458
+ except Exception as e:
459
+ print(f"❌ Heartbeat error: {e}")
460
+ if self.on_error:
461
+ self.on_error(e)
462
+
463
+ async def _receive_loop(self):
464
+ """Фоновая задача для получения сообщений"""
465
+ try:
466
+ async for msg in self._ws:
467
+ if msg.type == aiohttp.WSMsgType.TEXT:
468
+ await self._handle_message(msg.data)
469
+
470
+ elif msg.type == aiohttp.WSMsgType.ERROR:
471
+ print(f"❌ WebSocket error: {self._ws.exception()}")
472
+ break
473
+
474
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING):
475
+ print("⚠ WebSocket closed by server")
476
+ break
477
+
478
+ except asyncio.CancelledError:
479
+ pass
480
+ except Exception as e:
481
+ print(f"❌ Receive loop error: {e}")
482
+ if self.on_error:
483
+ self.on_error(e)
484
+
485
+ finally:
486
+ # Переподключение при разрыве
487
+ if self._running and self.auto_reconnect:
488
+ print("⚠ Connection lost, reconnecting...")
489
+ await self._reconnect()
490
+ else:
491
+ await self.disconnect()
492
+
493
+ async def _handle_message(self, data: str):
494
+ """Обработка входящего сообщения"""
495
+ try:
496
+ response = json.loads(data)
497
+
498
+ method = response.get("method")
499
+
500
+ # PONG ответ
501
+ if method == "PING":
502
+ self._last_pong_time = asyncio.get_event_loop().time()
503
+ if response.get("data") == "PONG":
504
+ print("♥ PONG received")
505
+ pass
506
+
507
+ # Входящее сообщение
508
+ elif method == "RECEIVE_MESSAGE":
509
+ if response.get("success") and self.on_message:
510
+ message_data = json.loads(response.get("data", "{}"))
511
+ message = ReceivedChatMessage(**message_data)
512
+ self.on_message(message)
513
+
514
+ # Ответ на отправку
515
+ elif method == "SEND_MESSAGE":
516
+ if not response.get("success"):
517
+ print(f"⚠ Send failed: {response.get('msg')}")
518
+
519
+ except Exception as e:
520
+ print(f"❌ Error handling message: {e}")
521
+ if self.on_error:
522
+ self.on_error(e)
523
+
524
+ async def _reconnect(self):
525
+ """Переподключение WebSocket"""
526
+ print("🔄 Reconnecting...")
527
+ await self._cleanup()
528
+
529
+ max_retries = 5
530
+ retry_delay = 2
531
+
532
+ for attempt in range(max_retries):
533
+ try:
534
+ await asyncio.sleep(retry_delay * (attempt + 1))
535
+ await self.connect()
536
+ print("✓ Reconnected successfully")
537
+ return
538
+
539
+ except Exception as e:
540
+ print(f"❌ Reconnect attempt {attempt + 1} failed: {e}")
541
+
542
+ print("❌ Failed to reconnect after max retries")
543
+ await self.disconnect()
544
+
545
+ async def _send_ping(self):
546
+ """Отправка PING"""
547
+ request = WSRequest(method=WSMethod.PING)
548
+ await self._send_raw(request)
549
+ print(end="p")
550
+
551
+
552
+ # ============ WebSocket Client ============
553
+ class MEXCP2PWebSocketClient:
554
+ """
555
+ Асинхронный WebSocket клиент для чата MEXC P2P
556
+
557
+ Поддерживает:
558
+ - Отправку/получение текстовых сообщений
559
+ - Отправку/получение медиа (изображения, видео, файлы)
560
+ - Автоматический heartbeat (PING/PONG)
561
+ - Переподключение при разрыве соединения
562
+ """
563
+
564
+ WS_URL = "wss://fiat.mexc.com/ws"
565
+ PING_INTERVAL = 5 # секунды
566
+ PING_TIMEOUT = 60 # если нет PONG 60 сек - разрыв
567
+
568
+ def __init__(
569
+ self,
570
+ listen_key: str,
571
+ conversation_id: int = None,
572
+ on_message: Optional[Callable[[ReceivedChatMessage], None]] = None,
573
+ on_error: Optional[Callable[[Exception], None]] = None,
574
+ on_close: Optional[Callable[[], None]] = None,
575
+ auto_reconnect: bool = True,
576
+ ):
577
+ """
578
+ Args:
579
+ listen_key: Ключ авторизации (из HTTP API)
580
+ conversation_id: ID чат-сессии
581
+ on_message: Callback для входящих сообщений
582
+ on_error: Callback для ошибок
583
+ on_close: Callback при закрытии соединения
584
+ auto_reconnect: Автоматическое переподключение
585
+ """
586
+ self.listen_key = listen_key
587
+ self.conversation_id = conversation_id
588
+ self.on_message = on_message
589
+ self.on_error = on_error
590
+ self.on_close = on_close
591
+ self.auto_reconnect = auto_reconnect
592
+
593
+ self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
594
+ self._session: Optional[aiohttp.ClientSession] = None
595
+ self._running = False
596
+ self._ping_task: Optional[asyncio.Task] = None
597
+ self._receive_task: Optional[asyncio.Task] = None
598
+ self._last_pong_time = 0
599
+
600
+ @property
601
+ def is_connected(self) -> bool:
602
+ """Проверка активного соединения"""
603
+ return self._ws is not None and not self._ws.closed
604
+
605
+ async def connect(self):
606
+ """Установка WebSocket соединения"""
607
+ if self.is_connected:
608
+ return
609
+
610
+ url = f"{self.WS_URL}?listenKey={self.listen_key}"
611
+ if self.conversation_id:
612
+ url += f"&conversationId={self.conversation_id}"
613
+
614
+ self._session = aiohttp.ClientSession()
615
+
616
+ try:
617
+ self._ws = await self._session.ws_connect(url)
618
+ self._running = True
619
+ self._last_pong_time = asyncio.get_event_loop().time()
620
+
621
+ # Запуск фоновых задач
622
+ self._ping_task = asyncio.create_task(self._heartbeat_loop())
623
+ self._receive_task = asyncio.create_task(self._receive_loop())
624
+
625
+ print(f"✓ WebSocket connected to conversation {self.conversation_id}")
626
+
627
+ except Exception as e:
628
+ await self._cleanup()
629
+ raise Exception(f"Failed to connect WebSocket: {e}")
630
+
631
+ async def disconnect(self):
632
+ """Закрытие WebSocket соединения"""
633
+ self._running = False
634
+
635
+ if self._ping_task:
636
+ self._ping_task.cancel()
637
+ if self._receive_task:
638
+ self._receive_task.cancel()
639
+
640
+ await self._cleanup()
641
+
642
+ if self.on_close:
643
+ self.on_close()
644
+
645
+ print("✓ WebSocket disconnected")
646
+
647
+ async def _cleanup(self):
648
+ """Очистка ресурсов"""
649
+ if self._ws and not self._ws.closed:
650
+ await self._ws.close()
651
+
652
+ if self._session and not self._session.closed:
653
+ await self._session.close()
654
+
655
+ self._ws = None
656
+ self._session = None
657
+
658
+ async def _send_raw(self, request: WSRequest):
659
+ """Отправка сырого WebSocket сообщения"""
660
+ if not self.is_connected:
661
+ raise ConnectionError("WebSocket not connected")
662
+
663
+ message = request.model_dump_json()
664
+ await self._ws.send_str(message)
665
+
666
+ async def _heartbeat_loop(self):
667
+ """Фоновая задача для PING/PONG"""
668
+ try:
669
+ while self._running and self.is_connected:
670
+ await asyncio.sleep(self.PING_INTERVAL)
671
+
672
+ # Проверка таймаута PONG
673
+ current_time = asyncio.get_event_loop().time()
674
+ if current_time - self._last_pong_time > self.PING_TIMEOUT:
675
+ print("⚠ PING timeout, reconnecting...")
676
+ if self.auto_reconnect:
677
+ await self._reconnect()
678
+ else:
679
+ await self.disconnect()
680
+ break
681
+
682
+ # Отправка PING
683
+ await self._send_ping()
684
+
685
+ except asyncio.CancelledError:
686
+ pass
687
+ except Exception as e:
688
+ print(f"❌ Heartbeat error: {e}")
689
+ if self.on_error:
690
+ self.on_error(e)
691
+
692
+ async def _receive_loop(self):
693
+ """Фоновая задача для получения сообщений"""
694
+ try:
695
+ async for msg in self._ws:
696
+ if msg.type == aiohttp.WSMsgType.TEXT:
697
+ await self._handle_message(msg.data)
698
+
699
+ elif msg.type == aiohttp.WSMsgType.ERROR:
700
+ print(f"❌ WebSocket error: {self._ws.exception()}")
701
+ break
702
+
703
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING):
704
+ print("⚠ WebSocket closed by server")
705
+ break
706
+
707
+ except asyncio.CancelledError:
708
+ pass
709
+ except Exception as e:
710
+ print(f"❌ Receive loop error: {e}")
711
+ if self.on_error:
712
+ self.on_error(e)
713
+
714
+ finally:
715
+ # Переподключение при разрыве
716
+ if self._running and self.auto_reconnect:
717
+ print("⚠ Connection lost, reconnecting...")
718
+ await self._reconnect()
719
+ else:
720
+ await self.disconnect()
721
+
722
+ async def _handle_message(self, data: str):
723
+ """Обработка входящего сообщения"""
724
+ try:
725
+ response = json.loads(data)
726
+
727
+ method = response.get("method")
728
+
729
+ # PONG ответ
730
+ if method == "PING":
731
+ self._last_pong_time = asyncio.get_event_loop().time()
732
+ if response.get("data") == "PONG":
733
+ print("♥ PONG received")
734
+ pass
735
+
736
+ # Входящее сообщение
737
+ elif method == "RECEIVE_MESSAGE":
738
+ if response.get("success") and self.on_message:
739
+ message_data = json.loads(response.get("data", "{}"))
740
+ message = ReceivedChatMessage(**message_data)
741
+ self.on_message(message)
742
+
743
+ # Ответ на отправку
744
+ elif method == "SEND_MESSAGE":
745
+ if not response.get("success"):
746
+ print(f"⚠ Send failed: {response.get('msg')}")
747
+
748
+ except Exception as e:
749
+ print(f"❌ Error handling message: {e}")
750
+ if self.on_error:
751
+ self.on_error(e)
752
+
753
+ async def _reconnect(self):
754
+ """Переподключение WebSocket"""
755
+ print("🔄 Reconnecting...")
756
+ await self._cleanup()
757
+
758
+ max_retries = 5
759
+ retry_delay = 2
760
+
761
+ for attempt in range(max_retries):
762
+ try:
763
+ await asyncio.sleep(retry_delay * (attempt + 1))
764
+ await self.connect()
765
+ print("✓ Reconnected successfully")
766
+ return
767
+
768
+ except Exception as e:
769
+ print(f"❌ Reconnect attempt {attempt + 1} failed: {e}")
770
+
771
+ print("❌ Failed to reconnect after max retries")
772
+ await self.disconnect()
773
+
774
+ async def _send_ping(self):
775
+ """Отправка PING"""
776
+ request = WSRequest(method=WSMethod.PING)
777
+ await self._send_raw(request)
778
+ print(end="p")
779
+
780
+ # ============ Public Message Sending Methods ============
781
+
782
+ async def send_text(self, content: str) -> bool:
783
+ """
784
+ Отправка текстового сообщения
785
+
786
+ Args:
787
+ content: Текст сообщения
788
+
789
+ Returns:
790
+ bool: Успешность отправки
791
+ """
792
+ message = SendTextMessage(content=content, conversationId=self.conversation_id)
793
+
794
+ request = WSRequest(method=WSMethod.SEND_MESSAGE, params=message.model_dump())
795
+
796
+ try:
797
+ await self._send_raw(request)
798
+ return True
799
+ except Exception as e:
800
+ print(f"❌ Failed to send text: {e}")
801
+ return False
802
+
803
+ async def send_image(self, image_url: str, thumb_url: str) -> bool:
804
+ """
805
+ Отправка изображения
806
+
807
+ Args:
808
+ image_url: URL полного изображения
809
+ thumb_url: URL превью
810
+
811
+ Returns:
812
+ bool: Успешность отправки
813
+ """
814
+ message = SendImageMessage(imageUrl=image_url, imageThumbUrl=thumb_url, conversationId=self.conversation_id)
815
+
816
+ request = WSRequest(method=WSMethod.SEND_MESSAGE, params=message.model_dump_json())
817
+
818
+ try:
819
+ await self._send_raw(request)
820
+ return True
821
+ except Exception as e:
822
+ print(f"❌ Failed to send image: {e}")
823
+ return False
824
+
825
+ async def send_video(self, video_url: str, thumb_url: str) -> bool:
826
+ """
827
+ Отправка видео
828
+
829
+ Args:
830
+ video_url: URL видео
831
+ thumb_url: URL превью
832
+
833
+ Returns:
834
+ bool: Успешность отправки
835
+ """
836
+ message = SendVideoMessage(videoUrl=video_url, imageThumbUrl=thumb_url, conversationId=self.conversation_id)
837
+
838
+ request = WSRequest(method=WSMethod.SEND_MESSAGE, params=message.model_dump_json())
839
+
840
+ try:
841
+ await self._send_raw(request)
842
+ return True
843
+ except Exception as e:
844
+ print(f"❌ Failed to send video: {e}")
845
+ return False
846
+
847
+ async def send_file(self, file_url: str) -> bool:
848
+ """
849
+ Отправка файла
850
+
851
+ Args:
852
+ file_url: URL файла
853
+
854
+ Returns:
855
+ bool: Успешность отправки
856
+ """
857
+ message = SendFileMessage(fileUrl=file_url, conversationId=self.conversation_id)
858
+
859
+ request = WSRequest(method=WSMethod.SEND_MESSAGE, params=message.model_dump_json())
860
+
861
+ try:
862
+ await self._send_raw(request)
863
+ return True
864
+ except Exception as e:
865
+ print(f"❌ Failed to send file: {e}")
866
+ return False
867
+
868
+ async def send_message(
869
+ self,
870
+ content: Optional[str] = None,
871
+ image_url: Optional[str] = None,
872
+ image_thumb_url: Optional[str] = None,
873
+ video_url: Optional[str] = None,
874
+ file_url: Optional[str] = None,
875
+ ) -> bool:
876
+ """
877
+ Универсальная отправка сообщения (автоопределение типа)
878
+
879
+ Args:
880
+ content: Текст (для TEXT)
881
+ image_url: URL изображения (для IMAGE)
882
+ image_thumb_url: URL превью (для IMAGE/VIDEO)
883
+ video_url: URL видео (для VIDEO)
884
+ file_url: URL файла (для FILE)
885
+
886
+ Returns:
887
+ bool: Успешность отправки
888
+ """
889
+ if content:
890
+ return await self.send_text(content)
891
+ elif image_url and image_thumb_url:
892
+ return await self.send_image(image_url, image_thumb_url)
893
+ elif video_url and image_thumb_url:
894
+ return await self.send_video(video_url, image_thumb_url)
895
+ elif file_url:
896
+ return await self.send_file(file_url)
897
+ else:
898
+ raise ValueError("No valid message content provided")
899
+
900
+
901
+ # ============ Context Manager для удобства ============
902
+ class MEXCP2PChatSession:
903
+ """
904
+ Высокоуровневая обертка для чат-сессии
905
+ Автоматически управляет HTTP и WebSocket клиентами
906
+ """
907
+
908
+ def __init__(
909
+ self,
910
+ api_key: str,
911
+ api_secret: str,
912
+ order_no: str,
913
+ on_message: Optional[Callable[[ReceivedChatMessage], None]] = None,
914
+ on_error: Optional[Callable[[Exception], None]] = None,
915
+ auto_reconnect: bool = True,
916
+ ):
917
+ self.api_key = api_key
918
+ self.api_secret = api_secret
919
+ self.order_no = order_no
920
+ self.on_message = on_message
921
+ self.on_error = on_error
922
+ self.auto_reconnect = auto_reconnect
923
+
924
+ self.http_client: Optional[MEXCP2PApiClient] = None
925
+ self.ws_client: Optional[MEXCP2PWebSocketClient] = None
926
+ self.conversation_id: Optional[int] = None
927
+
928
+ async def __aenter__(self):
929
+ # Инициализация HTTP клиента
930
+ self.http_client = MEXCP2PApiClient(self.api_key, self.api_secret)
931
+ await self.http_client.__aenter__()
932
+
933
+ # Получение conversation ID
934
+ conv_response = await self.http_client.get_chat_conversation(self.order_no)
935
+ self.conversation_id = conv_response.data.get("conversationId")
936
+
937
+ if not self.conversation_id:
938
+ raise ValueError("Failed to get conversation ID")
939
+
940
+ # Генерация listenKey
941
+ listen_key_response = await self.http_client.generate_listen_key()
942
+ listen_key = listen_key_response.listenKey
943
+
944
+ # Подключение WebSocket
945
+ self.ws_client = MEXCP2PWebSocketClient(
946
+ listen_key=listen_key,
947
+ conversation_id=self.conversation_id,
948
+ on_message=self.on_message,
949
+ on_error=self.on_error,
950
+ auto_reconnect=self.auto_reconnect,
951
+ )
952
+
953
+ await self.ws_client.connect()
954
+
955
+ return self
956
+
957
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
958
+ if self.ws_client:
959
+ await self.ws_client.disconnect()
960
+
961
+ if self.http_client:
962
+ await self.http_client.__aexit__(exc_type, exc_val, exc_tb)
963
+
964
+ async def send_text(self, text: str) -> bool:
965
+ """Отправка текста"""
966
+ return await self.ws_client.send_text(text)
967
+
968
+ async def send_image(self, image_url: str, thumb_url: str) -> bool:
969
+ """Отправка изображения"""
970
+ return await self.ws_client.send_image(image_url, thumb_url)
971
+
972
+ async def send_video(self, video_url: str, thumb_url: str) -> bool:
973
+ """Отправка видео"""
974
+ return await self.ws_client.send_video(video_url, thumb_url)
975
+
976
+ async def send_file(self, file_url: str) -> bool:
977
+ """Отправка файла"""
978
+ return await self.ws_client.send_file(file_url)
979
+
980
+ async def upload_and_send_file(self, file_data: bytes, filename: str) -> bool:
981
+ """
982
+ Загрузка файла через HTTP API и отправка в чат
983
+
984
+ Args:
985
+ file_data: Бинарные данные файла
986
+ filename: Имя файла
987
+
988
+ Returns:
989
+ bool: Успешность операции
990
+ """
991
+ # Загрузка файла
992
+ upload_response = await self.http_client.upload_file(file_data, filename)
993
+
994
+ if upload_response.code != 0:
995
+ print(f"❌ File upload failed: {upload_response.msg}")
996
+ return False
997
+
998
+ file_id = upload_response.data.get("fileId")
999
+
1000
+ # Получение URL файла
1001
+ download_response = await self.http_client.download_file(file_id)
1002
+
1003
+ if download_response.get("code") != 0:
1004
+ print("❌ File URL retrieval failed")
1005
+ return False
1006
+
1007
+ file_url = download_response["data"]["fileUrl"]
1008
+
1009
+ # Отправка в чат
1010
+ return await self.send_file(file_url)
1011
+
1012
+ async def get_message_history(self, limit: int = 20, page: int = 1) -> list[ReceivedChatMessage]:
1013
+ """
1014
+ Получение истории сообщений
1015
+
1016
+ Args:
1017
+ limit: Количество сообщений
1018
+ page: Номер страницы
1019
+
1020
+ Returns:
1021
+ List[ReceivedChatMessage]: Список сообщений
1022
+ """
1023
+ response = await self.http_client.get_chat_messages(
1024
+ conversation_id=self.conversation_id, page=page, limit=limit
1025
+ )
1026
+
1027
+ messages_data = response.data.get("messages", [])
1028
+ return [ReceivedChatMessage(**msg) for msg in messages_data]
1029
+
1030
+
1031
+ # ============ Usage Examples ============
1032
+ async def example_simple_chat():
1033
+ """Простой пример чата"""
1034
+
1035
+ def on_message(msg: ReceivedChatMessage):
1036
+ if msg.type == ChatMessageType.TEXT:
1037
+ print(f"📩 [{msg.fromNickName}]: {msg.content}")
1038
+ elif msg.type == ChatMessageType.IMAGE:
1039
+ print(f"📷 [{msg.fromNickName}] sent image: {msg.imageUrl}")
1040
+ elif msg.type == ChatMessageType.VIDEO:
1041
+ print(f"🎥 [{msg.fromNickName}] sent video: {msg.videoUrl}")
1042
+ elif msg.type == ChatMessageType.FILE:
1043
+ print(f"📎 [{msg.fromNickName}] sent file: {msg.fileUrl}")
1044
+
1045
+ def on_error(error: Exception):
1046
+ print(f"❌ Error: {error}")
1047
+
1048
+ api_key = "your_api_key"
1049
+ api_secret = "your_api_secret"
1050
+ order_no = "your_order_no"
1051
+
1052
+ async with MEXCP2PChatSession(
1053
+ api_key=api_key,
1054
+ api_secret=api_secret,
1055
+ order_no=order_no,
1056
+ on_message=on_message,
1057
+ on_error=on_error,
1058
+ auto_reconnect=True,
1059
+ ) as chat:
1060
+ # Отправка текста
1061
+ await chat.send_text("Hello! How are you?")
1062
+
1063
+ # Получение истории
1064
+ history = await chat.get_message_history(limit=10)
1065
+ print(f"📜 Loaded {len(history)} messages from history")
1066
+
1067
+ # Держим соединение открытым
1068
+ await asyncio.sleep(60)
1069
+
1070
+
1071
+ async def example_manual_websocket():
1072
+ """Пример ручного управления WebSocket"""
1073
+
1074
+ api_key = "your_api_key"
1075
+ api_secret = "your_api_secret"
1076
+
1077
+ # Получаем listenKey и conversation_id через HTTP API
1078
+ async with MEXCP2PApiClient(api_key, api_secret) as http_client:
1079
+ # Получение conversation ID
1080
+ conv = await http_client.get_chat_conversation("order_123")
1081
+ conversation_id = conv.data["conversationId"]
1082
+
1083
+ # Получение listenKey
1084
+ key_response = await http_client.generate_listen_key()
1085
+ listen_key = key_response.listenKey
1086
+
1087
+ # Создание WebSocket клиента
1088
+ def on_message(msg: ReceivedChatMessage):
1089
+ print(f"Received: {msg.content}")
1090
+
1091
+ ws_client = MEXCP2PWebSocketClient(
1092
+ listen_key=listen_key, conversation_id=conversation_id, on_message=on_message, auto_reconnect=True
1093
+ )
1094
+
1095
+ await ws_client.connect()
1096
+
1097
+ try:
1098
+ # Отправка сообщений
1099
+ await ws_client.send_text("Test message 1")
1100
+ await asyncio.sleep(1)
1101
+ await ws_client.send_text("Test message 2")
1102
+
1103
+ # Ожидание входящих сообщений
1104
+ await asyncio.sleep(30)
1105
+
1106
+ finally:
1107
+ await ws_client.disconnect()
1108
+
1109
+
1110
+ async def example_file_sending():
1111
+ """Пример отправки файла"""
1112
+
1113
+ api_key = "your_api_key"
1114
+ api_secret = "your_api_secret"
1115
+ order_no = "your_order_no"
1116
+
1117
+ async with MEXCP2PChatSession(api_key=api_key, api_secret=api_secret, order_no=order_no) as chat:
1118
+ # Загрузка и отправка файла
1119
+ with open("document.pdf", "rb") as f:
1120
+ file_data = f.read()
1121
+
1122
+ success = await chat.upload_and_send_file(file_data=file_data, filename="document.pdf")
1123
+
1124
+ if success:
1125
+ print("✓ File sent successfully")
1126
+ else:
1127
+ print("❌ Failed to send file")
1128
+
1129
+
1130
+ async def example_bot():
1131
+ """Пример простого бота для автоответов"""
1132
+
1133
+ async def handle_message(msg: ReceivedChatMessage):
1134
+ # Игнорируем свои сообщения
1135
+ if msg.self_:
1136
+ return
1137
+
1138
+ # Автоответ на текст
1139
+ if msg.type == ChatMessageType.TEXT:
1140
+ if "price" in msg.content.lower():
1141
+ await chat.send_text("Our current price is 70,000 USD")
1142
+ elif "hello" in msg.content.lower():
1143
+ await chat.send_text("Hello! How can I help you?")
1144
+
1145
+ api_key = "your_api_key"
1146
+ api_secret = "your_api_secret"
1147
+ order_no = "your_order_no"
1148
+
1149
+ async with MEXCP2PChatSession(
1150
+ api_key=api_key, api_secret=api_secret, order_no=order_no, on_message=handle_message, auto_reconnect=True
1151
+ ) as chat:
1152
+ # Бот работает бесконечно
1153
+ while True:
1154
+ await asyncio.sleep(1)
1155
+
1156
+
1157
+ # ============ Usage Example ============
1158
+ async def main():
1159
+ # Выбери пример для запуска:
1160
+
1161
+ # asyncio.run(example_simple_chat())
1162
+ # asyncio.run(example_manual_websocket())
1163
+ # asyncio.run(example_file_sending())
1164
+ # asyncio.run(example_bot())
1165
+
1166
+ """Пример использования клиента"""
1167
+ from x_model import init_db
1168
+ from xync_client.loader import TORM
1169
+
1170
+ await init_db(TORM, True)
1171
+
1172
+ ex = await models.Ex[12]
1173
+ agent = (
1174
+ await models.Agent.filter(
1175
+ actor__ex=ex,
1176
+ status__gte=AgentStatus.race,
1177
+ auth__isnull=False,
1178
+ actor__person__user__status=UserStatus.ACTIVE,
1179
+ actor__person__user__pm_agents__isnull=False,
1180
+ )
1181
+ .prefetch_related("actor__ex", "actor__person__user__gmail")
1182
+ .first()
1183
+ )
1184
+
1185
+ async with MEXCP2PApiClient(agent.auth["key"], agent.auth["sec"]) as client:
1186
+ # Генерация listenKey для WebSocket
1187
+ listen_key = await client.generate_listen_key()
1188
+ print(f"ListenKey: {listen_key.listenKey}")
1189
+
1190
+ # await ws_prv(listen_key.listenKey)
1191
+ # Получение рыночных объявлений
1192
+ # market_ads = await client.get_market_ads(
1193
+ # fiat_unit="RUB", coin_id="128f589271cb4951b03e71e6323eb7be", side=Side.SELL.name, page=1
1194
+ # )
1195
+
1196
+ # print(f"Found {len(market_ads.data)} ads")
1197
+
1198
+ # Создание ордера
1199
+ # if market_ads.data:
1200
+ # first_ad = market_ads.data[0]
1201
+ # order_request = CreateOrderRequest(advNo=first_ad.advNo, amount=Decimal("100"), userConfirmPaymentId=123)
1202
+ #
1203
+ # order_response = await client.create_order(order_request)
1204
+ # print(f"Created order: {order_response.data}")
1205
+ #
1206
+ # # Получение деталей ордера
1207
+ # order_detail = await client.get_order_detail("order_id_here")
1208
+ # print(f"Order state: {order_detail.data.state}")
1209
+
1210
+ # Создание WebSocket клиента
1211
+ def on_message(msg: ReceivedChatMessage):
1212
+ print(f"Received: {msg.content}")
1213
+
1214
+ ws_client = MEXCWebSocketClient(
1215
+ ws_token="d9381d8193ad0859f1ea240041bd7004493d2030a4b4a2c861e4fd9c1b08fdcc",
1216
+ on_message=on_message,
1217
+ auto_reconnect=True,
1218
+ )
1219
+
1220
+ await ws_client.connect()
1221
+
1222
+ try:
1223
+ # Отправка сообщений
1224
+ wsr = WSRequest(method="SUBSCRIPTION", params=["otc@private.p2p.orders.pb"], id=12)
1225
+
1226
+ await ws_client._send_raw(wsr)
1227
+
1228
+ # Ожидание входящих сообщений
1229
+ await asyncio.sleep(12)
1230
+
1231
+ finally:
1232
+ await ws_client.disconnect()
1233
+
1234
+ # # Создание объявления
1235
+ # ad_request = CreateUpdateAdRequest(
1236
+ # payTimeLimit=15,
1237
+ # initQuantity=100,
1238
+ # price=87,
1239
+ # coinId="5989b56ba96a43599dbeeca5bb053f43",
1240
+ # side=Side.BUY.name,
1241
+ # fiatUnit="USD",
1242
+ # payMethod="1",
1243
+ # minSingleTransAmount=500,
1244
+ # maxSingleTransAmount=150000,
1245
+ # userAllTradeCountMin=0,
1246
+ # userAllTradeCountMax=100,
1247
+ # )
1248
+ # ad_response = await client.create_or_update_ad(ad_request)
1249
+ # print(f"Created ad: {ad_response.data}")
1250
+
1251
+
1252
+ if __name__ == "__main__":
1253
+ import asyncio
1254
+
1255
+ asyncio.run(main())