Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

dotorm

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dotorm - npm Package Compare versions

Comparing version
2.0.9
to
2.0.10
+5
-5
dotorm/__init__.py

@@ -26,4 +26,4 @@ """

One2one,
AttachmentMany2one,
AttachmentOne2many,
PolymorphicMany2one,
PolymorphicOne2many,
)

@@ -50,3 +50,3 @@

__version__ = "2.0.9"
__version__ = "2.1.0"

@@ -74,4 +74,4 @@ __all__ = [

"One2one",
"AttachmentMany2one",
"AttachmentOne2many",
"PolymorphicMany2one",
"PolymorphicOne2many",
# Model

@@ -78,0 +78,0 @@ "DotModel",

@@ -157,3 +157,3 @@ """CRUD operations mixin."""

if fields is None:
fields = ["id"]
fields = store_fields

@@ -160,0 +160,0 @@ # поставить защиту, хотя по идее защита есть в ОРМ

"""Many2many query builder."""
from typing import TYPE_CHECKING, Literal
from typing import TYPE_CHECKING, Literal, Type
if TYPE_CHECKING:
from ..protocol import BuilderProtocol
from ...orm.model import DotModel
from ...model import DotModel

@@ -18,3 +19,3 @@

id: int,
relation_table: "DotModel",
relation_table: Type["DotModel"],
many2many_table: str,

@@ -62,3 +63,3 @@ column1: str,

ids: list[int],
relation_table: "DotModel",
relation_table: Type["DotModel"],
many2many_table: str,

@@ -65,0 +66,0 @@ column1: str,

@@ -9,3 +9,3 @@ """Relations query builder."""

from ..request_builder import RequestBuilder
from ...fields import AttachmentMany2one, Field, Many2many, Many2one, One2many
from ...fields import PolymorphicMany2one, Field, Many2many, Many2one, One2many

@@ -22,2 +22,3 @@

records: list | None = None,
fields_nested: dict[str, list[str]] | None = None,
) -> list[RequestBuilder]:

@@ -38,8 +39,12 @@ """

for name, field in fields_relation:
# TODO: взможно ошибка от дублирвоания полей
# Default fields for relations
fields = ["id"]
if field.relation_table and field.relation_table.get_fields().get(
"name"
):
fields.append("name")
# добавить вложенные поля от пользователя
fields = []
if fields_nested:
custom_fields = fields_nested.get(name)
if custom_fields:
fields += custom_fields
if field.relation_table:
fields = field.relation_table.get_store_fields()

@@ -77,3 +82,3 @@ req: RequestBuilder | None = None

elif isinstance(field, (Many2one, AttachmentMany2one)):
elif isinstance(field, (Many2one, PolymorphicMany2one)):
ids_m2o: list[int] = [

@@ -80,0 +85,0 @@ getattr(record, name)

"""Protocol defining what mixins expect from the base class."""
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
from typing import TYPE_CHECKING, Literal, Protocol, Type, runtime_checkable

@@ -42,3 +42,3 @@ from ..components.filter_parser import FilterExpression

ids: list[int],
relation_table: "DotModel",
relation_table: Type["DotModel"],
many2many_table: str,

@@ -54,3 +54,3 @@ column1: str,

id: int,
relation_table: "DotModel",
relation_table: Type["DotModel"],
many2many_table: str,

@@ -57,0 +57,0 @@ column1: str,

@@ -9,4 +9,4 @@ """Request builder for relation queries."""

from ..fields import (
AttachmentMany2one,
AttachmentOne2many,
PolymorphicMany2one,
PolymorphicOne2many,
Field,

@@ -30,3 +30,3 @@ Many2many,

FormRelationField = (
Many2many | One2many | Many2one | AttachmentMany2one | AttachmentOne2many
Many2many | One2many | Many2one | PolymorphicMany2one | PolymorphicOne2many
)

@@ -56,4 +56,4 @@

Many2one: "prepare_list_ids",
AttachmentMany2one: "prepare_list_ids",
AttachmentOne2many: "prepare_list_ids",
PolymorphicMany2one: "prepare_list_ids",
PolymorphicOne2many: "prepare_list_ids",
}

@@ -85,33 +85,2 @@

@dataclass(slots=True)
class RequestBuilderForm(RequestBuilder):
"""
Container for form relation query request.
Extends RequestBuilder with form-specific prepare functions.
"""
# Override mapping for form context
_PREPARE_FUNCS: ClassVar[dict[type, str]] = {
Many2many: "prepare_form_ids",
One2many: "prepare_form_ids",
Many2one: "prepare_form_ids",
AttachmentMany2one: "prepare_form_ids",
AttachmentOne2many: "prepare_form_ids",
}
@property
def function_prepare(self) -> Callable:
"""
Returns appropriate prepare function for form context.
Form context uses prepare_form_ids/prepare_form_id methods.
"""
for field_type, method_name in self._PREPARE_FUNCS.items():
if isinstance(self.field, field_type):
return getattr(self.field.relation_table, method_name)
return getattr(self.field.relation_table, "prepare_form_id")
def create_request_builder(

@@ -123,7 +92,5 @@ stmt: str | None,

fields: list[str] | None = None,
*,
form_mode: bool = False,
) -> RequestBuilder:
"""
Factory function for creating appropriate RequestBuilder.
Factory function for creating RequestBuilder.

@@ -136,6 +103,5 @@ Args:

fields: Fields to select (default: ["id", "name"])
form_mode: If True, creates RequestBuilderForm
Returns:
RequestBuilder or RequestBuilderForm instance
RequestBuilder instance
"""

@@ -145,4 +111,3 @@ if fields is None:

cls = RequestBuilderForm if form_mode else RequestBuilder
return cls(
return RequestBuilder(
stmt=stmt,

@@ -149,0 +114,0 @@ value=value,

@@ -146,5 +146,5 @@ """

# Поэтому добавляем OR IS NULL
if op == "!=":
clause = f"({field} IS NULL OR {field} != %s)"
return clause, (value,)
# if op == "!=":
# clause = f"({field} IS NULL OR {field} != %s)"
# return clause, (value,)
clause = f"{field} {op} %s"

@@ -151,0 +151,0 @@ return clause, (value,)

@@ -75,3 +75,3 @@ """ORM field definitions."""

relation_table_field: str | None = None
_relation_table: "DotModel | None" = None
_relation_table: Type["DotModel"] | None = None

@@ -170,3 +170,3 @@ def __init__(self, **kwargs: Any) -> None:

@property
def relation_table(self) -> "DotModel | None":
def relation_table(self):
# если модель задана через лямбда функцию

@@ -440,5 +440,7 @@ if (

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
def __init__(self, relation_table: T, **kwargs: Any) -> None:
def __init__(
self, relation_table: Type["DotModel"], **kwargs: Any
) -> None:
self._relation_table = relation_table

@@ -448,3 +450,3 @@ super().__init__(**kwargs)

class AttachmentMany2one[T: "DotModel"](Field[T]):
class PolymorphicMany2one[T: "DotModel"](Field[T]):
"""Many-to-one attachment field."""

@@ -455,5 +457,7 @@

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
def __init__(self, relation_table: T, **kwargs: Any) -> None:
def __init__(
self, relation_table: Type["DotModel"], **kwargs: Any
) -> None:
self._relation_table = relation_table

@@ -463,3 +467,3 @@ super().__init__(**kwargs)

class AttachmentOne2many[T: "DotModel"](Field[list[T]]):
class PolymorphicOne2many[T: "DotModel"](Field[list[T]]):
"""One-to-many attachment field."""

@@ -470,3 +474,3 @@

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
relation_table_field: str

@@ -476,3 +480,3 @@

self,
relation_table: T,
relation_table: Type["DotModel"],
relation_table_field: str,

@@ -560,8 +564,10 @@ **kwargs: Any,

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
many2many_table: str
column1: str
column2: str
def __init__(
self,
relation_table: T,
relation_table: Type["DotModel"],
many2many_table: str,

@@ -599,3 +605,3 @@ column1: str,

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
relation_table_field: str

@@ -605,3 +611,3 @@

self,
relation_table: T,
relation_table: Type["DotModel"],
relation_table_field: str,

@@ -621,7 +627,7 @@ **kwargs: Any,

relation = True
relation_table: "DotModel"
relation_table: Type["DotModel"]
def __init__(
self,
relation_table: T,
relation_table: Type["DotModel"],
relation_table_field: str,

@@ -628,0 +634,0 @@ **kwargs: Any,

@@ -45,3 +45,3 @@ """DotModel - main ORM model class."""

from .fields import (
AttachmentOne2many,
PolymorphicOne2many,
Field,

@@ -53,3 +53,3 @@ JSONField,

One2one,
AttachmentMany2one,
PolymorphicMany2one,
)

@@ -264,8 +264,2 @@

@classmethod
def prepare_form_ids(cls, rows: list[dict]):
"""Deserialize from list of dicts to list of objects."""
records = [cls.prepare_form_id([r]) for r in rows]
return records
@classmethod
def prepare_form_id(cls, r: list):

@@ -382,3 +376,3 @@ """Deserialize from dict to object."""

if isinstance(
field, (Many2many, One2many, AttachmentOne2many, One2one)
field, (Many2many, One2many, PolymorphicOne2many, One2one)
)

@@ -393,3 +387,3 @@ ]

for name, field in cls.get_fields().items()
if isinstance(field, (AttachmentMany2one, AttachmentOne2many))
if isinstance(field, (PolymorphicMany2one, PolymorphicOne2many))
]

@@ -419,3 +413,3 @@

if field.store
and not isinstance(field, (Many2one, AttachmentMany2one))
and not isinstance(field, (Many2one, PolymorphicMany2one))
]

@@ -670,3 +664,3 @@

elif isinstance(
field_class, (Many2many, One2many, AttachmentOne2many)
field_class, (Many2many, One2many, PolymorphicOne2many)
):

@@ -686,11 +680,21 @@ if mode == JsonMode.LIST:

elif mode == JsonMode.FORM:
# При FORM (get) field это dict с data/fields/total
fields_json[field_name] = {
"data": [
# При FORM (get) field может быть:
# list объектов (get с fields_nested)
# dict с data/fields/total (legacy)
if isinstance(field, dict):
fields_json[field_name] = {
"data": [
rec.json(mode=JsonMode.NESTED_LIST)
for rec in field["data"]
],
"fields": field["fields"],
"total": field["total"],
}
elif isinstance(field, list):
fields_json[field_name] = [
rec.json(mode=JsonMode.NESTED_LIST)
for rec in field["data"]
],
"fields": field["fields"],
"total": field["total"],
}
for rec in field
]
else:
fields_json[field_name] = field

@@ -697,0 +701,0 @@ # Сериализуем JSONField в строку при записи в БД

@@ -13,3 +13,3 @@ """DDL Mixin - provides table creation functionality."""

from ...fields import AttachmentMany2one, Field, Many2many, Many2one
from ...fields import PolymorphicMany2one, Field, Many2many, Many2one

@@ -135,3 +135,3 @@

if (field.store and not field.relation) or isinstance(
field, (Many2one, AttachmentMany2one)
field, (Many2one, PolymorphicMany2one)
):

@@ -138,0 +138,0 @@ # Создаём строку с определением поля и добавляем её в список custom_fields.

"""Many2many ORM operations mixin."""
import asyncio
from typing import TYPE_CHECKING, Literal, Self
from typing import TYPE_CHECKING, Literal

@@ -13,3 +12,3 @@ if TYPE_CHECKING:

from ...fields import AttachmentMany2one, Field, Many2many, Many2one, One2many
from ...fields import PolymorphicMany2one, Field, Many2many, Many2one, One2many
from ...decorators import hybridmethod

@@ -116,10 +115,13 @@ from ..utils import execute_maybe_parallel

async def _records_list_get_relation(
cls, session, fields_relation, records
cls,
session,
fields_relation,
records,
fields_nested: dict[str, list[str]] | None = None,
):
"""Load relations for a list of records (batch)."""
# Use dialect from class
dialect = cls._dialect
request_list = cls._builder.build_search_relation(
fields_relation, records
fields_relation, records, fields_nested
)

@@ -143,36 +145,46 @@ execute_list = [

if isinstance(req.field, (Many2one, AttachmentMany2one)):
if isinstance(req.field, (Many2one, PolymorphicMany2one)):
# Сначала инициализируем все записи None
for rec in records:
rec_field_raw = getattr(rec, req.field_name)
if isinstance(rec_field_raw, Field):
setattr(rec, req.field_name, None)
# Теперь маппим найденные результаты
for rec in records:
rec_field_raw = getattr(rec, req.field_name)
for res_model in result:
if rec_field_raw == res_model.id:
setattr(rec, req.field_name, res_model)
break
if isinstance(req.field, One2many):
# Сначала инициализируем все записи пустым списком
for rec in records:
for res_model in result:
res_field_id = getattr(
res_model, req.field.relation_table_field
)
old_value = getattr(rec, req.field_name)
if isinstance(old_value, Field):
setattr(rec, req.field_name, [])
# Теперь добавляем найденные результаты
for res_model in result:
res_field_id = getattr(
res_model, req.field.relation_table_field
)
for rec in records:
if rec.id == res_field_id:
old_value = getattr(rec, req.field_name)
if isinstance(old_value, Field):
old_value = []
# иначе добавляем ид в список
old_value.append(res_model)
setattr(rec, req.field_name, old_value)
getattr(rec, req.field_name).append(res_model)
break
if isinstance(req.field, Many2many):
# Сначала инициализируем все записи пустым списком
for rec in records:
for res_model in result:
old_value = getattr(rec, req.field_name)
if isinstance(old_value, Field):
setattr(rec, req.field_name, [])
# Теперь добавляем найденные результаты
for res_model in result:
for rec in records:
if rec.id == res_model.m2m_id:
old_value = getattr(rec, req.field_name)
# если еще не задано то пустой список
if isinstance(old_value, Field):
old_value = []
# иначе добавляем ид в список
old_value.append(res_model)
setattr(rec, req.field_name, old_value)
getattr(rec, req.field_name).append(res_model)
break
# Удаляем служебный атрибут m2m_id
for res_model in result:
# удалить атрибут m2m_id
del res_model.__dict__["m2m_id"]

@@ -5,2 +5,4 @@ """Primary ORM operations mixin."""

from ...fields import Field
from ...access import Operation

@@ -62,33 +64,91 @@ from ...components.dialect import POSTGRES

self,
payload: "_M | None" = None,
fields=None,
payload: "_M",
fields: list[str] | None = None,
session=None,
):
"""
Обновить запись.
Автоматически обрабатывает и store поля (SQL UPDATE),
и relation поля (O2M/M2M: created/deleted/selected/unselected,
attachments: PolymorphicMany2one).
После обновления БД store-поля из payload синхронизируются в self.
Args:
payload: Данные для обновления (экземпляр модели).
fields: Список полей для обновления.
Если None — обновляются все заданные поля из payload.
session: DB сессия
Example:
# Store поля
await record.update(User(name="New", email="new@test.com"))
# Store + relations
await record.update(User(name="New", role_ids={"selected": [1, 2]}))
# Конкретные поля
await record.update(payload, fields=["name", "email"])
"""
await self._check_access(Operation.UPDATE, record_ids=[self.id])
session = self._get_db_session(session)
if payload is None:
payload = self
# Автоопределение полей если не указаны
if fields is None:
fields = [
name
for name, field_class in payload.get_fields().items()
if not isinstance(getattr(payload, name), Field)
and name != "id"
]
if not fields:
fields = []
return
if fields:
payload_dict = payload.json(
include=set(fields),
exclude_none=True,
only_store=True,
mode=JsonMode.UPDATE,
)
else:
payload_dict = payload.json(
exclude=payload.get_none_update_fields_set(),
exclude_none=True,
exclude_unset=True,
only_store=True,
mode=JsonMode.UPDATE,
)
await self._update_relations(payload, fields, session)
stmt, values = self._builder.build_update(payload_dict, self.id)
return await session.execute(stmt, values)
# Синхронизировать self с payload после успешного обновления
if payload is not self:
self._sync_after_update(payload, fields)
def _sync_after_update(self, payload: "_M", fields: list[str]):
"""
Синхронизировать self с payload после успешного update.
Копируем только store-поля (скаляры, M2O FK) из payload в self.
Relation-поля (O2M, M2M) не синхронизируются — в payload они
в формате команд {created/deleted/selected/unselected},
а на self — список объектов. Если нужны актуальные relations
после update — следует перечитать запись из БД.
Аналогично другим ORM:
- SQLAlchemy: expire + lazy reload при обращении (доп. SELECT)
- Django: self уже мутирован до save(), M2M — отдельные операции
- Tortoise: self уже мутирован до save(), M2M — отдельные операции
"""
store_fields = set(self.get_store_fields())
for name in fields:
if name in store_fields:
setattr(self, name, getattr(payload, name))
async def _update_store(
self,
payload: "_M",
fields: list[str],
session,
):
"""Прямой SQL UPDATE для store полей. Без access check и relations."""
payload_dict = payload.json(
include=set(fields),
exclude_unset=True,
exclude_none=True,
only_store=True,
mode=JsonMode.UPDATE,
)
if payload_dict:
stmt, values = self._builder.build_update(payload_dict, self.id)
return await session.execute(stmt, values)
@hybridmethod

@@ -188,3 +248,42 @@ async def update_bulk(

@hybridmethod
async def get(self, id, fields: list[str] = [], session=None) -> Self:
async def get(
self,
id,
fields: list[str] = [],
fields_nested: dict[str, list[str]] | None = None,
session=None,
) -> Self:
"""
Получить запись по ID.
Args:
id: ID записи
fields: Список полей для загрузки (store + relation).
fields_nested: Словарь вложенных полей для relation.
Если передан — relation поля из fields загружаются:
M2O → объект модели или None
O2M → список объектов []
M2M → список объектов []
Если не передан — только store поля (M2O = integer FK).
Пример: {"user_id": ["id", "name"], "tag_ids": ["id", "name"]}
session: DB сессия
Returns:
Экземпляр модели
Raises:
ValueError: Если запись не найдена
Example:
# Только store поля
chat = await Chat.get(5)
chat.user_id # → 42 (int)
# С relations
chat = await Chat.get(5,
fields=["id", "name", "user_id", "message_ids"],
fields_nested={"user_id": ["id", "name"]}
)
chat.user_id # → User(id=42, name="John")
"""
cls = self.__class__

@@ -196,3 +295,13 @@

stmt, values = cls._builder.build_get(id, fields)
# Фильтруем fields — оставляем только store поля для SQL
store_fields = cls.get_store_fields()
fields_store = (
[f for f in fields if f in store_fields] if fields else []
)
if not fields_store:
fields_store = list(store_fields)
if "id" not in fields_store:
fields_store.append("id")
stmt, values = cls._builder.build_get(id, fields_store)
record = await session.execute(

@@ -205,2 +314,9 @@ stmt, values, prepare=cls.prepare_form_id

assert isinstance(record, cls)
# Загрузка relations если передан fields_nested
if fields_nested is not None and fields:
await cls._get_load_relations(
record, fields, fields_nested, session
)
return record

@@ -207,0 +323,0 @@

@@ -8,8 +8,6 @@ """Relations ORM operations mixin."""

from ...access import Operation
from ...builder.request_builder import (
RequestBuilderForm,
)
from ...fields import (
AttachmentMany2one,
AttachmentOne2many,
PolymorphicMany2one,
PolymorphicOne2many,
Field,
Many2many,

@@ -42,4 +40,4 @@ Many2one,

- exists - have one record or not
- get_with_relations - get single record with relations
- update_with_relations - update record with relations
- _get_load_relations - load relations for single record (used by get())
- _update_relations - update record with relations (used by update())

@@ -62,3 +60,4 @@ Expects DotModel to provide:

self,
fields: list[str] = ["id"],
fields: list[str] | None = None,
fields_nested: dict[str, list[str]] | None = None,
start: int | None = None,

@@ -73,4 +72,51 @@ end: int | None = None,

) -> list[Self]:
"""
Поиск записей с поддержкой фильтрации, пагинации и загрузки relations.
Выполняет оптимизированные batch-запросы для relation полей:
один запрос на тип relation для всех найденных записей.
Args:
fields: Список полей для загрузки (store + relation).
По умолчанию ["id"].
start: Начальный индекс для пагинации (OFFSET)
end: Конечный индекс (не используется напрямую, см. limit)
limit: Максимальное количество записей. По умолчанию 1000.
order: Направление сортировки "DESC" или "ASC"
sort: Поле для сортировки. По умолчанию "id".
filter: Фильтр в формате FilterExpression.
Например: [("active", "=", True), ("name", "ilike", "%test%")]
raw: Если True - возвращает сырые данные без преобразования в модели
session: DB сессия (опционально)
Returns:
Список экземпляров модели с загруженными данными.
Relation поля инициализируются:
- Many2one → модель или None
- One2many → список моделей или []
- Many2many → список моделей или []
Example:
# Найти активных пользователей с их ролями
users = await User.search(
fields=["id", "name", "email", "role_ids"],
filter=[("active", "=", True)],
limit=50,
order="ASC",
sort="name"
)
for user in users:
if user.role_ids: # Корректно работает ([] если пусто)
print(f"{user.name}: {len(user.role_ids)} roles")
Note:
Relations загружаются batch-запросами (один запрос на тип relation
для всех найденных записей). Для одной записи используйте
get(id, fields, fields_nested).
"""
cls = self.__class__
if fields is None:
fields = self.get_store_fields()
# Access check + apply domain filter

@@ -81,5 +127,2 @@ filter = await cls._check_access(Operation.READ, filter=filter)

# Use dialect from class
dialect = cls._dialect
stmt, values = cls._builder.build_search(

@@ -101,3 +144,3 @@ fields, start, end, limit, order, sort, filter

await cls._records_list_get_relation(
session, fields_relation, records
session, fields_relation, records, fields_nested
)

@@ -160,36 +203,28 @@

@classmethod
async def get_with_relations(
async def _get_load_relations(
cls,
id,
fields=None,
fields_info={},
session=None,
) -> Self:
"""Get record with relations loaded."""
if not fields:
fields = []
session = cls._get_db_session(session)
record,
fields: list[str],
fields_nested: dict[str, list[str]],
session,
):
"""
Загрузка relation полей для одной записи.
dialect = cls._dialect
Используется из get() когда передан fields_nested.
Для каждого relation поля выполняет отдельный запрос.
# защита, оставить только те поля, которые действительно хранятся в базе
fields_store = [
name for name in cls.get_store_fields() if name in fields
]
# если вдруг они не заданы, или таких нет, взять все
if not fields_store:
fields_store = [name for name in cls.get_store_fields()]
if "id" not in fields_store:
fields_store.append("id")
Формат результата единообразен с search():
M2O → объект модели или None
O2M → список объектов
M2M → список объектов
stmt, values = cls._builder.build_get(id, fields_store)
record_raw: list[Any] = await session.execute(stmt, values)
if not record_raw:
raise ValueError("Record not found")
record = cls(**record_raw[0])
# защита, оставить только те поля, которые являются отношениями (m2m, o2m, m2o)
# добавлена информаци о вложенных полях
Args:
record: Экземпляр модели с загруженными store полями
fields: Список запрошенных полей (store + relation)
fields_nested: Словарь вложенных полей для relations
session: DB сессия
"""
fields_relation = [
(name, field, fields_info.get(name))
(name, field)
for name, field in cls.get_relation_fields()

@@ -199,187 +234,124 @@ if name in fields

# если есть хоть одна запись и вообще нужно читать поля связей
if record and fields_relation:
request_list = []
execute_list = []
if not fields_relation:
return
# добавить запрос на o2m
for name, field, fields_nested in fields_relation:
relation_table = field.relation_table
relation_table_field = field.relation_table_field
execute_list = []
request_meta = [] # (field_name, field_type, relation)
if not fields_nested and relation_table:
fields_select = ["id"]
if relation_table.get_fields().get("name"):
fields_select.append("name")
if isinstance(field, AttachmentMany2one):
fields_select = (
relation_table.get_store_fields_omit_m2o()
)
else:
fields_select = fields_nested
for name, field in fields_relation:
relation_table = field.relation_table
relation_table_field = field.relation_table_field
if (
isinstance(field, (Many2one, AttachmentMany2one))
and relation_table
):
m2o_id = getattr(record, name)
stmt, val = relation_table._builder.build_get(
m2o_id, fields=fields_select
)
req = RequestBuilderForm(
stmt=stmt,
value=val,
field_name=name,
field=field,
# Определяем какие поля вложенной модели загружать
nested = fields_nested.get(name)
if nested:
fields_select = nested
elif relation_table:
fields_select = ["id"]
if relation_table.get_fields().get("name"):
fields_select.append("name")
if isinstance(field, PolymorphicMany2one):
fields_select = relation_table.get_store_fields_omit_m2o()
else:
continue
if (
isinstance(field, (Many2one, PolymorphicMany2one))
and relation_table
):
m2o_id = getattr(record, name)
if m2o_id is None or isinstance(m2o_id, Field):
setattr(record, name, None)
continue
execute_list.append(
relation_table.search(
fields=fields_select,
filter=[("id", "=", m2o_id)],
limit=1,
)
request_list.append(req)
execute_list.append(
session.execute(
req.stmt,
req.value,
prepare=req.function_prepare,
cursor=req.function_cursor,
)
)
# если m2m или o2m необходимо посчитать длину, для пагинации
if isinstance(field, Many2many):
params = {
"id": record.id,
"comodel": relation_table,
"relation": field.many2many_table,
"column1": field.column1,
"column2": field.column2,
"fields": fields_select,
"order": "desc",
"start": 0,
"end": 40,
"sort": "id",
"limit": 40,
}
# records
execute_list.append(cls.get_many2many(**params))
params["fields"] = ["id"]
params["start"] = None
params["end"] = None
params["limit"] = None
# len
execute_list.append(cls.get_many2many(**params))
req = RequestBuilderForm(
stmt=None,
value=None,
field_name=name,
field=field,
)
request_meta.append((name, "m2o"))
elif isinstance(field, Many2many) and relation_table:
execute_list.append(
cls.get_many2many(
id=record.id,
comodel=relation_table,
relation=field.many2many_table,
column1=field.column1,
column2=field.column2,
fields=fields_select,
limit=None,
)
request_list.append(req)
)
request_meta.append((name, "m2m"))
if isinstance(field, One2many) and relation_table:
params = {
"start": 0,
"end": 40,
"limit": 40,
"fields": fields_select,
"filter": [(relation_table_field, "=", record.id)],
}
execute_list.append(relation_table.search(**params))
params["fields"] = ["id"]
params["start"] = None
params["end"] = None
params["limit"] = 1000
execute_list.append(relation_table.search(**params))
req = RequestBuilderForm(
stmt=None,
value=None,
field_name=name,
field=field,
elif (
isinstance(field, One2many)
and relation_table
and relation_table_field
):
execute_list.append(
relation_table.search(
fields=fields_select,
filter=[(relation_table_field, "=", record.id)],
limit=1000,
)
request_list.append(req)
)
request_meta.append((name, "o2m"))
if isinstance(field, AttachmentOne2many) and relation_table:
params = {
"start": 0,
"end": 40,
"limit": 40,
"fields": relation_table.get_store_fields_omit_m2o(),
"filter": [
elif isinstance(field, PolymorphicOne2many) and relation_table:
execute_list.append(
relation_table.search(
fields=relation_table.get_store_fields_omit_m2o(),
filter=[
("res_id", "=", record.id),
("res_model", "=", record.__table__),
],
}
execute_list.append(relation_table.search(**params))
params["fields"] = ["id"]
params["start"] = None
params["end"] = None
params["limit"] = 1000
execute_list.append(relation_table.search(**params))
req = RequestBuilderForm(
stmt=None,
value=None,
field_name=name,
field=field,
fields=relation_table.get_store_fields_omit_m2o(),
limit=1000,
)
request_list.append(req)
)
request_meta.append((name, "o2m"))
if isinstance(field, One2one) and relation_table:
params = {
"limit": 1,
"fields": fields_select,
"filter": [(relation_table_field, "=", record.id)],
}
execute_list.append(relation_table.search(**params))
req = RequestBuilderForm(
stmt=None,
value=None,
field_name=name,
field=field,
elif (
isinstance(field, One2one)
and relation_table
and relation_table_field
):
execute_list.append(
relation_table.search(
fields=fields_select,
filter=[(relation_table_field, "=", record.id)],
limit=1,
)
request_list.append(req)
)
request_meta.append((name, "m2o"))
# выполняем последовательно в транзакции, параллельно вне транзакции
results = await execute_maybe_parallel(execute_list)
if not execute_list:
return
# добавляем атрибуты к исходному объекту,
# получая удобное обращение через дот-нотацию
i = 0
for request_builder in request_list:
result = results[i]
results = await execute_maybe_parallel(execute_list)
if isinstance(
request_builder.field,
(Many2one, AttachmentMany2one, One2one),
):
# m2o нужно распаковать так как он тоже в списке
# если пустой список, то установить None
result = result[0] if result else None
for i, (name, rel_type) in enumerate(request_meta):
result = results[i]
if rel_type == "m2o":
setattr(record, name, result[0] if result else None)
else:
# o2m, m2m → список
setattr(record, name, result if result else [])
if isinstance(
request_builder.field,
(Many2many, One2many, AttachmentOne2many),
):
# если m2m или o2m необбзодимо взять два результатата
# так как один из них это число всех строк таблицы
# для пагинации
fields_info = request_builder.field.relation_table.get_fields_info_list(
request_builder.fields
)
result = {
"data": result,
"fields": fields_info,
"total": len(results[i + 1]),
}
i += 1
async def _update_relations(
self, payload: _M, update_fields: list[str], session=None
):
"""
Обновить запись с поддержкой relation полей (M2M, O2M, attachments).
setattr(record, request_builder.field_name, result)
i += 1
Вызывается из update() когда fields содержит relation поля.
Обрабатывает: store поля (через update) + attachments + O2M/M2M.
return record
async def update_with_relations(
self, payload: _M, fields=[], session=None
):
"""Update record with relations."""
Args:
payload: Экземпляр модели с новыми значениями полей
update_fields: Список полей для обновления
session: DB сессия
"""
session = self._get_db_session(session)

@@ -391,3 +363,3 @@

for name, field in self.get_relation_fields_attachment()
if name in fields
if name in update_fields
]

@@ -397,3 +369,3 @@

for name, field in fields_attachments:
if isinstance(field, AttachmentMany2one):
if isinstance(field, PolymorphicMany2one):
field_obj = getattr(payload, name)

@@ -412,9 +384,9 @@ if field_obj and field.relation_table:

# Update stored fields
# Update store fields
fields_store = [
name for name in self.get_store_fields() if name in fields
name for name in self.get_store_fields() if name in update_fields
]
# Обновление сущности в базе без связей
if fields_store:
record_raw = await self.update(payload, fields, session)
await self._update_store(payload, fields_store, session)

@@ -426,3 +398,3 @@ # защита, оставить только те поля, которые являются отношениями (m2m, o2m)

for name, field in self.get_relation_fields_m2m_o2m()
if name in fields
if name in update_fields
]

@@ -432,3 +404,2 @@

request_list = []
field_list = []

@@ -448,10 +419,9 @@ for name, field in fields_relation:

if isinstance(field, (One2many, AttachmentOne2many)):
field_list.append(field)
if isinstance(field, (One2many, PolymorphicOne2many)):
# заменить в связанных полях виртуальный ид на вновь созданный
for obj in field_obj["created"]:
for obj in field_obj.get("created", []):
for k, v in obj.items():
f = getattr(field.relation_table, k)
if (
isinstance(f, (Many2one, AttachmentMany2one))
isinstance(f, (Many2one, PolymorphicMany2one))
and v == "VirtualId"

@@ -463,10 +433,10 @@ ):

field.relation_table(**obj)
for obj in field_obj["created"]
for obj in field_obj.get("created", [])
]
if isinstance(field, AttachmentOne2many):
if isinstance(field, PolymorphicOne2many):
for obj in data_created:
obj.res_id = self.id
if field_obj["created"]:
if field_obj.get("created", []):
request_list.append(

@@ -483,10 +453,8 @@ field.relation_table.create_bulk(data_created)

if isinstance(field, Many2many):
field_list.append(field)
# Replace virtual ID
for obj in field_obj["created"]:
for obj in field_obj.get("created", []):
for k, v in obj.items():
f = getattr(field.relation_table, k)
if (
isinstance(f, (Many2one, AttachmentMany2one))
isinstance(f, (Many2one, PolymorphicMany2one))
and v == "VirtualId"

@@ -498,6 +466,6 @@ ):

field.relation_table(**obj)
for obj in field_obj["created"]
for obj in field_obj.get("created", [])
]
if field_obj["created"]:
if field_obj.get("created", []):
created_ids = await field.relation_table.create_bulk(

@@ -527,5 +495,3 @@ data_created

# выполняем последовательно
results = await execute_maybe_parallel(request_list)
return record_raw
for coro in request_list:
await coro

@@ -98,3 +98,3 @@ """Protocols defining what ORM mixins expect from the model class."""

self,
payload: Self | None = None,
payload: Self,
fields: Any = None,

@@ -144,6 +144,31 @@ session: Any = None,

records: list[Any],
fields_nested: dict[str, list[str]] | None = None,
) -> None: ...
# From OrmRelationsMixin
@classmethod
async def _get_load_relations(
cls,
record: Any,
fields: list[str],
fields_nested: dict[str, list[str]],
session: Any,
) -> None: ...
async def _update_relations(
self,
payload: Any,
update_fields: list[str],
session: Any,
) -> None: ...
async def _update_store(
self,
payload: Any,
fields: list[str],
session: Any,
) -> Any: ...
# From DDLMixin
@staticmethod
def format_default_value(value: Any) -> str: ...
Metadata-Version: 2.4
Name: dotorm
Version: 2.0.9
Version: 2.0.10
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

@@ -7,3 +7,3 @@ [build-system]

name = "dotorm"
version = "2.0.9"
version = "2.0.10"
description = "Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access"

@@ -10,0 +10,0 @@ readme = "README.md"