xync-db 0.5.5.dev9__tar.gz → 0.5.5.dev11__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 (41) hide show
  1. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/PKG-INFO +1 -1
  2. xync_db-0.5.5.dev11/src/xync_db/migrations/models/0012_country_ccn3.py +42 -0
  3. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/models/CLAUDE.md +7 -0
  4. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/models/__init__.py +27 -2
  5. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/.gitignore +0 -0
  6. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/README.md +0 -0
  7. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/pyproject.toml +0 -0
  8. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/__init__.py +0 -0
  9. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/config.py +0 -0
  10. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/enums.py +0 -0
  11. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/exceptions.py +0 -0
  12. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/graph.py +0 -0
  13. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/i18n.py +0 -0
  14. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/login_store.py +0 -0
  15. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/logo.png +0 -0
  16. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/CLAUDE.md +0 -0
  17. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/__init__.py +0 -0
  18. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/0001_initial.py +0 -0
  19. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/0002_triggers.py +0 -0
  20. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/0003_seed.py +0 -0
  21. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/__init__.py +0 -0
  22. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/__init__.py +0 -0
  23. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
  24. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/a_bin_seed.py +0 -0
  25. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/a_country_seed.py +0 -0
  26. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
  27. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
  28. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/c_net_seed.py +0 -0
  29. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/countries.json +0 -0
  30. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/ranges.csv +0 -0
  31. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
  32. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/py.typed +0 -0
  33. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/shared.py +0 -0
  34. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/__init__.py +0 -0
  35. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/__init__.py +0 -0
  36. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/actor.py +0 -0
  37. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/ad.py +0 -0
  38. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/common.py +0 -0
  39. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/cred.py +0 -0
  40. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/order.py +0 -0
  41. {xync_db-0.5.5.dev9 → xync_db-0.5.5.dev11}/src/xync_db/typs/db/pm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xync-db
3
- Version: 0.5.5.dev9
3
+ Version: 0.5.5.dev11
4
4
  Summary: Xync DB schema: Tortoise ORM models, enums, pydantic DTOs.
5
5
  Author-email: Mike Artemiev <mixartemev@gmail.com>
6
6
  License: LicenseRef-EULA
@@ -0,0 +1,42 @@
1
+ """`country.code` → `country.ccn3`: колонка была телефонным кодом (нигде в логике не читалась),
2
+ становится числовым ISO 3166-1. Значения переустанавливает канонический сидер стран
3
+ (`a_country_seed.country_seed`), который заодно доводит уже развёрнутую БД до полного набора
4
+ mledoze (250 стран + 2 meta) с реальными валютами вместо USD-заглушек.
5
+
6
+ Fresh-миграции получают колонку `ccn3` сразу из `0001_initial`, поэтому здесь — транзитное
7
+ переименование ТОЛЬКО для БД, созданных до этого изменения (guarded: срабатывает лишь пока в
8
+ таблице ещё есть колонка `code`; на свежей схеме — no-op, без повторного ресида).
9
+ """
10
+
11
+ from typing import ClassVar
12
+
13
+ from tortoise.migrations.migration import Migration as BaseMigration
14
+ from tortoise.migrations.operations import RunPython
15
+
16
+ from xync_db.migrations.models.seed.a_country_seed import country_seed
17
+
18
+
19
+ async def country_ccn3(_apps, schema_editor) -> None:
20
+ db = schema_editor.client
21
+ has_code = await db.execute_query_dict(
22
+ "SELECT 1 FROM information_schema.columns WHERE table_name = 'country' AND column_name = 'code'"
23
+ )
24
+ if not has_code: # свежая схема — колонка уже `ccn3` (0001), переносить нечего
25
+ return
26
+ await db.execute_script('ALTER TABLE "country" RENAME COLUMN "code" TO "ccn3";')
27
+ await country_seed(db)
28
+
29
+
30
+ async def country_ccn3_down(_apps, schema_editor) -> None:
31
+ db = schema_editor.client
32
+ has_ccn3 = await db.execute_query_dict(
33
+ "SELECT 1 FROM information_schema.columns WHERE table_name = 'country' AND column_name = 'ccn3'"
34
+ )
35
+ if has_ccn3:
36
+ await db.execute_script('ALTER TABLE "country" RENAME COLUMN "ccn3" TO "code";')
37
+
38
+
39
+ class Migration(BaseMigration):
40
+ atomic = False
41
+ dependencies: ClassVar = [("models", "0011_pair_scale")]
42
+ operations: ClassVar = [RunPython(country_ccn3, country_ccn3_down)]
@@ -110,6 +110,13 @@ actor.set_user(username_id):
110
110
  ### `exclude(**kwargs)` negates each kwarg separately (NOT Django's `NOT(AND)`)
111
111
  Tortoise `QuerySet.exclude(a=1, b=2)` → `NOT(a=1) AND NOT(b=2)` = `a!=1 AND b!=2` (drops a row if EITHER holds), NOT Django's `NOT(a=1 AND b=2)`. Source: `tortoise.queryset.QuerySet._filter_or_exclude` appends `~Q(**{k:v})` per kwarg, AND-joined. To exclude the **intersection**, wrap in ONE `Q`: `exclude(Q(a=1, b=2))` → `NOT(a=1 AND b=2)`. Real bug: `User.get_hot_ads` `exclude(hex__isnull=True, ad__maker__ex_id__in=SHARE_HEX_EX_IDS)` hid all hex-less ads AND all share-hex ads separately instead of only the unshared-share-hex ones; fixed with `exclude(Q(...))` + `from tortoise.expressions import Q`.
112
112
 
113
+ ### Filtering by an m2m field multiplies rows — add `.distinct()`
114
+ `filter(credexs__cred__ovr_pm_id=0)` is a JOIN through `myad_cred`, so a `MyAd` with TWO matching
115
+ credentials comes back **twice**. Real bug: `/hot/ads?mine=true` showed every 2-payment ad doubled
116
+ (the taker path hid it — `_dedup_hot_by_slot` collapses by slot) and `BaseAgentClient.ads_share`
117
+ shared it twice. Fixed with `.distinct()` in `MyAd.hot_mads_query` (test `test_hot_mads_distinct.py`);
118
+ `unshared_ad_ids` already had it. Rule: any queryset filtered through an m2m relation needs `.distinct()`.
119
+
113
120
  ### id=0 rows are unhashable — never `set[Model]`
114
121
  The XyncPay `Pm` row has **pk id=0** (norm='xyncpay'). Tortoise's `Model.__hash__` does `if not self.pk: raise TypeError(... unhashable)` — and `not 0` is `True`, so a fully-saved id=0 row is unhashable; putting it in a `set[Model]` (literal, `set(...)`, comprehension, or a pydantic `set[Model]` field) raises at construction. Rule: never build a `set` of model instances if any row can be id=0 (Pm especially, ovr_pm_id=0). Diff/dedup m2m by `pm.id` instead (`{p.id: p for p in pms}`, `{p.id for p in ...}`) — int sets are always hashable. Bit us in `Ad.upsert`/`_sync_pms` (the pms m2m path: `AdXIn.pms` is `list[Pm]` not `set`, diffs by id). Test `test_ad_upsert_pms_sync`.
115
122
 
@@ -1421,6 +1421,27 @@ class Ad(Model, TsTrait):
1421
1421
  "price",
1422
1422
  }
1423
1423
 
1424
+ @classmethod
1425
+ def maker_hot_ads(cls, maker_id: int, cur_id: int, coin_id: int, is_sell: bool):
1426
+ """ВСЕ hot-объявы (с XyncPay-credex) мейкера в ОДНОМ направлении (пара+сторона) — ими ads_fresh
1427
+ и обновляет цены, и считает занятость слотов. Два обязательных свойства выборки:
1428
+ - `.distinct()`: credexs — m2m, объява с ДВУМЯ XyncPay-платёжками приезжает JOIN'ом дважды
1429
+ (тот же класс бага, что чинил distinct в `hot_mads_query`);
1430
+ - БЕЗ `.limit()`: срезать выборку бюджетом `same_dir_ad` нельзя — хвост направления тогда не
1431
+ обновляется (висит на бирже со старым курсом) И не попадает в занятые слоты, а ads_fresh
1432
+ досоздаёт в «свободный» слот дубль уже стоящей объявы (Bybit отбивает его 90043).
1433
+ `exid=0` (заготовка, на бирже ещё нет) и `my_ad.blocked` — вне hot-оборота."""
1434
+ return cls.filter(
1435
+ exid__not=0,
1436
+ my_ad__blocked=False,
1437
+ my_ad__credexs__cred__ovr_pm_id=0,
1438
+ my_ad__credexs__cred__pmcur__cur_id=cur_id,
1439
+ pair_side__pair__coin_id=coin_id,
1440
+ pair_side__pair__cur_id=cur_id,
1441
+ pair_side__is_sell=is_sell,
1442
+ maker_id=maker_id,
1443
+ ).distinct()
1444
+
1424
1445
  @staticmethod
1425
1446
  def slot_of(min_fiat: int, filtered: bool, minimum: int) -> tuple[int, int] | None:
1426
1447
  """Слот hot-объявы — пара `(множитель_мин_лимита, фильтрованность)` по её нижнему лимиту и
@@ -1567,8 +1588,12 @@ class MyAd(Model): # Road
1567
1588
  fltr["ad__status"] = AdStatus.active
1568
1589
  if ex_ids:
1569
1590
  fltr["ad__maker__ex_id__in"] = ex_ids
1570
- return cls.filter(**fltr).prefetch_related(
1571
- "ad__pair_side__pair__coin", "ad__pair_side__pair__cur", "ad__maker__ex"
1591
+ # distinct: credexs — m2m, и объява с двумя платёжками (2 credex с ovr_pm_id=0) иначе # noqa: RUF003
1592
+ # приезжает JOIN'ом дважды (дубли строк в /hot/ads?mine=true и двойной ads_share) # noqa: RUF003
1593
+ return (
1594
+ cls.filter(**fltr)
1595
+ .distinct()
1596
+ .prefetch_related("ad__pair_side__pair__coin", "ad__pair_side__pair__cur", "ad__maker__ex")
1572
1597
  )
1573
1598
 
1574
1599
  @classmethod
File without changes
File without changes