xync-db 0.5.5.dev31__tar.gz → 0.5.5.dev64__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.
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/PKG-INFO +1 -1
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/enums.py +29 -15
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/CLAUDE.md +1 -1
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/0001_initial.py +109 -25
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/0002_triggers.py +106 -22
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/0003_seed.py +1 -1
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_actor_seed.py +13 -1
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_tg_seed.py +32 -1
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/models/CLAUDE.md +16 -2
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/models/__init__.py +190 -47
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/.gitignore +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/README.md +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/pyproject.toml +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/config.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/exceptions.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/graph.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/i18n.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/login_store.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/logo.png +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_bin_seed.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_country_seed.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/c_net_seed.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/countries.json +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/ranges.csv +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/py.typed +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/shared.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/__init__.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/actor.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/ad.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/common.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/cred.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/order.py +0 -0
- {xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/typs/db/pm.py +0 -0
|
@@ -176,12 +176,9 @@ class AgentStatus(IntEnum):
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
class SynonymType(IntEnum):
|
|
179
|
-
name = 1
|
|
180
179
|
ppo = 2
|
|
181
|
-
from_party = 3
|
|
182
|
-
|
|
183
|
-
slip_req = 5
|
|
184
|
-
slip_send = 6
|
|
180
|
+
from_party = 3 # с чьего лица платит покупатель: sell — какое мейкер требует, buy — с какого шлёт сам
|
|
181
|
+
slip = 5 # sell-объява: какой чек мейкер требует, buy: какой шлёт (бывш. slip_req/slip_send)
|
|
185
182
|
abuser = 7
|
|
186
183
|
scale = 8
|
|
187
184
|
slavic = 9
|
|
@@ -203,15 +200,14 @@ class Boundary(IntEnum):
|
|
|
203
200
|
|
|
204
201
|
|
|
205
202
|
class SbpStrict(IntEnum):
|
|
206
|
-
no = 0
|
|
207
203
|
sbp = 1
|
|
208
204
|
card = 2
|
|
209
205
|
|
|
210
206
|
|
|
211
|
-
class
|
|
207
|
+
class FromParty(IntEnum):
|
|
212
208
|
fst = 0
|
|
213
209
|
fam = 1
|
|
214
|
-
lk = 2
|
|
210
|
+
lk = 2
|
|
215
211
|
trd = 3
|
|
216
212
|
|
|
217
213
|
|
|
@@ -222,17 +218,35 @@ class Slip(IntEnum):
|
|
|
222
218
|
pdf_mail = 3
|
|
223
219
|
|
|
224
220
|
|
|
225
|
-
class NameType(IntEnum):
|
|
226
|
-
no_slavic = 0
|
|
227
|
-
slavic = 1
|
|
228
|
-
fake = 2
|
|
229
|
-
|
|
230
|
-
|
|
231
221
|
class AbuserType(IntEnum):
|
|
232
|
-
no = 0
|
|
233
222
|
rating = 1 # рейтинг: жмет "Оплачено" сразу, "Отмена" по апелляции
|
|
234
223
|
fake = 2 # реклама
|
|
235
224
|
cash = 3 # типа кэш обменник, но хз
|
|
225
|
+
long = 4 # ждать дольше 15 мин (оплату/отпуск)
|
|
226
|
+
add_vrf = 5 # сверх чека — видео из лк/выписка и т.п.
|
|
227
|
+
fee = 6 # доплата/комиссия сверх суммы ордера
|
|
228
|
+
rate = 7 # цена объявления не совпадает с реальным (сделка будет по другому курсу)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class CredType(
|
|
232
|
+
IntEnum
|
|
233
|
+
): # требование к реквизитам (искл: foreign — предупреждение мейкера-продавца о своей зарубежной карте)
|
|
234
|
+
no_llc = 1 # не ИП/юрлицо: «с ип не работаю»
|
|
235
|
+
rus_num = 2 # только российские, обычно номер телефона: «только на номера рф»
|
|
236
|
+
foreign = 3 # продавец просит платёж на зарубежную (не российскую) карту/номер: «перевод на зарубежную карту»
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class GeminiModel(IntEnum): # модель Gemini (`GeminiReq`); имя в API, порядок силы и квоты — `cond_ai.MODELS`
|
|
240
|
+
lite_3_5 = 5
|
|
241
|
+
lite_3_1 = 6
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class GeminiRes(IntEnum): # исход запроса к Gemini (`GeminiReq`)
|
|
245
|
+
ok = 0
|
|
246
|
+
minute = 1 # прочие 429 (…PerMinute…, отказ без квоты): ключ на минуту мимо этой модели
|
|
247
|
+
day = 2 # 429 …PerDay…: ключ выбыл по этой модели до сброса квоты (`GeminiKey.pick`)
|
|
248
|
+
bad_key = 4 # 401/403/API_KEY_INVALID: ключ выбыл по всем моделям на сутки
|
|
249
|
+
fail = 5 # прочие сбои (таймаут; 5xx — перегружена модель: условие — к следующей)
|
|
236
250
|
|
|
237
251
|
|
|
238
252
|
class ExStatus(IntEnum):
|
|
@@ -93,7 +93,7 @@ Loads PM logo files from `seed/xync_files.sql` if < 100 files exist. These are T
|
|
|
93
93
|
## models/0004_all_seed.py — Full Seed
|
|
94
94
|
|
|
95
95
|
Orchestrates the seed scripts in order:
|
|
96
|
-
1. `a_actor_seed` — Exchanges, currencies (56 base), coins, XyncPay PM, origin user (id=0)
|
|
96
|
+
1. `a_actor_seed` — Exchanges, currencies (56 base), coins, XyncPay PM, e-wallets yoomoney/cupis/pyypl/alipay (Bank + Pm typ=emoney + RUB pmcur), origin user (id=0)
|
|
97
97
|
2. `a_tg_seed` — Telegram DCs, app config, PM name mappings (`pm_rep`)
|
|
98
98
|
3. `a_country_seed` — **all** countries + their currencies from the bundled `seed/countries.json`
|
|
99
99
|
(generated from mledoze/countries), single source, BEFORE the exchange seeders. Sets `ccn3`
|
|
@@ -10,7 +10,7 @@ from tortoise.fields.db_defaults import Now
|
|
|
10
10
|
from tortoise.migrations import operations as ops
|
|
11
11
|
from tortoise.migrations.constraints import CheckConstraint
|
|
12
12
|
from x_auth.enums import Lang, PeerType, Role
|
|
13
|
-
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field, UniqBinaryField
|
|
13
|
+
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field, UInt16Field, UniqBinaryField
|
|
14
14
|
|
|
15
15
|
from xync_db.enums import (
|
|
16
16
|
AbuserType,
|
|
@@ -19,14 +19,17 @@ from xync_db.enums import (
|
|
|
19
19
|
AgentStatus,
|
|
20
20
|
Boundary,
|
|
21
21
|
CoinType,
|
|
22
|
+
CredType,
|
|
22
23
|
DepType,
|
|
23
24
|
ExAction,
|
|
24
25
|
ExStatus,
|
|
25
26
|
ExType,
|
|
26
27
|
FileType,
|
|
28
|
+
FromParty,
|
|
29
|
+
GeminiModel,
|
|
30
|
+
GeminiRes,
|
|
27
31
|
HotStatus,
|
|
28
32
|
OrderStatus,
|
|
29
|
-
Party,
|
|
30
33
|
PersonStatus,
|
|
31
34
|
PmType,
|
|
32
35
|
SbpStrict,
|
|
@@ -70,12 +73,97 @@ class Migration(migrations.Migration):
|
|
|
70
73
|
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
71
74
|
("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
|
|
72
75
|
("updated_at", DatetimeSecField(db_default=Now(), auto_now=True, auto_now_add=False)),
|
|
73
|
-
("raw_txt", fields.CharField(
|
|
76
|
+
("raw_txt", fields.CharField(db_index=True, max_length=4095)),
|
|
74
77
|
("last_ver", fields.CharField(null=True, max_length=4095)),
|
|
75
78
|
],
|
|
76
79
|
options={"table": "cond", "app": "models", "pk_attr": "id"},
|
|
77
80
|
bases=["Model", "TsTrait"],
|
|
78
81
|
),
|
|
82
|
+
ops.CreateModel(
|
|
83
|
+
name="CondPrompt",
|
|
84
|
+
fields=[
|
|
85
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
86
|
+
("txt", fields.TextField(unique=False)),
|
|
87
|
+
("is_jev", fields.BooleanField(db_default=False)),
|
|
88
|
+
(
|
|
89
|
+
"cond",
|
|
90
|
+
fields.ForeignKeyField(
|
|
91
|
+
"models.Cond",
|
|
92
|
+
source_field="cond_id",
|
|
93
|
+
null=True,
|
|
94
|
+
db_constraint=True,
|
|
95
|
+
to_field="id",
|
|
96
|
+
related_name="prompts",
|
|
97
|
+
on_delete=OnDelete.SET_NULL,
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
("remark", fields.TextField(null=True, unique=False)),
|
|
101
|
+
("note", fields.TextField(null=True, unique=False)),
|
|
102
|
+
("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
|
|
103
|
+
],
|
|
104
|
+
options={
|
|
105
|
+
"table": "cond_prompt",
|
|
106
|
+
"app": "models",
|
|
107
|
+
"pk_attr": "id",
|
|
108
|
+
"table_description": "Версия промпта разбора условий (`xync_service.common.cond_ai`): системного промпта Gemini или вопросов JEV",
|
|
109
|
+
},
|
|
110
|
+
bases=["Model"],
|
|
111
|
+
),
|
|
112
|
+
ops.CreateModel(
|
|
113
|
+
name="GeminiKey",
|
|
114
|
+
fields=[
|
|
115
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
116
|
+
("key", fields.CharField(unique=True, max_length=63)),
|
|
117
|
+
],
|
|
118
|
+
options={
|
|
119
|
+
"table": "gemini_key",
|
|
120
|
+
"app": "models",
|
|
121
|
+
"pk_attr": "id",
|
|
122
|
+
"table_description": "Ключ Gemini API free tier (разбор условий, `xync_service.common.cond_ai`). Квота своя на каждую модель, расход и",
|
|
123
|
+
},
|
|
124
|
+
bases=["Model"],
|
|
125
|
+
),
|
|
126
|
+
ops.CreateModel(
|
|
127
|
+
name="GeminiReq",
|
|
128
|
+
fields=[
|
|
129
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
130
|
+
(
|
|
131
|
+
"key",
|
|
132
|
+
fields.ForeignKeyField(
|
|
133
|
+
"models.GeminiKey",
|
|
134
|
+
source_field="key_id",
|
|
135
|
+
db_constraint=True,
|
|
136
|
+
to_field="id",
|
|
137
|
+
related_name="reqs",
|
|
138
|
+
on_delete=OnDelete.CASCADE,
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
(
|
|
142
|
+
"model",
|
|
143
|
+
fields.IntEnumField(
|
|
144
|
+
description="lite_3_5: 5\nlite_3_1: 6",
|
|
145
|
+
enum_type=GeminiModel,
|
|
146
|
+
generated=False,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
(
|
|
150
|
+
"res",
|
|
151
|
+
fields.IntEnumField(
|
|
152
|
+
description="ok: 0\nminute: 1\nday: 2\nbad_key: 4\nfail: 5",
|
|
153
|
+
enum_type=GeminiRes,
|
|
154
|
+
generated=False,
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
|
|
158
|
+
],
|
|
159
|
+
options={
|
|
160
|
+
"table": "gemini_req",
|
|
161
|
+
"app": "models",
|
|
162
|
+
"pk_attr": "id",
|
|
163
|
+
"table_description": "Запрос к Gemini (`cond_ai._generate`) — история за сутки (старше удаляет `CondAi` на входе): по ней",
|
|
164
|
+
},
|
|
165
|
+
bases=["Model"],
|
|
166
|
+
),
|
|
79
167
|
ops.CreateModel(
|
|
80
168
|
name="CondSim",
|
|
81
169
|
fields=[
|
|
@@ -621,27 +709,21 @@ class Migration(migrations.Migration):
|
|
|
621
709
|
on_delete=OnDelete.CASCADE,
|
|
622
710
|
),
|
|
623
711
|
),
|
|
624
|
-
(
|
|
625
|
-
"to_party",
|
|
626
|
-
fields.IntEnumField(
|
|
627
|
-
null=True, description="fst: 0\nfam: 1\nlk: 2\ntrd: 3", enum_type=Party, generated=False
|
|
628
|
-
),
|
|
629
|
-
),
|
|
630
712
|
(
|
|
631
713
|
"from_party",
|
|
632
714
|
fields.IntEnumField(
|
|
633
|
-
null=True, description="fst: 0\nfam: 1\nlk: 2\ntrd: 3", enum_type=
|
|
715
|
+
null=True, description="fst: 0\nfam: 1\nlk: 2\ntrd: 3", enum_type=FromParty, generated=False
|
|
634
716
|
),
|
|
635
717
|
),
|
|
636
|
-
("ppo", UInt1Field(null=True)),
|
|
637
718
|
(
|
|
638
|
-
"
|
|
719
|
+
"to_party",
|
|
639
720
|
fields.IntEnumField(
|
|
640
|
-
null=True, description="
|
|
721
|
+
null=True, description="fst: 0\nfam: 1\nlk: 2\ntrd: 3", enum_type=FromParty, generated=False
|
|
641
722
|
),
|
|
642
723
|
),
|
|
724
|
+
("ppo", UInt1Field(null=True)),
|
|
643
725
|
(
|
|
644
|
-
"
|
|
726
|
+
"slip",
|
|
645
727
|
fields.IntEnumField(
|
|
646
728
|
null=True, description="no: 0\nscreen: 1\npdf: 2\npdf_mail: 3", enum_type=Slip, generated=False
|
|
647
729
|
),
|
|
@@ -649,27 +731,28 @@ class Migration(migrations.Migration):
|
|
|
649
731
|
(
|
|
650
732
|
"abuser",
|
|
651
733
|
fields.IntEnumField(
|
|
652
|
-
|
|
653
|
-
|
|
734
|
+
null=True,
|
|
735
|
+
description="rating: 1\nfake: 2\ncash: 3\nlong: 4\nadd_vrf: 5\nfee: 6\nrate: 7",
|
|
654
736
|
enum_type=AbuserType,
|
|
655
737
|
generated=False,
|
|
656
738
|
),
|
|
657
739
|
),
|
|
658
740
|
("slavic", fields.BooleanField(null=True)),
|
|
741
|
+
(
|
|
742
|
+
"cred",
|
|
743
|
+
fields.IntEnumField(
|
|
744
|
+
null=True, description="no_llc: 1\nrus_num: 2\nforeign: 3", enum_type=CredType, generated=False
|
|
745
|
+
),
|
|
746
|
+
),
|
|
659
747
|
("mtl_like", fields.BooleanField(null=True)),
|
|
660
748
|
("scale", fields.SmallIntField(null=True)),
|
|
661
749
|
("bank_side", fields.BooleanField(null=True)),
|
|
662
750
|
(
|
|
663
751
|
"sbp_strict",
|
|
664
|
-
fields.IntEnumField(
|
|
665
|
-
description="no: 0\nsbp: 1\ncard: 2",
|
|
666
|
-
db_default=SbpStrict.no,
|
|
667
|
-
enum_type=SbpStrict,
|
|
668
|
-
generated=False,
|
|
669
|
-
),
|
|
752
|
+
fields.IntEnumField(null=True, description="sbp: 1\ncard: 2", enum_type=SbpStrict, generated=False),
|
|
670
753
|
),
|
|
671
754
|
("contact", fields.CharField(null=True, max_length=127)),
|
|
672
|
-
("
|
|
755
|
+
("is_intermediary", fields.BooleanField(null=True)),
|
|
673
756
|
(
|
|
674
757
|
"banks",
|
|
675
758
|
fields.ManyToManyField(
|
|
@@ -930,7 +1013,7 @@ class Migration(migrations.Migration):
|
|
|
930
1013
|
"typ",
|
|
931
1014
|
fields.IntEnumField(
|
|
932
1015
|
db_index=True,
|
|
933
|
-
description="
|
|
1016
|
+
description="ppo: 2\nfrom_party: 3\nslip: 5\nabuser: 7\nscale: 8\nslavic: 9\nmtl_like: 10\nbank: 11\nbank_side: 12\nsbp_strict: 13\ncontact: 14",
|
|
934
1017
|
enum_type=SynonymType,
|
|
935
1018
|
generated=False,
|
|
936
1019
|
),
|
|
@@ -1361,7 +1444,7 @@ class Migration(migrations.Migration):
|
|
|
1361
1444
|
name="Actor",
|
|
1362
1445
|
fields=[
|
|
1363
1446
|
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
1364
|
-
("exid",
|
|
1447
|
+
("exid", UInt16Field()),
|
|
1365
1448
|
("name", fields.CharField(max_length=63)),
|
|
1366
1449
|
(
|
|
1367
1450
|
"ex",
|
|
@@ -1379,6 +1462,7 @@ class Migration(migrations.Migration):
|
|
|
1379
1462
|
fields.ForeignKeyField(
|
|
1380
1463
|
"models.Person",
|
|
1381
1464
|
source_field="person_id",
|
|
1465
|
+
null=True,
|
|
1382
1466
|
db_constraint=True,
|
|
1383
1467
|
to_field="id",
|
|
1384
1468
|
related_name="actors",
|
|
@@ -18,20 +18,15 @@ Channels emitted:
|
|
|
18
18
|
w3addr_wanted / w3addr_filled -- web3 address handshake (0009)
|
|
19
19
|
|
|
20
20
|
Guards (RAISE, без NOTIFY): tx_before (баланс/иммутабельность/монотонность), tx_no_delete,
|
|
21
|
-
ad_exid_ex_uniq (0008, exid уникален per-exchange)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
(b) stamp `note = "{ex_id}:{exid}"` — the per-actor identity key that `Ex.person_save` and
|
|
31
|
-
`Agent.upsert` resolve bootstrap persons by, and that `0010_person_note_unique` makes UNIQUE.
|
|
32
|
-
|
|
33
|
-
The `status = -1` (USER_DEF) branch is unchanged: an agent person pre-created by `Agent.upsert`
|
|
34
|
-
triggers the `actor_person_needs_def_user` NOTIFY so an admin is asked whose agent it is.
|
|
21
|
+
ad_exid_ex_uniq (0008, exid уникален per-exchange), ad_cond_side_uniq/cond_txt_side_uniq (unique
|
|
22
|
+
(cond.raw_txt, ad.pair_side.is_sell)). GC: ad_cond_gc/cond_new_gc — условие без объяв удаляется (cond_gc).
|
|
23
|
+
Utility: _user_balance(uid, cid), _tx_pack(...).
|
|
24
|
+
|
|
25
|
+
`actor_new` trigger: only the `status = -1` (USER_DEF) branch — an agent person pre-created by
|
|
26
|
+
`Agent.upsert` triggers the `actor_person_needs_def_user` NOTIFY so an admin is asked whose agent it is.
|
|
27
|
+
Persons of actors are created ONLY in Python (`Ex.person_save` / `Agent.upsert`, which stamp
|
|
28
|
+
`note = "{ex_id}:{exid}"`, D-035); the former auto-person branch was a duplicate path (every
|
|
29
|
+
`Actor.create` site passes a person). A person-less actor is a maker masked by the exchange (D-042).
|
|
35
30
|
"""
|
|
36
31
|
|
|
37
32
|
from typing import ClassVar
|
|
@@ -43,17 +38,12 @@ from tortoise.migrations.operations import RunSQL
|
|
|
43
38
|
TRIGGERS_SQL = """
|
|
44
39
|
-- before new Actor
|
|
45
40
|
CREATE OR REPLACE FUNCTION actor_new() returns trigger as $actor_new_trg$
|
|
46
|
-
DECLARE
|
|
47
|
-
p_id INT;
|
|
48
41
|
BEGIN
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
IF NEW.person_id IS NULL THEN
|
|
52
|
-
INSERT INTO person (status, note) VALUES (0, NEW.ex_id || ':' || NEW.exid) RETURNING id INTO p_id;
|
|
53
|
-
NEW.person_id := p_id;
|
|
42
|
+
-- персону актору заводит ТОЛЬКО Python (Ex.person_save / Agent.upsert, note = "{ex_id}:{exid}", D-035);
|
|
43
|
+
-- актор без person_id — замаскированный биржей мейкер (D-042), персону ему не выдумываем.
|
|
54
44
|
-- агентской персоне уже проставлен status=-1 (USER_DEF) в Agent.upsert → зовём админа
|
|
55
45
|
-- определить юзера (actor_person_needs_def_user).
|
|
56
|
-
|
|
46
|
+
IF (SELECT status FROM person WHERE id = NEW.person_id) = -1 THEN
|
|
57
47
|
PERFORM pg_notify('actor_person_needs_def_user', row_to_json(NEW)::varchar);
|
|
58
48
|
END IF;
|
|
59
49
|
RETURN NEW;
|
|
@@ -369,6 +359,98 @@ $ad_exid_ex_uniq_trg$ LANGUAGE plpgsql;
|
|
|
369
359
|
CREATE OR REPLACE TRIGGER ad_exid_ex_uniq BEFORE INSERT OR UPDATE ON "ad" FOR EACH ROW EXECUTE FUNCTION ad_exid_ex_uniq();
|
|
370
360
|
"""
|
|
371
361
|
|
|
362
|
+
# Cond: unique (raw_txt, ads.pair_side.is_sell) — разбор условия (from_party/to_party) зависит от стороны объяв, поэтому
|
|
363
|
+
# тот же текст у buy и sell объяв — два условия, а у условия объявы только одной стороны. Tortoise uniq через join не
|
|
364
|
+
# выразит → BEFORE-RAISE на записи объявы (cond_id/сторона) и на смене текста условия (кодом не меняется: сменился
|
|
365
|
+
# текст объявы — она к условию нового текста, `Cond.side_get`; гард — от ручного SQL). Одновременные
|
|
366
|
+
# записи одного текста идут по очереди (advisory-lock на текст до конца транзакции): иначе обе проверки прошли бы.
|
|
367
|
+
COND_SIDE_UNIQ_SQL = """
|
|
368
|
+
CREATE OR REPLACE FUNCTION ad_cond_side_uniq() returns trigger as $ad_cond_side_uniq_trg$
|
|
369
|
+
DECLARE
|
|
370
|
+
txt VARCHAR;
|
|
371
|
+
side BOOL;
|
|
372
|
+
conflict_cond INT;
|
|
373
|
+
conflict_ad INT;
|
|
374
|
+
BEGIN
|
|
375
|
+
IF NEW.cond_id IS NULL THEN
|
|
376
|
+
RETURN NEW;
|
|
377
|
+
END IF;
|
|
378
|
+
SELECT raw_txt INTO txt FROM cond WHERE id = NEW.cond_id;
|
|
379
|
+
PERFORM pg_advisory_xact_lock(hashtext(txt));
|
|
380
|
+
SELECT is_sell INTO side FROM pair_side WHERE id = NEW.pair_side_id;
|
|
381
|
+
-- конфликт: объявы этой стороны — при другом условии этого текста, либо объявы другой стороны — при этом
|
|
382
|
+
SELECT a.cond_id, a.id INTO conflict_cond, conflict_ad
|
|
383
|
+
FROM ad a JOIN pair_side ps ON ps.id = a.pair_side_id JOIN cond c ON c.id = a.cond_id
|
|
384
|
+
WHERE c.raw_txt = txt AND a.id != NEW.id
|
|
385
|
+
AND ((ps.is_sell = side AND a.cond_id != NEW.cond_id) OR (ps.is_sell != side AND a.cond_id = NEW.cond_id))
|
|
386
|
+
LIMIT 1;
|
|
387
|
+
IF conflict_ad IS NOT NULL THEN
|
|
388
|
+
RAISE EXCEPTION 'ad#% (is_sell=%) -> cond#%: (cond.raw_txt, ad.pair_side.is_sell) must be unique, conflicts with ad#% of cond#%',
|
|
389
|
+
NEW.id, side, NEW.cond_id, conflict_ad, conflict_cond;
|
|
390
|
+
END IF;
|
|
391
|
+
RETURN NEW;
|
|
392
|
+
END
|
|
393
|
+
$ad_cond_side_uniq_trg$ LANGUAGE plpgsql;
|
|
394
|
+
CREATE OR REPLACE TRIGGER ad_cond_side_uniq BEFORE INSERT OR UPDATE OF cond_id, pair_side_id ON "ad"
|
|
395
|
+
FOR EACH ROW EXECUTE FUNCTION ad_cond_side_uniq();
|
|
396
|
+
|
|
397
|
+
CREATE OR REPLACE FUNCTION cond_txt_side_uniq() returns trigger as $cond_txt_side_uniq_trg$
|
|
398
|
+
DECLARE
|
|
399
|
+
conflict_cond INT;
|
|
400
|
+
BEGIN
|
|
401
|
+
PERFORM pg_advisory_xact_lock(hashtext(NEW.raw_txt));
|
|
402
|
+
-- конфликт: другое условие нового текста, чьи объявы той же стороны, что и объявы этого
|
|
403
|
+
SELECT o.cond_id INTO conflict_cond
|
|
404
|
+
FROM ad o JOIN pair_side ops ON ops.id = o.pair_side_id JOIN cond c ON c.id = o.cond_id
|
|
405
|
+
WHERE c.raw_txt = NEW.raw_txt AND c.id != NEW.id AND ops.is_sell IN (
|
|
406
|
+
SELECT ps.is_sell FROM ad a JOIN pair_side ps ON ps.id = a.pair_side_id WHERE a.cond_id = NEW.id
|
|
407
|
+
)
|
|
408
|
+
LIMIT 1;
|
|
409
|
+
IF conflict_cond IS NOT NULL THEN
|
|
410
|
+
RAISE EXCEPTION 'cond#% raw_txt: (cond.raw_txt, ad.pair_side.is_sell) must be unique, conflicts with cond#%',
|
|
411
|
+
NEW.id, conflict_cond;
|
|
412
|
+
END IF;
|
|
413
|
+
RETURN NEW;
|
|
414
|
+
END
|
|
415
|
+
$cond_txt_side_uniq_trg$ LANGUAGE plpgsql;
|
|
416
|
+
CREATE OR REPLACE TRIGGER cond_txt_side_uniq BEFORE UPDATE OF raw_txt ON "cond"
|
|
417
|
+
FOR EACH ROW EXECUTE FUNCTION cond_txt_side_uniq();
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
# Cond живёт, пока у него есть объявы. Осиротело (объява перецеплена на другое условие — мейкер сменил текст, — или
|
|
421
|
+
# удалена) — удаляем сразу; новое (`Cond.side_get`), к которому до конца ЕГО транзакции не прицепилась объява, —
|
|
422
|
+
# на коммите (DEFERRED): поэтому условие создаётся только в одной транзакции с записью объявы. Тот же advisory-lock на
|
|
423
|
+
# текст, что у ad_cond_side_uniq: иначе параллельная запись объявы в это условие прошла бы мимо NOT EXISTS, а удаление
|
|
424
|
+
# условия унесло бы её каскадом (ad.cond_id ON DELETE CASCADE).
|
|
425
|
+
COND_GC_SQL = """
|
|
426
|
+
CREATE OR REPLACE FUNCTION cond_gc() returns trigger as $cond_gc_trg$
|
|
427
|
+
DECLARE
|
|
428
|
+
cid INT;
|
|
429
|
+
txt VARCHAR;
|
|
430
|
+
BEGIN
|
|
431
|
+
IF TG_TABLE_NAME = 'cond' THEN
|
|
432
|
+
cid := NEW.id;
|
|
433
|
+
ELSIF TG_OP = 'DELETE' OR OLD.cond_id IS DISTINCT FROM NEW.cond_id THEN
|
|
434
|
+
cid := OLD.cond_id;
|
|
435
|
+
ELSE
|
|
436
|
+
RETURN NULL;
|
|
437
|
+
END IF;
|
|
438
|
+
SELECT raw_txt INTO txt FROM cond WHERE id = cid;
|
|
439
|
+
IF txt IS NULL THEN
|
|
440
|
+
RETURN NULL;
|
|
441
|
+
END IF;
|
|
442
|
+
PERFORM pg_advisory_xact_lock(hashtext(txt));
|
|
443
|
+
DELETE FROM cond c WHERE c.id = cid AND NOT EXISTS (SELECT 1 FROM ad a WHERE a.cond_id = cid);
|
|
444
|
+
RETURN NULL;
|
|
445
|
+
END
|
|
446
|
+
$cond_gc_trg$ LANGUAGE plpgsql;
|
|
447
|
+
CREATE OR REPLACE TRIGGER ad_cond_gc AFTER UPDATE OF cond_id OR DELETE ON "ad"
|
|
448
|
+
FOR EACH ROW WHEN (OLD.cond_id IS NOT NULL) EXECUTE FUNCTION cond_gc();
|
|
449
|
+
DROP TRIGGER IF EXISTS cond_new_gc ON "cond"; -- CONSTRAINT TRIGGER не поддерживает OR REPLACE
|
|
450
|
+
CREATE CONSTRAINT TRIGGER cond_new_gc AFTER INSERT ON "cond" DEFERRABLE INITIALLY DEFERRED
|
|
451
|
+
FOR EACH ROW EXECUTE FUNCTION cond_gc();
|
|
452
|
+
"""
|
|
453
|
+
|
|
372
454
|
# web3-хендшейк адресов (0009): net_addr_ins (вставка пустого val → зовём получателя завести адрес,
|
|
373
455
|
# канал `w3addr_wanted`) и net_addr_upd (val NULL→real → сообщаем отправителям pending-W3Send, канал
|
|
374
456
|
# `w3addr_filled`). (`ALTER net_addr.val DROP NOT NULL` уехал в 0001 — теперь val создаётся nullable.)
|
|
@@ -411,5 +493,7 @@ class Migration(BaseMigration):
|
|
|
411
493
|
RunSQL(CREDEX_ORPHAN_SQL),
|
|
412
494
|
RunSQL(AGENT_AUTH_SQL),
|
|
413
495
|
RunSQL(AD_EXID_EX_UNIQ_SQL),
|
|
496
|
+
RunSQL(COND_SIDE_UNIQ_SQL),
|
|
497
|
+
RunSQL(COND_GC_SQL),
|
|
414
498
|
RunSQL(NET_ADDR_TRIGGERS_SQL),
|
|
415
499
|
]
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Порядок (зависимости):
|
|
8
8
|
1. files — PM-логотипы из `seed/xync_files.sql` (если файлов < 100)
|
|
9
|
-
2. a_actor_seed — биржи, 56 базовых валют, монеты, XyncPay PM,
|
|
9
|
+
2. a_actor_seed — биржи, 56 базовых валют, монеты, XyncPay PM, эл. кошельки ЮMoney/ЦУПИС/Pyypl/Alipay (Bank+Pm+RUB), origin user
|
|
10
10
|
3. a_tg_seed — Telegram DC/App, нормализация имён PM (`pm_rep`)
|
|
11
11
|
4. a_country_seed — ВСЕ страны + их валюты из mledoze (`seed/countries.json`), ДО биржевых
|
|
12
12
|
5. a_bin_seed — BIN-диапазоны + банки из binlist/data (`seed/ranges.csv`), ПОСЛЕ стран (линк по alpha-2)
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_actor_seed.py
RENAMED
|
@@ -2,7 +2,7 @@ from tortoise import BaseDBAsyncClient
|
|
|
2
2
|
from x_auth.models import Username
|
|
3
3
|
|
|
4
4
|
from xync_db.enums import ExAction, ExStatus, ExType, PmType, UserStatus
|
|
5
|
-
from xync_db.models import Coin, CoinEx, Cur, CurEx, Ex, ExStat, Pm, User
|
|
5
|
+
from xync_db.models import Bank, Coin, CoinEx, Cur, CurEx, Ex, ExStat, Pm, PmCur, User
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
async def actor_seed(db: BaseDBAsyncClient) -> str:
|
|
@@ -284,6 +284,18 @@ async def actor_seed(db: BaseDBAsyncClient) -> str:
|
|
|
284
284
|
|
|
285
285
|
xyncpay, _ = await Pm.update_or_create(dict(ok=True, qr=True, fee=0, typ=PmType.emoney), id=0, norm="xyncpay")
|
|
286
286
|
|
|
287
|
+
# эл. кошельки: в p2p-условиях RUB их называют наравне с банками → свой Bank (Pm с банком попадает в список
|
|
288
|
+
# банков разбора условий, `CondAi._pms`) + PmCur RUB; ЦУПИС биржи не отдают — Pm заводим сами. Pyypl/Alipay
|
|
289
|
+
# не рублёвые — df_cur им не ставим (Alipay — CNY в `refine_pms`)
|
|
290
|
+
rub = await Cur.get(ticker="RUB")
|
|
291
|
+
wallets = ("yoomoney", None, rub), ("cupis", "цупис", rub), ("pyypl", None, None), ("alipay", None, None)
|
|
292
|
+
for norm, alias, df_cur in wallets:
|
|
293
|
+
bank, _ = await Bank.get_or_create(name=norm)
|
|
294
|
+
pm, _ = await Pm.update_or_create(
|
|
295
|
+
dict(typ=PmType.emoney, bank=bank, df_cur=df_cur, alias=alias), norm=norm, country_id=None
|
|
296
|
+
)
|
|
297
|
+
await PmCur.get_or_create(pm=pm, cur=rub)
|
|
298
|
+
|
|
287
299
|
# origin user
|
|
288
300
|
id0, _ = await Username.get_or_create({"phone": 852_6_497_5811}, id=7807393311, username="XyncPay")
|
|
289
301
|
try:
|
|
@@ -51,11 +51,14 @@ async def tg_seed(_: BaseDBAsyncClient) -> str:
|
|
|
51
51
|
"ArmBussinessBank": ("ArmBusinessBank", 20),
|
|
52
52
|
"Asia United Bank (QR) lightnin...": ("Asia United Bank (QR) lightning", 11),
|
|
53
53
|
"Bank Credit Dnepro": ("Credit Dnipro", 14),
|
|
54
|
+
"Banco de Desarollo Rural": ("Banco de desarrollo Rural", 4),
|
|
55
|
+
"Banco Itau de Chile": ("Banco Itau Chile", 4),
|
|
54
56
|
"Banistimo Panamá": ("Banistmo Panama", 14),
|
|
55
57
|
"Banque du Developpement Local BDL": ("Banque de développement local (BDL)", 3),
|
|
56
58
|
"Banque de développement local (BDL)": ("Banque de développement local (BDL)", None),
|
|
57
59
|
"BDO": ("Banco De Oro (BDO)", None),
|
|
58
60
|
"Careen Pay": ("Careem Pay", None),
|
|
61
|
+
"CenterCreditBank": ("CenterCredit Bank", 4),
|
|
59
62
|
"Cash in Person Transaction": ("Cash in Person", 2),
|
|
60
63
|
"Cash Plus": ("Cashplus", 12),
|
|
61
64
|
"Chinabank": ("China Bank", 14),
|
|
@@ -66,8 +69,11 @@ async def tg_seed(_: BaseDBAsyncClient) -> str:
|
|
|
66
69
|
"Elkart": ("Elcart", 20),
|
|
67
70
|
"Erste Group Bank": ("Erste Bank", 3),
|
|
68
71
|
"eZ Cash": ("eZCash", 3),
|
|
72
|
+
"Grab Pay": ("GrabPay", 9),
|
|
69
73
|
"GTB Bank (Guarantee Trust Bank)": ("GTBank", None),
|
|
70
74
|
"IMEpay": ("IME Pay", 2),
|
|
75
|
+
"Kapital Bank": ("KapitalBank", 12), # азербайджанский Kapital Bank = «KapitalBank» Bybit/Htx/Mexc
|
|
76
|
+
"Kapital Bank Instant": ("KapitalBank", 12),
|
|
71
77
|
"Liv KSA": ("Liv.KSA", 14),
|
|
72
78
|
"Liv.KSA": ("Liv.KSA", 3),
|
|
73
79
|
"Local Bank (R-Green)": ("Sberbank", None),
|
|
@@ -84,23 +90,46 @@ async def tg_seed(_: BaseDBAsyncClient) -> str:
|
|
|
84
90
|
"LINE Bank by Hana Bank": ("LINE Pay", 3),
|
|
85
91
|
"Local Bank (S-Green)": ("Sberbank", 7),
|
|
86
92
|
"m 10": ("m10", 3),
|
|
93
|
+
"MICB": ("MICB Bank", 9), # «Bank» в цели — флаг банка для set_pms (Pm.bank), norm не меняется
|
|
87
94
|
"M-PESA kenya(Safaricom)": ("M-PESA kenya (Safaricom)", 11),
|
|
88
95
|
"Mercado Pago": ("Mercadopago", 11),
|
|
96
|
+
"Metrobank": ("Metropolitan Bank of the Philippines", 12),
|
|
97
|
+
"Millenium": ("Millennium", None),
|
|
98
|
+
"Mpesa Pochi la Biashara": ("M-PESA Pochi la Biashara", None),
|
|
89
99
|
"NaranjaX": ("Naranja X", 2),
|
|
100
|
+
"National Bank of Egyp (NBE)": ("National Bank of Egypt (NBE)", 12),
|
|
101
|
+
"OptimaBank": ("Optima Bank", 12),
|
|
102
|
+
"Oschabank": ("Oschadbank", 12),
|
|
103
|
+
"Otkritie FC Bank": ("Otkritie Bank", 12),
|
|
104
|
+
"PagoMóvil": ("Pago Movil", None),
|
|
105
|
+
"Palm Pay": ("PalmPay", None),
|
|
90
106
|
"Philippine National Bank (PNB)": ("Philippines National Bank (PNB)", 12),
|
|
91
107
|
"Philippine National Bank": ("Philippines National Bank", 3),
|
|
108
|
+
"Pochta Bank": ("Post Bank", 12),
|
|
109
|
+
"PUMB": ("PUMB Bank", None),
|
|
110
|
+
# страна в цели: у Bybit ПриватБанк с Ukraine, у Htx/Mexc без страны — Pm уникален по (norm, country)
|
|
111
|
+
"Privat Bank": ("Privatbank (Ukraine)", None),
|
|
112
|
+
"Privat Bank (Universal Card)": ("Privatbank (Ukraine)", 12),
|
|
113
|
+
"PSB": ("Promsvyazbank", 9),
|
|
92
114
|
"SBP - Fast Bank Transfer": ("SBP", None),
|
|
93
115
|
"Sovkombank": ("Sovcombank", 9),
|
|
94
116
|
"SBP Fast Bank Transfer": ("SBP", 14),
|
|
95
117
|
"Rabbit LINE Pay": ("LINE Pay", 20),
|
|
118
|
+
"Rabitabank": ("Rabita Bank", 12),
|
|
96
119
|
"Raiffaisen Bank": ("Raiffeisen Bank", 14),
|
|
97
120
|
"Raiffaizen": ("Raiffeisen", 14),
|
|
121
|
+
"Raiffeisen Bank": ("Raiffeisenbank", 12),
|
|
98
122
|
"Russian Standart Bank": ("Russian Standard Bank", 4),
|
|
123
|
+
"Santander México": ("Santander Mexico", 4),
|
|
124
|
+
"Scotiabank Perú": ("Scotiabank Peru", 12),
|
|
99
125
|
"Sovkm Bank": ("Sovcombank", 3),
|
|
126
|
+
"Sport Bank": ("Sportbank", 12),
|
|
127
|
+
"Superbank": ("Super Bank", 12),
|
|
100
128
|
"Tinkoff": ("T-Bank", None),
|
|
101
129
|
"Tinkoff Bank": ("T-Bank", 2),
|
|
102
130
|
"Transfer with Specific Bank": ("Transfers with specific bank", 2),
|
|
103
131
|
"TrueMoney": ("True Money", 20),
|
|
132
|
+
"UBRR": ("UBRR Bank", 9),
|
|
104
133
|
"TrueMoney (Cambodia)": ("True Money (Cambodia)", 2),
|
|
105
134
|
"TrueMoney (cambodia)": ("True Money (Cambodia)", 14),
|
|
106
135
|
"United Bank of Africa": ("United Bank for Africa", 4),
|
|
@@ -113,7 +142,9 @@ async def tg_seed(_: BaseDBAsyncClient) -> str:
|
|
|
113
142
|
"YapıKredi": ("Yapi Kredi", 1), # noqa: RUF001 - Turkish dotless-i is the exchange-side spelling
|
|
114
143
|
"Zain Cash - Business": ("ZainCash - Business", 14),
|
|
115
144
|
"Zain Cash": ("ZainCash", None),
|
|
116
|
-
|
|
145
|
+
# страна в цели: у Bybit ZaloPay с Southeast Asia, у Htx/Mexc без страны
|
|
146
|
+
"Zalo Pay": ("ZaloPay (Southeast Asia)", None),
|
|
147
|
+
"ZaloPay": ("ZaloPay (Southeast Asia)", None),
|
|
117
148
|
"Zirraat": ("Ziraat", 3),
|
|
118
149
|
"СБП": ("SBP", 2),
|
|
119
150
|
"ЮMoney": ("YooMoney", None),
|
|
@@ -72,6 +72,8 @@ actor.set_user(username_id):
|
|
|
72
72
|
- `CredEx` = credential registered on exchange `(ex_id, exid)` unique
|
|
73
73
|
- `Fiat` = balance tracker (one-to-one with `Cred`): `amount`, `target`, `min_deposit`
|
|
74
74
|
- `PmAgent` = automation account for a PM `(pm_id, user_id)` unique
|
|
75
|
+
- `Pm` unique `(norm, country)`; дубли сливает ЕДИНСТВЕННЫЙ writer `Pm.merge(keep_id, dup_ids)` (переносит PmEx/PmCur/ad_pm/
|
|
76
|
+
cond_banks/креды/агентов, удаляет дубли) — плюс правило `PmRep` в seed `a_tg_seed.pm_reps`, чтобы `set_pms` не пересоздал дубль
|
|
75
77
|
|
|
76
78
|
### P2P Trading
|
|
77
79
|
`Ad` -> `MyAd` -> `Race` -> `RaceStat`, `Order` -> `Msg`/`Transfer`/`Task`, `Hot`
|
|
@@ -84,10 +86,22 @@ actor.set_user(username_id):
|
|
|
84
86
|
- `Transfer` = order payment record with `amount`, optional `File` (receipt)
|
|
85
87
|
|
|
86
88
|
### Conditions (Ad parsing)
|
|
87
|
-
`Cond` -> `CondParsed`, `Synonym`, `CondSim`
|
|
89
|
+
`Cond` -> `CondParsed`, `Synonym`, `CondSim`, `CondPrompt`
|
|
88
90
|
|
|
89
91
|
- `Cond.raw_txt` = original ad conditions text (4095 chars max, unique)
|
|
90
|
-
- `CondParsed` = structured parse result: `
|
|
92
|
+
- `CondParsed` = structured parse result: `from_party` / `to_party` (`FromParty`: с чьего / на чей счёт идёт платёж
|
|
93
|
+
покупателя), `ppo`, `slip` (`Slip`) — требует/шлёт понятно из направления объявы, abuse flags, `is_intermediary`
|
|
94
|
+
(мейкер-посредник «треугол»), linked bank `Pm`s
|
|
95
|
+
— все поля nullable, дефолт `None` (= «в условии не сказано»; никаких `*.no`-заглушек). Единый writer
|
|
96
|
+
`CondParsed.upsert(cond_id, bank_ids=None, **fields)` (бот-разметка /cond и Gemini, D-043); ответ админа
|
|
97
|
+
на «сохранить разбор?» — эмиттер `CondParsed.answer(cond_id, remark=None)` → pg-канал `cond_ai_answer`
|
|
98
|
+
`[cond_id, remark]` (null — «да», текст — «нет» с замечанием, "" — «нет», замечание пишется, true — «🔄»: объяву
|
|
99
|
+
условия заново с биржи — реальный exid замаскированного мейкера + свежий `raw_txt`, затем вопрос заново); возвращает, слушает
|
|
100
|
+
ли канал хоть один прогон (pg_stat_activity) — иначе ответ пропал бы молча
|
|
101
|
+
- `CondPrompt` = версии промпта разбора: системного промпта Gemini и (`is_jev`) вопросов JEV JSON-ом — у каждого
|
|
102
|
+
действует последняя (`id` max); пишет только `CondAi` (засев исходной + правки Claude по замечаниям админа, `cond` —
|
|
103
|
+
условие правки: его одобренный `CondParsed` — эталон регресс-проверки следующих). FK на `cond` — SET NULL: история
|
|
104
|
+
версий не теряется
|
|
91
105
|
- `Synonym` = condition term with `typ` (`SynonymType`), optional regex (`is_re`), boundary matching
|
|
92
106
|
|
|
93
107
|
### Transactions (XyncPay)
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
4
5
|
import re
|
|
5
6
|
import struct
|
|
6
7
|
import sys
|
|
7
8
|
from collections.abc import Collection
|
|
8
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime, timedelta
|
|
9
10
|
from decimal import Decimal
|
|
10
11
|
from time import time
|
|
11
12
|
from typing import ClassVar
|
|
12
13
|
from uuid import UUID
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
13
15
|
|
|
14
16
|
from aiohttp import ClientSession
|
|
15
17
|
from cryptography.exceptions import InvalidSignature
|
|
@@ -19,9 +21,10 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey,
|
|
|
19
21
|
from pyro_client.models import File # noqa: F401
|
|
20
22
|
from tortoise import BackwardFKRelation, BackwardOneToOneRelation, BaseDBAsyncClient, connections
|
|
21
23
|
from tortoise import Model as BaseModel
|
|
22
|
-
from tortoise.expressions import Q
|
|
24
|
+
from tortoise.expressions import Q, Subquery
|
|
23
25
|
from tortoise.fields import (
|
|
24
26
|
CASCADE,
|
|
27
|
+
SET_NULL,
|
|
25
28
|
BigIntField,
|
|
26
29
|
BinaryField,
|
|
27
30
|
BooleanField,
|
|
@@ -40,9 +43,10 @@ from tortoise.fields import (
|
|
|
40
43
|
OneToOneNullableRelation,
|
|
41
44
|
OneToOneRelation,
|
|
42
45
|
SmallIntField,
|
|
46
|
+
TextField,
|
|
43
47
|
UUIDField,
|
|
44
48
|
)
|
|
45
|
-
from tortoise.functions import Sum
|
|
49
|
+
from tortoise.functions import Count, Sum
|
|
46
50
|
from tortoise.migrations.constraints import CheckConstraint
|
|
47
51
|
from tortoise.signals import pre_save
|
|
48
52
|
from tortoise.timezone import now
|
|
@@ -63,7 +67,7 @@ from x_auth.models import ( # noqa: F401
|
|
|
63
67
|
)
|
|
64
68
|
from x_auth.models import Country as BaseCountry
|
|
65
69
|
from x_auth.models import User as TgUser
|
|
66
|
-
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field, UniqBinaryField
|
|
70
|
+
from x_model.field import DatetimeSecField, UInt1Field, UInt2Field, UInt4Field, UInt8Field, UInt16Field, UniqBinaryField
|
|
67
71
|
from x_model.models import TsTrait
|
|
68
72
|
|
|
69
73
|
from xync_db.enums import (
|
|
@@ -73,15 +77,18 @@ from xync_db.enums import (
|
|
|
73
77
|
AgentStatus,
|
|
74
78
|
Boundary,
|
|
75
79
|
CoinType,
|
|
80
|
+
CredType,
|
|
76
81
|
DepType,
|
|
77
82
|
ExAction,
|
|
78
83
|
ExStatus,
|
|
79
84
|
ExType,
|
|
85
|
+
FromParty,
|
|
86
|
+
GeminiModel,
|
|
87
|
+
GeminiRes,
|
|
80
88
|
HotStatus,
|
|
81
89
|
LinkType,
|
|
82
90
|
OrderErr,
|
|
83
91
|
OrderStatus,
|
|
84
|
-
Party,
|
|
85
92
|
PersonStatus,
|
|
86
93
|
PmType,
|
|
87
94
|
SbpStrict,
|
|
@@ -237,10 +244,11 @@ class Ex(Model):
|
|
|
237
244
|
client = sys.modules[module_name].ExClient
|
|
238
245
|
return client(self, **kwargs)
|
|
239
246
|
|
|
240
|
-
def
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
def agent_cls(self) -> type:
|
|
248
|
+
# per-Ex AgentClient: и конструктор живого агента (Agent.client), и classmethod'ы-ссылки, которым сессия
|
|
249
|
+
# не нужна (get_url/order_url) — зовутся по классу из prefetch'нутых данных в любом процессе
|
|
250
|
+
__import__(module := f"xync_client.exs.{self.name}.agent")
|
|
251
|
+
return sys.modules[module].AgentClient
|
|
244
252
|
|
|
245
253
|
|
|
246
254
|
class CurEx(BaseModel):
|
|
@@ -957,21 +965,9 @@ class User(TgUser, TsTrait):
|
|
|
957
965
|
ks = coin_scales.get((mad.ad.maker.ex_id, mad.ad.pair_side.pair.coin_id), 0)
|
|
958
966
|
return round(mad.ad.quantity * 10**-ks, ks)
|
|
959
967
|
|
|
960
|
-
# web/mob ссылки на объяву биржеспецифичны (формат/наличие share-hex), поэтому формирует их per-Ex
|
|
961
|
-
# AgentClient.get_url. Берём класс нужной биржи тем же __import__, что Agent.client, но дёргаем
|
|
962
|
-
# classmethod без живого агент-клиента: ссылку считаем из самой (prefetch'нутой) объявы, сессия не нужна.
|
|
963
|
-
url_clss: dict[str, type] = {}
|
|
964
|
-
|
|
965
|
-
def ad_url(mad: MyAd) -> tuple[str | None, str | None]:
|
|
966
|
-
name = mad.ad.maker.ex.name
|
|
967
|
-
if (acl := url_clss.get(name)) is None:
|
|
968
|
-
__import__(module := f"xync_client.exs.{name}.agent")
|
|
969
|
-
acl = url_clss[name] = sys.modules[module].AgentClient
|
|
970
|
-
return acl.get_url(mad)
|
|
971
|
-
|
|
972
968
|
take_sales, take_buys = [], []
|
|
973
969
|
for buy_ad in buy_ads:
|
|
974
|
-
web_url, mob_url =
|
|
970
|
+
web_url, mob_url = buy_ad.urls()
|
|
975
971
|
cur_scale = cur_scales[(buy_ad.ad.maker.ex_id, buy_ad.ad.pair_side.pair.cur_id)]
|
|
976
972
|
if mine: # для своих объяв лимит берем из самого объявления, без сверки с балансами
|
|
977
973
|
max_fiat = buy_ad.ad.max_fiat
|
|
@@ -1019,7 +1015,7 @@ class User(TgUser, TsTrait):
|
|
|
1019
1015
|
max_fiat = int(min(sell_ad.ad.max_fiat, deposit)) if sell_ad.ad.max_fiat else int(deposit)
|
|
1020
1016
|
if max_fiat < min_fiat:
|
|
1021
1017
|
continue # не хватает
|
|
1022
|
-
web_url, mob_url =
|
|
1018
|
+
web_url, mob_url = sell_ad.urls()
|
|
1023
1019
|
# есть ли у юзера actor (через person) на бирже этого объявления
|
|
1024
1020
|
actor_defined = await Actor.filter(person__user_id=self.id, ex_id=sell_ad.ad.maker.ex_id).exists()
|
|
1025
1021
|
take_buys.append(
|
|
@@ -1090,12 +1086,18 @@ class Forum(Model, TsTrait):
|
|
|
1090
1086
|
|
|
1091
1087
|
|
|
1092
1088
|
class Actor(Model):
|
|
1093
|
-
|
|
1089
|
+
# реальные id бирж влезают в uint8; >= MASKED — id, замаскированный биржей (Bybit userMaskId, 128 бит):
|
|
1090
|
+
# публичный листинг объяв прячет мейкера, реальный exid добудем отдельно (D-042)
|
|
1091
|
+
MASKED: ClassVar[int] = 1 << 64
|
|
1092
|
+
exid: int = UInt16Field()
|
|
1094
1093
|
name: str = CharField(63)
|
|
1095
1094
|
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "actors", on_update=CASCADE)
|
|
1096
1095
|
ex_id: int
|
|
1097
|
-
|
|
1098
|
-
|
|
1096
|
+
# NULL — у замаскированного актора: персону (Ex.person_save) заводим, когда известен реальный exid
|
|
1097
|
+
person: ForeignKeyNullableRelation[Person] = ForeignKeyField(
|
|
1098
|
+
"models.Person", "actors", on_update=CASCADE, null=True
|
|
1099
|
+
)
|
|
1100
|
+
person_id: int | None
|
|
1099
1101
|
|
|
1100
1102
|
agent: BackwardOneToOneRelation[Agent]
|
|
1101
1103
|
# conds: BackwardFKRelation[Cond]
|
|
@@ -1197,10 +1199,7 @@ class Agent(Model, TsTrait):
|
|
|
1197
1199
|
# computed = ["balance"]
|
|
1198
1200
|
|
|
1199
1201
|
def client(self, ex_cl, pm_clients=None, proxy=None):
|
|
1200
|
-
|
|
1201
|
-
__import__(module_name)
|
|
1202
|
-
client = sys.modules[module_name].AgentClient
|
|
1203
|
-
return client(
|
|
1202
|
+
return self.actor.ex.agent_cls()(
|
|
1204
1203
|
self,
|
|
1205
1204
|
ex_cl,
|
|
1206
1205
|
pm_clients or {},
|
|
@@ -1329,12 +1328,23 @@ class Agent(Model, TsTrait):
|
|
|
1329
1328
|
|
|
1330
1329
|
|
|
1331
1330
|
class Cond(Model, TsTrait):
|
|
1332
|
-
raw_txt:
|
|
1331
|
+
# unique (raw_txt, ads.pair_side.is_sell): тот же текст у buy и sell объяв — два условия (см. side_get). Tortoise
|
|
1332
|
+
# uniq через join не выразит → триггеры ad_cond_side_uniq / cond_txt_side_uniq (0002_triggers). Условие без объяв
|
|
1333
|
+
# не живёт: осиротевшее (объява перецеплена/удалена) и новое без объявы к коммиту удаляет cond_gc (0002_triggers)
|
|
1334
|
+
raw_txt: str = CharField(4095, db_index=True)
|
|
1333
1335
|
last_ver: str = CharField(4095, null=True)
|
|
1334
1336
|
# ads: BackwardFKRelation[Ad]
|
|
1335
1337
|
# parsed: BackwardOneToOneRelation[CondParsed]
|
|
1336
1338
|
# sim: BackwardOneToOneRelation[Cond]
|
|
1337
1339
|
|
|
1340
|
+
@classmethod
|
|
1341
|
+
async def side_get(cls, txt: str, is_sell: bool) -> Cond:
|
|
1342
|
+
"""Условие с текстом `txt` для объявы стороны `is_sell`. Сторона условия — сторона его объяв
|
|
1343
|
+
(ads.pair_side.is_sell), а разбор (from_party/to_party) от неё зависит, поэтому buy и sell объявам с
|
|
1344
|
+
одинаковым текстом — разные условия. Нет условия с объявами этой стороны — новое: звать только в одной
|
|
1345
|
+
транзакции с записью объявы в него, иначе его снесёт на коммите cond_new_gc."""
|
|
1346
|
+
return await cls.filter(raw_txt=txt, ads__pair_side__is_sell=is_sell).first() or await cls.create(raw_txt=txt)
|
|
1347
|
+
|
|
1338
1348
|
async def sims(self):
|
|
1339
1349
|
return {self.raw_txt: await self.sim.sims()} if await self.sim else self.raw_txt
|
|
1340
1350
|
|
|
@@ -1346,14 +1356,14 @@ class Synonym(BaseModel):
|
|
|
1346
1356
|
curs: ManyToManyRelation[Cur] = ManyToManyField(
|
|
1347
1357
|
"models.Cur", "synonym_cur", related_name="synonyms", on_update=CASCADE
|
|
1348
1358
|
)
|
|
1349
|
-
val: int | None = UInt2Field(null=True) # SynonymType dependent (e.g.,
|
|
1359
|
+
val: int | None = UInt2Field(null=True) # SynonymType dependent (e.g., approx for ppo.)
|
|
1350
1360
|
is_re: bool = BooleanField(db_default=False)
|
|
1351
1361
|
|
|
1352
1362
|
|
|
1353
1363
|
class CondSim(BaseModel):
|
|
1354
1364
|
cond: OneToOneRelation[Cond] = OneToOneField("models.Cond", "sim", primary_key=True, on_update=CASCADE)
|
|
1355
1365
|
cond_id: int # new
|
|
1356
|
-
similarity: int = UInt2Field(db_index=True) #
|
|
1366
|
+
similarity: int = UInt2Field(db_index=True) # (SequenceMatcher.ratio - 0.6) * 10_000 — xync_client.base.Ex.get_sim
|
|
1357
1367
|
cond_rel: ForeignKeyRelation[Cond] = ForeignKeyField("models.Cond", "sims_rel", on_update=CASCADE)
|
|
1358
1368
|
cond_rel_id: int # old
|
|
1359
1369
|
|
|
@@ -1413,6 +1423,36 @@ class Pm(Model):
|
|
|
1413
1423
|
table_description = "Payment methods"
|
|
1414
1424
|
unique_together = (("norm", "country"), ("alias", "country"))
|
|
1415
1425
|
|
|
1426
|
+
@classmethod
|
|
1427
|
+
async def merge(cls, keep_id: int, dup_ids: list[int]) -> None:
|
|
1428
|
+
"""ЕДИНСТВЕННЫЙ writer слияния дублей Pm в `keep_id`: биржевые строки (PmEx), валюты (PmCur; если у keep
|
|
1429
|
+
такая валюта уже есть — креды/лимиты дубля переезжают на его PmCur), объявы, условия, override-креды, агенты
|
|
1430
|
+
и topupable; при конфликте уникальности остаётся строка keep. Дубли удаляются. Креды, которые столкнутся
|
|
1431
|
+
уникальностью (person, pmcur, …), роняют всю транзакцию — это уже дедуп кредов, не Pm. Чтобы сидер
|
|
1432
|
+
(`set_pms`) не пересоздал дубль, его биржевые имена сводятся к keep правилом `PmRep` (seed `a_tg_seed`)."""
|
|
1433
|
+
async with in_transaction() as db:
|
|
1434
|
+
for dup in dup_ids: # по одному: два дубля с общей валютой, которой нет у keep, не столкнутся
|
|
1435
|
+
for sql in (
|
|
1436
|
+
"UPDATE pmex SET pm_id=$1 WHERE pm_id=$2",
|
|
1437
|
+
"UPDATE cred c SET pmcur_id=k.id FROM pmcur d JOIN pmcur k ON k.cur_id=d.cur_id AND k.pm_id=$1"
|
|
1438
|
+
" WHERE c.pmcur_id=d.id AND d.pm_id=$2",
|
|
1439
|
+
'UPDATE "limit" l SET pmcur_id=k.id FROM pmcur d JOIN pmcur k ON k.cur_id=d.cur_id AND k.pm_id=$1'
|
|
1440
|
+
" WHERE l.pmcur_id=d.id AND d.pm_id=$2",
|
|
1441
|
+
"DELETE FROM pmcur d USING pmcur k WHERE d.pm_id=$2 AND k.pm_id=$1 AND k.cur_id=d.cur_id",
|
|
1442
|
+
"UPDATE pmcur SET pm_id=$1 WHERE pm_id=$2",
|
|
1443
|
+
"INSERT INTO ad_pm (ad_id, pm_id) SELECT ad_id, $1 FROM ad_pm WHERE pm_id=$2 ON CONFLICT DO NOTHING",
|
|
1444
|
+
"INSERT INTO cond_banks (cond_parsed_id, pm_id) SELECT cond_parsed_id, $1 FROM cond_banks"
|
|
1445
|
+
" WHERE pm_id=$2 ON CONFLICT DO NOTHING",
|
|
1446
|
+
"UPDATE cred SET ovr_pm_id=$1 WHERE ovr_pm_id=$2",
|
|
1447
|
+
"UPDATE pm_agent d SET pm_id=$1 WHERE pm_id=$2"
|
|
1448
|
+
" AND NOT EXISTS (SELECT 1 FROM pm_agent k WHERE k.pm_id=$1 AND k.user_id=d.user_id)",
|
|
1449
|
+
"UPDATE topupable SET pm_id=$1 WHERE pm_id=$2"
|
|
1450
|
+
" AND NOT EXISTS (SELECT 1 FROM topupable WHERE pm_id=$1)",
|
|
1451
|
+
):
|
|
1452
|
+
await db.execute_query(sql, [keep_id, dup])
|
|
1453
|
+
# остатки (ad_pm/cond_banks/agent/topupable, столкнувшиеся с keep) уходят каскадом
|
|
1454
|
+
await db.execute_query("DELETE FROM pm WHERE id=$1", [dup])
|
|
1455
|
+
|
|
1416
1456
|
# class PydanticMeta(Model.PydanticMeta):
|
|
1417
1457
|
# max_recursion = 3
|
|
1418
1458
|
# backward_relations = True
|
|
@@ -1427,24 +1467,125 @@ class Pm(Model):
|
|
|
1427
1467
|
class CondParsed(Model, TsTrait):
|
|
1428
1468
|
cond: OneToOneNullableRelation[Cond] = OneToOneField("models.Cond", "parsed", on_update=CASCADE, null=True)
|
|
1429
1469
|
cond_id: int | None # new
|
|
1430
|
-
|
|
1431
|
-
|
|
1470
|
+
from_party: FromParty = IntEnumField(FromParty, null=True) # с чьего счёта платят
|
|
1471
|
+
to_party: FromParty | None = IntEnumField(FromParty, null=True) # на чей счёт платят
|
|
1432
1472
|
ppo: int = UInt1Field(null=True) # Payments per order
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
abuser: AbuserType = IntEnumField(AbuserType, db_default=AbuserType.no)
|
|
1473
|
+
slip: Slip = IntEnumField(Slip, null=True)
|
|
1474
|
+
abuser: AbuserType | None = IntEnumField(AbuserType, null=True)
|
|
1436
1475
|
slavic: bool = BooleanField(null=True)
|
|
1476
|
+
cred: CredType | None = IntEnumField(
|
|
1477
|
+
CredType, null=True
|
|
1478
|
+
) # требование к реквизитам тейкера; (foreign — предупреждение мейкера-продавца о своей зарубежной карте)
|
|
1437
1479
|
mtl_like: bool = BooleanField(null=True)
|
|
1438
1480
|
scale: int = SmallIntField(null=True)
|
|
1439
1481
|
bank_side: bool = BooleanField(null=True) # False - except these banks, True - only this banks
|
|
1440
|
-
sbp_strict: SbpStrict = IntEnumField(SbpStrict,
|
|
1482
|
+
sbp_strict: SbpStrict | None = IntEnumField(SbpStrict, null=True)
|
|
1441
1483
|
contact: str | None = CharField(127, null=True)
|
|
1442
|
-
|
|
1484
|
+
is_intermediary: bool | None = BooleanField(null=True) # мейкер-посредник («треугол»): сводит с чужим ордером
|
|
1443
1485
|
banks: ManyToManyRelation[Pm] = ManyToManyField("models.Pm", "cond_banks", related_name="conds", on_update=CASCADE)
|
|
1444
1486
|
|
|
1487
|
+
ANSWER = "cond_ai_answer" # pg-канал ответа админа «сохранить разбор Gemini?» (answer → lens.parse_conds_admin)
|
|
1488
|
+
|
|
1445
1489
|
class Meta:
|
|
1446
1490
|
table = "cond_parsed"
|
|
1447
1491
|
|
|
1492
|
+
@classmethod
|
|
1493
|
+
async def upsert(cls, cond_id: int, bank_ids: list[int] | None = None, **fields) -> CondParsed:
|
|
1494
|
+
"""ЕДИНСТВЕННЫЙ writer разбора условия: бот-разметка /cond (по полю) и Gemini (lens.parse_conds_admin,
|
|
1495
|
+
целиком). bank_ids: None — банки не трогаем, список — заменяем им cond_banks."""
|
|
1496
|
+
async with in_transaction():
|
|
1497
|
+
cp, _ = await cls.update_or_create(fields, cond_id=cond_id)
|
|
1498
|
+
if bank_ids is not None:
|
|
1499
|
+
await cp.banks.clear()
|
|
1500
|
+
await cp.banks.add(*await Pm.filter(id__in=bank_ids))
|
|
1501
|
+
return cp
|
|
1502
|
+
|
|
1503
|
+
@classmethod
|
|
1504
|
+
async def answer(cls, cond_id: int, remark: str | bool | None = None) -> bool:
|
|
1505
|
+
"""Ответ админа на вопрос «сохранить разбор Gemini?» (бот, routers/cond): `remark` None — «да», текст —
|
|
1506
|
+
«нет» с замечанием, что не так (по нему Claude правит промпт — `CondAi.fix`), "" — «нет», замечание пишется,
|
|
1507
|
+
True — «🔄»: объяву условия заново с биржи (реальный exid мейкера, свежий текст), затем вопрос заново.
|
|
1508
|
+
Ждёт его прогон lens.parse_conds_admin в ДРУГОМ процессе (скрипт/крон), а ответ ловит бот api — поэтому через
|
|
1509
|
+
pg_notify, как Agent.confirm_trade; на «да» разбор пишет сам ждущий (у него распарсенные поля).
|
|
1510
|
+
→ ждёт ли кто-то: без слушателя pg_notify молча пропадает (прогон упал/закончился) — бот скажет это админу.
|
|
1511
|
+
Слушатель виден в pg_stat_activity: его соединение после LISTEN больше ничего не выполняет."""
|
|
1512
|
+
_, [row] = await connections.get("default").execute_query(
|
|
1513
|
+
f"SELECT pg_notify('{cls.ANSWER}', jsonb_build_array($1::int, $2::jsonb)::varchar),"
|
|
1514
|
+
f" EXISTS(SELECT 1 FROM pg_stat_activity WHERE query = 'LISTEN \"{cls.ANSWER}\"') waits",
|
|
1515
|
+
[cond_id, json.dumps(remark)],
|
|
1516
|
+
)
|
|
1517
|
+
return row["waits"]
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
class CondPrompt(Model):
|
|
1521
|
+
"""Версия промпта разбора условий (`xync_service.common.cond_ai`): системного промпта Gemini или вопросов JEV
|
|
1522
|
+
(`is_jev`, JSON) — у каждого действует последняя. Новую пишет Claude по замечанию админа к отклонённому разбору
|
|
1523
|
+
(`CondAi.fix`); `cond` — это условие: когда админ одобрит его разбор, `CondParsed` условия станет эталоном
|
|
1524
|
+
регресс-проверки следующих правок."""
|
|
1525
|
+
|
|
1526
|
+
txt: str = TextField()
|
|
1527
|
+
is_jev: bool = BooleanField(db_default=False) # промпт JEV (её вопросы JSON-ом), иначе — Gemini
|
|
1528
|
+
cond: ForeignKeyNullableRelation[Cond] = ForeignKeyField( # удаление условия версию промпта не сносит
|
|
1529
|
+
"models.Cond", "prompts", on_update=CASCADE, on_delete=SET_NULL, null=True
|
|
1530
|
+
)
|
|
1531
|
+
cond_id: int | None
|
|
1532
|
+
remark: str | None = TextField(null=True) # замечание админа
|
|
1533
|
+
note: str | None = TextField(null=True) # что изменил Claude
|
|
1534
|
+
created_at: datetime = DatetimeSecField(auto_now_add=True, db_default=Now())
|
|
1535
|
+
|
|
1536
|
+
class Meta:
|
|
1537
|
+
table = "cond_prompt"
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
class GeminiKey(Model):
|
|
1541
|
+
"""Ключ Gemini API free tier (разбор условий, `xync_service.common.cond_ai`). Квота своя на каждую модель, расход и
|
|
1542
|
+
выбывание ключа — по истории его запросов (`GeminiReq`)."""
|
|
1543
|
+
|
|
1544
|
+
key: str = CharField(63, unique=True)
|
|
1545
|
+
# reqs: BackwardFKRelation[GeminiReq]
|
|
1546
|
+
|
|
1547
|
+
class Meta:
|
|
1548
|
+
table = "gemini_key"
|
|
1549
|
+
|
|
1550
|
+
@classmethod
|
|
1551
|
+
async def pick(cls, model: GeminiModel, rpm: int, rpd: int) -> GeminiKey | None:
|
|
1552
|
+
"""Ключ для запроса к `model` при квоте на ключ `rpm` запросов в минуту и `rpd` в сутки: запросов к ней с
|
|
1553
|
+
последнего сброса квоты (полночь по Тихоокеанскому — у всех ключей разом) меньше `rpd` и за минуту меньше `rpm`
|
|
1554
|
+
— сверх квоты не шлём; без 429 PerDay по ней и bad_key с последнего сброса, без прочих 429 по ней за минуту —
|
|
1555
|
+
отказ Google главнее счётчика; наименее нагруженный ею — ключи идут по кругу. None — ни одного: не ждать, а к
|
|
1556
|
+
следующей модели / громко упасть."""
|
|
1557
|
+
reset = datetime.now(ZoneInfo("America/Los_Angeles")).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
1558
|
+
minute = now() - timedelta(minutes=1)
|
|
1559
|
+
out = GeminiReq.filter(
|
|
1560
|
+
Q(res=GeminiRes.bad_key, created_at__gte=reset)
|
|
1561
|
+
| Q(res=GeminiRes.day, model=model, created_at__gte=reset)
|
|
1562
|
+
| Q(res=GeminiRes.minute, model=model, created_at__gte=minute)
|
|
1563
|
+
)
|
|
1564
|
+
return await (
|
|
1565
|
+
cls.exclude(id__in=Subquery(out.values("key_id")))
|
|
1566
|
+
.annotate(
|
|
1567
|
+
d=Count("reqs", _filter=Q(reqs__model=model, reqs__created_at__gte=reset)),
|
|
1568
|
+
m=Count("reqs", _filter=Q(reqs__model=model, reqs__created_at__gte=minute)),
|
|
1569
|
+
)
|
|
1570
|
+
.filter(d__lt=rpd, m__lt=rpm)
|
|
1571
|
+
.order_by("d", "id")
|
|
1572
|
+
.first()
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
class GeminiReq(Model):
|
|
1577
|
+
"""Запрос к Gemini (`cond_ai._generate`) — история за сутки (старше удаляет `CondAi` на входе): по ней
|
|
1578
|
+
`GeminiKey.pick` выбирает ключ, по ней же видно расход квот ключей по моделям."""
|
|
1579
|
+
|
|
1580
|
+
key: ForeignKeyRelation[GeminiKey] = ForeignKeyField("models.GeminiKey", "reqs", on_update=CASCADE)
|
|
1581
|
+
key_id: int
|
|
1582
|
+
model: GeminiModel = IntEnumField(GeminiModel)
|
|
1583
|
+
res: GeminiRes = IntEnumField(GeminiRes)
|
|
1584
|
+
created_at: datetime = DatetimeSecField(auto_now_add=True, db_default=Now())
|
|
1585
|
+
|
|
1586
|
+
class Meta:
|
|
1587
|
+
table = "gemini_req"
|
|
1588
|
+
|
|
1448
1589
|
|
|
1449
1590
|
class Ad(Model, TsTrait):
|
|
1450
1591
|
exid: int = UInt8Field(db_index=True)
|
|
@@ -1454,8 +1595,8 @@ class Ad(Model, TsTrait):
|
|
|
1454
1595
|
premium: int = SmallIntField(null=True, db_default=None) # *100
|
|
1455
1596
|
amount: int = UInt4Field(null=True) # /10^cur.scale
|
|
1456
1597
|
quantity: int = UInt8Field(null=True) # /10^coinex.scale
|
|
1457
|
-
min_fiat: int = UInt4Field() #
|
|
1458
|
-
max_fiat: int | None = UInt4Field(null=True) #
|
|
1598
|
+
min_fiat: int = UInt4Field() # целые единицы валюты, БЕЗ scale (ExClient._ad_xe2x: min — ceil, max — floor)
|
|
1599
|
+
max_fiat: int | None = UInt4Field(null=True) # целые единицы валюты, БЕЗ scale
|
|
1459
1600
|
auto_msg: str | None = CharField(4095, null=True)
|
|
1460
1601
|
status: AdStatus = IntEnumField(AdStatus, db_default=AdStatus.active)
|
|
1461
1602
|
filtered: bool = BooleanField(db_default=False)
|
|
@@ -1633,6 +1774,11 @@ class MyAd(Model): # Road
|
|
|
1633
1774
|
class Meta:
|
|
1634
1775
|
table = "my_ad"
|
|
1635
1776
|
|
|
1777
|
+
def urls(self) -> tuple[str | None, str | None]:
|
|
1778
|
+
# web/mob ссылки на объяву биржеспецифичны (формат/наличие share-hex) — строит per-Ex AgentClient.get_url
|
|
1779
|
+
# из самой объявы, без сессии. Требует ad__maker__ex + ad__pair_side__pair__coin/cur
|
|
1780
|
+
return self.ad.maker.ex.agent_cls().get_url(self)
|
|
1781
|
+
|
|
1636
1782
|
@classmethod
|
|
1637
1783
|
# запрос для получения объяв всех агентов этой биржи для разогрева
|
|
1638
1784
|
def hot_mads_query(cls, ex_ids: list[int], active: bool = True):
|
|
@@ -2374,11 +2520,8 @@ class Order(Model, TsTrait):
|
|
|
2374
2520
|
|
|
2375
2521
|
def ex_url(self) -> str:
|
|
2376
2522
|
# ссылка на этот ордер на его бирже — биржеспецифична (Bybit/orderList, Htx/tradeInfo…);
|
|
2377
|
-
# строит per-Ex AgentClient.order_url
|
|
2378
|
-
|
|
2379
|
-
module_name = f"xync_client.exs.{self.ad.maker.ex.name}.agent"
|
|
2380
|
-
__import__(module_name)
|
|
2381
|
-
return sys.modules[module_name].AgentClient.order_url(self.exid)
|
|
2523
|
+
# строит per-Ex AgentClient.order_url (classmethod из одного exid — сессия не нужна). Требует ad__maker__ex.
|
|
2524
|
+
return self.ad.maker.ex.agent_cls().order_url(self.exid)
|
|
2382
2525
|
|
|
2383
2526
|
async def get_chat(self):
|
|
2384
2527
|
await self.fetch_related("ad__pair_side", "msgs")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_bin_seed.py
RENAMED
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/a_country_seed.py
RENAMED
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/b_htx_seed.py
RENAMED
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/c_net_seed.py
RENAMED
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/countries.json
RENAMED
|
File without changes
|
|
File without changes
|
{xync_db-0.5.5.dev31 → xync_db-0.5.5.dev64}/src/xync_db/migrations/models/seed/xync_files.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|