xync-client 0.0.100__tar.gz → 0.0.155.dev2__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 (132) hide show
  1. {xync_client-0.0.100/xync_client.egg-info → xync_client-0.0.155.dev2}/PKG-INFO +7 -1
  2. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/pyproject.toml +6 -0
  3. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/TestEx.py +7 -7
  4. xync_client-0.0.155.dev2/xync_client/Abc/AdLoader.py +299 -0
  5. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Agent.py +94 -10
  6. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Ex.py +37 -28
  7. xync_client-0.0.155.dev2/xync_client/Abc/HasAbotUid.py +10 -0
  8. xync_client-0.0.155.dev2/xync_client/Abc/InAgent.py +10 -0
  9. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/PmAgent.py +42 -35
  10. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/xtype.py +24 -2
  11. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/ex.py +18 -2
  12. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/ex.py +3 -3
  13. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BitGet/ex.py +2 -2
  14. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/InAgent.py +232 -117
  15. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/agent.py +586 -575
  16. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/etype/ad.py +11 -56
  17. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/etype/cred.py +29 -9
  18. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/etype/order.py +55 -62
  19. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/ex.py +17 -4
  20. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Gate/ex.py +15 -5
  21. xync_client-0.0.155.dev2/xync_client/Gmail/__init__.py +137 -0
  22. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/agent.py +148 -30
  23. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/etype/ad.py +18 -11
  24. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/ex.py +9 -11
  25. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/KuCoin/ex.py +2 -2
  26. xync_client-0.0.155.dev2/xync_client/Mexc/agent.py +85 -0
  27. xync_client-0.0.155.dev2/xync_client/Mexc/api.py +636 -0
  28. xync_client-0.0.155.dev2/xync_client/Mexc/etype/order.py +639 -0
  29. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Mexc/ex.py +12 -10
  30. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Okx/ex.py +2 -2
  31. xync_client-0.0.155.dev2/xync_client/Pms/Payeer/__init__.py +262 -0
  32. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Payeer/login.py +32 -16
  33. xync_client-0.0.155.dev2/xync_client/Pms/Volet/__init__.py +293 -0
  34. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Volet/api.py +17 -13
  35. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/ex.py +2 -2
  36. xync_client-0.0.155.dev2/xync_client/details.py +44 -0
  37. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/loader.py +2 -1
  38. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/pm_unifier.py +1 -1
  39. {xync_client-0.0.100 → xync_client-0.0.155.dev2/xync_client.egg-info}/PKG-INFO +7 -1
  40. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client.egg-info/SOURCES.txt +6 -0
  41. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client.egg-info/requires.txt +6 -0
  42. xync_client-0.0.100/xync_client/Abc/InAgent.py +0 -21
  43. xync_client-0.0.100/xync_client/Gmail/__init__.py +0 -116
  44. xync_client-0.0.100/xync_client/Pms/Payeer/__init__.py +0 -166
  45. xync_client-0.0.100/xync_client/Pms/Volet/__init__.py +0 -239
  46. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/.env.sample +0 -0
  47. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/.gitignore +0 -0
  48. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/.pre-commit-config.yaml +0 -0
  49. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/README.md +0 -0
  50. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/__init__.py +0 -0
  51. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/makefile +0 -0
  52. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/setup.cfg +0 -0
  53. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/TestAgent.py +0 -0
  54. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/TestAsset.py +0 -0
  55. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/TestOrder.py +0 -0
  56. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Binance/test_binance.py +0 -0
  57. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Bybit/test_bybit.py +0 -0
  58. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Bybit/test_bybit_p2p.py +0 -0
  59. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Gate/test_gate.py +0 -0
  60. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Wallet/test_agent.py +0 -0
  61. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/Wallet/test_ex.py +0 -0
  62. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/__init__.py +0 -0
  63. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/tests/_todo_refact/_test_ex.py +0 -0
  64. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Asset.py +0 -0
  65. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Auth.py +0 -0
  66. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/BaseTest.py +0 -0
  67. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Exception.py +0 -0
  68. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Abc/Order.py +0 -0
  69. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/__init__.py +0 -0
  70. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/binance_async.py +0 -0
  71. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/earn_api.py +0 -0
  72. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/etype/ad.py +0 -0
  73. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/etype/pm.py +0 -0
  74. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/exceptions.py +0 -0
  75. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/sapi.py +0 -0
  76. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Binance/web_c2c.py +0 -0
  77. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/__init__.py +0 -0
  78. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/agent.py +0 -0
  79. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/base.py +0 -0
  80. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/etype/ad.py +0 -0
  81. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/etype/pm.py +0 -0
  82. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/req.mjs +0 -0
  83. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BingX/sign.js +0 -0
  84. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BitGet/__init__.py +0 -0
  85. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BitGet/agent.py +0 -0
  86. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BitGet/etype/ad.py +0 -0
  87. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/BitPapa/ex.py +0 -0
  88. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/order.py +0 -0
  89. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/web_earn.py +0 -0
  90. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/web_p2p.py +0 -0
  91. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Bybit/ws.py +0 -0
  92. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Gate/etype/ad.py +0 -0
  93. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Gate/premarket.py +0 -0
  94. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/earn.py +0 -0
  95. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/etype/__init__.py +0 -0
  96. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/etype/cred.py +0 -0
  97. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/etype/pm.py +0 -0
  98. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Htx/etype/test.py +0 -0
  99. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/KuCoin/etype/ad.py +0 -0
  100. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/KuCoin/etype/pm.py +0 -0
  101. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/KuCoin/web.py +0 -0
  102. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Mexc/etype/ad.py +0 -0
  103. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Mexc/etype/pm.py +0 -0
  104. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Okx/etype/ad.py +0 -0
  105. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Okx/etype/pm.py +0 -0
  106. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/.gitignore +0 -0
  107. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Alfa/__init__.py +0 -0
  108. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Alfa/state.json +0 -0
  109. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/MTS/__init__.py +0 -0
  110. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Ozon/__init__.py +0 -0
  111. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Payeer/.gitignore +0 -0
  112. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Payeer/api.py +0 -0
  113. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Sber/__init__.py +0 -0
  114. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Sber/utils.py +0 -0
  115. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Tinkoff/__init__.py +0 -0
  116. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Volet/_todo_req/req.mjs +0 -0
  117. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Volet/_todo_req/req.py +0 -0
  118. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Volet/pl.py +0 -0
  119. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Xync/__main__.py +0 -0
  120. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Xync/ed.py +0 -0
  121. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/Pms/Yandex/__init__.py +0 -0
  122. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/agent.py +0 -0
  123. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/asset.py +0 -0
  124. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/auth.py +0 -0
  125. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/inAgent.py +0 -0
  126. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/order.py +0 -0
  127. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/pyd.py +0 -0
  128. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/pyro.py +0 -0
  129. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/TgWallet/web.py +0 -0
  130. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client/__init__.py +0 -0
  131. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client.egg-info/dependency_links.txt +0 -0
  132. {xync_client-0.0.100 → xync_client-0.0.155.dev2}/xync_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xync-client
3
- Version: 0.0.100
3
+ Version: 0.0.155.dev2
4
4
  Author-email: Mike Artemiev <mixartemev@gmail.com>
5
5
  Project-URL: Homepage, https://gitlab.com/XyncNet/client
6
6
  Project-URL: Repository, https://gitlab.com/XyncNet/client
@@ -8,12 +8,18 @@ Requires-Python: >=3.11
8
8
  Requires-Dist: asynchuobi
9
9
  Requires-Dist: bs4
10
10
  Requires-Dist: bybit-p2p
11
+ Requires-Dist: google-api-python-client
12
+ Requires-Dist: google-auth-httplib2
13
+ Requires-Dist: google-auth-oauthlib
11
14
  Requires-Dist: requests-toolbelt
12
15
  Requires-Dist: msgspec
13
16
  Requires-Dist: python-binance
14
17
  Requires-Dist: pybit
15
18
  Requires-Dist: pyotp
19
+ Requires-Dist: pypng
16
20
  Requires-Dist: kurigram
21
+ Requires-Dist: payeer-api
22
+ Requires-Dist: playwright
17
23
  Requires-Dist: python-dotenv
18
24
  Requires-Dist: python-okx
19
25
  Requires-Dist: pyrogram-client
@@ -10,13 +10,19 @@ dependencies = [
10
10
  "asynchuobi",
11
11
  "bs4",
12
12
  "bybit-p2p",
13
+ "google-api-python-client",
14
+ "google-auth-httplib2",
15
+ "google-auth-oauthlib",
13
16
  "requests-toolbelt",
14
17
  "msgspec",
15
18
  "python-binance",
16
19
  "pybit",
17
20
  "pyotp",
21
+ "pypng",
18
22
  # "kurigram @ git+https://github.com/mixartemev/pyrogram.git",
19
23
  "kurigram",
24
+ "payeer-api",
25
+ "playwright",
20
26
  "python-dotenv",
21
27
  "python-okx",
22
28
  "pyrogram-client",
@@ -84,13 +84,13 @@ class TestEx(BaseTest):
84
84
  # logging.info(f"{client.ex.name}: {ExAction.cur_pms_map.name} - ok")
85
85
 
86
86
  # 22
87
- # async def test_coins(self, clients: list[BaseExClient]):
88
- # for client in clients:
89
- # coins: dict[str, CoinEx] = await client.coins()
90
- # ok = self.is_dict_of_objects(coins, CoinEx)
91
- # t, _ = await ExStat.update_or_create({"ok": ok}, ex=client.ex, action=ExAction.coins)
92
- # assert t.ok, "No coins"
93
- # logging.info(f"{client.ex.name}: {ExAction.coins.name} - ok")
87
+ async def test_coins(self, clients: list[BaseExClient]):
88
+ for client in clients:
89
+ coins: dict[str, models.CoinEx] = await client.coins()
90
+ ok = self.is_dict_of_objects(coins, models.CoinEx)
91
+ t, _ = await models.ExStat.update_or_create({"ok": ok}, ex=client.ex, action=ExAction.coins)
92
+ assert t.ok, "No coins"
93
+ logging.info(f"{client.ex.name}: {ExAction.coins.name} - ok")
94
94
 
95
95
  # # 23
96
96
  # async def test_pairs(self, clients: list[BaseExClient]):
@@ -0,0 +1,299 @@
1
+ import logging
2
+ import re
3
+ from asyncio import sleep
4
+ from collections import defaultdict
5
+ from difflib import SequenceMatcher
6
+
7
+ from tortoise.exceptions import OperationalError, IntegrityError
8
+ from xync_schema import models
9
+ from xync_schema.xtype import BaseAd
10
+
11
+
12
+ class AdLoader:
13
+ ex: models.Ex
14
+ all_conds: dict[int, tuple[str, set[int]]] = {}
15
+ cond_sims: dict[int, int] = defaultdict(set)
16
+ rcond_sims: dict[int, set[int]] = defaultdict(set) # backward
17
+ tree: dict = {}
18
+
19
+ async def old_conds_load(self):
20
+ # пока не порешали рейс-кондишн, очищаем сиротские условия при каждом запуске
21
+ # [await c.delete() for c in await Cond.filter(ads__isnull=True)]
22
+ self.all_conds = {
23
+ c.id: (c.raw_txt, {a.maker.exid for a in c.ads})
24
+ for c in await models.Cond.all().prefetch_related("ads__maker")
25
+ }
26
+ for curr, old in await models.CondSim.filter().values_list("cond_id", "cond_rel_id"):
27
+ self.cond_sims[curr] = old
28
+ self.rcond_sims[old] |= {curr}
29
+
30
+ self.build_tree()
31
+ a = set()
32
+
33
+ def check_tree(tre):
34
+ for p, c in tre.items():
35
+ a.add(p)
36
+ check_tree(c)
37
+
38
+ for pr, ch in self.tree.items():
39
+ check_tree(ch)
40
+ if ct := set(self.tree.keys()) & a:
41
+ logging.exception(f"cycle cids: {ct}")
42
+
43
+ async def person_name_update(self, name: str, exid: int) -> models.Person:
44
+ if actor := await models.Actor.get_or_none(exid=exid, ex=self.ex).prefetch_related("person"):
45
+ actor.person.name = name
46
+ await actor.person.save()
47
+ return actor.person
48
+ # tmp dirty fix
49
+ note = f"{self.ex.id}:{exid}"
50
+ if person := await models.Person.get_or_none(note__startswith=note):
51
+ person.name = name
52
+ await person.save()
53
+ return person
54
+ try:
55
+ return await models.Person.create(name=name, note=note)
56
+ except OperationalError as e:
57
+ raise e
58
+ await models.Actor.create(person=person, exid=exid, ex=self.ex)
59
+ return person
60
+ # person = await models.Person.create(note=f'{actor.ex_id}:{actor.exid}:{name}') # no person for just ads with no orders
61
+ # raise ValueError(f"Agent #{exid} not found")
62
+
63
+ async def ad_load(
64
+ self,
65
+ pad: BaseAd,
66
+ cid: int = None,
67
+ ps: models.PairSide = None,
68
+ maker: models.Actor = None,
69
+ coinex: models.CoinEx = None,
70
+ curex: models.CurEx = None,
71
+ rname: str = None,
72
+ ) -> models.Ad:
73
+ if not maker:
74
+ if not (maker := await models.Actor.get_or_none(exid=pad.userId, ex=self.ex)):
75
+ person = await models.Person.create(name=rname, note=f"{self.ex.id}:{pad.userId}:{pad.nickName}")
76
+ maker = await models.Actor.create(name=pad.nickName, person=person, exid=pad.userId, ex=self.ex)
77
+ if rname:
78
+ await self.person_name_update(rname, int(pad.userId))
79
+ ps = ps or await models.PairSide.get_or_none(
80
+ is_sell=pad.side,
81
+ pair__coin__ticker=pad.tokenId,
82
+ pair__cur__ticker=pad.currencyId,
83
+ ).prefetch_related("pair")
84
+ # if not ps or not ps.pair:
85
+ # ... # THB/USDC: just for initial filling
86
+ ad_upd = models.Ad.validate(pad.model_dump(by_alias=True))
87
+ cur_scale = 10 ** (curex or await models.CurEx.get(cur_id=ps.pair.cur_id, ex=self.ex)).scale
88
+ coin_scale = 10 ** (coinex or await models.CoinEx.get(coin_id=ps.pair.coin_id, ex=self.ex)).scale
89
+ amt = int(float(pad.quantity) * float(pad.price) * cur_scale)
90
+ mxf = pad.maxAmount and int(float(pad.maxAmount) * cur_scale)
91
+ df_unq = ad_upd.df_unq(
92
+ maker_id=maker.id,
93
+ pair_side_id=ps.id,
94
+ amount=amt if amt < 4_294_967_295 else 4_294_967_295,
95
+ quantity=int(float(pad.quantity) * coin_scale),
96
+ min_fiat=int(float(pad.minAmount) * cur_scale),
97
+ max_fiat=mxf if mxf < 4_294_967_295 else 4_294_967_295,
98
+ price=int(float(pad.price) * cur_scale),
99
+ premium=int(float(pad.premium) * 100),
100
+ cond_id=cid,
101
+ status=self.ad_status(ad_upd.status),
102
+ )
103
+ try:
104
+ ad_db, _ = await models.Ad.update_or_create(**df_unq)
105
+ except OperationalError as e:
106
+ raise e
107
+ await ad_db.pms.add(*(await models.Pm.filter(pmexs__ex=self.ex, pmexs__exid__in=pad.payments)))
108
+ return ad_db
109
+
110
+ async def cond_load( # todo: refact from Bybit Ad format to universal
111
+ self,
112
+ ad: BaseAd,
113
+ ps: models.PairSide = None,
114
+ force: bool = False,
115
+ rname: str = None,
116
+ coinex: models.CoinEx = None,
117
+ curex: models.CurEx = None,
118
+ pms_from_cond: bool = False,
119
+ ) -> tuple[models.Ad, bool]:
120
+ _sim, cid = None, None
121
+ ad_db = await models.Ad.get_or_none(exid=ad.id, maker__ex=self.ex).prefetch_related("cond")
122
+ # если точно такое условие уже есть в бд
123
+ if not (cleaned := clean(ad.remark)) or (cid := {oc[0]: ci for ci, oc in self.all_conds.items()}.get(cleaned)):
124
+ # и объява с таким ид уже есть, но у нее другое условие
125
+ if ad_db and ad_db.cond_id != cid:
126
+ # то обновляем ид ее условия
127
+ ad_db.cond_id = cid
128
+ await ad_db.save()
129
+ logging.info(f"{ad.nickName} upd cond#{ad_db.cond_id}->{cid}")
130
+ # old_cid = ad_db.cond_id # todo: solve race-condition, а пока что очищаем при каждом запуске
131
+ # if not len((old_cond := await Cond.get(id=old_cid).prefetch_related('ads')).ads):
132
+ # await old_cond.delete()
133
+ # logging.warning(f"Cond#{old_cid} deleted!")
134
+ return (ad_db or force and await self.ad_load(ad, cid, ps, coinex=coinex, curex=curex, rname=rname)), False
135
+ # если эта объява в таким ид уже есть в бд, но с другим условием (или без), а текущего условия еще нет в бд
136
+ if ad_db:
137
+ await ad_db.fetch_related("cond__ads", "maker")
138
+ if not ad_db.cond_id or (
139
+ # у измененного условия этой объявы есть другие объявы?
140
+ (rest_ads := set(ad_db.cond.ads) - {ad_db})
141
+ and
142
+ # другие объявы этого условия принадлежат другим юзерам
143
+ {ra.maker_id for ra in rest_ads} - {ad_db.maker_id}
144
+ ):
145
+ # создадим новое условие и присвоим его только текущей объяве
146
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
147
+ ad_db.cond_id = cond.id
148
+ await ad_db.save()
149
+ ad_db.cond = cond
150
+ return ad_db, True
151
+ # а если других объяв со старым условием этой обявы нет, либо они все этого же юзера
152
+ # обновляем условие (в тч во всех ЕГО объявах)
153
+ ad_db.cond.last_ver = ad_db.cond.raw_txt
154
+ ad_db.cond.raw_txt = cleaned
155
+ try:
156
+ await ad_db.cond.save()
157
+ except IntegrityError as e:
158
+ raise e
159
+ await self.cond_upd(ad_db.cond, {ad_db.maker.exid})
160
+ # и подправим коэфициенты похожести нового текста
161
+ await self.fix_rel_sims(ad_db.cond_id, cleaned)
162
+ return ad_db, False
163
+
164
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
165
+ ad_db = await self.ad_load(ad, cond.id, ps, coinex=coinex, curex=curex, rname=rname)
166
+ ad_db.cond = cond
167
+ return ad_db, True
168
+
169
+ async def cond_new(self, txt: str, uids: set[int]) -> models.Cond:
170
+ new_cond, _ = await models.Cond.update_or_create(raw_txt=txt)
171
+ # и максимально похожую связь для нового условия (если есть >= 60%)
172
+ await self.cond_upd(new_cond, uids)
173
+ return new_cond
174
+
175
+ async def cond_upd(self, cond: models.Cond, uids: set[int]):
176
+ self.all_conds[cond.id] = cond.raw_txt, uids
177
+ # и максимально похожую связь для нового условия (если есть >= 60%)
178
+ old_cid, sim = await self.cond_get_max_sim(cond.id, cond.raw_txt, uids)
179
+ await self.actual_sim(cond.id, old_cid, sim)
180
+
181
+ def find_in_tree(self, cid: int, old_cid: int) -> bool:
182
+ if p := self.cond_sims.get(old_cid):
183
+ if p == cid:
184
+ return True
185
+ return self.find_in_tree(cid, p)
186
+ return False
187
+
188
+ async def cond_get_max_sim(self, cid: int, txt: str, uids: set[int]) -> tuple[int | None, int | None]:
189
+ # находим все старые тексты похожие на 90% и более
190
+ if len(txt) < 15:
191
+ return None, None
192
+ sims: dict[int, int] = {}
193
+ for old_cid, (old_txt, old_uids) in self.all_conds.items():
194
+ if len(old_txt) < 15 or uids == old_uids:
195
+ continue
196
+ elif not self.can_add_sim(cid, old_cid):
197
+ continue
198
+ if sim := get_sim(txt, old_txt):
199
+ sims[old_cid] = sim
200
+ # если есть, берем самый похожий из них
201
+ if sims:
202
+ old_cid, sim = max(sims.items(), key=lambda x: x[1])
203
+ await sleep(0.3)
204
+ return old_cid, sim
205
+ return None, None
206
+
207
+ def can_add_sim(self, cid: int, old_cid: int) -> bool:
208
+ if cid == old_cid:
209
+ return False
210
+ elif self.cond_sims.get(cid) == old_cid:
211
+ return False
212
+ elif self.find_in_tree(cid, old_cid):
213
+ return False
214
+ elif self.cond_sims.get(old_cid) == cid:
215
+ return False
216
+ elif cid in self.rcond_sims.get(old_cid, {}):
217
+ return False
218
+ elif old_cid in self.rcond_sims.get(cid, {}):
219
+ return False
220
+ return True
221
+
222
+ async def fix_rel_sims(self, cid: int, new_txt: str):
223
+ for rel_sim in await models.CondSim.filter(cond_rel_id=cid).prefetch_related("cond"):
224
+ if sim := get_sim(new_txt, rel_sim.cond.raw_txt):
225
+ rel_sim.similarity = sim
226
+ await rel_sim.save()
227
+ else:
228
+ await rel_sim.delete()
229
+
230
+ async def actual_cond(self):
231
+ for curr, old in await models.CondSim.all().values_list("cond_id", "cond_rel_id"):
232
+ self.cond_sims[curr] = old
233
+ self.rcond_sims[old] |= {curr}
234
+ for cid, (txt, uids) in self.all_conds.items():
235
+ old_cid, sim = await self.cond_get_max_sim(cid, txt, uids)
236
+ await self.actual_sim(cid, old_cid, sim)
237
+ # хз бля чо это ваще
238
+ # for ad_db in await models.Ad.filter(direction__pairex__ex=self.ex).prefetch_related("cond", "maker"):
239
+ # ad = Ad(id=str(ad_db.exid), userId=str(ad_db.maker.exid), remark=ad_db.cond.raw_txt)
240
+ # await self.cond_upsert(ad, force=True)
241
+
242
+ async def actual_sim(self, cid: int, old_cid: int, sim: int):
243
+ if not sim:
244
+ return
245
+ if old_sim := await models.CondSim.get_or_none(cond_id=cid):
246
+ if old_sim.cond_rel_id != old_cid:
247
+ if sim > old_sim.similarity:
248
+ logging.warning(f"R {cid}: {old_sim.similarity}->{sim} ({old_sim.cond_rel_id}->{old_cid})")
249
+ await old_sim.update_from_dict({"similarity": sim, "old_rel_id": old_cid}).save()
250
+ self._cond_sim_upd(cid, old_cid)
251
+ elif sim != old_sim.similarity:
252
+ logging.info(f"{cid}: {old_sim.similarity}->{sim}")
253
+ await old_sim.update_from_dict({"similarity": sim}).save()
254
+ else:
255
+ await models.CondSim.create(cond_id=cid, cond_rel_id=old_cid, similarity=sim)
256
+ self._cond_sim_upd(cid, old_cid)
257
+
258
+ def _cond_sim_upd(self, cid: int, old_cid: int):
259
+ if old_old_cid := self.cond_sims.get(cid): # если старый cid уже был в дереве:
260
+ self.rcond_sims[old_old_cid].remove(cid) # удаляем из обратного
261
+ self.cond_sims[cid] = old_cid # а в прямом он автоматом переопределится, даже если и был
262
+ self.rcond_sims[old_cid] |= {cid} # ну и в обратное добавим новый
263
+
264
+ def build_tree(self):
265
+ set(self.cond_sims.keys()) | set(self.cond_sims.values())
266
+ tree = defaultdict(dict)
267
+ # Группируем родителей по детям
268
+ for child, par in self.cond_sims.items():
269
+ tree[par] |= {child: {}} # todo: make from self.rcond_sim
270
+
271
+ # Строим дерево снизу вверх
272
+ def subtree(node):
273
+ if not node:
274
+ return node
275
+ for key in node:
276
+ subnode = tree.pop(key, {})
277
+ d = subtree(subnode)
278
+ node[key] |= d # actual tree rebuilding here!
279
+ return node # todo: refact?
280
+
281
+ # Находим корни / без родителей
282
+ roots = set(self.cond_sims.values()) - set(self.cond_sims.keys())
283
+ for root in roots:
284
+ _ = subtree(tree[root])
285
+
286
+ self.tree = tree
287
+
288
+
289
+ def get_sim(s1, s2) -> int:
290
+ sim = int((SequenceMatcher(None, s1, s2).ratio() - 0.6) * 10_000)
291
+ return sim if sim > 0 else 0
292
+
293
+
294
+ def clean(s) -> str:
295
+ clear = r"[^\w\s.,!?;:()\-]"
296
+ repeat = r"(.)\1{2,}"
297
+ s = re.sub(clear, "", s).lower()
298
+ s = re.sub(repeat, r"\1", s)
299
+ return s.replace("\n\n", "\n").replace(" ", " ").strip(" \n/.,!?-")
@@ -1,21 +1,80 @@
1
1
  from abc import abstractmethod
2
+ from asyncio.tasks import gather
3
+ from collections import defaultdict
2
4
 
5
+ from playwright.async_api import async_playwright
3
6
  from pydantic import BaseModel
4
7
  from pyro_client.client.file import FileClient
8
+ from x_client import df_hdrs
5
9
  from x_client.aiohttp import Client as HttpClient
10
+ from xync_bot import XyncBot
11
+ from xync_client.Abc.PmAgent import PmAgentClient
12
+ from xync_schema.enums import UserStatus
13
+
14
+ from xync_client.Abc.InAgent import BaseInAgentClient
15
+
16
+ from xync_client.Bybit.etype.order import TakeAdReq
6
17
  from xync_schema import models
7
- from xync_schema.models import OrderStatus, Coin, Cur, Ad, AdStatus, Actor
18
+ from xync_schema.models import OrderStatus, Coin, Cur, Ad, AdStatus, Actor, Agent
8
19
  from xync_schema.xtype import BaseAd
9
20
 
10
21
  from xync_client.Abc.Ex import BaseExClient
11
- from xync_client.Abc.xtype import CredExOut, BaseOrderReq, BaseAdUpdate
12
-
13
-
14
- class BaseAgentClient(HttpClient):
15
- def __init__(self, actor: Actor, bot: FileClient, headers: dict[str, str] = None, cookies: dict[str, str] = None):
16
- self.actor: Actor = actor
17
- super().__init__(actor.ex.host_p2p, headers, cookies)
18
- self.ex_client: BaseExClient = self.actor.ex.client(bot)
22
+ from xync_client.Abc.xtype import CredExOut, BaseOrderReq, BaseAdUpdate, AdUpd
23
+ from xync_client.Gmail import GmClient
24
+
25
+
26
+ class BaseAgentClient(HttpClient, BaseInAgentClient):
27
+ actor: Actor
28
+ agent: Agent
29
+ bbot: XyncBot
30
+ fbot: FileClient
31
+ ex_client: BaseExClient
32
+ pm_clients: dict[int, PmAgentClient] # {pm_id: PmAgentClient}
33
+
34
+ def __init__(
35
+ self,
36
+ agent: Agent,
37
+ ex_client: BaseExClient,
38
+ fbot: FileClient,
39
+ bbot: XyncBot,
40
+ headers: dict[str, str] = df_hdrs,
41
+ cookies: dict[str, str] = None,
42
+ ):
43
+ self.bbot = bbot
44
+ self.fbot = fbot
45
+ self.agent: Agent = agent
46
+ self.actor: Actor = agent.actor
47
+ self.gmail = agent.actor.person.user.gmail and GmClient(agent.actor.person.user)
48
+ self.ex_client: BaseExClient = ex_client
49
+ self.pm_clients: dict[int, PmAgentClient] = defaultdict()
50
+ super().__init__(self.actor.ex.host_p2p, headers, cookies)
51
+
52
+ async def start(self, debug: bool = False):
53
+ tasks = []
54
+ if not self.is_started:
55
+ if self.agent.status & 1: # race
56
+ tasks.append(self.start_race())
57
+
58
+ if self.agent.status & 2: # in agent
59
+ if not self.pm_clients:
60
+ pm_agents = await models.PmAgent.filter(
61
+ active=True,
62
+ auth__isnull=False,
63
+ user__status=UserStatus.ACTIVE,
64
+ ).prefetch_related("pm", "user__gmail")
65
+ # payeer_cl = Client(actor.person.user.username_id)
66
+ pw = await async_playwright().start()
67
+ browser = await pw.chromium.launch(
68
+ channel="chrome-beta" if debug else "chromium-headless-shell", headless=not debug
69
+ )
70
+ self.pm_clients = {pma.pm_id: pma.client(browser, self.bbot) for pma in pm_agents}
71
+ [tasks.append(pmcl.start()) for pmcl in self.pm_clients.values()]
72
+ # tasks.append(self.start_listen())
73
+
74
+ if self.agent.status & 4: # for further
75
+ ...
76
+ self.is_started = True
77
+ return await gather(*tasks)
19
78
 
20
79
  # 0: Получшение ордеров в статусе status, по монете coin, в валюте coin, в направлении is_sell: bool
21
80
  @abstractmethod
@@ -78,9 +137,31 @@ class BaseAgentClient(HttpClient):
78
137
  @abstractmethod
79
138
  async def ad_new(self, ad: BaseAd) -> Ad: ...
80
139
 
140
+ async def ad_upd(self, ad_upd_req: AdUpd) -> Ad:
141
+ pmex_exids = await models.PmEx.filter(ex_id=self.actor.ex_id, pm_id__in=ad_upd_req.pm_ids).values_list(
142
+ "exid", flat=True
143
+ )
144
+ credexs = await models.CredEx.filter(
145
+ ex_id=self.actor.ex_id,
146
+ cred__pmcur__pm_id__in=ad_upd_req.pm_ids,
147
+ cred__pmcur__cur_id=ad_upd_req.cur_id,
148
+ cred__person_id=self.actor.person_id,
149
+ ).prefetch_related("cred__pmcur")
150
+ coinex = await models.CoinEx.get(coin_id=ad_upd_req.coin_id, ex=self.ex_client.ex)
151
+ curex = await models.CurEx.get(cur_id=ad_upd_req.cur_id, ex=self.ex_client.ex)
152
+ # override
153
+ ad_upd_req.coin_id = coinex.exid
154
+ ad_upd_req.cur_id = curex.exid
155
+ ad_upd_req.pm_ids = pmex_exids
156
+ ad_upd_req.credexs = credexs
157
+ ad_upd_req.price = round(ad_upd_req.price, curex.scale)
158
+ ad_upd_req.amount = round(ad_upd_req.amount, curex.scale)
159
+ ad_upd_req.quantity = round(ad_upd_req.amount / ad_upd_req.price, coinex.scale)
160
+ return await self._ad_upd(ad_upd_req)
161
+
81
162
  # 31: Редактирование объявления
82
163
  @abstractmethod
83
- async def ad_upd(self, ad: BaseAdUpdate) -> Ad: ...
164
+ async def _ad_upd(self, ad: BaseAdUpdate) -> Ad: ...
84
165
 
85
166
  # 32: Удаление
86
167
  @abstractmethod
@@ -115,6 +196,9 @@ class BaseAgentClient(HttpClient):
115
196
  @abstractmethod
116
197
  async def my_assets(self) -> dict: ...
117
198
 
199
+ @abstractmethod
200
+ async def take_ad(self, req: TakeAdReq): ...
201
+
118
202
  # Сохранение объявления (с Pm/Cred-ами) в бд
119
203
  # async def ad_pydin2db(self, ad_pydin: AdSaleIn | AdBuyIn) -> Ad:
120
204
  # ad_db = await self.ex_client.ad_pydin2db(ad_pydin)
@@ -11,11 +11,13 @@ from xync_schema import models
11
11
  from xync_schema.enums import FileType
12
12
  from xync_schema.xtype import CurEx, CoinEx, BaseAd, BaseAdIn
13
13
 
14
- from xync_client.Abc.xtype import PmEx, MapOfIdsList
14
+ from xync_client.Abc.AdLoader import AdLoader
15
+ from xync_client.Abc.xtype import PmEx, MapOfIdsList, GetAds
15
16
  from xync_client.pm_unifier import PmUnifier, PmUni
16
17
 
17
18
 
18
- class BaseExClient(HttpClient):
19
+ class BaseExClient(HttpClient, AdLoader):
20
+ host: str = None
19
21
  cur_map: dict[int, str] = {}
20
22
  unifier_class: type = PmUnifier
21
23
  logo_pre_url: str
@@ -33,7 +35,7 @@ class BaseExClient(HttpClient):
33
35
  ):
34
36
  self.ex = ex
35
37
  self.bot = bot
36
- super().__init__(getattr(ex, attr), headers, cookies, proxy and proxy.str())
38
+ super().__init__(self.host or getattr(ex, attr), headers, cookies, proxy and proxy.str())
37
39
 
38
40
  @abstractmethod
39
41
  def pm_type_map(self, typ: models.PmEx) -> str: ...
@@ -62,16 +64,19 @@ class BaseExClient(HttpClient):
62
64
  @abstractmethod
63
65
  async def pairs(self) -> tuple[MapOfIdsList, MapOfIdsList]: ...
64
66
 
67
+ async def _x2e_ads(self, req: GetAds) -> GetAds: # {ad.id: ad}
68
+ req.coin_id = await models.CoinEx.get(coin_id=req.coin_id, ex=self.ex).values_list("exid", flat=True)
69
+ req.cur_id = await models.CurEx.get(cur_id=req.cur_id, ex=self.ex).values_list("exid", flat=True)
70
+ req.pm_ids = await models.PmEx.filter(ex=self.ex, pm_id__in=req.pm_ids).values_list("exid", flat=True)
71
+ return req
72
+
65
73
  # 24: Список объяв по (buy/sell, cur, coin, pm)
74
+ async def ads(self, req: GetAds, lim: int = None, vm_filter: bool = False, **kwargs) -> list[BaseAd]:
75
+ return await self._ads(await self._x2e_ads(req), lim, vm_filter, **kwargs)
76
+
66
77
  @abstractmethod
67
- async def ads(
68
- self,
69
- coin_exid: str,
70
- cur_exid: str,
71
- is_sell: bool,
72
- pm_exids: list[str | int] = None,
73
- amount: int = None,
74
- lim: int = None,
78
+ async def _ads(
79
+ self, req: GetAds, lim: int = None, vm_filter: bool = False, **kwargs
75
80
  ) -> list[BaseAd]: # {ad.id: ad}
76
81
  ...
77
82
 
@@ -91,9 +96,15 @@ class BaseExClient(HttpClient):
91
96
  async def set_curs(self, cookies: dict = None) -> bool:
92
97
  # Curs
93
98
  cur_pyds: dict[str, CurEx] = await self.curs()
99
+ old_curs = {c.ticker: c.id for c in await models.Cur.all()}
94
100
  curs: dict[int | str, models.Cur] = {
95
- exid: (await models.Cur.update_or_create({"rate": cur_pyd.rate or 0}, ticker=cur_pyd.ticker))[0]
96
- for exid, cur_pyd in cur_pyds.items()
101
+ exid: (
102
+ await models.Cur.update_or_create(
103
+ {"rate": cur_pyd.rate or 0, "id": old_curs.get(cur_pyd.ticker, await models.Cur.all().count() + 1)},
104
+ ticker=cur_pyd.ticker,
105
+ )
106
+ )[0]
107
+ for i, (exid, cur_pyd) in enumerate(cur_pyds.items())
97
108
  }
98
109
  curexs = [
99
110
  models.CurEx(**c.model_dump(exclude_none=True), cur=curs[c.exid], ex=self.ex) for c in cur_pyds.values()
@@ -209,7 +220,7 @@ class BaseExClient(HttpClient):
209
220
  coinexs: dict[str, CoinEx] = await self.coins()
210
221
  coins_db: dict[int, models.Coin] = {
211
222
  c.exid: (
212
- await models.Coin.update_or_create({"scale": c.scale} if c.scale is not None else {}, ticker=c.ticker)
223
+ await models.Coin.update_or_create({"scale": c.scale or self.coin_scales[c.ticker]}, ticker=c.ticker)
213
224
  )[0]
214
225
  for c in coinexs.values()
215
226
  }
@@ -236,24 +247,22 @@ class BaseExClient(HttpClient):
236
247
  }
237
248
  prs: tuple[dict, dict] = await self.pairs()
238
249
  for is_sell in (0, 1):
239
- pss: list[models.PairSide] = []
240
250
  for cur, coinz in prs[is_sell].items():
241
251
  for coin in coinz:
242
252
  pair, _ = await models.Pair.get_or_create(coin=coins[coin], cur=curs[cur])
243
253
  # pairex, _ = await models.PairEx.get_or_create(pair=pair, ex=self.ex) # todo: разные ли комишки на покупку и продажу?
244
- pss += [models.PairSide(is_sell=is_sell, pair=pair)]
245
- await models.PairSide.bulk_create(pss, ignore_conflicts=True)
254
+ await models.PairSide.update_or_create(is_sell=is_sell, pair=pair)
246
255
  return True
247
256
 
248
257
  # Сохранение чужого объявления (с Pm-ами) в бд
249
- async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
250
- dct = ad_pydin.model_dump()
251
- dct["exid"] = dct.pop("id")
252
- ad_in = models.Ad.validate(dct)
253
- ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
254
- await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
255
- await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
256
- return ad_db
258
+ # async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
259
+ # dct = ad_pydin.model_dump()
260
+ # dct["exid"] = dct.pop("id")
261
+ # ad_in = models.Ad.validate(dct)
262
+ # ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
263
+ # await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
264
+ # await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
265
+ # return ad_db
257
266
 
258
267
  async def file_upsert(self, url: str, ss: ClientSession = None) -> models.File:
259
268
  if not (file := await models.File.get_or_none(name__startswith=url.split("?")[0])):
@@ -267,12 +276,12 @@ class BaseExClient(HttpClient):
267
276
  # fr = await pbot.get_file(file.ref) # check
268
277
  return file
269
278
 
270
- async def _proc(self, resp: ClientResponse, data_key: str = None, bp: dict | str = None) -> dict | str:
279
+ async def _proc(self, resp: ClientResponse, bp: dict | str = None) -> dict | str:
271
280
  if resp.status in (403,):
272
281
  proxy = await models.Proxy.filter(valid=True, country__short__not="US").order_by("-updated_at").first()
273
282
  cookies = self.session.cookie_jar.filter_cookies(self.session._base_url)
274
283
  self.session = ClientSession(
275
284
  self.session._base_url, headers=self.session.headers, cookies=cookies or None, proxy=proxy.str()
276
285
  )
277
- return await self.METHS[resp.method](self, resp.url.path, bp, data_key=data_key)
278
- return await super()._proc(resp, data_key, bp)
286
+ return await self.METHS[resp.method](self, resp.url.path, bp)
287
+ return await super()._proc(resp, bp)
@@ -0,0 +1,10 @@
1
+ from PGram import Bot
2
+ from aiogram.types import Message
3
+
4
+
5
+ class HasAbotUid:
6
+ abot: Bot
7
+ uid: int
8
+
9
+ async def receive(self, text: str, photo: bytes = None, video: bytes = None) -> Message:
10
+ return await self.abot.send(self.uid, txt=text, photo=photo, video=video)
@@ -0,0 +1,10 @@
1
+ from abc import abstractmethod
2
+
3
+
4
+ class BaseInAgentClient:
5
+ @abstractmethod
6
+ async def start_listen(self) -> bool: ...
7
+
8
+ # 3N: [T] - Уведомление об одобрении запроса на сделку
9
+ @abstractmethod
10
+ async def request_accepted_notify(self) -> int: ... # id