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.
Files changed (50) hide show
  1. dotorm/__init__.py +87 -0
  2. dotorm/access.py +151 -0
  3. dotorm/builder/__init__.py +0 -0
  4. dotorm/builder/builder.py +72 -0
  5. dotorm/builder/helpers.py +63 -0
  6. dotorm/builder/mixins/__init__.py +11 -0
  7. dotorm/builder/mixins/crud.py +246 -0
  8. dotorm/builder/mixins/m2m.py +110 -0
  9. dotorm/builder/mixins/relations.py +96 -0
  10. dotorm/builder/protocol.py +63 -0
  11. dotorm/builder/request_builder.py +144 -0
  12. dotorm/components/__init__.py +18 -0
  13. dotorm/components/dialect.py +99 -0
  14. dotorm/components/filter_parser.py +195 -0
  15. dotorm/databases/__init__.py +13 -0
  16. dotorm/databases/abstract/__init__.py +25 -0
  17. dotorm/databases/abstract/dialect.py +134 -0
  18. dotorm/databases/abstract/pool.py +10 -0
  19. dotorm/databases/abstract/session.py +67 -0
  20. dotorm/databases/abstract/types.py +36 -0
  21. dotorm/databases/clickhouse/__init__.py +8 -0
  22. dotorm/databases/clickhouse/pool.py +60 -0
  23. dotorm/databases/clickhouse/session.py +100 -0
  24. dotorm/databases/mysql/__init__.py +13 -0
  25. dotorm/databases/mysql/pool.py +69 -0
  26. dotorm/databases/mysql/session.py +128 -0
  27. dotorm/databases/mysql/transaction.py +39 -0
  28. dotorm/databases/postgres/__init__.py +23 -0
  29. dotorm/databases/postgres/pool.py +133 -0
  30. dotorm/databases/postgres/session.py +174 -0
  31. dotorm/databases/postgres/transaction.py +82 -0
  32. dotorm/decorators.py +379 -0
  33. dotorm/exceptions.py +9 -0
  34. dotorm/fields.py +604 -0
  35. dotorm/integrations/__init__.py +0 -0
  36. dotorm/integrations/pydantic.py +275 -0
  37. dotorm/model.py +802 -0
  38. dotorm/orm/__init__.py +15 -0
  39. dotorm/orm/mixins/__init__.py +13 -0
  40. dotorm/orm/mixins/access.py +67 -0
  41. dotorm/orm/mixins/ddl.py +250 -0
  42. dotorm/orm/mixins/many2many.py +175 -0
  43. dotorm/orm/mixins/primary.py +218 -0
  44. dotorm/orm/mixins/relations.py +513 -0
  45. dotorm/orm/protocol.py +147 -0
  46. dotorm/orm/utils.py +39 -0
  47. dotorm-2.0.8.dist-info/METADATA +1240 -0
  48. dotorm-2.0.8.dist-info/RECORD +50 -0
  49. dotorm-2.0.8.dist-info/WHEEL +4 -0
  50. 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."""