xync-client 0.0.155__py3-none-any.whl → 0.0.162__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.
xync_client/Abc/Ex.py CHANGED
@@ -1,12 +1,18 @@
1
1
  import logging
2
+ import re
2
3
  from abc import abstractmethod
3
4
  from asyncio import sleep
5
+ from collections import defaultdict
6
+ from difflib import SequenceMatcher
4
7
 
5
8
  from aiohttp import ClientSession, ClientResponse
9
+ from google.protobuf.internal.wire_format import INT32_MAX
6
10
  from msgspec import Struct
11
+ from pydantic import BaseModel
7
12
  from pyro_client.client.file import FileClient
8
- from tortoise.exceptions import MultipleObjectsReturned, IntegrityError
13
+ from tortoise.exceptions import MultipleObjectsReturned, IntegrityError, OperationalError
9
14
  from x_client.aiohttp import Client as HttpClient
15
+ from xync_client.Bybit.etype.ad import AdsReq
10
16
  from xync_schema import models
11
17
  from xync_schema.enums import FileType
12
18
  from xync_schema.xtype import CurEx, CoinEx, BaseAd, BaseAdIn
@@ -24,6 +30,19 @@ class BaseExClient(HttpClient, AdLoader):
24
30
  bot: FileClient
25
31
  ex: models.Ex
26
32
 
33
+ coin_x2e: dict[int, tuple[str, int]] = {}
34
+ coin_e2x: dict[str, int] = {}
35
+ cur_x2e: dict[int, tuple[str, int, int]] = {}
36
+ cur_e2x: dict[str, int] = {}
37
+ pm_x2e: dict[int, str] = {}
38
+ pm_e2x: dict[str, int] = {}
39
+ pairs_e2x: dict[int, dict[int, tuple[int, int]]] = defaultdict(defaultdict)
40
+ pairs_x2e: dict[int, tuple[int, int, int]] = defaultdict(defaultdict)
41
+ actor_x2e: dict[int, int] = {}
42
+ actor_e2x: dict[int, int] = {}
43
+ ad_x2e: dict[int, int] = {}
44
+ ad_e2x: dict[int, int] = {}
45
+
27
46
  def __init__(
28
47
  self,
29
48
  ex: models.Ex,
@@ -64,21 +83,115 @@ class BaseExClient(HttpClient, AdLoader):
64
83
  @abstractmethod
65
84
  async def pairs(self) -> tuple[MapOfIdsList, MapOfIdsList]: ...
66
85
 
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
86
+ # converters
87
+ async def x2e_coin(self, coin_id: int) -> tuple[str, int]: # coinex.exid
88
+ if not self.coin_x2e.get(coin_id):
89
+ exid, scale = await models.CoinEx.get(coin_id=coin_id, ex=self.ex).values_list("exid", "scale")
90
+ self.coin_x2e[coin_id] = exid, scale
91
+ self.coin_e2x[exid] = coin_id
92
+ return self.coin_x2e[coin_id]
93
+
94
+ async def e2x_coin(self, exid: str) -> int: # coin.id
95
+ if not self.coin_e2x.get(exid):
96
+ coin_id, scale = await models.CoinEx.get(exid=exid, ex=self.ex).values_list("coin_id", "scale")
97
+ self.coin_x2e[coin_id] = exid, scale
98
+ self.coin_e2x[exid] = coin_id
99
+ return self.coin_e2x[exid]
100
+
101
+ async def x2e_cur(self, cur_id: int) -> tuple[str, int, int]: # curex.exid
102
+ if not self.cur_x2e.get(cur_id):
103
+ exid, scale, mnm = await models.CurEx.get(cur_id=cur_id, ex=self.ex).values_list("exid", "scale", "minimum")
104
+ self.cur_x2e[cur_id] = exid, scale, mnm
105
+ self.cur_e2x[exid] = cur_id
106
+ return self.cur_x2e[cur_id]
107
+
108
+ async def e2x_cur(self, exid: str) -> int: # cur.id
109
+ if not self.cur_e2x.get(exid):
110
+ cur_id, scale, mnm = await models.CurEx.get(exid=exid, ex=self.ex).values_list("cur_id", "scale", "minimum")
111
+ self.cur_e2x[exid] = cur_id
112
+ self.cur_x2e[cur_id] = exid, scale, mnm
113
+ return self.cur_e2x[exid]
114
+
115
+ async def x2e_pm(self, pm_id: int) -> str: # pmex.exid
116
+ if not self.pm_x2e.get(pm_id):
117
+ self.pm_x2e[pm_id] = (await models.PmEx.get(pm_id=pm_id, ex=self.ex)).exid
118
+ self.pm_e2x[self.pm_x2e[pm_id]] = pm_id
119
+ return self.pm_x2e[pm_id]
120
+
121
+ async def e2x_pm(self, exid: str) -> int: # pm.id
122
+ if not self.pm_e2x.get(exid):
123
+ self.pm_e2x[exid] = (await models.PmEx.get(exid=exid, ex=self.ex)).pm_id
124
+ self.pm_x2e[self.pm_e2x[exid]] = exid
125
+ return self.pm_e2x[exid]
126
+
127
+ # pair_side
128
+ async def ccs2pair(self, coin_id: int, cur_id: int, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
129
+ if not self.pairs_e2x.get(coin_id, {}).get(cur_id):
130
+ self.pairs_e2x[coin_id][cur_id] = (
131
+ await models.PairSide.filter(pair__coin_id=coin_id, pair__cur_id=coin_id)
132
+ .order_by("is_sell")
133
+ .values_list("id", flat=True)
134
+ )
135
+ return self.pairs_e2x[coin_id][cur_id][int(is_sell)]
136
+
137
+ async def pair2ccs(self, xid: int) -> tuple[int, int, int]: # coinex.exid
138
+ if not self.pairs_x2e.get(xid):
139
+ ps = await models.PairSide.get(id=xid).prefetch_related("pair")
140
+ self.pairs_x2e[xid] = ps.pair.coin_id, ps.pair.cur_id, ps.is_sell
141
+ return self.pairs_x2e[xid]
142
+
143
+ async def e2x_pair(self, coin_exid: str, cur_exid: str, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
144
+ coin_id = await self.e2x_coin(coin_exid)
145
+ cur_id = await self.e2x_cur(cur_exid)
146
+ return await self.ccs2pair(coin_id, cur_id, is_sell)
147
+
148
+ async def x2e_pair(self, pair_side_id: int) -> tuple[str, str, int]: # coinex.exid
149
+ coin_id, cur_id, is_sell = await self.pair2ccs(pair_side_id)
150
+ coin_exid = await self.x2e_coin(coin_id)
151
+ cur_exid = await self.x2e_cur(cur_id)
152
+ return coin_exid[0], cur_exid[0], is_sell
153
+
154
+ async def x2e_actor(self, actor_id: int) -> int: # actor.exid
155
+ if not self.actor_x2e.get(actor_id):
156
+ self.actor_x2e[actor_id] = (await models.Actor[actor_id]).exid
157
+ self.actor_e2x[self.actor_x2e[actor_id]] = actor_id
158
+ return self.actor_x2e[actor_id]
159
+
160
+ async def e2x_actor(self, exid: int) -> int: # actor.id
161
+ if not self.actor_e2x.get(exid):
162
+ self.actor_e2x[exid] = (await models.Actor.get(exid=exid, ex=self.ex)).id
163
+ self.actor_x2e[self.actor_e2x[exid]] = exid
164
+ return self.actor_e2x[exid]
165
+
166
+ async def x2e_ad(self, ad_id: int) -> int: # ad.exid
167
+ if not self.ad_x2e.get(ad_id):
168
+ self.ad_x2e[ad_id] = (await models.Ad[ad_id]).exid
169
+ self.ad_e2x[self.ad_x2e[ad_id]] = ad_id
170
+ return self.ad_x2e[ad_id]
171
+
172
+ async def e2x_ad(self, exid: int) -> int: # ad.id
173
+ if not self.ad_e2x.get(exid):
174
+ self.ad_e2x[exid] = (await models.Ad.get(exid=exid, maker__ex=self.ex)).id
175
+ self.ad_x2e[self.ad_e2x[exid]] = exid
176
+ return self.ad_e2x[exid]
72
177
 
73
178
  # 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)
179
+ async def ads(self, xreq: GetAds, **kwargs) -> list[BaseAd]:
180
+ self.ex.etype().ad.AdsReq
181
+ ereq = AdsReq(
182
+ coin_id=(await self.x2e_coin(xreq.coin_id))[0],
183
+ cur_id=(await self.x2e_cur(xreq.cur_id))[0],
184
+ is_sell=str(int(xreq.is_sell)),
185
+ pm_ids=[await self.x2e_pm(pid) for pid in xreq.pm_ids],
186
+ # size=str(xreq.limit),
187
+ # page=str(xreq.page),
188
+ )
189
+ if xreq.amount:
190
+ ereq.amount = str(xreq.amount)
191
+ return await self._ads(ereq, **kwargs)
76
192
 
77
193
  @abstractmethod
78
- async def _ads(
79
- self, req: GetAds, lim: int = None, vm_filter: bool = False, **kwargs
80
- ) -> list[BaseAd]: # {ad.id: ad}
81
- ...
194
+ async def _ads(self, ereq: BaseModel, **kwargs) -> list[BaseAd]: ...
82
195
 
83
196
  # 42: Чужая объява по id
84
197
  @abstractmethod
@@ -285,3 +398,299 @@ class BaseExClient(HttpClient, AdLoader):
285
398
  )
286
399
  return await self.METHS[resp.method](self, resp.url.path, bp)
287
400
  return await super()._proc(resp, bp)
401
+
402
+ # ad cond loader
403
+ all_conds: dict[int, tuple[str, set[int]]] = {}
404
+ cond_sims: dict[int, int] = defaultdict(set)
405
+ rcond_sims: dict[int, set[int]] = defaultdict(set) # backward
406
+ tree: dict = {}
407
+
408
+ async def old_conds_load(self):
409
+ # пока не порешали рейс-кондишн, очищаем сиротские условия при каждом запуске
410
+ # [await c.delete() for c in await Cond.filter(ads__isnull=True)]
411
+ self.all_conds = {
412
+ c.id: (c.raw_txt, {a.maker.exid for a in c.ads})
413
+ for c in await models.Cond.all().prefetch_related("ads__maker")
414
+ }
415
+ for curr, old in await models.CondSim.filter().values_list("cond_id", "cond_rel_id"):
416
+ self.cond_sims[curr] = old
417
+ self.rcond_sims[old] |= {curr}
418
+
419
+ self.build_tree()
420
+ a = set()
421
+
422
+ def check_tree(tre):
423
+ for p, c in tre.items():
424
+ a.add(p)
425
+ check_tree(c)
426
+
427
+ for pr, ch in self.tree.items():
428
+ check_tree(ch)
429
+ if ct := set(self.tree.keys()) & a:
430
+ logging.exception(f"cycle cids: {ct}")
431
+
432
+ async def person_name_update(self, name: str, exid: int) -> models.Person:
433
+ if actor := await models.Actor.get_or_none(exid=exid, ex=self.ex).prefetch_related("person"):
434
+ # мб уже есть персона с этим же name, note, tg_id
435
+ if person := await models.Person.get_or_none(
436
+ name=name, note=actor.person.note, tg_id=actor.person.tg_id, id__not=actor.person_id
437
+ ):
438
+ actor.person = person
439
+ else:
440
+ actor.person.name = name
441
+ await actor.person.save()
442
+ return actor.person
443
+ # tmp dirty fix
444
+ note = f"{self.ex.id}:{exid}"
445
+ if person := await models.Person.get_or_none(note__startswith=note, name__not=name):
446
+ person.name = name
447
+ await person.save()
448
+ return person
449
+ try:
450
+ return (await models.Person.update_or_create(name=name, note=note))[0]
451
+ except OperationalError as e:
452
+ raise e
453
+ await models.Actor.create(person=person, exid=exid, ex=self.ex)
454
+ return person
455
+ # person = await models.Person.create(note=f'{actor.ex_id}:{actor.exid}:{name}') # no person for just ads with no orders
456
+ # raise ValueError(f"Agent #{exid} not found")
457
+
458
+ async def ad_load(
459
+ self,
460
+ pad: BaseAd,
461
+ cid: int = None,
462
+ ps: models.PairSide = None,
463
+ maker: models.Actor = None,
464
+ coinex: models.CoinEx = None,
465
+ curex: models.CurEx = None,
466
+ rname: str = None,
467
+ ) -> models.Ad:
468
+ # self.e2x_actor()
469
+ if not maker:
470
+ if not (maker := await models.Actor.get_or_none(exid=pad.userId, ex=self.ex)):
471
+ person = await models.Person.create(name=rname, note=f"{self.ex.id}:{pad.userId}:{pad.nickName}")
472
+ maker = await models.Actor.create(name=pad.nickName, person=person, exid=pad.userId, ex=self.ex)
473
+ if rname:
474
+ await self.person_name_update(rname, int(pad.userId))
475
+ ps = ps or await models.PairSide.get_or_none(
476
+ is_sell=pad.side,
477
+ pair__coin__ticker=pad.tokenId,
478
+ pair__cur__ticker=pad.currencyId,
479
+ ).prefetch_related("pair")
480
+ # if not ps or not ps.pair:
481
+ # ... # THB/USDC: just for initial filling
482
+ ad_upd = models.Ad.validate(pad.model_dump(by_alias=True))
483
+ cur_scale = 10 ** (curex or await models.CurEx.get(cur_id=ps.pair.cur_id, ex=self.ex)).scale
484
+ coin_scale = 10 ** (coinex or await models.CoinEx.get(coin_id=ps.pair.coin_id, ex=self.ex)).scale
485
+ amt = int(float(pad.quantity) * float(pad.price) * cur_scale)
486
+ mxf = pad.maxAmount and int(float(pad.maxAmount) * cur_scale)
487
+ df_unq = ad_upd.df_unq(
488
+ maker_id=maker.id,
489
+ pair_side_id=ps.id,
490
+ amount=min(amt, INT32_MAX),
491
+ quantity=int(float(pad.quantity) * coin_scale),
492
+ min_fiat=int(float(pad.minAmount) * cur_scale),
493
+ max_fiat=min(mxf, INT32_MAX),
494
+ price=int(float(pad.price) * cur_scale),
495
+ premium=int(float(pad.premium) * 100),
496
+ cond_id=cid,
497
+ status=self.ad_status(ad_upd.status),
498
+ )
499
+ try:
500
+ ad_db, _ = await models.Ad.update_or_create(**df_unq)
501
+ except OperationalError as e:
502
+ raise e
503
+ await ad_db.pms.clear()
504
+ await ad_db.pms.add(*(await models.Pm.filter(pmexs__ex=self.ex, pmexs__exid__in=pad.payments)))
505
+ return ad_db
506
+
507
+ async def cond_load( # todo: refact from Bybit Ad format to universal
508
+ self,
509
+ ad: BaseAd,
510
+ ps: models.PairSide = None,
511
+ force: bool = False,
512
+ rname: str = None,
513
+ coinex: models.CoinEx = None,
514
+ curex: models.CurEx = None,
515
+ pms_from_cond: bool = False,
516
+ ) -> tuple[models.Ad, bool]:
517
+ _sim, cid = None, None
518
+ ad_db = await models.Ad.get_or_none(exid=ad.id, maker__ex=self.ex).prefetch_related("cond")
519
+ # если точно такое условие уже есть в бд
520
+ if not (cleaned := clean(ad.remark)) or (cid := {oc[0]: ci for ci, oc in self.all_conds.items()}.get(cleaned)):
521
+ # и объява с таким ид уже есть, но у нее другое условие
522
+ if ad_db and ad_db.cond_id != cid:
523
+ # то обновляем ид ее условия
524
+ ad_db.cond_id = cid
525
+ await ad_db.save()
526
+ logging.info(f"{ad.nickName} upd cond#{ad_db.cond_id}->{cid}")
527
+ # old_cid = ad_db.cond_id # todo: solve race-condition, а пока что очищаем при каждом запуске
528
+ # if not len((old_cond := await Cond.get(id=old_cid).prefetch_related('ads')).ads):
529
+ # await old_cond.delete()
530
+ # logging.warning(f"Cond#{old_cid} deleted!")
531
+ return (ad_db or force and await self.ad_load(ad, cid, ps, coinex=coinex, curex=curex, rname=rname)), False
532
+ # если эта объява в таким ид уже есть в бд, но с другим условием (или без), а текущего условия еще нет в бд
533
+ if ad_db:
534
+ await ad_db.fetch_related("cond__ads", "maker")
535
+ if not ad_db.cond_id or (
536
+ # у измененного условия этой объявы есть другие объявы?
537
+ (rest_ads := set(ad_db.cond.ads) - {ad_db})
538
+ and
539
+ # другие объявы этого условия принадлежат другим юзерам
540
+ {ra.maker_id for ra in rest_ads} - {ad_db.maker_id}
541
+ ):
542
+ # создадим новое условие и присвоим его только текущей объяве
543
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
544
+ ad_db.cond_id = cond.id
545
+ await ad_db.save()
546
+ ad_db.cond = cond
547
+ return ad_db, True
548
+ # а если других объяв со старым условием этой обявы нет, либо они все этого же юзера
549
+ # обновляем условие (в тч во всех ЕГО объявах)
550
+ ad_db.cond.last_ver = ad_db.cond.raw_txt
551
+ ad_db.cond.raw_txt = cleaned
552
+ try:
553
+ await ad_db.cond.save()
554
+ except IntegrityError as e:
555
+ raise e
556
+ await self.cond_upd(ad_db.cond, {ad_db.maker.exid})
557
+ # и подправим коэфициенты похожести нового текста
558
+ await self.fix_rel_sims(ad_db.cond_id, cleaned)
559
+ return ad_db, False
560
+
561
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
562
+ ad_db = await self.ad_load(ad, cond.id, ps, coinex=coinex, curex=curex, rname=rname)
563
+ ad_db.cond = cond
564
+ return ad_db, True
565
+
566
+ async def cond_new(self, txt: str, uids: set[int]) -> models.Cond:
567
+ new_cond, _ = await models.Cond.update_or_create(raw_txt=txt)
568
+ # и максимально похожую связь для нового условия (если есть >= 60%)
569
+ await self.cond_upd(new_cond, uids)
570
+ return new_cond
571
+
572
+ async def cond_upd(self, cond: models.Cond, uids: set[int]):
573
+ self.all_conds[cond.id] = cond.raw_txt, uids
574
+ # и максимально похожую связь для нового условия (если есть >= 60%)
575
+ old_cid, sim = await self.cond_get_max_sim(cond.id, cond.raw_txt, uids)
576
+ await self.actual_sim(cond.id, old_cid, sim)
577
+
578
+ def find_in_tree(self, cid: int, old_cid: int) -> bool:
579
+ if p := self.cond_sims.get(old_cid):
580
+ if p == cid:
581
+ return True
582
+ return self.find_in_tree(cid, p)
583
+ return False
584
+
585
+ async def cond_get_max_sim(self, cid: int, txt: str, uids: set[int]) -> tuple[int | None, int | None]:
586
+ # находим все старые тексты похожие на 90% и более
587
+ if len(txt) < 15:
588
+ return None, None
589
+ sims: dict[int, int] = {}
590
+ for old_cid, (old_txt, old_uids) in self.all_conds.items():
591
+ if len(old_txt) < 15 or uids == old_uids:
592
+ continue
593
+ elif not self.can_add_sim(cid, old_cid):
594
+ continue
595
+ if sim := get_sim(txt, old_txt):
596
+ sims[old_cid] = sim
597
+ # если есть, берем самый похожий из них
598
+ if sims:
599
+ old_cid, sim = max(sims.items(), key=lambda x: x[1])
600
+ await sleep(0.3)
601
+ return old_cid, sim
602
+ return None, None
603
+
604
+ def can_add_sim(self, cid: int, old_cid: int) -> bool:
605
+ if cid == old_cid:
606
+ return False
607
+ elif self.cond_sims.get(cid) == old_cid:
608
+ return False
609
+ elif self.find_in_tree(cid, old_cid):
610
+ return False
611
+ elif self.cond_sims.get(old_cid) == cid:
612
+ return False
613
+ elif cid in self.rcond_sims.get(old_cid, {}):
614
+ return False
615
+ elif old_cid in self.rcond_sims.get(cid, {}):
616
+ return False
617
+ return True
618
+
619
+ async def fix_rel_sims(self, cid: int, new_txt: str):
620
+ for rel_sim in await models.CondSim.filter(cond_rel_id=cid).prefetch_related("cond"):
621
+ if sim := get_sim(new_txt, rel_sim.cond.raw_txt):
622
+ rel_sim.similarity = sim
623
+ await rel_sim.save()
624
+ else:
625
+ await rel_sim.delete()
626
+
627
+ async def actual_cond(self):
628
+ for curr, old in await models.CondSim.all().values_list("cond_id", "cond_rel_id"):
629
+ self.cond_sims[curr] = old
630
+ self.rcond_sims[old] |= {curr}
631
+ for cid, (txt, uids) in self.all_conds.items():
632
+ old_cid, sim = await self.cond_get_max_sim(cid, txt, uids)
633
+ await self.actual_sim(cid, old_cid, sim)
634
+ # хз бля чо это ваще
635
+ # for ad_db in await models.Ad.filter(direction__pairex__ex=self.ex).prefetch_related("cond", "maker"):
636
+ # ad = Ad(id=str(ad_db.exid), userId=str(ad_db.maker.exid), remark=ad_db.cond.raw_txt)
637
+ # await self.cond_upsert(ad, force=True)
638
+
639
+ async def actual_sim(self, cid: int, old_cid: int, sim: int):
640
+ if not sim:
641
+ return
642
+ if old_sim := await models.CondSim.get_or_none(cond_id=cid):
643
+ if old_sim.cond_rel_id != old_cid:
644
+ if sim > old_sim.similarity:
645
+ logging.warning(f"R {cid}: {old_sim.similarity}->{sim} ({old_sim.cond_rel_id}->{old_cid})")
646
+ await old_sim.update_from_dict({"similarity": sim, "old_rel_id": old_cid}).save()
647
+ self._cond_sim_upd(cid, old_cid)
648
+ elif sim != old_sim.similarity:
649
+ logging.info(f"{cid}: {old_sim.similarity}->{sim}")
650
+ await old_sim.update_from_dict({"similarity": sim}).save()
651
+ else:
652
+ await models.CondSim.create(cond_id=cid, cond_rel_id=old_cid, similarity=sim)
653
+ self._cond_sim_upd(cid, old_cid)
654
+
655
+ def _cond_sim_upd(self, cid: int, old_cid: int):
656
+ if old_old_cid := self.cond_sims.get(cid): # если старый cid уже был в дереве:
657
+ self.rcond_sims[old_old_cid].remove(cid) # удаляем из обратного
658
+ self.cond_sims[cid] = old_cid # а в прямом он автоматом переопределится, даже если и был
659
+ self.rcond_sims[old_cid] |= {cid} # ну и в обратное добавим новый
660
+
661
+ def build_tree(self):
662
+ set(self.cond_sims.keys()) | set(self.cond_sims.values())
663
+ tree = defaultdict(dict)
664
+ # Группируем родителей по детям
665
+ for child, par in self.cond_sims.items():
666
+ tree[par] |= {child: {}} # todo: make from self.rcond_sim
667
+
668
+ # Строим дерево снизу вверх
669
+ def subtree(node):
670
+ if not node:
671
+ return node
672
+ for key in node:
673
+ subnode = tree.pop(key, {})
674
+ d = subtree(subnode)
675
+ node[key] |= d # actual tree rebuilding here!
676
+ return node # todo: refact?
677
+
678
+ # Находим корни / без родителей
679
+ roots = set(self.cond_sims.values()) - set(self.cond_sims.keys())
680
+ for root in roots:
681
+ _ = subtree(tree[root])
682
+
683
+ self.tree = tree
684
+
685
+
686
+ def get_sim(s1, s2) -> int:
687
+ sim = SequenceMatcher(None, s1, s2).ratio() - 0.6
688
+ return int(sim * 10_000) if sim > 0 else 0
689
+
690
+
691
+ def clean(s) -> str:
692
+ clear = r"[^\w\s.,!?;:()\-]"
693
+ repeat = r"(.)\1{2,}"
694
+ s = re.sub(clear, "", s).lower()
695
+ s = re.sub(repeat, r"\1", s)
696
+ return s.replace("\n\n", "\n").replace(" ", " ").strip(" \n/.,!?-")
xync_client/Abc/Order.py CHANGED
@@ -1,27 +1,20 @@
1
1
  from abc import abstractmethod
2
2
 
3
- from asynchuobi.enums import OrderType
4
- from x_client.aiohttp import Client as HttpClient
5
- from xync_schema.models import Order, Actor
3
+ from xync_schema.models import Order
6
4
 
7
5
  from xync_client.Bybit.agent import AgentClient
8
6
 
9
7
 
10
- class BaseOrderClient(HttpClient):
8
+ class BaseOrderClient:
11
9
  order: Order
12
10
  im_maker: bool
13
11
  im_seller: bool
14
12
 
15
- @abstractmethod
16
- @property
17
- def type_map(self) -> dict[OrderType, str]: ...
18
-
19
- def __init__(self, actor: Actor, order: Order):
13
+ def __init__(self, order: Order, agent_client: AgentClient):
20
14
  self.order = order
21
- self.im_maker = order.taker_id != actor.id # or order.ad.agent_id == agent.id
22
- self.im_seller = order.ad.direction.sell and self.im_maker
23
- super().__init__(actor.ex.host_p2p)
24
- self.agent_client = AgentClient(actor)
15
+ self.im_maker = order.taker_id != agent_client.actor.id # or order.ad.agent_id == agent.id
16
+ self.im_seller = order.ad.pair_side.is_sell and self.im_maker
17
+ self.agent_client = agent_client
25
18
 
26
19
  # 2: [T] Отмена запроса на сделку
27
20
  @abstractmethod
@@ -37,7 +30,7 @@ class BaseOrderClient(HttpClient):
37
30
 
38
31
  # 5: [B] Перевод сделки в состояние "оплачено", c отправкой чека
39
32
  @abstractmethod
40
- async def mark_payed(self, receipt): ...
33
+ async def mark_payed(self, receipt: bytes = None): ...
41
34
 
42
35
  # 6: [B] Отмена сделки
43
36
  @abstractmethod
xync_client/Abc/xtype.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import Literal
1
+ from typing import Literal, ClassVar
2
2
 
3
- from pydantic import BaseModel, model_validator
3
+ from pydantic import BaseModel, model_validator, model_serializer
4
4
  from x_model.types import BaseUpd
5
5
  from xync_schema.enums import PmType
6
6
  from xync_schema.models import Country, Pm, Ex, CredEx
@@ -14,6 +14,28 @@ FlatDict = dict[int | str, str]
14
14
  MapOfIdsList = dict[int | str, list[int | str]]
15
15
 
16
16
 
17
+ class RemapBase(BaseModel):
18
+ # Переопределяешь это в наследнике:
19
+ _remap: ClassVar[dict[str, dict]] = {}
20
+
21
+ @model_validator(mode="before")
22
+ def _map_in(cls, data):
23
+ data = dict(data)
24
+ for field, mapping in cls._remap.items():
25
+ if field in data:
26
+ data[field] = mapping.get(data[field], data[field])
27
+ return data
28
+
29
+ @model_serializer
30
+ def _map_out(self):
31
+ data = self.__dict__.copy()
32
+ for field, mapping in self._remap.items():
33
+ reverse = {v: k for k, v in mapping.items()}
34
+ if field in data:
35
+ data[field] = reverse.get(data[field], data[field])
36
+ return data
37
+
38
+
17
39
  class PmTrait:
18
40
  typ: PmType | None = None
19
41
  logo: str | None = None
@@ -114,17 +136,27 @@ class GetAds(BaseModel):
114
136
  coin_id: int | str
115
137
  cur_id: int | str
116
138
  is_sell: bool
117
- pm_ids: list[int | str] | None = None
139
+ pm_ids: list[int | str] = []
118
140
  amount: int | None = None
141
+ vm_only: bool = False
142
+ limit: int = 20
143
+ page: int = 1
144
+ # todo: add?
145
+ # canTrade: bool = False
146
+ # userId: str = "" # int
147
+ # verificationFilter
148
+ kwargs: dict = {}
119
149
 
120
150
 
121
151
  class AdUpd(BaseAdUpdate, GetAds):
122
152
  price: float
123
153
  pm_ids: list[int | str]
124
154
  amount: float
155
+ max_amount: float | None = None
125
156
  premium: float | None = None
126
157
  credexs: list[CredEx] | None = None
127
158
  quantity: float | None = None
159
+ cond: str | None = None
128
160
 
129
161
  class Config:
130
162
  arbitrary_types_allowed = True