dotorm
Advanced tools
@@ -49,3 +49,3 @@ """ | ||
| __version__ = "2.0.6" | ||
| __version__ = "2.0.7" | ||
@@ -52,0 +52,0 @@ __all__ = [ |
@@ -164,3 +164,4 @@ """CRUD operations mixin.""" | ||
| if sort not in store_fields: | ||
| raise ValueError(f"Invalid sort field: {sort}") | ||
| sort = store_fields[0] | ||
| # raise ValueError(f"Invalid sort field: {sort}") | ||
@@ -199,1 +200,49 @@ fields_store_stmt = ", ".join( | ||
| return stmt, val | ||
| def build_search_count( | ||
| self: "BuilderProtocol", | ||
| filter: FilterExpression | None = None, | ||
| ) -> tuple[str, tuple]: | ||
| """ | ||
| Build COUNT query with filter. | ||
| Args: | ||
| filter: Filter expression | ||
| Returns: | ||
| Tuple of (query, values) | ||
| """ | ||
| where = "" | ||
| where_values: tuple = () | ||
| if filter: | ||
| where_clause, where_values = self.filter_parser.parse(filter) | ||
| where = f"WHERE {where_clause}" | ||
| stmt = f"SELECT COUNT(*) as count FROM {self.table} {where}" | ||
| return stmt, where_values | ||
| def build_exists( | ||
| self: "BuilderProtocol", | ||
| filter: FilterExpression | None = None, | ||
| ) -> tuple[str, tuple]: | ||
| """ | ||
| Build EXISTS query with filter. | ||
| Args: | ||
| filter: Filter expression | ||
| Returns: | ||
| Tuple of (query, values) | ||
| """ | ||
| where = "" | ||
| where_values: tuple = () | ||
| if filter: | ||
| where_clause, where_values = self.filter_parser.parse(filter) | ||
| where = f"WHERE {where_clause}" | ||
| stmt = f"SELECT 1 FROM {self.table} {where} LIMIT 1" | ||
| return stmt, where_values |
@@ -8,3 +8,3 @@ """Protocol defining what mixins expect from the base class.""" | ||
| if TYPE_CHECKING: | ||
| from ..orm.model import DotModel | ||
| from ..model import DotModel | ||
| from ..fields import Field | ||
@@ -11,0 +11,0 @@ from ..components.dialect import Dialect |
@@ -118,2 +118,10 @@ """ | ||
| elif op in ("=", "!=", ">", "<", ">=", "<="): | ||
| # None -> IS NULL / IS NOT NULL | ||
| if value is None: | ||
| if op == "=": | ||
| return f"{field} IS NULL", () | ||
| elif op == "!=": | ||
| return f"{field} IS NOT NULL", () | ||
| else: | ||
| raise ValueError(f"Operator '{op}' cannot be used with None") | ||
| clause = f"{field} {op} %s" | ||
@@ -120,0 +128,0 @@ return clause, (value,) |
+70
-1
@@ -5,2 +5,3 @@ """ | ||
| @hybridmethod - Π΄Π΅ΠΊΠΎΡΠ°ΡΠΎΡ Π΄Π»Ρ Π³ΠΈΠ±ΡΠΈΠ΄Π½ΡΡ ΠΌΠ΅ΡΠΎΠ΄ΠΎΠ² (ΡΠ°Π±ΠΎΡΠ°ΡΡ Π ΠΊΠ°ΠΊ classmethod Π ΠΊΠ°ΠΊ instance). | ||
| @onchange - Π΄Π΅ΠΊΠΎΡΠ°ΡΠΎΡ Π΄Π»Ρ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ² ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΠΏΠΎΠ»Π΅ΠΉ. | ||
| @model - Π΄Π΅ΠΊΠΎΡΠ°ΡΠΎΡ Π΄Π»Ρ Π±ΠΈΠ·Π½Π΅Ρ-ΠΌΠ΅ΡΠΎΠ΄ΠΎΠ² ΠΌΠΎΠ΄Π΅Π»ΠΈ. | ||
@@ -297,2 +298,70 @@ | ||
| # ΠΠΊΡΠΏΠΎΡΡΠΈΡΡΠ΅ΠΌ Π΄Π΅ΠΊΠΎΡΠ°ΡΠΎΡΡ | ||
| __all__ = ["hybridmethod"] | ||
| __all__ = ["hybridmethod", "onchange"] | ||
| def onchange(*fields: str): | ||
| """ | ||
| ΠΠ΅ΠΊΠΎΡΠ°ΡΠΎΡ Π΄Π»Ρ ΡΠ΅Π³ΠΈΡΡΡΠ°ΡΠΈΠΈ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ² ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΠΏΠΎΠ»Π΅ΠΉ. | ||
| ΠΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ ΡΠΊΠ°Π·Π°Π½Π½ΡΡ ΠΏΠΎΠ»Π΅ΠΉ Π½Π° ΡΡΠΎΠ½ΡΠ΅Π½Π΄Π΅ | ||
| Π²ΡΠ·ΡΠ²Π°Π΅ΡΡΡ Π΄Π΅ΠΊΠΎΡΠΈΡΠΎΠ²Π°Π½Π½ΡΠΉ ΠΌΠ΅ΡΠΎΠ΄, ΠΊΠΎΡΠΎΡΡΠΉ ΠΌΠΎΠΆΠ΅Ρ Π²Π΅ΡΠ½ΡΡΡ Π·Π½Π°ΡΠ΅Π½ΠΈΡ Π΄Π»Ρ | ||
| ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ Π΄ΡΡΠ³ΠΈΡ ΠΏΠΎΠ»Π΅ΠΉ ΡΠΎΡΠΌΡ. | ||
| ΠΡΠΈΠΌΠ΅ΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΡ: | ||
| ```python | ||
| from backend.base.system.dotorm.dotorm.decorators import onchange | ||
| class ChatConnector(DotModel): | ||
| @onchange('type') | ||
| async def _onchange_type(self) -> dict: | ||
| '''ΠΡΠ·ΡΠ²Π°Π΅ΡΡΡ ΠΏΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ ΠΏΠΎΠ»Ρ type''' | ||
| if self.type == 'telegram': | ||
| return { | ||
| 'connector_url': 'https://api.telegram.org', | ||
| 'category': 'messenger', | ||
| } | ||
| return {} | ||
| @onchange('category', 'type') | ||
| async def _onchange_category_type(self) -> dict: | ||
| '''ΠΡΠ·ΡΠ²Π°Π΅ΡΡΡ ΠΏΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ category ΠΈΠ»ΠΈ type''' | ||
| # self ΡΠΎΠ΄Π΅ΡΠΆΠΈΡ ΡΠ΅ΠΊΡΡΠΈΠ΅ Π·Π½Π°ΡΠ΅Π½ΠΈΡ ΡΠΎΡΠΌΡ | ||
| return {'name': f'{self.category} - {self.type}'} | ||
| ``` | ||
| ΠΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅: | ||
| - ΠΠ΅ΡΠΎΠ΄ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ async | ||
| - self Π·Π°ΠΏΠΎΠ»Π½ΡΠ΅ΡΡΡ ΡΠ΅ΠΊΡΡΠΈΠΌΠΈ Π·Π½Π°ΡΠ΅Π½ΠΈΡΠΌΠΈ ΡΠΎΡΠΌΡ | ||
| - ΠΠ΅ΡΠΎΠ΄ Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ dict Ρ ΠΏΠΎΠ»ΡΠΌΠΈ Π΄Π»Ρ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ | ||
| - ΠΡΡΡΠΎΠΉ dict {} ΠΎΠ·Π½Π°ΡΠ°Π΅Ρ "Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ ΠΌΠ΅Π½ΡΡΡ" | ||
| - Π¦Π΅ΠΏΠΎΡΠΊΠΈ onchange ΠΠ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°ΡΡΡΡ (Π΅ΡΠ»ΠΈ onchange ΠΌΠ΅Π½ΡΠ΅Ρ ΠΏΠΎΠ»Π΅ | ||
| Ρ ΠΊΠΎΡΠΎΡΠΎΠ³ΠΎ ΡΠΎΠΆΠ΅ Π΅ΡΡΡ onchange, Π²ΡΠΎΡΠΎΠΉ ΠΠ Π²ΡΠ·ΡΠ²Π°Π΅ΡΡΡ) | ||
| Args: | ||
| *fields: ΠΠΌΠ΅Π½Π° ΠΏΠΎΠ»Π΅ΠΉ, ΠΏΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ ΠΊΠΎΡΠΎΡΡΡ Π²ΡΠ·ΡΠ²Π°ΡΡ ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊ | ||
| Returns: | ||
| ΠΠ΅ΠΊΠΎΡΠ°ΡΠΎΡ ΡΡΠ½ΠΊΡΠΈΠΈ | ||
| """ | ||
| def decorator(func: Callable[..., Coroutine[Any, Any, dict]]): | ||
| # ΠΠΎΠΌΠ΅ΡΠ°Π΅ΠΌ ΡΡΠ½ΠΊΡΠΈΡ ΠΊΠ°ΠΊ onchange ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊ | ||
| func._onchange_fields = fields | ||
| func._is_onchange = True | ||
| @functools.wraps(func) | ||
| async def wrapper(self, *args, **kwargs) -> dict: | ||
| result = await func(self, *args, **kwargs) | ||
| # ΠΠ°ΡΠ°Π½ΡΠΈΡΡΠ΅ΠΌ ΡΡΠΎ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ - ΡΠ»ΠΎΠ²Π°ΡΡ | ||
| if result is None: | ||
| return {} | ||
| return result | ||
| # ΠΠ΅ΡΠ΅Π½ΠΎΡΠΈΠΌ ΠΌΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅ Π½Π° wrapper | ||
| wrapper._onchange_fields = fields | ||
| wrapper._is_onchange = True | ||
| return wrapper | ||
| return decorator |
+85
-1
@@ -211,3 +211,87 @@ """ORM field definitions.""" | ||
| class Selection(Char): ... | ||
| class Selection(Char): | ||
| """ | ||
| Selection field - Π²ΡΠ±ΠΎΡ ΠΈΠ· ΡΠΏΠΈΡΠΊΠ° ΠΎΠΏΡΠΈΠΉ. | ||
| Π₯ΡΠ°Π½ΠΈΡΡΡ ΠΊΠ°ΠΊ VARCHAR, Π½ΠΎ ΠΈΠΌΠ΅Π΅Ρ ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΡΠΉ Π½Π°Π±ΠΎΡ Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΡ Π·Π½Π°ΡΠ΅Π½ΠΈΠΉ. | ||
| ΠΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅Ρ ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΠ΅ ΡΠ΅ΡΠ΅Π· @extend Ρ selection_add: | ||
| # ΠΠ°Π·ΠΎΠ²Π°Ρ ΠΌΠΎΠ΄Π΅Π»Ρ | ||
| class ChatConnector(DotModel): | ||
| __table__ = "chat_connector" | ||
| type = Selection( | ||
| options=[("internal", "Internal")], | ||
| default="internal", | ||
| ) | ||
| # Π Π°ΡΡΠΈΡΠ΅Π½ΠΈΠ΅ ΠΈΠ· Π΄ΡΡΠ³ΠΎΠ³ΠΎ ΠΌΠΎΠ΄ΡΠ»Ρ | ||
| @extend(ChatConnector) | ||
| class ChatConnectorTelegramMixin: | ||
| type = Selection(selection_add=[("telegram", "Telegram")]) | ||
| Args: | ||
| options: Π‘ΠΏΠΈΡΠΎΠΊ ΠΊΠΎΡΡΠ΅ΠΆΠ΅ΠΉ (value, label) - Π±Π°Π·ΠΎΠ²ΡΠ΅ ΠΎΠΏΡΠΈΠΈ | ||
| selection_add: ΠΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½ΡΠ΅ ΠΎΠΏΡΠΈΠΈ Π΄Π»Ρ ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΡ ΡΡΡΠ΅ΡΡΠ²ΡΡΡΠ΅Π³ΠΎ ΠΏΠΎΠ»Ρ | ||
| default: ΠΠ½Π°ΡΠ΅Π½ΠΈΠ΅ ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ | ||
| required: ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΠΎΠ΅ ΠΏΠΎΠ»Π΅ | ||
| """ | ||
| def __init__( | ||
| self, | ||
| options: list[tuple[str, str]] | None = None, | ||
| selection_add: list[tuple[str, str]] | None = None, | ||
| **kwargs | ||
| ): | ||
| # ΠΠ°Π·ΠΎΠ²ΡΠ΅ ΠΎΠΏΡΠΈΠΈ | ||
| self._base_options: list[tuple[str, str]] = options or [] | ||
| # ΠΠΏΡΠΈΠΈ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π½ΡΠ΅ ΡΠ΅ΡΠ΅Π· selection_add (ΠΈΠ· @extend) | ||
| self._added_options: list[tuple[str, str]] = [] | ||
| # selection_add ΠΏΡΠΈ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ (Π΄Π»Ρ @extend) | ||
| self._selection_add = selection_add | ||
| # ΠΠ»Ρ Char Π½ΡΠΆΠ΅Π½ max_length | ||
| if "max_length" not in kwargs: | ||
| kwargs["max_length"] = 64 | ||
| super().__init__(**kwargs) | ||
| @property | ||
| def options(self) -> list[tuple[str, str]]: | ||
| """ΠΡΠ΅ ΠΎΠΏΡΠΈΠΈ Π²ΠΊΠ»ΡΡΠ°Ρ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π½ΡΠ΅ ΡΠ΅ΡΠ΅Π· extend.""" | ||
| return self._base_options + self._added_options | ||
| @options.setter | ||
| def options(self, value: list[tuple[str, str]]): | ||
| """Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ Π±Π°Π·ΠΎΠ²ΡΠ΅ ΠΎΠΏΡΠΈΠΈ.""" | ||
| self._base_options = value or [] | ||
| def add_options(self, new_options: list[tuple[str, str]]) -> None: | ||
| """ | ||
| ΠΠΎΠ±Π°Π²ΠΈΡΡ ΠΎΠΏΡΠΈΠΈ ΠΊ ΠΏΠΎΠ»Ρ. | ||
| ΠΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ ΡΠΈΡΡΠ΅ΠΌΠΎΠΉ ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΠΉ (@extend) Π΄Π»Ρ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΡ | ||
| Π½ΠΎΠ²ΡΡ Π·Π½Π°ΡΠ΅Π½ΠΈΠΉ Π² Selection ΠΏΠΎΠ»Π΅. | ||
| Args: | ||
| new_options: Π‘ΠΏΠΈΡΠΎΠΊ ΠΊΠΎΡΡΠ΅ΠΆΠ΅ΠΉ (value, label) | ||
| """ | ||
| for opt in new_options: | ||
| if opt not in self._base_options and opt not in self._added_options: | ||
| self._added_options.append(opt) | ||
| def get_values(self) -> list[str]: | ||
| """ΠΠΎΠ»ΡΡΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ Π΄ΠΎΠΏΡΡΡΠΈΠΌΡΡ Π·Π½Π°ΡΠ΅Π½ΠΈΠΉ (Π±Π΅Π· labels).""" | ||
| return [opt[0] for opt in self.options] | ||
| def get_label(self, value: str) -> str | None: | ||
| """ΠΠΎΠ»ΡΡΠΈΡΡ label Π΄Π»Ρ Π·Π½Π°ΡΠ΅Π½ΠΈΡ.""" | ||
| for opt_value, opt_label in self.options: | ||
| if opt_value == value: | ||
| return opt_label | ||
| return None | ||
| def is_selection_add(self) -> bool: | ||
| """ΠΡΠΎΠ²Π΅ΡΠΈΡΡ ΡΠ²Π»ΡΠ΅ΡΡΡ Π»ΠΈ ΡΡΠΎ ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΠ΅ΠΌ (selection_add).""" | ||
| return self._selection_add is not None | ||
@@ -214,0 +298,0 @@ |
+77
-3
@@ -126,2 +126,4 @@ """DotModel - main ORM model class.""" | ||
| __table__: ClassVar[str] | ||
| # create table in db | ||
| __auto_create__: ClassVar[bool] = True | ||
| # path name for route ednpoints CRUD | ||
@@ -569,4 +571,8 @@ __route__: ClassVar[str] | ||
| # ΠΈΠ½Π°ΡΠ΅ Π²Π·ΡΡΡ Π·Π½Π°ΡΠ΅Π½ΠΈΠ΅ ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ ΠΈΠ»ΠΈ None | ||
| if not field.default is None: | ||
| fields_json[field_name] = field.default | ||
| if field.default is not None: | ||
| # Π΅ΡΠ»ΠΈ default - callable (Π»ΡΠΌΠ±Π΄Π° ΠΈΠ»ΠΈ ΡΡΠ½ΠΊΡΠΈΡ), Π²ΡΠ·ΡΠ²Π°Π΅ΠΌ Π΅Ρ | ||
| if callable(field.default): | ||
| fields_json[field_name] = field.default() | ||
| else: | ||
| fields_json[field_name] = field.default | ||
| else: | ||
@@ -599,3 +605,3 @@ fields_json[field_name] = None | ||
| } | ||
| for rec in field | ||
| for rec in field["data"] | ||
| ] | ||
@@ -652,4 +658,72 @@ elif mode == JsonMode.FORM: | ||
| @classmethod | ||
| def get_onchange_fields(cls) -> list[str]: | ||
| """ | ||
| ΠΠΎΠ»ΡΡΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ ΠΏΠΎΠ»Π΅ΠΉ Ρ ΠΊΠΎΡΠΎΡΡΡ Π΅ΡΡΡ onchange ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΈ. | ||
| ΠΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ ΡΡΠΎΠ½ΡΠ΅Π½Π΄ΠΎΠΌ Π΄Π»Ρ ΠΎΠΏΡΠ΅Π΄Π΅Π»Π΅Π½ΠΈΡ Π·Π° ΠΊΠ°ΠΊΠΈΠΌΠΈ ΠΏΠΎΠ»ΡΠΌΠΈ ΡΠ»Π΅Π΄ΠΈΡΡ. | ||
| Returns: | ||
| Π‘ΠΏΠΈΡΠΎΠΊ ΠΈΠΌΡΠ½ ΠΏΠΎΠ»Π΅ΠΉ Ρ onchange ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠ°ΠΌΠΈ | ||
| """ | ||
| fields_with_onchange = set() | ||
| for attr_name in dir(cls): | ||
| if attr_name.startswith("_"): | ||
| attr = getattr(cls, attr_name, None) | ||
| if attr and callable(attr) and hasattr(attr, "_is_onchange"): | ||
| onchange_fields = getattr(attr, "_onchange_fields", ()) | ||
| fields_with_onchange.update(onchange_fields) | ||
| return list(fields_with_onchange) | ||
| @classmethod | ||
| def _get_onchange_handlers(cls, field_name: str) -> list[str]: | ||
| """ | ||
| ΠΠΎΠ»ΡΡΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ ΠΌΠ΅ΡΠΎΠ΄ΠΎΠ²-ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ² Π΄Π»Ρ ΡΠΊΠ°Π·Π°Π½Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ»Ρ. | ||
| Args: | ||
| field_name: ΠΠΌΡ ΠΏΠΎΠ»Ρ | ||
| Returns: | ||
| Π‘ΠΏΠΈΡΠΎΠΊ ΠΈΠΌΡΠ½ ΠΌΠ΅ΡΠΎΠ΄ΠΎΠ²-ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ² | ||
| """ | ||
| handlers = [] | ||
| for attr_name in dir(cls): | ||
| if attr_name.startswith("_"): | ||
| attr = getattr(cls, attr_name, None) | ||
| if attr and callable(attr) and hasattr(attr, "_is_onchange"): | ||
| onchange_fields = getattr(attr, "_onchange_fields", ()) | ||
| if field_name in onchange_fields: | ||
| handlers.append(attr_name) | ||
| return handlers | ||
| async def execute_onchange(self, field_name: str) -> dict: | ||
| """ | ||
| ΠΡΠΏΠΎΠ»Π½ΠΈΡΡ Π²ΡΠ΅ onchange ΠΎΠ±ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΈ Π΄Π»Ρ ΡΠΊΠ°Π·Π°Π½Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ»Ρ. | ||
| ΠΠ΅ΡΠ΅Π΄ Π²ΡΠ·ΠΎΠ²ΠΎΠΌ self Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ Π·Π°ΠΏΠΎΠ»Π½Π΅Π½ ΡΠ΅ΠΊΡΡΠΈΠΌΠΈ Π·Π½Π°ΡΠ΅Π½ΠΈΡΠΌΠΈ ΡΠΎΡΠΌΡ. | ||
| Args: | ||
| field_name: ΠΠΌΡ ΠΈΠ·ΠΌΠ΅Π½ΡΠ½Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ»Ρ | ||
| Returns: | ||
| ΠΠ±ΡΠ΅Π΄ΠΈΠ½ΡΠ½Π½ΡΠΉ dict ΡΠΎ Π·Π½Π°ΡΠ΅Π½ΠΈΡΠΌΠΈ Π΄Π»Ρ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ ΡΠΎΡΠΌΡ | ||
| """ | ||
| result = {} | ||
| handlers = self._get_onchange_handlers(field_name) | ||
| for handler_name in handlers: | ||
| handler = getattr(self, handler_name, None) | ||
| if handler and callable(handler): | ||
| handler_result = await handler() | ||
| if handler_result: | ||
| result.update(handler_result) | ||
| return result | ||
| # Backward compatibility alias | ||
| Model = DotModel |
@@ -105,3 +105,12 @@ """DDL Mixin - provides table creation functionality.""" | ||
| async def __create_table__(cls, session=None): | ||
| """ΠΠ΅ΡΠΎΠ΄ Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΡΠ°Π±Π»ΠΈΡΡ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½ΡΡ , ΠΎΡΠ½ΠΎΠ²Π°Π½Π½ΠΎΠΉ Π½Π° Π°ΡΡΠΈΠ±ΡΡΠ°Ρ ΠΊΠ»Π°ΡΡΠ°.""" | ||
| """ΠΠ΅ΡΠΎΠ΄ Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΡΠ°Π±Π»ΠΈΡΡ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½ΡΡ , ΠΎΡΠ½ΠΎΠ²Π°Π½Π½ΠΎΠΉ Π½Π° Π°ΡΡΠΈΠ±ΡΡΠ°Ρ ΠΊΠ»Π°ΡΡΠ°. | ||
| ΠΡΠ»ΠΈ __auto_create__ = False, ΠΏΡΠΎΠΏΡΡΠΊΠ°Π΅Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΡΠ°Π±Π»ΠΈΡΡ. | ||
| ΠΡΠΎ ΠΏΠΎΠ»Π΅Π·Π½ΠΎ Π΄Π»Ρ ΡΠ²ΡΠ·ΡΡΡΠΈΡ ΡΠ°Π±Π»ΠΈΡ many2many, ΠΊΠΎΡΠΎΡΡΠ΅ ΡΠΎΠ·Π΄Π°ΡΡΡΡ | ||
| Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΠΏΡΠΈ ΡΠΎΠ·Π΄Π°Π½ΠΈΠΈ ΠΎΡΠ½ΠΎΠ²Π½ΠΎΠΉ ΠΌΠΎΠ΄Π΅Π»ΠΈ. | ||
| """ | ||
| # ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ ΡΠ»Π°Π³ __auto_create__ | ||
| if not cls.__auto_create__: | ||
| return [] | ||
| session = cls._get_db_session(session) | ||
@@ -166,2 +175,3 @@ | ||
| if field.relation and isinstance(field, Many2many): | ||
| # id_column = '"id" SERIAL PRIMARY KEY' | ||
| column1 = f'"{field.column1}" INTEGER NOT NULL' | ||
@@ -168,0 +178,0 @@ column2 = f'"{field.column2}" INTEGER NOT NULL' |
@@ -39,2 +39,4 @@ """Relations ORM operations mixin.""" | ||
| - search - search records with relation loading | ||
| - search_count - count records matching filter | ||
| - exists - have one record or not | ||
| - get_with_relations - get single record with relations | ||
@@ -96,2 +98,54 @@ - update_with_relations - update record with relations | ||
| @hybridmethod | ||
| async def search_count( | ||
| self, | ||
| filter: FilterExpression | None = None, | ||
| session=None, | ||
| ) -> int: | ||
| """ | ||
| Count records matching the filter. | ||
| Args: | ||
| filter: Filter expression | ||
| session: Database session | ||
| Returns: | ||
| Number of matching records | ||
| """ | ||
| cls = self.__class__ | ||
| session = cls._get_db_session(session) | ||
| stmt, values = cls._builder.build_search_count(filter) | ||
| result = await session.execute(stmt, values) | ||
| if result and len(result) > 0: | ||
| return result[0].get("count", 0) | ||
| return 0 | ||
| @hybridmethod | ||
| async def exists( | ||
| self, | ||
| filter: FilterExpression | None = None, | ||
| session=None, | ||
| ) -> bool: | ||
| """ | ||
| Check if any record matches the filter. | ||
| More efficient than search_count for existence checks. | ||
| Args: | ||
| filter: Filter expression | ||
| session: Database session | ||
| Returns: | ||
| True if at least one record exists | ||
| """ | ||
| cls = self.__class__ | ||
| session = cls._get_db_session(session) | ||
| stmt, values = cls._builder.build_exists(filter) | ||
| result = await session.execute(stmt, values) | ||
| return bool(result) | ||
| @classmethod | ||
@@ -104,3 +158,3 @@ async def get_with_relations( | ||
| session=None, | ||
| ) -> Self | None: | ||
| ) -> Self: | ||
| """Get record with relations loaded.""" | ||
@@ -126,3 +180,3 @@ if not fields: | ||
| if not record_raw: | ||
| return None | ||
| raise ValueError("Record not found") | ||
| record = cls(**record_raw[0]) | ||
@@ -129,0 +183,0 @@ |
@@ -32,2 +32,3 @@ """Protocols defining what ORM mixins expect from the model class.""" | ||
| __table__: ClassVar[str] | ||
| __auto_create__: ClassVar[bool] = True | ||
| _pool: ClassVar[Union["aiomysql.Pool", "asyncpg.Pool"]] | ||
@@ -34,0 +35,0 @@ _no_transaction: ClassVar[Type] |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: dotorm | ||
| Version: 2.0.6 | ||
| Version: 2.0.7 | ||
| Summary: Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access | ||
@@ -5,0 +5,0 @@ Project-URL: Homepage, https://github.com/shurshilov/dotorm |
+1
-1
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "dotorm" | ||
| version = "2.0.6" | ||
| version = "2.0.7" | ||
| description = "Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access" | ||
@@ -10,0 +10,0 @@ readme = "README.md" |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
302194
4.43%4574
6.45%