You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP β†’
Socket
Book a DemoSign 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 - pypi Package Compare versions

Comparing version
2.0.7
to
2.0.8
+151
dotorm/access.py
"""
ΠšΠΎΠ½Ρ‚Π΅ΠΊΡΡ‚ доступа для DotORM.
ИспользованиС:
# ΠŸΡ€ΠΈ стартС прилоТСния (security/app.py):
set_access_checker(SecurityAccessChecker(env))
# ΠŸΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ запросС (verify_access):
set_access_session(session) # Session ΠΈΠ· security модуля
# Π’ DotModel автоматичСски:
# - check_table_access() ΠΏΠ΅Ρ€Π΅Π΄ CRUD опСрациями
# - check_row_access() для ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Ρ… записСй (get/update/delete)
# - get_domain_filter() для Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ Π²Ρ‹Π±ΠΎΡ€ΠΊΠΈ (search)
"""
from contextvars import ContextVar
from enum import StrEnum
from typing import TypeVar, Generic
class Operation(StrEnum):
"""ΠžΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ доступа."""
READ = "read"
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
# Generic Ρ‚ΠΈΠΏ для Session
TSession = TypeVar("TSession")
class AccessChecker(Generic[TSession]):
"""
Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ класс ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ доступа.
По ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅Ρ‚ всё.
ΠœΠΎΠ΄ΡƒΠ»ΡŒ security наслСдуСт ΠΈ пСрСопрСдСляСт ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹.
"""
async def check_access(
self,
session: TSession,
model: str,
operation: Operation,
record_ids: list[int] | None = None,
) -> tuple[bool, list]:
"""
Единая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° доступа: ACL + Rules.
Args:
session: БСссия ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
model: Имя ΠΌΠΎΠ΄Π΅Π»ΠΈ (Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹)
operation: ΠžΠΏΠ΅Ρ€Π°Ρ†ΠΈΡ (read/create/update/delete)
record_ids: ID записСй (для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Rules)
Returns:
(has_access, domain_filter):
- has_access: True Ссли доступ Ρ€Π°Π·Ρ€Π΅ΡˆΡ‘Π½
- domain_filter: Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ для search (пустой Ссли Π½Π΅ Π½ΡƒΠΆΠ΅Π½)
"""
return True, []
async def check_table_access(
self,
session: TSession,
model: str,
operation: Operation,
) -> bool:
"""
ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ доступ ΠΊ Ρ‚Π°Π±Π»ΠΈΡ†Π΅ (ACL ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ).
"""
return True
async def check_row_access(
self,
session: TSession,
model: str,
operation: Operation,
record_ids: list[int],
) -> bool:
"""
ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ доступ ΠΊ записям (Rules ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ).
Для ΠΎΠ΄Π½ΠΎΠΉ ΠΈΠ»ΠΈ Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… записСй провСряСт Ρ‡Ρ‚ΠΎ ΠΎΠ½ΠΈ
ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚ ΠΏΠΎΠ΄ domain ΠΈΠ· Rules.
"""
return True
async def get_domain_filter(
self,
session: TSession,
model: str,
operation: Operation,
) -> list:
"""
Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ domain-Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ для ограничСния Π²Ρ‹Π±ΠΎΡ€ΠΊΠΈ.
Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для search β€” добавляСтся ΠΊ filter Π”Πž запроса.
"""
return []
class AccessDenied(Exception):
"""Доступ Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½."""
def __init__(self, message: str = "Access denied"):
self.message = message
super().__init__(self.message)
# ============================================================
# State
# ============================================================
_state: dict = {"checker": AccessChecker()}
_access_session: ContextVar = ContextVar("access_session", default=None)
# ============================================================
# Public API
# ============================================================
def set_access_checker(checker: AccessChecker) -> None:
"""УстанавливаСт AccessChecker (ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· ΠΏΡ€ΠΈ стартС)."""
_state["checker"] = checker
def get_access_checker() -> AccessChecker:
"""Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ AccessChecker."""
return _state["checker"]
def set_access_session(session) -> None:
"""УстанавливаСт сСссию для Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ запроса."""
_access_session.set(session)
def get_access_session():
"""Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ сСссию Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ запроса."""
return _access_session.get()
def clear_access_session() -> None:
"""ΠžΡ‡ΠΈΡ‰Π°Π΅Ρ‚ сСссию (послС Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ post_init)."""
_access_session.set(None)
"""Access control mixin for DotModel."""
from typing import TYPE_CHECKING
from ...access import (
get_access_checker,
get_access_session,
AccessDenied,
Operation,
)
if TYPE_CHECKING:
from ..protocol import DotModelProtocol
_Base = DotModelProtocol
else:
_Base = object
class AccessMixin(_Base):
"""
Mixin Π΄ΠΎΠ±Π°Π²Π»ΡΡŽΡ‰ΠΈΠΉ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ доступа Π² CRUD ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ.
Если AccessSession Π½Π΅ установлСна β€” ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ.
SystemSession Π΄Π°Ρ‘Ρ‚ ΠΏΠΎΠ»Π½Ρ‹ΠΉ доступ.
"""
@classmethod
async def _check_access(
cls,
operation: Operation,
record_ids: list[int] | None = None,
filter: list | None = None,
) -> list | None:
"""
Π•Π΄ΠΈΠ½Ρ‹ΠΉ ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ доступа.
Args:
operation: Operation.READ / CREATE / UPDATE / DELETE
record_ids: ID записСй (для get/update/delete)
filter: ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ (для search)
Returns:
ΠœΠΎΠ΄ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ filter с domain (для search)
Raises:
AccessDenied: Ссли доступ Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½
"""
session = get_access_session()
if session is None:
return filter
checker = get_access_checker()
has_access, domain = await checker.check_access(
session, cls.__table__, operation, record_ids
)
if not has_access:
raise AccessDenied(
f"No {operation.value} access to {cls.__table__}"
)
if domain:
return filter + domain if filter else domain
return filter
"""Utility functions for dotorm."""
import asyncio
from typing import Any, Coroutine, Sequence
async def execute_maybe_parallel(
coroutines: Sequence[Coroutine[Any, Any, Any]],
) -> list[Any]:
"""
Execute coroutines in parallel or sequentially depending on transaction context.
If inside a transaction (single connection), executes sequentially to avoid
asyncpg "another operation is in progress" error.
If outside transaction (pool), executes in parallel for better performance.
Args:
coroutines: List of coroutines to execute
Returns:
List of results in the same order as input coroutines
"""
from ..databases.postgres.transaction import get_current_session
if not coroutines:
return []
# Check if we're inside a transaction
if get_current_session() is not None:
# Inside transaction - execute sequentially
results = []
for coro in coroutines:
result = await coro
results.append(result)
return results
else:
# Outside transaction - execute in parallel
return list(await asyncio.gather(*coroutines))
+2
-3

@@ -32,3 +32,3 @@ """

from .model import DotModel
from .model import Model, JsonMode, depends
from .model import Model, JsonMode

@@ -50,3 +50,3 @@ # Components

__version__ = "2.0.7"
__version__ = "2.0.8"

@@ -80,3 +80,2 @@ __all__ = [

"JsonMode",
"depends",
# Components

@@ -83,0 +82,0 @@ "Dialect",

@@ -17,10 +17,3 @@ """Request builder for relation queries."""

# Re-export from components for backward compatibility
from ..components.filter_parser import (
FilterExpression,
FilterTriplet,
SQLOperator,
)
class FetchMode(Enum):

@@ -27,0 +20,0 @@ """Database cursor fetch mode."""

+3
-1

@@ -125,3 +125,5 @@ """

else:
raise ValueError(f"Operator '{op}' cannot be used with None")
raise ValueError(
f"Operator '{op}' cannot be used with None"
)
clause = f"{field} {op} %s"

@@ -128,0 +130,0 @@ return clause, (value,)

@@ -60,2 +60,8 @@ """PostgreSQL connection pool management."""

except (
asyncpg.InvalidCatalogNameError,
asyncpg.ConnectionDoesNotExistError,
):
# Π‘Π” Π½Π΅ сущСствуСт β€” пробрасываСм Π½Π°Π²Π΅Ρ€Ρ… для создания
raise
except (
ConnectionError,

@@ -103,3 +109,3 @@ TimeoutError,

"""
stmt_foreign_keys = []
stmt_foreign_keys: list[tuple[str, str]] = []

@@ -112,9 +118,19 @@ async with ContainerTransaction(self.pool) as session:

# создаСм внСшниС ΠΊΠ»ΡŽΡ‡ΠΈ, для ссылочной цСлостности
# Π½Π° поля many2one Π° Ρ‚Π°ΠΊΠΆΠ΅ Π½Π° ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ ΠΏΠΎΠ»Π΅ΠΉ many2many
for stmt_foreign_key in stmt_foreign_keys:
try:
await session.execute(stmt_foreign_key)
except asyncpg.exceptions.DuplicateObjectError:
# FK already exists
pass
if not stmt_foreign_keys:
return
# ДСдупликация ΠΏΠΎ ΠΈΠΌΠ΅Π½ΠΈ FK (M2M Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π΄ΡƒΠ±Π»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒΡΡ)
unique_fks = {
fk_name: fk_sql for fk_name, fk_sql in stmt_foreign_keys
}
# ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ FK ΠΎΠ΄Π½ΠΈΠΌ запросом
existing_fk_result = await session.execute(
"SELECT conname FROM pg_constraint WHERE contype = 'f'"
)
existing_fk_names = {row["conname"] for row in existing_fk_result}
# Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ FK
for fk_name, fk_sql in unique_fks.items():
if fk_name not in existing_fk_names:
await session.execute(fk_sql)

@@ -181,2 +181,16 @@ """

def depends(*field_names: str) -> Callable[[Callable], Callable]:
"""
Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для вычисляСмых ΠΏΠΎΠ»Π΅ΠΉ.
Π£ΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚, ΠΎΡ‚ ΠΊΠ°ΠΊΠΈΡ… ΠΏΠΎΠ»Π΅ΠΉ зависит вычислСниС.
ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Π΅ зависимости.
"""
def decorator(func: Callable) -> Callable:
func.compute_deps = set(field_names)
return func
return decorator
# def model(

@@ -361,4 +375,4 @@ # func: Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]],

# ΠŸΠ΅Ρ€Π΅Π½ΠΎΡΠΈΠΌ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅ Π½Π° wrapper
wrapper._onchange_fields = fields
wrapper._is_onchange = True
wrapper._onchange_fields = fields # type: ignore
wrapper._is_onchange = True # type: ignore

@@ -365,0 +379,0 @@ return wrapper

@@ -37,2 +37,7 @@ """ORM field definitions."""

relation_table_field - Field name in related model
schema_required - Override required status in API schema validation
True = required in schema (even if nullable)
False = optional in schema (even if not nullable)
None = auto-detect from type annotation
"""

@@ -50,2 +55,3 @@

required: bool | None = None
schema_required: bool | None = None
sql_type: str

@@ -67,2 +73,5 @@ indexable: bool = True

def __init__(self, **kwargs: Any) -> None:
# schema_required - пСрСопрСдСляСт ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Π² API схСмС
self.schema_required = kwargs.pop("schema_required", None)
# добавляСм ΠΏΠΎΠ»Π΅ required для удобства Ρ€Π°Π±ΠΎΡ‚Ρ‹

@@ -80,9 +89,2 @@ # ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ пСрСопрСдСляСт null

self.store = kwargs.pop("store", self.store)
# self.primary_key = kwargs.pop("primary_key", False)
# self.null = kwargs.pop("null", True)
# self.unique = kwargs.pop("unique", False)
# self.description = kwargs.pop("description", None)
# self.default = kwargs.pop("default", None)
# self.ondelete = "restrict" if self.required else "set null"
# self.ondelete = kwargs.pop("null", self.null)
self.ondelete = (

@@ -218,7 +220,7 @@ "set null" if kwargs.pop("null", self.null) else "restrict"

Selection field - Π²Ρ‹Π±ΠΎΡ€ ΠΈΠ· списка ΠΎΠΏΡ†ΠΈΠΉ.
Π₯ранится ΠΊΠ°ΠΊ VARCHAR, Π½ΠΎ ΠΈΠΌΠ΅Π΅Ρ‚ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½Π½Ρ‹ΠΉ Π½Π°Π±ΠΎΡ€ допустимых Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ.
ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Ρ‡Π΅Ρ€Π΅Π· @extend с selection_add:
# Базовая модСль

@@ -231,3 +233,3 @@ class ChatConnector(DotModel):

)
# Π Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ ΠΈΠ· Π΄Ρ€ΡƒΠ³ΠΎΠ³ΠΎ модуля

@@ -237,3 +239,3 @@ @extend(ChatConnector)

type = Selection(selection_add=[("telegram", "Telegram")])
Args:

@@ -245,3 +247,3 @@ options: Бписок ΠΊΠΎΡ€Ρ‚Π΅ΠΆΠ΅ΠΉ (value, label) - Π±Π°Π·ΠΎΠ²Ρ‹Π΅ ΠΎΠΏΡ†ΠΈΠΈ

"""
def __init__(

@@ -251,3 +253,3 @@ self,

selection_add: list[tuple[str, str]] | None = None,
**kwargs
**kwargs,
):

@@ -260,9 +262,9 @@ # Π‘Π°Π·ΠΎΠ²Ρ‹Π΅ ΠΎΠΏΡ†ΠΈΠΈ

self._selection_add = selection_add
# Для Char Π½ΡƒΠΆΠ΅Π½ max_length
if "max_length" not in kwargs:
kwargs["max_length"] = 64
super().__init__(**kwargs)
@property

@@ -272,3 +274,3 @@ def options(self) -> list[tuple[str, str]]:

return self._base_options + self._added_options
@options.setter

@@ -278,10 +280,10 @@ 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:

@@ -291,9 +293,12 @@ new_options: Бписок ΠΊΠΎΡ€Ρ‚Π΅ΠΆΠ΅ΠΉ (value, label)

for opt in new_options:
if opt not in self._base_options and opt not in self._added_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:

@@ -305,3 +310,3 @@ """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ label для значСния."""

return None
def is_selection_add(self) -> bool:

@@ -308,0 +313,0 @@ """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ являСтся Π»ΠΈ это Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ΠΌ (selection_add)."""

try:
from pydantic import create_model, ConfigDict
from pydantic.fields import FieldInfo, Field
from pydantic.fields import Field
except:

@@ -38,3 +38,4 @@ print("pydantic lib not installed")

**params,
) # type: ignore
# field_name=(list[Literal[*allowed_fields]], ...),
)
fields_relation.append(SchemaGetFieldRelationInput)

@@ -229,6 +230,5 @@

if isinstance(field_value, DotField):
if not field_value.null:
required = Ellipsis
else:
required = None
# Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΌΠΎΠ΄Π΅Π»ΠΈ для опрСдСлСния ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
is_required = cls._is_field_required(name, field_value)
required = Ellipsis if is_required else None

@@ -235,0 +235,0 @@ # Ссли Π΅ΡΡ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ

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

import json
from types import UnionType
from typing import (

@@ -12,2 +13,3 @@ TYPE_CHECKING,

Any,
Awaitable,
Callable,

@@ -18,2 +20,4 @@ ClassVar,

dataclass_transform,
get_origin,
get_args,
)

@@ -60,18 +64,5 @@

UPDATE = 4
NESTED_LIST = 5 # Для Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Ρ… записСй Π²Π½ΡƒΡ‚Ρ€ΠΈ FORM
def depends(*field_names: str) -> Callable[[Callable], Callable]:
"""
Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для вычисляСмых ΠΏΠΎΠ»Π΅ΠΉ.
Π£ΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚, ΠΎΡ‚ ΠΊΠ°ΠΊΠΈΡ… ΠΏΠΎΠ»Π΅ΠΉ зависит вычислСниС.
ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Π΅ зависимости.
"""
def decorator(func: Callable) -> Callable:
func.compute_deps = set(field_names)
return func
return decorator
@dataclass_transform(kw_only_default=True, field_specifiers=(Field,))

@@ -86,2 +77,3 @@ class ModelMetaclass(ABCMeta): ...

from .orm.mixins.relations import OrmRelationsMixin
from .orm.mixins.access import AccessMixin

@@ -94,2 +86,3 @@

OrmPrimaryMixin,
AccessMixin,
metaclass=ModelMetaclass,

@@ -444,18 +437,4 @@ ):

for name, field in cls.get_fields().items():
if field.default is not None:
if callable(field.default):
# Ссли ΠΊΠΎΡ€ΡƒΡ‚ΠΈΠ½Π° Ρ‚ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π°Π²Π΅ΠΉΡ‚
if asyncio.iscoroutinefunction(field.default):
res = await field.default()
default_values.update({name: res})
# ΠΈΠ½Π°Ρ‡Π΅ просто Π²Ρ‹Π·ΠΎΠ²
else:
default_values.update({name: field.default()})
else:
default_values.update({name: field.default})
elif isinstance(field, (One2many, Many2many)):
# Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ для m2m ΠΈ o2m
# Ссли с ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ список Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»Π΅ΠΉ,
# Ρ‚ΠΎ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
# Для One2many ΠΈ Many2many всСгда Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ структуру x2m_default
if isinstance(field, (One2many, Many2many)):
fields_nested = fields_client_nested.get(name)

@@ -473,2 +452,14 @@ if fields_nested:

elif field.default is not None:
if callable(field.default):
# Ссли ΠΊΠΎΡ€ΡƒΡ‚ΠΈΠ½Π° Ρ‚ΠΎ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π°Π²Π΅ΠΉΡ‚
if asyncio.iscoroutinefunction(field.default):
res = await field.default()
default_values.update({name: res})
# ΠΈΠ½Π°Ρ‡Π΅ просто Π²Ρ‹Π·ΠΎΠ²
else:
default_values.update({name: field.default()})
else:
default_values.update({name: field.default})
return default_values

@@ -495,2 +486,65 @@

@classmethod
def _is_field_required(cls, field_name: str, field: Field) -> bool:
"""
ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅Ρ‚, являСтся Π»ΠΈ ΠΏΠΎΠ»Π΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌ для API схСмы.
ΠŸΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚:
1. schema_required (Ссли Π·Π°Π΄Π°Π½) β€” явноС ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ для схСмы
2. Primary key β€” всСгда нСобязатСлСн (автогСнСрация)
3. required (Ссли Π·Π°Π΄Π°Π½) β€” общая ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ
4. Аннотация Ρ‚ΠΈΠΏΠ° β€” Π°Π²Ρ‚ΠΎΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅
Args:
field_name: Имя поля
field: ΠžΠ±ΡŠΠ΅ΠΊΡ‚ Field
Returns:
True Ссли ΠΏΠΎΠ»Π΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ Π² схСмС, False ΠΈΠ½Π°Ρ‡Π΅
"""
# 1. schema_required ΠΈΠΌΠ΅Π΅Ρ‚ Π²Ρ‹ΡΡˆΠΈΠΉ ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚ для схСмы
if field.schema_required is not None:
return field.schema_required
# 2. Primary key Π½Π΅ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ Π²Π²ΠΎΠ΄Π° ΠΎΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
if field.primary_key:
return False
# 3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ явно Π·Π°Π΄Π°Π½Π½Ρ‹ΠΉ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ required
if field.required is not None:
return field.required
# 4. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΡŽ Ρ‚ΠΈΠΏΠ°
annotations = getattr(cls, "__annotations__", {})
if field_name not in annotations:
# Если Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΠΈ Π½Π΅Ρ‚, смотрим Π½Π° null ΠΈΠ· поля
return not field.null
py_type = annotations[field_name]
# Бтроковая аннотация: "Model | None"
if isinstance(py_type, str):
if "None" in py_type:
return False
# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π° list Π² строкС
if py_type.startswith("list[") or py_type.startswith("List["):
return False
return True
origin = get_origin(py_type)
# Бписки Π½Π΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ (One2many, Many2many)
if origin is list:
return False
# Union Ρ‚ΠΈΠΏ: провСряСм Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ None
if origin is UnionType or origin is Union:
args = get_args(py_type)
if type(None) in args:
return False
return True
# ΠŸΡ€ΠΎΡΡ‚ΠΎΠΉ Ρ‚ΠΈΠΏ Π±Π΅Π· None - обязатСлСн
return True
@classmethod
def get_fields_info_list(cls, fields_list: list[str]):

@@ -501,2 +555,3 @@ """Get field info for list view."""

if name in fields_list:
required = cls._is_field_required(name, field)
if field.relation:

@@ -512,2 +567,3 @@ fields_info.append(

),
"required": required,
}

@@ -521,2 +577,3 @@ )

"options": field.options or [],
"required": required,
}

@@ -532,2 +589,3 @@ )

if name in fields_list:
required = cls._is_field_required(name, field)
if field.relation:

@@ -544,2 +602,3 @@ fields_info.append(

"relatedField": (field.relation_table_field or ""),
"required": required,
}

@@ -553,2 +612,3 @@ )

"options": field.options or [],
"required": required,
}

@@ -613,2 +673,3 @@ )

if mode == JsonMode.LIST:
# ΠŸΡ€ΠΈ search: field это list
fields_json[field_name] = [

@@ -619,8 +680,20 @@ {

}
for rec in field
]
elif mode == JsonMode.NESTED_LIST:
# ВлоТСнная сСриализация ΠΈΠ· FORM: field это dict с data
fields_json[field_name] = [
{
"id": rec.id,
"name": rec.name or str(rec.id),
}
for rec in field["data"]
]
elif mode == JsonMode.FORM:
# TODO: Ρ‚ΡƒΡ‚ Π½Π°Π΄ΠΎ ΠΎΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π΅ поля ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π΅ΡΡ‚ΡŒ Π² Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚
# ΠŸΡ€ΠΈ FORM (get) field это dict с data/fields/total
fields_json[field_name] = {
"data": [rec.json() for rec in field["data"]],
"data": [
rec.json(mode=JsonMode.NESTED_LIST)
for rec in field["data"]
],
"fields": field["fields"],

@@ -685,7 +758,9 @@ "total": field["total"],

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)
# ΠŸΡ€ΠΎΠΏΡƒΡΠΊΠ°Π΅ΠΌ dunder ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹
if attr_name.startswith("__"):
continue
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)

@@ -708,8 +783,10 @@ return list(fields_with_onchange)

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)
# ΠŸΡ€ΠΎΠΏΡƒΡΠΊΠ°Π΅ΠΌ dunder ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹
if attr_name.startswith("__"):
continue
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)

@@ -734,3 +811,3 @@ return handlers

for handler_name in handlers:
handler = getattr(self, handler_name, None)
handler: Awaitable | None = getattr(self, handler_name, None)
if handler and callable(handler):

@@ -737,0 +814,0 @@ handler_result = await handler()

@@ -110,2 +110,5 @@ """DDL Mixin - provides table creation functionality."""

автоматичСски ΠΏΡ€ΠΈ создании основной ΠΌΠΎΠ΄Π΅Π»ΠΈ.
Returns:
list[tuple[str, str]]: Бписок ΠΊΠΎΡ€Ρ‚Π΅ΠΆΠ΅ΠΉ (fk_name, fk_sql) для создания FK
"""

@@ -122,5 +125,5 @@ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Ρ„Π»Π°Π³ __auto_create__

fields_created: list = []
# Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ запрос Π½Π° добавлСния FK
many2one_fields_fk: list[str] = []
many2many_fields_fk: list[str] = []
# Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ запрос Π½Π° добавлСния FK: список ΠΊΠΎΡ€Ρ‚Π΅ΠΆΠ΅ΠΉ (fk_name, fk_sql)
many2one_fields_fk: list[tuple[str, str]] = []
many2many_fields_fk: list[tuple[str, str]] = []
# запросы Π½Π° созданиС индСксов

@@ -153,7 +156,12 @@ index_statements: list[str] = []

if isinstance(field, Many2one):
# Π½Π΅ Π·Π°Π±Ρ‹Ρ‚ΡŒ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ FK для many2one
# ALTER TABLE %s ADD FOREIGN KEY (%s) REFERENCES %s(%s) ON DELETE %s",
many2one_fields_fk.append(
f"""ALTER TABLE IF EXISTS {cls.__table__} ADD FOREIGN KEY ({field_name}) REFERENCES "{field.relation_table.__table__}" (id) ON DELETE {field.ondelete}"""
# FK с ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π½Π½Ρ‹ΠΌ CONSTRAINT
fk_name = f"fk_{cls.__table__}_{field_name}"
fk_sql = (
f'ALTER TABLE IF EXISTS "{cls.__table__}" '
f'ADD CONSTRAINT "{fk_name}" '
f'FOREIGN KEY ("{field_name}") '
f'REFERENCES "{field.relation_table.__table__}" (id) '
f"ON DELETE {field.ondelete}"
)
many2one_fields_fk.append((fk_name, fk_sql))

@@ -168,3 +176,3 @@ # созданиС индСкса для поля с index=True

index_statements.append(
f'CREATE INDEX IF NOT EXISTS "{index_name}" ON {cls.__table__} ("{field_name}")'
f'CREATE INDEX IF NOT EXISTS "{index_name}" ON "{cls.__table__}" ("{field_name}")'
)

@@ -178,16 +186,31 @@

if field.relation and isinstance(field, Many2many):
# id_column = '"id" SERIAL PRIMARY KEY'
column1 = f'"{field.column1}" INTEGER NOT NULL'
column2 = f'"{field.column2}" INTEGER NOT NULL'
create_table_sql = f"""\
CREATE TABLE IF NOT EXISTS {field.many2many_table} (\
CREATE TABLE IF NOT EXISTS "{field.many2many_table}" (\
{', '.join([column1, column2])}\
);
"""
many2many_fields_fk.append(
f"""ALTER TABLE IF EXISTS "{field.many2many_table}" ADD FOREIGN KEY ({field.column2}) REFERENCES {cls.__table__} (id) ON DELETE {field.ondelete}"""
# FK с ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π½Π½Ρ‹ΠΌΠΈ CONSTRAINT
fk_name1 = f"fk_{field.many2many_table}_{field.column2}"
fk_sql1 = (
f'ALTER TABLE IF EXISTS "{field.many2many_table}" '
f'ADD CONSTRAINT "{fk_name1}" '
f'FOREIGN KEY ("{field.column2}") '
f'REFERENCES "{cls.__table__}" (id) '
f"ON DELETE {field.ondelete}"
)
many2many_fields_fk.append(
f"""ALTER TABLE IF EXISTS "{field.many2many_table}" ADD FOREIGN KEY ({field.column1}) REFERENCES "{field.relation_table.__table__}" (id) ON DELETE {field.ondelete}"""
fk_name2 = f"fk_{field.many2many_table}_{field.column1}"
fk_sql2 = (
f'ALTER TABLE IF EXISTS "{field.many2many_table}" '
f'ADD CONSTRAINT "{fk_name2}" '
f'FOREIGN KEY ("{field.column1}") '
f'REFERENCES "{field.relation_table.__table__}" (id) '
f"ON DELETE {field.ondelete}"
)
many2many_fields_fk.append((fk_name1, fk_sql1))
many2many_fields_fk.append((fk_name2, fk_sql2))
await session.execute(create_table_sql)

@@ -198,3 +221,3 @@

index_statements.append(
f'CREATE INDEX IF NOT EXISTS "{m2m_index_name}" ON {field.many2many_table} ("{field.column1}", "{field.column2}")'
f'CREATE INDEX IF NOT EXISTS "{m2m_index_name}" ON "{field.many2many_table}" ("{field.column1}", "{field.column2}")'
)

@@ -204,3 +227,3 @@

create_table_sql = f"""\
CREATE TABLE IF NOT EXISTS {cls.__table__} (\
CREATE TABLE IF NOT EXISTS "{cls.__table__}" (\
{', '.join(fields_created_declaration)}\

@@ -212,13 +235,18 @@ );"""

# Ссли Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΈ созданы, Π½ΠΎ появились Π½ΠΎΠ²Ρ‹Π΅ поля
# Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΠΈΡ… Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ
# ΠžΠŸΠ’Π˜ΠœΠ˜Π—ΠΠ¦Π˜Π―: ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ всС ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ ΠžΠ”ΠΠ˜Πœ запросом
existing_columns_sql = f"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = '{cls.__table__}'
"""
existing_columns_result = await session.execute(existing_columns_sql)
existing_columns = {
row["column_name"] for row in existing_columns_result
}
# ДобавляСм Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ
for field_name, field_declaration in fields_created:
sql = f"""SELECT column_name FROM information_schema.columns
WHERE table_name='{cls.__table__}' and column_name='{field_name}';"""
field_exist = await session.execute(sql)
# field_exist Π±ΡƒΠ΄Π΅Ρ‚ пустым списком [] Ссли ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ Π½Π΅Ρ‚
if not field_exist:
if field_name not in existing_columns:
await session.execute(
f"""ALTER TABLE {cls.__table__} ADD COLUMN {field_declaration};"""
f'ALTER TABLE "{cls.__table__}" ADD COLUMN {field_declaration};'
)

@@ -231,7 +259,1 @@

return many2one_fields_fk + many2many_fields_fk
# async def __aiter__(self) -> typing.Iterator[Self]:
# """ Return an iterator over ``self``. """
# recs = await self.search()
# for rec in recs:
# yield rec

@@ -15,2 +15,3 @@ """Many2many ORM operations mixin."""

from ...decorators import hybridmethod
from ..utils import execute_maybe_parallel

@@ -133,4 +134,4 @@

]
# Ссли ΠΎΠ΄ΠΈΠ½ ΠΈΠ· запросов с ошибкой сразу ΠΏΡ€Π΅ΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΈ Π²Ρ‹ΠΊΠΈΠ½ΡƒΡ‚ΡŒ ΠΎΡˆΠΈΠ±ΠΊΡƒ
results = await asyncio.gather(*execute_list)
# выполняСм ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ Π² Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ, ΠΏΠ°Ρ€Π°Π»Π»Π΅Π»ΡŒΠ½ΠΎ Π²Π½Π΅ Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ
results = await execute_maybe_parallel(execute_list)

@@ -137,0 +138,0 @@ # ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ (ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹Ρ… ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… запросов) ΠΏΠΎΠ»Π΅ΠΉ связСй

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

from ...access import Operation
from ...components.dialect import POSTGRES
from ...model import JsonMode
from ...decorators import hybridmethod
if TYPE_CHECKING:

@@ -14,7 +19,3 @@ from ..protocol import DotModelProtocol

from ...components.dialect import POSTGRES
from ...model import JsonMode
from ...decorators import hybridmethod
# TypeVar for generic payload - accepts any DotModel subclass

@@ -43,2 +44,4 @@ _M = TypeVar("_M", bound="DotModel")

async def delete(self, session=None):
await self._check_access(Operation.DELETE, record_ids=[self.id])
session = self._get_db_session(session)

@@ -51,2 +54,6 @@ stmt = self._builder.build_delete()

cls = self.__class__
# Одна ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° для всСх ID
await cls._check_access(Operation.DELETE, record_ids=ids)
session = cls._get_db_session(session)

@@ -62,2 +69,4 @@ stmt = cls._builder.build_delete_bulk(len(ids))

):
await self._check_access(Operation.UPDATE, record_ids=[self.id])
session = self._get_db_session(session)

@@ -69,3 +78,2 @@ if payload is None:

# БСриализация Π² ORM слоС, Π° Π½Π΅ Π² Builder
if fields:

@@ -98,5 +106,8 @@ payload_dict = payload.json(

cls = self.__class__
# Одна ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° для всСх ID
await cls._check_access(Operation.UPDATE, record_ids=ids)
session = cls._get_db_session(session)
# БСриализация Π² ORM слоС
payload_dict = payload.json(

@@ -115,5 +126,8 @@ exclude=payload.get_none_update_fields_set(),

cls = self.__class__
# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ table access Π΄ΠΎ создания
await cls._check_access(Operation.CREATE)
session = cls._get_db_session(session)
# БСриализация Π² ORM слоС
payload_dict = payload.json(

@@ -128,3 +142,2 @@ exclude=payload.get_none_update_fields_set(),

# Use dialect instead of _is_postgres
if cls._dialect.supports_returning:

@@ -134,15 +147,22 @@ stmt += " RETURNING id"

assert record is not None
return record[0]["id"]
record_id = record[0]["id"]
else:
record = await session.execute(stmt, values, cursor="lastrowid")
assert record is not None
record_id = record
# TODO: созданиС relations полСй
record = await session.execute(stmt, values, cursor="lastrowid")
assert record is not None
return record
# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ row access послС создания (для Rules Ρ‚ΠΈΠΏΠ° "Ρ‚ΠΎΠ»ΡŒΠΊΠΎ свои записи")
await cls._check_access(Operation.CREATE, record_ids=[record_id])
return record_id
@hybridmethod
async def create_bulk(self, payload: list[_M], session=None):
cls = self.__class__
# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ table access Π΄ΠΎ создания
await cls._check_access(Operation.CREATE)
session = cls._get_db_session(session)
# Π˜ΡΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ primary_key поля
exclude_fields = {

@@ -154,3 +174,2 @@ name

# БСриализация
payloads_dicts = [

@@ -169,2 +188,8 @@ p.json(

records = await session.execute(stmt, values, cursor="fetch")
# ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ row access послС создания
if records:
created_ids = [r["id"] for r in records]
await cls._check_access(Operation.CREATE, record_ids=created_ids)
return records

@@ -175,2 +200,5 @@

cls = self.__class__
await cls._check_access(Operation.READ, record_ids=[id])
session = cls._get_db_session(session)

@@ -184,3 +212,2 @@

if not record:
# return None
raise ValueError("Record not found")

@@ -196,3 +223,2 @@ assert isinstance(record, cls)

# Use dialect for column name
if cls._dialect == POSTGRES:

@@ -199,0 +225,0 @@ prepare = lambda rows: [r["count"] for r in rows]

"""Relations ORM operations mixin."""
import asyncio
from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar
from ...components.filter_parser import FilterExpression
from ...decorators import hybridmethod
from ...access import Operation
from ...builder.request_builder import (
RequestBuilderForm,
)
from ...fields import (
AttachmentMany2one,
AttachmentOne2many,
Many2many,
Many2one,
One2many,
One2one,
)
from ..utils import execute_maybe_parallel

@@ -19,16 +32,3 @@ if TYPE_CHECKING:

from ...builder.request_builder import (
FilterExpression,
RequestBuilderForm,
)
from ...fields import (
AttachmentMany2one,
AttachmentOne2many,
Many2many,
Many2one,
One2many,
One2one,
)
class OrmRelationsMixin(_Base):

@@ -72,2 +72,6 @@ """

cls = self.__class__
# Access check + apply domain filter
filter = await cls._check_access(Operation.READ, filter=filter)
session = cls._get_db_session(session)

@@ -332,4 +336,4 @@

# Ссли ΠΎΠ΄ΠΈΠ½ ΠΈΠ· запросов с ошибкой сразу ΠΏΡ€Π΅ΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ ΠΈ Π²Ρ‹ΠΊΠΈΠ½ΡƒΡ‚ΡŒ ΠΎΡˆΠΈΠ±ΠΊΡƒ
results = await asyncio.gather(*execute_list)
# выполняСм ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ Π² Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ, ΠΏΠ°Ρ€Π°Π»Π»Π΅Π»ΡŒΠ½ΠΎ Π²Π½Π΅ Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ
results = await execute_maybe_parallel(execute_list)

@@ -509,8 +513,5 @@ # добавляСм Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹ ΠΊ исходному ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρƒ,

# 1 conn
results = tuple()
for request in request_list:
res = await asyncio.gather(request)
results += tuple(res)
# выполняСм ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ
results = await execute_maybe_parallel(request_list)
return record_raw

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

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

@@ -45,2 +46,11 @@ import asyncpg

# Access control (from AccessMixin)
@classmethod
async def _check_access(
cls,
operation: "Operation",
record_ids: list[int] | None = None,
filter: list | None = None,
) -> list | None: ...
# Field introspection

@@ -47,0 +57,0 @@ @classmethod

Metadata-Version: 2.4
Name: dotorm
Version: 2.0.7
Version: 2.0.8
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.7"
version = "2.0.8"
description = "Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access"

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