xync-db 0.2.0__tar.gz → 0.2.2.dev17__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 (36) hide show
  1. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/PKG-INFO +1 -1
  2. xync_db-0.2.2.dev17/src/xync_db/migrations/models/0005_credex_del.py +40 -0
  3. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/models/__init__.py +57 -39
  4. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/.gitignore +0 -0
  5. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/README.md +0 -0
  6. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/pyproject.toml +0 -0
  7. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/__init__.py +0 -0
  8. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/config.py +0 -0
  9. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/enums.py +0 -0
  10. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/exceptions.py +0 -0
  11. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/graph.py +0 -0
  12. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/i18n.py +0 -0
  13. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/logo.png +0 -0
  14. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/CLAUDE.md +0 -0
  15. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/__init__.py +0 -0
  16. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/0001_initial.py +0 -0
  17. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/0002_triggers.py +0 -0
  18. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/0003_files_seed.py +0 -0
  19. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/0004_all_seed.py +0 -0
  20. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/__init__.py +0 -0
  21. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/seed/__init__.py +0 -0
  22. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
  23. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
  24. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
  25. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
  26. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/models/CLAUDE.md +0 -0
  27. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/py.typed +0 -0
  28. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/shared.py +0 -0
  29. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/__init__.py +0 -0
  30. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/__init__.py +0 -0
  31. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/actor.py +0 -0
  32. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/ad.py +0 -0
  33. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/common.py +0 -0
  34. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/cred.py +0 -0
  35. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/src/xync_db/typs/db/order.py +0 -0
  36. {xync_db-0.2.0 → xync_db-0.2.2.dev17}/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.2.0
3
+ Version: 0.2.2.dev17
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,40 @@
1
+ """Per-CredEx delete trigger → `credexs_del` channel.
2
+
3
+ Lets a user unregister ONE credential from ONE exchange (settings/creds edit form): deleting a
4
+ single `credex` row reactively removes it from that exchange (mirror of `credexs_new`, which posts
5
+ a freshly inserted `CredEx(exid=0)`). The handler is `api.handlers.creds.credex_del`.
6
+
7
+ The default `plsql("credex")` DEL trigger (from 0002) only carried `OLD.id`, useless post-cascade.
8
+ We override it: emit `[person_id, ex_id, exid]` (person via the still-living parent `cred`) — exactly
9
+ what the handler needs to find the agent and call `acl.cred_del(exid)`.
10
+
11
+ Crucially we SKIP the notify when the parent `cred` is already gone: that's a `Cred` cascade-delete,
12
+ where the BEFORE-DELETE `cred_del()` trigger already snapshotted every credex into `creds_del`. Firing
13
+ here too would double-delete on the exchange. A standalone credex delete leaves the cred alive → notify.
14
+ """
15
+
16
+ from typing import ClassVar
17
+
18
+ from tortoise.migrations.migration import Migration as BaseMigration
19
+ from tortoise.migrations.operations import RunSQL
20
+
21
+ CREDEX_DEL_SQL = """
22
+ CREATE OR REPLACE FUNCTION credex_del() returns trigger as $credex_del_trg$
23
+ DECLARE
24
+ pid INT;
25
+ BEGIN
26
+ -- person via the parent cred; NULL ⇒ cred already cascade-deleted ⇒ cred_del() handled it ⇒ skip
27
+ SELECT person_id INTO pid FROM cred WHERE id = OLD.cred_id;
28
+ IF pid IS NOT NULL THEN
29
+ PERFORM pg_notify('credexs_del', jsonb_build_array(pid, OLD.ex_id, OLD.exid)::varchar);
30
+ END IF;
31
+ RETURN NULL;
32
+ END
33
+ $credex_del_trg$ LANGUAGE plpgsql;
34
+ CREATE OR REPLACE TRIGGER credex_del AFTER DELETE ON "credex" FOR EACH ROW EXECUTE FUNCTION credex_del();
35
+ """
36
+
37
+
38
+ class Migration(BaseMigration):
39
+ dependencies: ClassVar = [("models", "0004_all_seed")]
40
+ operations: ClassVar = [RunSQL(CREDEX_DEL_SQL)]
@@ -321,25 +321,37 @@ class Person(Model, TsTrait):
321
321
  # pm_agents: BackwardFKRelation[PmAgent]
322
322
 
323
323
  async def merge(self, new_person: Person):
324
- """Вмерджить new_person в self: перенести акторов и креды, удалить new_person."""
325
- await new_person.fetch_related("creds", "actors")
326
- for cred in new_person.creds:
327
- cred.person = self
328
- await cred.save(update_fields=["person_id"])
329
- for actor in new_person.actors:
330
- actor.person = self
331
- await actor.save(update_fields=["person_id"])
332
- self.name = self.name or new_person.name
333
- await self.save(update_fields=["name"])
334
- # после переноса у new_person не должно остаться связей # noqa: RUF003 - intentional Russian
335
- await new_person.fetch_related("creds", "actors")
336
- if not len(new_person.creds) + len(new_person.actors) and new_person.user_id in (self.user_id, None):
324
+ """Вмерджить new_person в self (перенести акторов и креды) и удалить new_person — АТОМАРНО,
325
+ одной транзакцией: сбой посреди переноса не должен оставлять пол-мерджа (актор на одной
326
+ персоне, кред на другой) и травить окружающую транзакцию запроса.
327
+
328
+ Коллизия uniq(person,pmcur,ovr_pm)/(person,pmcur,detail): у self уже есть «тот же» кред
329
+ (типично XyncPay по той же pmcur, забутстрапленный на другой бирже) — простой re-point
330
+ person_id упал бы IntegrityError. Молча сливать финансовые креды нельзя, поэтому громко
331
+ падаем ValueError (аномалию данных разруливает человек); транзакция всё откатывает чисто."""
332
+ if new_person.user_id not in (self.user_id, None):
333
+ raise ValueError(f"refuse to merge user-bound person#{new_person.id} into person#{self.id}")
334
+ async with in_transaction():
335
+ await new_person.fetch_related("creds", "actors")
336
+ for cred in new_person.creds:
337
+ if dup := await Cred.get_or_none(
338
+ person=self, pmcur_id=cred.pmcur_id, ovr_pm_id=cred.ovr_pm_id
339
+ ) or await Cred.get_or_none(person=self, pmcur_id=cred.pmcur_id, detail=cred.detail):
340
+ txt = (
341
+ f"У старого person#{self.id}:{self.name}:{self.note} уже есть cred#{dup.id}:{dup.detail} " # noqa: RUF001
342
+ f"c pmcur:{dup.pmcur_id}:{dup.ovr_pm_id}, а мы на него пытаем перевесить " # noqa: RUF001
343
+ f"новый cred#{cred.id}:{cred.detail} c pmcur:{cred.pmcur_id}:{cred.ovr_pm_id} "
344
+ f"нового person#{new_person.id}:{new_person.name}:{new_person.note}"
345
+ )
346
+ raise ValueError(txt)
347
+ cred.person = self
348
+ await cred.save(update_fields=["person_id"])
349
+ for actor in new_person.actors:
350
+ actor.person = self
351
+ await actor.save(update_fields=["person_id"])
352
+ self.name = self.name or new_person.name
353
+ await self.save(update_fields=["name"])
337
354
  await new_person.delete()
338
- else:
339
- raise ValueError(
340
- f"Merge to person#{self.id} failed: new person#{new_person.id} still has "
341
- f"{len(new_person.creds)} creds, {len(new_person.actors)} actors, user_id={new_person.user_id}"
342
- )
343
355
 
344
356
 
345
357
  class User(TgUser, TsTrait):
@@ -902,27 +914,25 @@ class Actor(Model):
902
914
  logging.error(txt)
903
915
  return 3, txt, user
904
916
 
905
- merged = False
906
- for i, person in enumerate(user_persons):
907
- if [a for a in person.actors if a.ex_id]:
908
- # персона юзера уже занята актором(ами)мерджим только если имена совпадают
909
- if self.person.name == person.name:
910
- await person.merge(self.person)
911
- merged = True
912
- elif i:
913
- # у юзера >1 персоны без акторов — аномалия # noqa: RUF003 - intentional Russian
914
- txt = (
915
- f"У {i + 1}-й персоны #{person.id} нет акторов (юзер #{username_id}), но персон без акторов уже > 1" # noqa: RUF001
916
- )
917
- logging.error(txt)
918
- return 2, txt, user
919
- else:
920
- # первая персона юзера без акторов (базовая от триггера) — мерджим
921
- await person.merge(self.person)
922
- merged = True
923
-
924
- if not merged:
925
- # все персоны юзера заняты акторами с другими именами — оставляем новую, привязываем к юзеру # noqa: RUF003
917
+ # Цель мерджа выбираем БЕЗ зависимости от порядка выборки (старый цикл с `elif i` ошибочно
918
+ # ловил «аномалию», если пустая базовая персона оказывалась не первой):
919
+ # 1) занятая персона с тем же именем — тот же человек на другой бирже;
920
+ # 2) иначе базовая пустая персона юзера (её единственный акторвнутренний ex0).
921
+ # Пустых (без биржевого актора) персон у юзера норм быть не больше одной — базовая от триггера.
922
+ empties = [p for p in user_persons if not any(a.ex_id for a in p.actors)]
923
+ if len(empties) > 1:
924
+ txt = f"У юзера #{username_id} больше одной персоны без биржевых акторов — аномалия" # noqa: RUF001
925
+ logging.error(txt)
926
+ return 2, txt, user
927
+ target = next(
928
+ (p for p in user_persons if any(a.ex_id for a in p.actors) and p.name == self.person.name), None
929
+ ) or (empties[0] if empties else None)
930
+ if target:
931
+ await target.merge(self.person)
932
+ else:
933
+ # все персоны юзера заняты акторами с другими именами — это другой человек под тем же # noqa: RUF003
934
+ # ТГ-аккаунтом: оставляем новую персону актора и привязываем к юзеру. Осиротевшей пустой
935
+ # базы тут не остаётся — будь она, стала бы target выше и была бы поглощена.
926
936
  self.person.user = user
927
937
  await self.person.save(update_fields=["user_id"])
928
938
 
@@ -1578,6 +1588,14 @@ class CredEx(Model):
1578
1588
  credex, _ = await cls.update_or_create({"exid": exid}, cred_id=cred_id, ex_id=ex_id)
1579
1589
  return credex
1580
1590
 
1591
+ @classmethod
1592
+ async def remove(cls, *, cred_id: int, ex_id: int) -> bool:
1593
+ """ЕДИНСТВЕННЫЙ standalone-делитор CredEx (single-writer-правило): сносит строку (cred, ex).
1594
+ Снятие креда с ОДНОЙ биржи из формы — удаление на бирже реактивно: BEFORE/AFTER-DELETE триггер
1595
+ `credex_del` (миграция 0005) положит `[person_id, ex_id, exid]` в `credexs_del` → handler вызовет
1596
+ `acl.cred_del(exid)`. Каскадное удаление при сносе самого Cred идёт другим путём (`creds_del`)."""
1597
+ return bool(await cls.filter(cred_id=cred_id, ex_id=ex_id).delete())
1598
+
1581
1599
  class Meta:
1582
1600
  table_description = "Credential on Exchange"
1583
1601
  unique_together = (("ex", "exid"),)
File without changes
File without changes
File without changes