dotorm 2.0.8__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.
- dotorm/__init__.py +87 -0
- dotorm/access.py +151 -0
- dotorm/builder/__init__.py +0 -0
- dotorm/builder/builder.py +72 -0
- dotorm/builder/helpers.py +63 -0
- dotorm/builder/mixins/__init__.py +11 -0
- dotorm/builder/mixins/crud.py +246 -0
- dotorm/builder/mixins/m2m.py +110 -0
- dotorm/builder/mixins/relations.py +96 -0
- dotorm/builder/protocol.py +63 -0
- dotorm/builder/request_builder.py +144 -0
- dotorm/components/__init__.py +18 -0
- dotorm/components/dialect.py +99 -0
- dotorm/components/filter_parser.py +195 -0
- dotorm/databases/__init__.py +13 -0
- dotorm/databases/abstract/__init__.py +25 -0
- dotorm/databases/abstract/dialect.py +134 -0
- dotorm/databases/abstract/pool.py +10 -0
- dotorm/databases/abstract/session.py +67 -0
- dotorm/databases/abstract/types.py +36 -0
- dotorm/databases/clickhouse/__init__.py +8 -0
- dotorm/databases/clickhouse/pool.py +60 -0
- dotorm/databases/clickhouse/session.py +100 -0
- dotorm/databases/mysql/__init__.py +13 -0
- dotorm/databases/mysql/pool.py +69 -0
- dotorm/databases/mysql/session.py +128 -0
- dotorm/databases/mysql/transaction.py +39 -0
- dotorm/databases/postgres/__init__.py +23 -0
- dotorm/databases/postgres/pool.py +133 -0
- dotorm/databases/postgres/session.py +174 -0
- dotorm/databases/postgres/transaction.py +82 -0
- dotorm/decorators.py +379 -0
- dotorm/exceptions.py +9 -0
- dotorm/fields.py +604 -0
- dotorm/integrations/__init__.py +0 -0
- dotorm/integrations/pydantic.py +275 -0
- dotorm/model.py +802 -0
- dotorm/orm/__init__.py +15 -0
- dotorm/orm/mixins/__init__.py +13 -0
- dotorm/orm/mixins/access.py +67 -0
- dotorm/orm/mixins/ddl.py +250 -0
- dotorm/orm/mixins/many2many.py +175 -0
- dotorm/orm/mixins/primary.py +218 -0
- dotorm/orm/mixins/relations.py +513 -0
- dotorm/orm/protocol.py +147 -0
- dotorm/orm/utils.py +39 -0
- dotorm-2.0.8.dist-info/METADATA +1240 -0
- dotorm-2.0.8.dist-info/RECORD +50 -0
- dotorm-2.0.8.dist-info/WHEEL +4 -0
- dotorm-2.0.8.dist-info/licenses/LICENSE +21 -0
dotorm/fields.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""ORM field definitions."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from decimal import Decimal as PythonDecimal
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Type
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .model import DotModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("dotorm")
|
|
13
|
+
from .exceptions import OrmConfigurationFieldException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Field[FieldType]:
|
|
17
|
+
"""
|
|
18
|
+
Base field class.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
sql_type - DB field type
|
|
22
|
+
indexable - Can the field be indexed?
|
|
23
|
+
store - Is the field stored in DB? (False for computed/virtual)
|
|
24
|
+
required - Analog of null but reversed
|
|
25
|
+
|
|
26
|
+
index - Create index in database?
|
|
27
|
+
primary_key - Is the field primary key?
|
|
28
|
+
null - Is the column nullable?
|
|
29
|
+
unique - Is the field unique?
|
|
30
|
+
description - Field description
|
|
31
|
+
default - Default value
|
|
32
|
+
options - List options for selection field
|
|
33
|
+
|
|
34
|
+
relation - Is this a relation field?
|
|
35
|
+
relation_table - Related model class
|
|
36
|
+
relation_table_field - Field name in related model
|
|
37
|
+
|
|
38
|
+
schema_required - Override required status in API schema validation
|
|
39
|
+
True = required in schema (even if nullable)
|
|
40
|
+
False = optional in schema (even if not nullable)
|
|
41
|
+
None = auto-detect from type annotation
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# DB attributes
|
|
45
|
+
index: bool = False
|
|
46
|
+
primary_key: bool = False
|
|
47
|
+
null: bool = True
|
|
48
|
+
unique: bool = False
|
|
49
|
+
description: str | None = None
|
|
50
|
+
ondelete: str = "set null"
|
|
51
|
+
|
|
52
|
+
# ORM attributes
|
|
53
|
+
required: bool | None = None
|
|
54
|
+
schema_required: bool | None = None
|
|
55
|
+
sql_type: str
|
|
56
|
+
indexable: bool = True
|
|
57
|
+
store: bool = True
|
|
58
|
+
default: FieldType | None = None
|
|
59
|
+
|
|
60
|
+
string: str = ""
|
|
61
|
+
options: list[str] | None = None
|
|
62
|
+
compute: Callable | None = None
|
|
63
|
+
# compute_deps: Set[str]
|
|
64
|
+
# is_computed: bool = False
|
|
65
|
+
relation: bool = False
|
|
66
|
+
relation_table_field: str | None = None
|
|
67
|
+
# наверное перенести в класс relation
|
|
68
|
+
_relation_table: "DotModel | None" = None
|
|
69
|
+
|
|
70
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
71
|
+
# schema_required - переопределяет обязательность в API схеме
|
|
72
|
+
self.schema_required = kwargs.pop("schema_required", None)
|
|
73
|
+
|
|
74
|
+
# добавляем поле required для удобства работы
|
|
75
|
+
# которое переопределяет null
|
|
76
|
+
self.required = kwargs.pop("required", None)
|
|
77
|
+
if self.required is not None:
|
|
78
|
+
if self.required:
|
|
79
|
+
self.null = False
|
|
80
|
+
else:
|
|
81
|
+
self.null = True
|
|
82
|
+
|
|
83
|
+
# self.compute_deps: Set[str] = kwargs.pop("compute_deps", set())
|
|
84
|
+
self.indexable = kwargs.pop("indexable", self.indexable)
|
|
85
|
+
self.store = kwargs.pop("store", self.store)
|
|
86
|
+
self.ondelete = (
|
|
87
|
+
"set null" if kwargs.pop("null", self.null) else "restrict"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for name, value in kwargs.items():
|
|
91
|
+
setattr(self, name, value)
|
|
92
|
+
self.validation()
|
|
93
|
+
|
|
94
|
+
# обман тайп чекера.
|
|
95
|
+
# TODO: В идеале, сделать так чтобы тип поля менялся если это инстанс или если это класс.
|
|
96
|
+
# 1. Возможно это необходимо сделать в классе скорей всего модели
|
|
97
|
+
# 2. Или перейти на pep-0593 (Integer = Annotated[int, Integer(primary_key=True)])
|
|
98
|
+
# но тогда в классе не будет типа Field и мы получим такую же ситуаци но в классе
|
|
99
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> FieldType:
|
|
100
|
+
return super().__new__(cls)
|
|
101
|
+
|
|
102
|
+
def validation(self):
|
|
103
|
+
if not self.indexable and (self.unique or self.index):
|
|
104
|
+
raise OrmConfigurationFieldException(
|
|
105
|
+
f"{self.__class__.__name__} can't be indexed"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if self.primary_key:
|
|
109
|
+
# UNIQUE or PRIMARY KEY constraint to prevent duplicate values
|
|
110
|
+
self.unique = True
|
|
111
|
+
|
|
112
|
+
if self.sql_type == "INTEGER":
|
|
113
|
+
self.sql_type = "SERIAL"
|
|
114
|
+
elif self.sql_type == "BIGINT":
|
|
115
|
+
self.sql_type = "BIGSERIAL"
|
|
116
|
+
elif self.sql_type == "SMALLINT":
|
|
117
|
+
self.sql_type = "SMALLSERIAL"
|
|
118
|
+
else:
|
|
119
|
+
raise OrmConfigurationFieldException(
|
|
120
|
+
f"{self.__class__.__name__} primary_key supported only for integer, bigint, smallint fields"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not self.store:
|
|
124
|
+
raise OrmConfigurationFieldException(
|
|
125
|
+
f"{self.__class__.__name__} primary_key required store db"
|
|
126
|
+
)
|
|
127
|
+
if self.null:
|
|
128
|
+
log.debug(
|
|
129
|
+
f"{self.__class__.__name__} can't be both null=True and primary_key=True. Null will be ignored."
|
|
130
|
+
)
|
|
131
|
+
self.null = False
|
|
132
|
+
if self.index:
|
|
133
|
+
# self.index = False
|
|
134
|
+
raise OrmConfigurationFieldException(
|
|
135
|
+
f"{self.__class__.__name__} can't be both index=True and primary_key=True. Primary key have index already."
|
|
136
|
+
)
|
|
137
|
+
# первичный ключ уже автоинкрементируется как SERIAL и имеет значение по умолчанию
|
|
138
|
+
# DEFAULT nextval('tablename_colname_seq')
|
|
139
|
+
if self.default:
|
|
140
|
+
# self.default = None
|
|
141
|
+
raise OrmConfigurationFieldException(
|
|
142
|
+
f"{self.__class__.__name__} can't be both default=True and primary_key=True. Primary key autoincrement already."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if self.unique:
|
|
146
|
+
if self.index:
|
|
147
|
+
# self.index = False
|
|
148
|
+
raise OrmConfigurationFieldException(
|
|
149
|
+
f"{self.__class__.__name__} can't be both index=True and unique=True. Index will be ignored."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def relation_table(self) -> "DotModel | None":
|
|
154
|
+
# если модель задана через лямбда функцию
|
|
155
|
+
if (
|
|
156
|
+
self._relation_table
|
|
157
|
+
and not isinstance(self._relation_table, type)
|
|
158
|
+
and callable(self._relation_table)
|
|
159
|
+
):
|
|
160
|
+
return self._relation_table()
|
|
161
|
+
# если модель задана классом
|
|
162
|
+
return self._relation_table
|
|
163
|
+
|
|
164
|
+
@relation_table.setter
|
|
165
|
+
def relation_table(self, table):
|
|
166
|
+
self._relation_table = table
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class Integer(Field[int]):
|
|
170
|
+
"""Integer field (32-bit signed)."""
|
|
171
|
+
|
|
172
|
+
field_type = int
|
|
173
|
+
sql_type = "INTEGER"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class BigInteger(Field[int]):
|
|
177
|
+
"""Big integer field (64-bit signed)."""
|
|
178
|
+
|
|
179
|
+
sql_type = "BIGINT"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class SmallInteger(Field[int]):
|
|
183
|
+
"""Small integer field (16-bit signed)."""
|
|
184
|
+
|
|
185
|
+
sql_type = "SMALLINT"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Char(Field[str]):
|
|
189
|
+
"""Character field with optional max_length."""
|
|
190
|
+
|
|
191
|
+
field_type = str
|
|
192
|
+
|
|
193
|
+
def __init__(self, max_length: int | None = None, **kwargs: Any) -> None:
|
|
194
|
+
if max_length:
|
|
195
|
+
if not isinstance(max_length, int):
|
|
196
|
+
raise OrmConfigurationFieldException(
|
|
197
|
+
"'max_length' should be int, got %s" % type(max_length)
|
|
198
|
+
)
|
|
199
|
+
if max_length < 1:
|
|
200
|
+
raise OrmConfigurationFieldException(
|
|
201
|
+
"'max_length' must be >= 1"
|
|
202
|
+
)
|
|
203
|
+
self.max_length = max_length
|
|
204
|
+
super().__init__(**kwargs)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def sql_type(self) -> str:
|
|
208
|
+
if self.max_length:
|
|
209
|
+
return f"VARCHAR({self.max_length})"
|
|
210
|
+
return "VARCHAR"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Selection(Char):
|
|
214
|
+
"""
|
|
215
|
+
Selection field - выбор из списка опций.
|
|
216
|
+
|
|
217
|
+
Хранится как VARCHAR, но имеет ограниченный набор допустимых значений.
|
|
218
|
+
|
|
219
|
+
Поддерживает расширение через @extend с selection_add:
|
|
220
|
+
|
|
221
|
+
# Базовая модель
|
|
222
|
+
class ChatConnector(DotModel):
|
|
223
|
+
__table__ = "chat_connector"
|
|
224
|
+
type = Selection(
|
|
225
|
+
options=[("internal", "Internal")],
|
|
226
|
+
default="internal",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Расширение из другого модуля
|
|
230
|
+
@extend(ChatConnector)
|
|
231
|
+
class ChatConnectorTelegramMixin:
|
|
232
|
+
type = Selection(selection_add=[("telegram", "Telegram")])
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
options: Список кортежей (value, label) - базовые опции
|
|
236
|
+
selection_add: Дополнительные опции для расширения существующего поля
|
|
237
|
+
default: Значение по умолчанию
|
|
238
|
+
required: Обязательное поле
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
options: list[tuple[str, str]] | None = None,
|
|
244
|
+
selection_add: list[tuple[str, str]] | None = None,
|
|
245
|
+
**kwargs,
|
|
246
|
+
):
|
|
247
|
+
# Базовые опции
|
|
248
|
+
self._base_options: list[tuple[str, str]] = options or []
|
|
249
|
+
# Опции добавленные через selection_add (из @extend)
|
|
250
|
+
self._added_options: list[tuple[str, str]] = []
|
|
251
|
+
# selection_add при инициализации (для @extend)
|
|
252
|
+
self._selection_add = selection_add
|
|
253
|
+
|
|
254
|
+
# Для Char нужен max_length
|
|
255
|
+
if "max_length" not in kwargs:
|
|
256
|
+
kwargs["max_length"] = 64
|
|
257
|
+
|
|
258
|
+
super().__init__(**kwargs)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def options(self) -> list[tuple[str, str]]:
|
|
262
|
+
"""Все опции включая добавленные через extend."""
|
|
263
|
+
return self._base_options + self._added_options
|
|
264
|
+
|
|
265
|
+
@options.setter
|
|
266
|
+
def options(self, value: list[tuple[str, str]]):
|
|
267
|
+
"""Установить базовые опции."""
|
|
268
|
+
self._base_options = value or []
|
|
269
|
+
|
|
270
|
+
def add_options(self, new_options: list[tuple[str, str]]) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Добавить опции к полю.
|
|
273
|
+
|
|
274
|
+
Используется системой расширений (@extend) для добавления
|
|
275
|
+
новых значений в Selection поле.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
new_options: Список кортежей (value, label)
|
|
279
|
+
"""
|
|
280
|
+
for opt in new_options:
|
|
281
|
+
if (
|
|
282
|
+
opt not in self._base_options
|
|
283
|
+
and opt not in self._added_options
|
|
284
|
+
):
|
|
285
|
+
self._added_options.append(opt)
|
|
286
|
+
|
|
287
|
+
def get_values(self) -> list[str]:
|
|
288
|
+
"""Получить список допустимых значений (без labels)."""
|
|
289
|
+
return [opt[0] for opt in self.options]
|
|
290
|
+
|
|
291
|
+
def get_label(self, value: str) -> str | None:
|
|
292
|
+
"""Получить label для значения."""
|
|
293
|
+
for opt_value, opt_label in self.options:
|
|
294
|
+
if opt_value == value:
|
|
295
|
+
return opt_label
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def is_selection_add(self) -> bool:
|
|
299
|
+
"""Проверить является ли это расширением (selection_add)."""
|
|
300
|
+
return self._selection_add is not None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Text(Field[str]):
|
|
304
|
+
"""Large text field."""
|
|
305
|
+
|
|
306
|
+
field_type = str
|
|
307
|
+
indexable = False
|
|
308
|
+
sql_type = "TEXT"
|
|
309
|
+
|
|
310
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
311
|
+
super().__init__(**kwargs)
|
|
312
|
+
if self.unique:
|
|
313
|
+
raise OrmConfigurationFieldException(
|
|
314
|
+
"TextField doesn't support unique indexes, consider CharField or another strategy"
|
|
315
|
+
)
|
|
316
|
+
if self.index:
|
|
317
|
+
raise OrmConfigurationFieldException(
|
|
318
|
+
"TextField can't be indexed, consider CharField"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
class _db_mysql:
|
|
322
|
+
sql_type = "LONGTEXT"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Boolean(Field[bool]):
|
|
326
|
+
"""Boolean field."""
|
|
327
|
+
|
|
328
|
+
field_type = bool
|
|
329
|
+
sql_type = "BOOL"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class Decimal(Field[PythonDecimal]):
|
|
333
|
+
"""Accurate decimal field."""
|
|
334
|
+
|
|
335
|
+
def __init__(
|
|
336
|
+
self, max_digits: int, decimal_places: int, **kwargs: Any
|
|
337
|
+
) -> None:
|
|
338
|
+
if int(max_digits) < 1:
|
|
339
|
+
raise OrmConfigurationFieldException("'max_digits' must be >= 1")
|
|
340
|
+
if int(decimal_places) < 0:
|
|
341
|
+
raise OrmConfigurationFieldException(
|
|
342
|
+
"'decimal_places' must be >= 0"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
self.max_digits = int(max_digits)
|
|
346
|
+
self.decimal_places = int(decimal_places)
|
|
347
|
+
super().__init__(**kwargs)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def sql_type(self) -> str:
|
|
351
|
+
return f"DECIMAL({self.max_digits},{self.decimal_places})"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class Datetime(Field[datetime.datetime]):
|
|
355
|
+
"""Datetime field."""
|
|
356
|
+
|
|
357
|
+
sql_type = "TIMESTAMPTZ"
|
|
358
|
+
|
|
359
|
+
class _db_mysql:
|
|
360
|
+
sql_type = "DATETIME(6)"
|
|
361
|
+
|
|
362
|
+
class _db_postgres:
|
|
363
|
+
sql_type = "TIMESTAMPTZ"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class Date(Field[datetime.date]):
|
|
367
|
+
"""Date field."""
|
|
368
|
+
|
|
369
|
+
sql_type = "DATE"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class Time(Field[datetime.time]):
|
|
373
|
+
"""Time field."""
|
|
374
|
+
|
|
375
|
+
sql_type = "TIME"
|
|
376
|
+
|
|
377
|
+
class _db_mysql:
|
|
378
|
+
sql_type = "TIME(6)"
|
|
379
|
+
|
|
380
|
+
class _db_postgres:
|
|
381
|
+
sql_type = "TIMETZ"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Float(Field[float]):
|
|
385
|
+
"""Float (double) field."""
|
|
386
|
+
|
|
387
|
+
sql_type = "DOUBLE PRECISION"
|
|
388
|
+
|
|
389
|
+
class _db_mysql:
|
|
390
|
+
sql_type = "DOUBLE"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class JSONField(Field[dict | list]):
|
|
394
|
+
"""JSON field."""
|
|
395
|
+
|
|
396
|
+
sql_type = "JSONB"
|
|
397
|
+
indexable = False
|
|
398
|
+
|
|
399
|
+
class _db_mysql:
|
|
400
|
+
sql_type = "JSON"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class Binary(Field[bytes]):
|
|
404
|
+
"""Binary bytes field."""
|
|
405
|
+
|
|
406
|
+
sql_type = "BYTEA"
|
|
407
|
+
indexable = False
|
|
408
|
+
|
|
409
|
+
class _db_mysql:
|
|
410
|
+
sql_type = "VARBINARY"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ==================== RELATION FIELDS ====================
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class Many2one[T: "DotModel"](Field[T]):
|
|
417
|
+
"""Many-to-one relation field."""
|
|
418
|
+
|
|
419
|
+
field_type = Type
|
|
420
|
+
sql_type = "INTEGER"
|
|
421
|
+
relation = True
|
|
422
|
+
relation_table: "DotModel"
|
|
423
|
+
|
|
424
|
+
def __init__(self, relation_table: T, **kwargs: Any) -> None:
|
|
425
|
+
self._relation_table = relation_table
|
|
426
|
+
super().__init__(**kwargs)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class AttachmentMany2one[T: "DotModel"](Field[T]):
|
|
430
|
+
"""Many-to-one attachment field."""
|
|
431
|
+
|
|
432
|
+
field_type = Type
|
|
433
|
+
sql_type = "INTEGER"
|
|
434
|
+
relation = True
|
|
435
|
+
relation_table: "DotModel"
|
|
436
|
+
|
|
437
|
+
def __init__(self, relation_table: T, **kwargs: Any) -> None:
|
|
438
|
+
self._relation_table = relation_table
|
|
439
|
+
super().__init__(**kwargs)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class AttachmentOne2many[T: "DotModel"](Field[list[T]]):
|
|
443
|
+
"""One-to-many attachment field."""
|
|
444
|
+
|
|
445
|
+
field_type = list[Type]
|
|
446
|
+
store = False
|
|
447
|
+
relation = True
|
|
448
|
+
relation_table: "DotModel"
|
|
449
|
+
relation_table_field: str
|
|
450
|
+
|
|
451
|
+
def __init__(
|
|
452
|
+
self,
|
|
453
|
+
relation_table: T,
|
|
454
|
+
relation_table_field: str,
|
|
455
|
+
**kwargs: Any,
|
|
456
|
+
) -> None:
|
|
457
|
+
self._relation_table = relation_table
|
|
458
|
+
self.relation_table_field = relation_table_field
|
|
459
|
+
super().__init__(**kwargs)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# class Many2manyAccessor[T: "DotModel"]:
|
|
463
|
+
# """
|
|
464
|
+
# Accessor для работы с M2M полем на экземпляре модели.
|
|
465
|
+
|
|
466
|
+
# Позволяет использовать удобный синтаксис:
|
|
467
|
+
# await chat.member_ids.link([user1_id, user2_id])
|
|
468
|
+
# await chat.member_ids.unlink([user_id])
|
|
469
|
+
|
|
470
|
+
# Вместо:
|
|
471
|
+
# await chat.link_many2many(field=Chat.member_ids, values=[[chat.id, user_id]])
|
|
472
|
+
# """
|
|
473
|
+
|
|
474
|
+
# __slots__ = ("_instance", "_field", "_data")
|
|
475
|
+
|
|
476
|
+
# def __init__(
|
|
477
|
+
# self,
|
|
478
|
+
# instance: "DotModel",
|
|
479
|
+
# field: "Many2many[T]",
|
|
480
|
+
# data: list[T] | None = None,
|
|
481
|
+
# ):
|
|
482
|
+
# self._instance = instance
|
|
483
|
+
# self._field = field
|
|
484
|
+
# self._data = data
|
|
485
|
+
|
|
486
|
+
# async def link(self, ids: list[int], session=None):
|
|
487
|
+
# """
|
|
488
|
+
# Добавить связи M2M.
|
|
489
|
+
|
|
490
|
+
# Args:
|
|
491
|
+
# ids: Список ID записей для связывания
|
|
492
|
+
# session: Сессия БД
|
|
493
|
+
|
|
494
|
+
# Example:
|
|
495
|
+
# await chat.member_ids.link([user1_id, user2_id])
|
|
496
|
+
# """
|
|
497
|
+
# values = [[self._instance.id, id] for id in ids]
|
|
498
|
+
# return await self._instance.link_many2many(
|
|
499
|
+
# self._field, values, session
|
|
500
|
+
# )
|
|
501
|
+
|
|
502
|
+
# async def unlink(self, ids: list[int], session=None):
|
|
503
|
+
# """
|
|
504
|
+
# Удалить связи M2M.
|
|
505
|
+
|
|
506
|
+
# Args:
|
|
507
|
+
# ids: Список ID записей для отвязывания
|
|
508
|
+
# session: Сессия БД
|
|
509
|
+
|
|
510
|
+
# Example:
|
|
511
|
+
# await chat.member_ids.unlink([user_id])
|
|
512
|
+
# """
|
|
513
|
+
# return await self._instance.unlink_many2many(self._field, ids, session)
|
|
514
|
+
|
|
515
|
+
# # Поддержка итерации по загруженным данным
|
|
516
|
+
# def __iter__(self):
|
|
517
|
+
# if self._data is None:
|
|
518
|
+
# return iter([])
|
|
519
|
+
# return iter(self._data)
|
|
520
|
+
|
|
521
|
+
# def __len__(self):
|
|
522
|
+
# if self._data is None:
|
|
523
|
+
# return 0
|
|
524
|
+
# return len(self._data)
|
|
525
|
+
|
|
526
|
+
# def __bool__(self):
|
|
527
|
+
# return self._data is not None and len(self._data) > 0
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class Many2many[T: "DotModel"](Field[list[T]]):
|
|
531
|
+
"""Many-to-many relation field."""
|
|
532
|
+
|
|
533
|
+
field_type = list[Type]
|
|
534
|
+
store = False
|
|
535
|
+
relation = True
|
|
536
|
+
relation_table: "DotModel"
|
|
537
|
+
many2many_table: str
|
|
538
|
+
|
|
539
|
+
def __init__(
|
|
540
|
+
self,
|
|
541
|
+
relation_table: T,
|
|
542
|
+
many2many_table: str,
|
|
543
|
+
column1: str,
|
|
544
|
+
column2: str,
|
|
545
|
+
**kwargs: Any,
|
|
546
|
+
) -> None:
|
|
547
|
+
self.relation_table = relation_table
|
|
548
|
+
self.many2many_table = many2many_table
|
|
549
|
+
self.column1: str = column1
|
|
550
|
+
self.column2 = column2
|
|
551
|
+
super().__init__(**kwargs)
|
|
552
|
+
|
|
553
|
+
# def __get__(
|
|
554
|
+
# self, instance: "DotModel | None", owner: type
|
|
555
|
+
# ) -> "Many2many[T] | Many2manyAccessor[T]":
|
|
556
|
+
# if instance is None:
|
|
557
|
+
# # Доступ через класс — возвращаем дескриптор (для get_fields и т.д.)
|
|
558
|
+
# return self
|
|
559
|
+
# # Доступ через экземпляр — возвращаем accessor
|
|
560
|
+
# # Получаем данные если они были загружены в __dict__
|
|
561
|
+
# data = instance.__dict__.get(self._field_name)
|
|
562
|
+
# return Many2manyAccessor(instance, self, data)
|
|
563
|
+
|
|
564
|
+
# def __set_name__(self, owner: type, name: str):
|
|
565
|
+
# self._field_name = name
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class One2many[T: "DotModel"](Field[list[T]]):
|
|
569
|
+
"""One-to-many relation field."""
|
|
570
|
+
|
|
571
|
+
field_type = list[Type]
|
|
572
|
+
store = False
|
|
573
|
+
relation = True
|
|
574
|
+
relation_table: "DotModel"
|
|
575
|
+
relation_table_field: str
|
|
576
|
+
|
|
577
|
+
def __init__(
|
|
578
|
+
self,
|
|
579
|
+
relation_table: T,
|
|
580
|
+
relation_table_field: str,
|
|
581
|
+
**kwargs: Any,
|
|
582
|
+
) -> None:
|
|
583
|
+
self._relation_table = relation_table
|
|
584
|
+
self.relation_table_field = relation_table_field
|
|
585
|
+
super().__init__(**kwargs)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class One2one[T: "DotModel"](Field[T]):
|
|
589
|
+
"""One-to-one relation field."""
|
|
590
|
+
|
|
591
|
+
field_type = Type
|
|
592
|
+
store = False
|
|
593
|
+
relation = True
|
|
594
|
+
relation_table: "DotModel"
|
|
595
|
+
|
|
596
|
+
def __init__(
|
|
597
|
+
self,
|
|
598
|
+
relation_table: T,
|
|
599
|
+
relation_table_field: str,
|
|
600
|
+
**kwargs: Any,
|
|
601
|
+
) -> None:
|
|
602
|
+
self._relation_table = relation_table
|
|
603
|
+
self.relation_table_field = relation_table_field
|
|
604
|
+
super().__init__(**kwargs)
|
|
File without changes
|