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/decorators.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Декораторы для DotORM моделей - улучшенная версия.
|
|
3
|
+
|
|
4
|
+
@hybridmethod - декоратор для гибридных методов (работают И как classmethod И как instance).
|
|
5
|
+
@onchange - декоратор для обработчиков изменения полей.
|
|
6
|
+
@model - декоратор для бизнес-методов модели.
|
|
7
|
+
|
|
8
|
+
Эта версия улучшает типизацию через:
|
|
9
|
+
1. Generic типы с ParamSpec для точных параметров
|
|
10
|
+
2. @overload для корректной работы IDE
|
|
11
|
+
3. __slots__ для производительности
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import functools
|
|
16
|
+
from typing import (
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
TypeVar,
|
|
19
|
+
Generic,
|
|
20
|
+
Callable,
|
|
21
|
+
Any,
|
|
22
|
+
Coroutine,
|
|
23
|
+
overload,
|
|
24
|
+
ParamSpec,
|
|
25
|
+
Concatenate,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
# TypeVar для типизации
|
|
32
|
+
_T = TypeVar("_T")
|
|
33
|
+
_P = ParamSpec("_P")
|
|
34
|
+
_R = TypeVar("_R")
|
|
35
|
+
_R_co = TypeVar("_R_co", covariant=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class hybridmethod(Generic[_T, _P, _R]):
|
|
39
|
+
"""
|
|
40
|
+
Декоратор для гибридных методов (работают И как classmethod И как instance).
|
|
41
|
+
|
|
42
|
+
При вызове из класса (Model.method(...)) автоматически создает пустой instance.
|
|
43
|
+
При вызове из instance (self.method(...)) использует существующий instance.
|
|
44
|
+
|
|
45
|
+
Преимущества:
|
|
46
|
+
- Полная обратная совместимость с существующим кодом
|
|
47
|
+
- Упрощенный синтаксис в @model методах
|
|
48
|
+
- self.__class__ всегда правильный класс
|
|
49
|
+
- Точные типы параметров и возвращаемого значения
|
|
50
|
+
- IDE автокомплит работает корректно
|
|
51
|
+
|
|
52
|
+
Примеры использования:
|
|
53
|
+
```python
|
|
54
|
+
from backend.base.system.dotorm.dotorm.decorators import hybridmethod
|
|
55
|
+
from typing import Self
|
|
56
|
+
|
|
57
|
+
class DotModel:
|
|
58
|
+
|
|
59
|
+
@hybridmethod
|
|
60
|
+
async def get(self, id: int, fields: list[str] = []) -> Self:
|
|
61
|
+
'''Получить запись по ID.'''
|
|
62
|
+
cls = self.__class__
|
|
63
|
+
stmt, values = cls._builder.build_get(id, fields)
|
|
64
|
+
record = await session.execute(stmt, values)
|
|
65
|
+
return record
|
|
66
|
+
|
|
67
|
+
@hybridmethod
|
|
68
|
+
async def search(self, filter=None, **kwargs) -> list[Self]:
|
|
69
|
+
'''Поиск записей.'''
|
|
70
|
+
cls = self.__class__
|
|
71
|
+
stmt, values = cls._builder.build_search(filter, **kwargs)
|
|
72
|
+
records = await session.execute(stmt, values)
|
|
73
|
+
return records
|
|
74
|
+
|
|
75
|
+
# ✅ Вариант 1: Вызов из класса (обратная совместимость)
|
|
76
|
+
user: User = await User.get(1) # Type: User ✅
|
|
77
|
+
users: list[User] = await User.search() # Type: list[User] ✅
|
|
78
|
+
|
|
79
|
+
# ✅ Вариант 2: Вызов из instance
|
|
80
|
+
@model
|
|
81
|
+
async def create_link(self, external_id: str) -> Self:
|
|
82
|
+
link_id = await self.create(payload=link) # Type: int ✅
|
|
83
|
+
return await self.get(link_id) # Type: Self ✅
|
|
84
|
+
|
|
85
|
+
# ✅ Вариант 3: Явный пустой instance
|
|
86
|
+
Model = ChatExternalChat()
|
|
87
|
+
link = await Model.create_link("ext_123")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Типизация:
|
|
91
|
+
Декоратор сохраняет точные типы через:
|
|
92
|
+
- Generic[_T, _P, _R] для типа класса, параметров и результата
|
|
93
|
+
- ParamSpec для точных типов параметров
|
|
94
|
+
- @overload для корректной работы IDE в обоих контекстах
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
__slots__ = ("func", "__wrapped__", "name", "__dict__")
|
|
98
|
+
|
|
99
|
+
func: Callable[..., Coroutine[Any, Any, _R]]
|
|
100
|
+
__wrapped__: Callable[..., Any]
|
|
101
|
+
__annotations__: dict[str, Any]
|
|
102
|
+
name: str
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self, func: Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]
|
|
106
|
+
) -> None:
|
|
107
|
+
self.func = func
|
|
108
|
+
self.__wrapped__ = func
|
|
109
|
+
functools.update_wrapper(self, func)
|
|
110
|
+
self.__annotations__ = getattr(func, "__annotations__", {})
|
|
111
|
+
self.name = ""
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
def __get__(
|
|
115
|
+
self, instance: None, owner: type[_T]
|
|
116
|
+
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
117
|
+
"""Вызов из класса: Model.method(...)"""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def __get__(
|
|
122
|
+
self, instance: _T, owner: type[_T]
|
|
123
|
+
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
124
|
+
"""Вызов из instance: self.method(...)"""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def __get__(
|
|
128
|
+
self, instance: _T | None, owner: type[_T]
|
|
129
|
+
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
|
130
|
+
"""
|
|
131
|
+
Дескриптор протокол - возвращает bound метод.
|
|
132
|
+
|
|
133
|
+
@overload позволяет IDE понимать типы в обоих случаях:
|
|
134
|
+
- Model.get(1) -> IDE знает что возвращает Self
|
|
135
|
+
- self.get(1) -> IDE знает что возвращает Self
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
instance: Экземпляр класса или None (если вызов из класса)
|
|
139
|
+
owner: Класс владелец
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Async функция с сохраненными типами параметров и результата
|
|
143
|
+
"""
|
|
144
|
+
if instance is None:
|
|
145
|
+
# Вызов из класса: Model.method(...)
|
|
146
|
+
# Автоматически создаем пустой instance
|
|
147
|
+
@functools.wraps(self.func)
|
|
148
|
+
async def class_method(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
|
149
|
+
empty_instance = owner()
|
|
150
|
+
return await self.func(empty_instance, *args, **kwargs)
|
|
151
|
+
|
|
152
|
+
class_method.__annotations__ = self.__annotations__
|
|
153
|
+
return class_method
|
|
154
|
+
else:
|
|
155
|
+
# Вызов из instance: self.method(...)
|
|
156
|
+
# Используем существующий instance
|
|
157
|
+
@functools.wraps(self.func)
|
|
158
|
+
async def instance_method(
|
|
159
|
+
*args: _P.args, **kwargs: _P.kwargs
|
|
160
|
+
) -> _R:
|
|
161
|
+
return await self.func(instance, *args, **kwargs)
|
|
162
|
+
|
|
163
|
+
instance_method.__annotations__ = self.__annotations__
|
|
164
|
+
return instance_method
|
|
165
|
+
|
|
166
|
+
def __set_name__(self, owner: type[Any], name: str) -> None:
|
|
167
|
+
"""Сохраняем имя метода для отладки."""
|
|
168
|
+
self.name = name
|
|
169
|
+
|
|
170
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
171
|
+
"""
|
|
172
|
+
Fallback для прямого вызова (не используется в runtime).
|
|
173
|
+
|
|
174
|
+
Этот метод нужен для:
|
|
175
|
+
1. Поддержки типизации в IDE
|
|
176
|
+
2. Корректной работы inspect модуля
|
|
177
|
+
"""
|
|
178
|
+
return self.func(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def depends(*field_names: str) -> Callable[[Callable], Callable]:
|
|
182
|
+
"""
|
|
183
|
+
Декоратор для вычисляемых полей.
|
|
184
|
+
Указывает, от каких полей зависит вычисление.
|
|
185
|
+
Поддерживает вложенные зависимости.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def decorator(func: Callable) -> Callable:
|
|
189
|
+
func.compute_deps = set(field_names)
|
|
190
|
+
return func
|
|
191
|
+
|
|
192
|
+
return decorator
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# def model(
|
|
196
|
+
# func: Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]],
|
|
197
|
+
# ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]:
|
|
198
|
+
# """
|
|
199
|
+
# Декоратор для бизнес-методов модели (фабричные методы, поиск, бизнес-логика).
|
|
200
|
+
|
|
201
|
+
# Помечает метод как "метод уровня модели" - работает на уровне класса,
|
|
202
|
+
# а не с конкретными записями. self может быть пустым экземпляром.
|
|
203
|
+
|
|
204
|
+
# Преимущества:
|
|
205
|
+
# - self.__class__ всегда правильный (с расширениями при наследовании)
|
|
206
|
+
# - Instance метод - легко расширять через наследование
|
|
207
|
+
# - Работает с @hybridmethod для self.get(), self.search()
|
|
208
|
+
# - Правильная типизация с Self
|
|
209
|
+
|
|
210
|
+
# Примеры использования:
|
|
211
|
+
# ```python
|
|
212
|
+
#
|
|
213
|
+
# from typing import Self
|
|
214
|
+
|
|
215
|
+
# class ChatExternalChat(DotModel):
|
|
216
|
+
|
|
217
|
+
# @model
|
|
218
|
+
# async def create_link(
|
|
219
|
+
# self,
|
|
220
|
+
# external_id: str,
|
|
221
|
+
# connector_id: int,
|
|
222
|
+
# chat_id: int
|
|
223
|
+
# ) -> Self:
|
|
224
|
+
# '''Создать связь между внешним и внутренним чатом.'''
|
|
225
|
+
# # self - пустой экземпляр
|
|
226
|
+
# # self.__class__ - ChatExternalChat (или подкласс!)
|
|
227
|
+
|
|
228
|
+
# link = self.__class__(
|
|
229
|
+
# external_id=external_id,
|
|
230
|
+
# connector_id=connector_id,
|
|
231
|
+
# chat_id=chat_id
|
|
232
|
+
# )
|
|
233
|
+
|
|
234
|
+
# # self.create(), self.get() работают благодаря @hybridmethod
|
|
235
|
+
# link_id: int = await self.create(payload=link)
|
|
236
|
+
# return await self.get(link_id)
|
|
237
|
+
|
|
238
|
+
# @model
|
|
239
|
+
# async def find_by_external_id(
|
|
240
|
+
# self,
|
|
241
|
+
# external_id: str,
|
|
242
|
+
# connector_id: int
|
|
243
|
+
# ) -> Self | None:
|
|
244
|
+
# '''Найти связь по внешнему ID.'''
|
|
245
|
+
# results: list[Self] = await self.search(
|
|
246
|
+
# filter=[
|
|
247
|
+
# ("external_id", "=", external_id),
|
|
248
|
+
# ("connector_id", "=", connector_id),
|
|
249
|
+
# ],
|
|
250
|
+
# limit=1,
|
|
251
|
+
# )
|
|
252
|
+
# return results[0] if results else None
|
|
253
|
+
|
|
254
|
+
# # Использование
|
|
255
|
+
# Chat = ChatExternalChat() # Пустой экземпляр
|
|
256
|
+
# link = await Chat.create_link("ext_123", 1, 42)
|
|
257
|
+
# found = await Chat.find_by_external_id("ext_123", 1)
|
|
258
|
+
# ```
|
|
259
|
+
|
|
260
|
+
# Расширение через наследование:
|
|
261
|
+
# ```python
|
|
262
|
+
# class TelegramChat(ChatExternalChat):
|
|
263
|
+
|
|
264
|
+
# @model
|
|
265
|
+
# async def create_link(
|
|
266
|
+
# self,
|
|
267
|
+
# external_id: str,
|
|
268
|
+
# connector_id: int,
|
|
269
|
+
# chat_id: int,
|
|
270
|
+
# thread_id: int | None = None
|
|
271
|
+
# ) -> Self:
|
|
272
|
+
# '''Расширенное создание с Telegram-специфичными данными.'''
|
|
273
|
+
# # self.__class__ = TelegramChat автоматически!
|
|
274
|
+
# link = await super().create_link(external_id, connector_id, chat_id)
|
|
275
|
+
|
|
276
|
+
# if thread_id:
|
|
277
|
+
# link.telegram_thread_id = thread_id
|
|
278
|
+
# await link.update()
|
|
279
|
+
|
|
280
|
+
# return link
|
|
281
|
+
|
|
282
|
+
# # Использование - правильный тип автоматически
|
|
283
|
+
# Telegram = TelegramChat()
|
|
284
|
+
# telegram_link = await Telegram.create_link("tg_123", 1, 42, thread_id=999)
|
|
285
|
+
# # Type: TelegramChat ✅
|
|
286
|
+
# ```
|
|
287
|
+
# """
|
|
288
|
+
|
|
289
|
+
# @functools.wraps(func)
|
|
290
|
+
# async def wrapper(
|
|
291
|
+
# self_or_cls: _T | type[_T], *args: _P.args, **kwargs: _P.kwargs
|
|
292
|
+
# ) -> _R:
|
|
293
|
+
# # Поддержка вызова и из класса и из instance
|
|
294
|
+
# if isinstance(self_or_cls, type):
|
|
295
|
+
# # Вызов из класса: ChatExternalChat.create_link(...)
|
|
296
|
+
# instance: _T = self_or_cls()
|
|
297
|
+
# else:
|
|
298
|
+
# # Вызов из instance: chat.create_link(...)
|
|
299
|
+
# instance = self_or_cls
|
|
300
|
+
|
|
301
|
+
# return await func(instance, *args, **kwargs)
|
|
302
|
+
|
|
303
|
+
# # Помечаем метод специальным атрибутом для introspection
|
|
304
|
+
# wrapper._dotorm_model_method = True # type: ignore[attr-defined]
|
|
305
|
+
# wrapper._original_func = func # type: ignore[attr-defined]
|
|
306
|
+
|
|
307
|
+
# return wrapper
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Экспортируем декораторы
|
|
311
|
+
__all__ = ["hybridmethod", "onchange"]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def onchange(*fields: str):
|
|
315
|
+
"""
|
|
316
|
+
Декоратор для регистрации обработчиков изменения полей.
|
|
317
|
+
|
|
318
|
+
При изменении указанных полей на фронтенде
|
|
319
|
+
вызывается декорированный метод, который может вернуть значения для
|
|
320
|
+
обновления других полей формы.
|
|
321
|
+
|
|
322
|
+
Примеры использования:
|
|
323
|
+
```python
|
|
324
|
+
from backend.base.system.dotorm.dotorm.decorators import onchange
|
|
325
|
+
|
|
326
|
+
class ChatConnector(DotModel):
|
|
327
|
+
|
|
328
|
+
@onchange('type')
|
|
329
|
+
async def _onchange_type(self) -> dict:
|
|
330
|
+
'''Вызывается при изменении поля type'''
|
|
331
|
+
if self.type == 'telegram':
|
|
332
|
+
return {
|
|
333
|
+
'connector_url': 'https://api.telegram.org',
|
|
334
|
+
'category': 'messenger',
|
|
335
|
+
}
|
|
336
|
+
return {}
|
|
337
|
+
|
|
338
|
+
@onchange('category', 'type')
|
|
339
|
+
async def _onchange_category_type(self) -> dict:
|
|
340
|
+
'''Вызывается при изменении category или type'''
|
|
341
|
+
# self содержит текущие значения формы
|
|
342
|
+
return {'name': f'{self.category} - {self.type}'}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Поведение:
|
|
346
|
+
- Метод должен быть async
|
|
347
|
+
- self заполняется текущими значениями формы
|
|
348
|
+
- Метод возвращает dict с полями для обновления
|
|
349
|
+
- Пустой dict {} означает "ничего не менять"
|
|
350
|
+
- Цепочки onchange НЕ поддерживаются (если onchange меняет поле
|
|
351
|
+
у которого тоже есть onchange, второй НЕ вызывается)
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
*fields: Имена полей, при изменении которых вызывать обработчик
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Декоратор функции
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
def decorator(func: Callable[..., Coroutine[Any, Any, dict]]):
|
|
361
|
+
# Помечаем функцию как onchange обработчик
|
|
362
|
+
func._onchange_fields = fields
|
|
363
|
+
func._is_onchange = True
|
|
364
|
+
|
|
365
|
+
@functools.wraps(func)
|
|
366
|
+
async def wrapper(self, *args, **kwargs) -> dict:
|
|
367
|
+
result = await func(self, *args, **kwargs)
|
|
368
|
+
# Гарантируем что результат - словарь
|
|
369
|
+
if result is None:
|
|
370
|
+
return {}
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
# Переносим метаданные на wrapper
|
|
374
|
+
wrapper._onchange_fields = fields # type: ignore
|
|
375
|
+
wrapper._is_onchange = True # type: ignore
|
|
376
|
+
|
|
377
|
+
return wrapper
|
|
378
|
+
|
|
379
|
+
return decorator
|
dotorm/exceptions.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Exceptions related to ORM and builder."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OrmConfigurationFieldException(Exception):
|
|
5
|
+
"""Exception raised when wrong config model or fields."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OrmUpdateEmptyParamsException(Exception):
|
|
9
|
+
"""Exception raised when ORM doesn't have required params."""
|