xync-client 0.0.147__py3-none-any.whl → 0.0.150__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.

Potentially problematic release.


This version of xync-client might be problematic. Click here for more details.

@@ -0,0 +1,293 @@
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
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
+ # return await models.Person.create(note=f'{actor.ex_id}:{actor.exid}:{name}') # no person for just ads with no orders
49
+ raise ValueError(f"Agent #{exid} not found")
50
+
51
+ async def ad_load(
52
+ self,
53
+ pad: BaseAd,
54
+ cid: int = None,
55
+ ps: models.PairSide = None,
56
+ maker: models.Actor = None,
57
+ coinex: models.CoinEx = None,
58
+ curex: models.CurEx = None,
59
+ rname: str = None,
60
+ pms_from_cond: bool = False,
61
+ ) -> models.Ad:
62
+ if not maker:
63
+ if not (maker := await models.Actor.get_or_none(exid=pad.userId, ex=self.ex)):
64
+ person = await models.Person.create(name=rname, note=f"{self.ex.id}:{pad.userId}:{pad.nickName}")
65
+ maker = await models.Actor.create(name=pad.nickName, person=person, exid=pad.userId, ex=self.ex)
66
+ if rname:
67
+ await self.person_name_update(rname, int(pad.userId))
68
+ ps = ps or await models.PairSide.get_or_none(
69
+ is_sell=pad.side,
70
+ pair__coin__ticker=pad.tokenId,
71
+ pair__cur__ticker=pad.currencyId,
72
+ ).prefetch_related("pair")
73
+ # if not ps or not ps.pair:
74
+ # ... # THB/USDC: just for initial filling
75
+ ad_upd = models.Ad.validate(pad.model_dump(by_alias=True))
76
+ cur_scale = 10 ** (curex or await models.CurEx.get(cur_id=ps.pair.cur_id, ex=self.ex)).scale
77
+ coin_scale = 10 ** (coinex or await models.CoinEx.get(coin_id=ps.pair.coin_id, ex=self.ex)).scale
78
+ amt = int(float(pad.quantity) * float(pad.price) * cur_scale)
79
+ mxf = pad.maxAmount and int(float(pad.maxAmount) * cur_scale)
80
+ df_unq = ad_upd.df_unq(
81
+ maker_id=maker.id,
82
+ pair_side_id=ps.id,
83
+ amount=amt if amt < 4_294_967_295 else 4_294_967_295,
84
+ quantity=int(float(pad.quantity) * coin_scale),
85
+ min_fiat=int(float(pad.minAmount) * cur_scale),
86
+ max_fiat=mxf if mxf < 4_294_967_295 else 4_294_967_295,
87
+ price=int(float(pad.price) * cur_scale),
88
+ premium=int(float(pad.premium) * 100),
89
+ cond_id=cid,
90
+ )
91
+ try:
92
+ ad_db, _ = await models.Ad.update_or_create(**df_unq)
93
+ except OperationalError as e:
94
+ raise e
95
+ if not pms_from_cond:
96
+ await ad_db.pms.add(*(await models.Pm.filter(pmexs__ex=self.ex, pmexs__exid__in=pad.payments)))
97
+ return ad_db
98
+
99
+ async def cond_load( # todo: refact from Bybit Ad format to universal
100
+ self,
101
+ ad: BaseAd,
102
+ ps: models.PairSide = None,
103
+ force: bool = False,
104
+ rname: str = None,
105
+ coinex: models.CoinEx = None,
106
+ curex: models.CurEx = None,
107
+ pms_from_cond: bool = False,
108
+ ) -> tuple[models.Ad, bool]:
109
+ _sim, cid = None, None
110
+ ad_db = await models.Ad.get_or_none(exid=ad.id, maker__ex=self.ex).prefetch_related("cond")
111
+ # если точно такое условие уже есть в бд
112
+ if not (cleaned := clean(ad.remark)) or (cid := {oc[0]: ci for ci, oc in self.all_conds.items()}.get(cleaned)):
113
+ # и объява с таким ид уже есть, но у нее другое условие
114
+ if ad_db and ad_db.cond_id != cid:
115
+ # то обновляем ид ее условия
116
+ ad_db.cond_id = cid
117
+ await ad_db.save()
118
+ logging.info(f"{ad.nickName} upd cond#{ad_db.cond_id}->{cid}")
119
+ # old_cid = ad_db.cond_id # todo: solve race-condition, а пока что очищаем при каждом запуске
120
+ # if not len((old_cond := await Cond.get(id=old_cid).prefetch_related('ads')).ads):
121
+ # await old_cond.delete()
122
+ # logging.warning(f"Cond#{old_cid} deleted!")
123
+ return (
124
+ ad_db
125
+ or force
126
+ and await self.ad_load(
127
+ ad, cid, ps, coinex=coinex, curex=curex, rname=rname, pms_from_cond=pms_from_cond
128
+ )
129
+ ), False
130
+ # если эта объява в таким ид уже есть в бд, но с другим условием (или без), а текущего условия еще нет в бд
131
+ if ad_db:
132
+ await ad_db.fetch_related("cond__ads", "maker")
133
+ if not ad_db.cond_id or (
134
+ # у измененного условия этой объявы есть другие объявы?
135
+ (rest_ads := set(ad_db.cond.ads) - {ad_db})
136
+ and
137
+ # другие объявы этого условия принадлежат другим юзерам
138
+ {ra.maker_id for ra in rest_ads} - {ad_db.maker_id}
139
+ ):
140
+ # создадим новое условие и присвоим его только текущей объяве
141
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
142
+ ad_db.cond_id = cond.id
143
+ await ad_db.save()
144
+ ad_db.cond = cond
145
+ return ad_db, True
146
+ # а если других объяв со старым условием этой обявы нет, либо они все этого же юзера
147
+ # обновляем условие (в тч во всех ЕГО объявах)
148
+ ad_db.cond.last_ver = ad_db.cond.raw_txt
149
+ ad_db.cond.raw_txt = cleaned
150
+ await ad_db.cond.save()
151
+ await self.cond_upd(ad_db.cond, {ad_db.maker.exid})
152
+ # и подправим коэфициенты похожести нового текста
153
+ await self.fix_rel_sims(ad_db.cond_id, cleaned)
154
+ return ad_db, False
155
+
156
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
157
+ ad_db = await self.ad_load(
158
+ ad, cond.id, ps, coinex=coinex, curex=curex, rname=rname, pms_from_cond=pms_from_cond
159
+ )
160
+ ad_db.cond = cond
161
+ return ad_db, True
162
+
163
+ async def cond_new(self, txt: str, uids: set[int]) -> models.Cond:
164
+ new_cond, _ = await models.Cond.update_or_create(raw_txt=txt)
165
+ # и максимально похожую связь для нового условия (если есть >= 60%)
166
+ await self.cond_upd(new_cond, uids)
167
+ return new_cond
168
+
169
+ async def cond_upd(self, cond: models.Cond, uids: set[int]):
170
+ self.all_conds[cond.id] = cond.raw_txt, uids
171
+ # и максимально похожую связь для нового условия (если есть >= 60%)
172
+ old_cid, sim = await self.cond_get_max_sim(cond.id, cond.raw_txt, uids)
173
+ await self.actual_sim(cond.id, old_cid, sim)
174
+
175
+ def find_in_tree(self, cid: int, old_cid: int) -> bool:
176
+ if p := self.cond_sims.get(old_cid):
177
+ if p == cid:
178
+ return True
179
+ return self.find_in_tree(cid, p)
180
+ return False
181
+
182
+ async def cond_get_max_sim(self, cid: int, txt: str, uids: set[int]) -> tuple[int | None, int | None]:
183
+ # находим все старые тексты похожие на 90% и более
184
+ if len(txt) < 15:
185
+ return None, None
186
+ sims: dict[int, int] = {}
187
+ for old_cid, (old_txt, old_uids) in self.all_conds.items():
188
+ if len(old_txt) < 15 or uids == old_uids:
189
+ continue
190
+ elif not self.can_add_sim(cid, old_cid):
191
+ continue
192
+ if sim := get_sim(txt, old_txt):
193
+ sims[old_cid] = sim
194
+ # если есть, берем самый похожий из них
195
+ if sims:
196
+ old_cid, sim = max(sims.items(), key=lambda x: x[1])
197
+ await sleep(0.3)
198
+ return old_cid, sim
199
+ return None, None
200
+
201
+ def can_add_sim(self, cid: int, old_cid: int) -> bool:
202
+ if cid == old_cid:
203
+ return False
204
+ elif self.cond_sims.get(cid) == old_cid:
205
+ return False
206
+ elif self.find_in_tree(cid, old_cid):
207
+ return False
208
+ elif self.cond_sims.get(old_cid) == cid:
209
+ return False
210
+ elif cid in self.rcond_sims.get(old_cid, {}):
211
+ return False
212
+ elif old_cid in self.rcond_sims.get(cid, {}):
213
+ return False
214
+ return True
215
+
216
+ async def fix_rel_sims(self, cid: int, new_txt: str):
217
+ for rel_sim in await models.CondSim.filter(cond_rel_id=cid).prefetch_related("cond"):
218
+ if sim := get_sim(new_txt, rel_sim.cond.raw_txt):
219
+ rel_sim.similarity = sim
220
+ await rel_sim.save()
221
+ else:
222
+ await rel_sim.delete()
223
+
224
+ async def actual_cond(self):
225
+ for curr, old in await models.CondSim.all().values_list("cond_id", "cond_rel_id"):
226
+ self.cond_sims[curr] = old
227
+ self.rcond_sims[old] |= {curr}
228
+ for cid, (txt, uids) in self.all_conds.items():
229
+ old_cid, sim = await self.cond_get_max_sim(cid, txt, uids)
230
+ await self.actual_sim(cid, old_cid, sim)
231
+ # хз бля чо это ваще
232
+ # for ad_db in await models.Ad.filter(direction__pairex__ex=self.ex).prefetch_related("cond", "maker"):
233
+ # ad = Ad(id=str(ad_db.exid), userId=str(ad_db.maker.exid), remark=ad_db.cond.raw_txt)
234
+ # await self.cond_upsert(ad, force=True)
235
+
236
+ async def actual_sim(self, cid: int, old_cid: int, sim: int):
237
+ if not sim:
238
+ return
239
+ if old_sim := await models.CondSim.get_or_none(cond_id=cid):
240
+ if old_sim.cond_rel_id != old_cid:
241
+ if sim > old_sim.similarity:
242
+ logging.warning(f"R {cid}: {old_sim.similarity}->{sim} ({old_sim.cond_rel_id}->{old_cid})")
243
+ await old_sim.update_from_dict({"similarity": sim, "old_rel_id": old_cid}).save()
244
+ self._cond_sim_upd(cid, old_cid)
245
+ elif sim != old_sim.similarity:
246
+ logging.info(f"{cid}: {old_sim.similarity}->{sim}")
247
+ await old_sim.update_from_dict({"similarity": sim}).save()
248
+ else:
249
+ await models.CondSim.create(cond_id=cid, cond_rel_id=old_cid, similarity=sim)
250
+ self._cond_sim_upd(cid, old_cid)
251
+
252
+ def _cond_sim_upd(self, cid: int, old_cid: int):
253
+ if old_old_cid := self.cond_sims.get(cid): # если старый cid уже был в дереве:
254
+ self.rcond_sims[old_old_cid].remove(cid) # удаляем из обратного
255
+ self.cond_sims[cid] = old_cid # а в прямом он автоматом переопределится, даже если и был
256
+ self.rcond_sims[old_cid] |= {cid} # ну и в обратное добавим новый
257
+
258
+ def build_tree(self):
259
+ set(self.cond_sims.keys()) | set(self.cond_sims.values())
260
+ tree = defaultdict(dict)
261
+ # Группируем родителей по детям
262
+ for child, par in self.cond_sims.items():
263
+ tree[par] |= {child: {}} # todo: make from self.rcond_sim
264
+
265
+ # Строим дерево снизу вверх
266
+ def subtree(node):
267
+ if not node:
268
+ return node
269
+ for key in node:
270
+ subnode = tree.pop(key, {})
271
+ d = subtree(subnode)
272
+ node[key] |= d # actual tree rebuilding here!
273
+ return node # todo: refact?
274
+
275
+ # Находим корни / без родителей
276
+ roots = set(self.cond_sims.values()) - set(self.cond_sims.keys())
277
+ for root in roots:
278
+ _ = subtree(tree[root])
279
+
280
+ self.tree = tree
281
+
282
+
283
+ def get_sim(s1, s2) -> int:
284
+ sim = int((SequenceMatcher(None, s1, s2).ratio() - 0.6) * 10_000)
285
+ return sim if sim > 0 else 0
286
+
287
+
288
+ def clean(s) -> str:
289
+ clear = r"[^\w\s.,!?;:()\-]"
290
+ repeat = r"(.)\1{2,}"
291
+ s = re.sub(clear, "", s).lower()
292
+ s = re.sub(repeat, r"\1", s)
293
+ return s.replace("\n\n", "\n").replace(" ", " ").strip(" \n/.,!?-")
xync_client/Abc/Agent.py CHANGED
@@ -2,8 +2,10 @@ from abc import abstractmethod
2
2
 
3
3
  from pydantic import BaseModel
4
4
  from pyro_client.client.file import FileClient
5
+ from x_client import df_hdrs
5
6
  from x_client.aiohttp import Client as HttpClient
6
7
  from xync_bot import XyncBot
8
+ from xync_client.Bybit.etype.order import TakeAdReq
7
9
  from xync_schema import models
8
10
  from xync_schema.models import OrderStatus, Coin, Cur, Ad, AdStatus, Actor, Agent
9
11
  from xync_schema.xtype import BaseAd
@@ -22,7 +24,7 @@ class BaseAgentClient(HttpClient):
22
24
  agent: Agent,
23
25
  fbot: FileClient,
24
26
  bbot: XyncBot,
25
- headers: dict[str, str] = None,
27
+ headers: dict[str, str] = df_hdrs,
26
28
  cookies: dict[str, str] = None,
27
29
  ):
28
30
  self.bbot = bbot
@@ -131,6 +133,23 @@ class BaseAgentClient(HttpClient):
131
133
  @abstractmethod
132
134
  async def my_assets(self) -> dict: ...
133
135
 
136
+ @abstractmethod
137
+ async def _take_ad(self, req: TakeAdReq): ...
138
+
139
+ async def take_ad(self, req: TakeAdReq):
140
+ if req.is_sell:
141
+ fltr = dict(ex_id=self.actor.ex_id, cred__pmcur__pm_id=req.pm_id, cred__person_id=self.actor.person.id)
142
+ if req.cur_:
143
+ fltr |= dict(cred__pmcur__cur__ticker=req.cur_)
144
+ pmexs = await models.CredEx.filter(**fltr)
145
+ else:
146
+ pmexs = await models.PmEx.filter(ex_id=self.actor.ex_id, pm_id=req.pm_id)
147
+ if len(pmexs) > 1:
148
+ pmexs = [p for p in pmexs if p.name.endswith(f" ({req.cur_})")]
149
+ req.pm_id = pmexs[0].exid
150
+ req.quantity = round(req.amount / req.price, 4) # todo: to get the scale from coinEx
151
+ return await self._take_ad(req)
152
+
134
153
  # Сохранение объявления (с Pm/Cred-ами) в бд
135
154
  # async def ad_pydin2db(self, ad_pydin: AdSaleIn | AdBuyIn) -> Ad:
136
155
  # ad_db = await self.ex_client.ad_pydin2db(ad_pydin)
xync_client/Abc/Ex.py CHANGED
@@ -11,11 +11,12 @@ 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.AdLoader import AdLoader
14
15
  from xync_client.Abc.xtype import PmEx, MapOfIdsList
15
16
  from xync_client.pm_unifier import PmUnifier, PmUni
16
17
 
17
18
 
18
- class BaseExClient(HttpClient):
19
+ class BaseExClient(HttpClient, AdLoader):
19
20
  cur_map: dict[int, str] = {}
20
21
  unifier_class: type = PmUnifier
21
22
  logo_pre_url: str
@@ -72,6 +73,7 @@ class BaseExClient(HttpClient):
72
73
  pm_exids: list[str | int] = None,
73
74
  amount: int = None,
74
75
  lim: int = None,
76
+ vm_filter: bool = False,
75
77
  ) -> list[BaseAd]: # {ad.id: ad}
76
78
  ...
77
79
 
@@ -250,14 +252,14 @@ class BaseExClient(HttpClient):
250
252
  return True
251
253
 
252
254
  # Сохранение чужого объявления (с Pm-ами) в бд
253
- async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
254
- dct = ad_pydin.model_dump()
255
- dct["exid"] = dct.pop("id")
256
- ad_in = models.Ad.validate(dct)
257
- ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
258
- await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
259
- await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
260
- return ad_db
255
+ # async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
256
+ # dct = ad_pydin.model_dump()
257
+ # dct["exid"] = dct.pop("id")
258
+ # ad_in = models.Ad.validate(dct)
259
+ # ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
260
+ # await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
261
+ # await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
262
+ # return ad_db
261
263
 
262
264
  async def file_upsert(self, url: str, ss: ClientSession = None) -> models.File:
263
265
  if not (file := await models.File.get_or_none(name__startswith=url.split("?")[0])):