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.8
to
2.0.9
+1
-1
dotorm/__init__.py

@@ -49,3 +49,3 @@ """

__version__ = "2.0.8"
__version__ = "2.0.9"

@@ -52,0 +52,0 @@ __all__ = [

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

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

@@ -75,5 +75,7 @@

elif isinstance(field, Many2one):
elif isinstance(field, (Many2one, AttachmentMany2one)):
ids_m2o: list[int] = [
getattr(record, name) for record in records
getattr(record, name)
for record in records
if getattr(record, name) is not None # Фильтруем None
]

@@ -84,2 +86,5 @@ # оставляем только уникальные ид, так как в m2o несколько записей

# Если нет ни одного ID — пропускаем
if not ids_m2o:
continue
stmt, val = field.relation_table._builder.build_search(

@@ -86,0 +91,0 @@ fields=fields,

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

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

@@ -56,0 +58,0 @@

@@ -37,6 +37,21 @@ """

# Рекурсивный тип для фильтров
# FilterExpression - список элементов, где каждый элемент это:
# - FilterTriplet: ("field", "=", value) - условие
# - tuple[Literal["not"], FilterExpression]: ("not", [...]) - отрицание
# - Literal["and", "or"]: логический оператор между условиями
# - FilterExpression: [...] - вложенная группа (рекурсия)
#
# Примеры:
# [("a", "=", 1), ("b", "=", 2)] # a=1 AND b=2
# [("a", "=", 1), "or", ("b", "=", 2)] # a=1 OR b=2
# [("a", "=", 1), [("b", "=", 2), "or", ("c", "=", 3)]] # a=1 AND (b=2 OR c=3)
# [("not", [("a", "=", 1)])] # NOT (a=1)
FilterExpression = list[
FilterTriplet
| tuple[Literal["not"], "FilterExpression"]
| list[Union["FilterExpression", Literal["and", "or"]]],
Union[
FilterTriplet,
tuple[Literal["not"], "FilterExpression"],
Literal["and", "or"],
"FilterExpression",
]
]

@@ -129,2 +144,8 @@

)
# != с значением должен включать NULL строки
# В SQL: NULL != 1 возвращает NULL (не TRUE)
# Поэтому добавляем OR IS NULL
if op == "!=":
clause = f"({field} IS NULL OR {field} != %s)"
return clause, (value,)
clause = f"{field} {op} %s"

@@ -131,0 +152,0 @@ return clause, (value,)

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

import logging
from typing import TYPE_CHECKING, Any, Callable, Type
from typing import TYPE_CHECKING, Any, Callable, Type, Literal

@@ -16,3 +16,8 @@ if TYPE_CHECKING:

# Допустимые значения для ondelete (общие для PostgreSQL и MySQL InnoDB)
# SET DEFAULT не поддерживается MySQL InnoDB, поэтому исключён
OnDeleteAction = Literal["restrict", "no action", "cascade", "set null"]
VALID_ONDELETE_ACTIONS = ("restrict", "no action", "cascade", "set null")
class Field[FieldType]:

@@ -35,2 +40,7 @@ """

options - List options for selection field
ondelete - Foreign key ON DELETE action:
"restrict" - prevent deletion if referenced
"no action" - same as restrict (deferred in PostgreSQL)
"cascade" - delete child rows
"set null" - set foreign key to NULL

@@ -66,7 +76,4 @@ relation - Is this a relation field?

compute: Callable | None = None
# compute_deps: Set[str]
# is_computed: bool = False
relation: bool = False
relation_table_field: str | None = None
# наверное перенести в класс relation
_relation_table: "DotModel | None" = None

@@ -87,9 +94,23 @@

# self.compute_deps: Set[str] = kwargs.pop("compute_deps", set())
self.indexable = kwargs.pop("indexable", self.indexable)
self.store = kwargs.pop("store", self.store)
self.ondelete = (
"set null" if kwargs.pop("null", self.null) else "restrict"
)
# ondelete - явное указание действия при удалении родительской записи
# Если не указано явно, определяется автоматически на основе null
explicit_ondelete = kwargs.pop("ondelete", None)
if explicit_ondelete is not None:
# Валидация допустимых значений
if explicit_ondelete.lower() not in VALID_ONDELETE_ACTIONS:
raise OrmConfigurationFieldException(
f"Invalid ondelete value: '{explicit_ondelete}'. "
f"Must be one of: {', '.join(VALID_ONDELETE_ACTIONS)}"
)
self.ondelete = explicit_ondelete.lower()
else:
# Автоматическое определение на основе null
# null=True → set null (безопасно удаляет связь)
# null=False → restrict (защищает от удаления)
is_nullable = kwargs.get("null", self.null)
self.ondelete = "set null" if is_nullable else "restrict"
for name, value in kwargs.items():

@@ -100,6 +121,2 @@ setattr(self, name, value)

# обман тайп чекера.
# TODO: В идеале, сделать так чтобы тип поля менялся если это инстанс или если это класс.
# 1. Возможно это необходимо сделать в классе скорей всего модели
# 2. Или перейти на pep-0593 (Integer = Annotated[int, Integer(primary_key=True)])
# но тогда в классе не будет типа Field и мы получим такую же ситуаци но в классе
def __new__(cls, *args: Any, **kwargs: Any) -> FieldType:

@@ -153,3 +170,2 @@ return super().__new__(cls)

if self.index:
# self.index = False
raise OrmConfigurationFieldException(

@@ -156,0 +172,0 @@ f"{self.__class__.__name__} can't be both index=True and unique=True. Index will be ignored."

@@ -24,4 +24,5 @@ try:

fields_relation = []
# fields = []
for field_name, field in cls.__dict__.items():
# Используем get_all_fields() чтобы получить поля включая добавленные через @extend
for field_name, field in cls.get_all_fields().items():
if isinstance(field, DotField):

@@ -34,3 +35,8 @@ if not isinstance(field, (Many2many, One2many)):

# то это поле будет содержать просто список своих полей
allowed_fields = list(field.relation_table.get_fields())
allowed_fields = list(field.relation_table.get_all_fields())
# TODO: по идее должно быть так
# relation_table = field.relation_table
# if callable(relation_table):
# relation_table = relation_table()
# allowed_fields = list(relation_table.get_all_fields())
params = {field_name: (list[Literal[*allowed_fields]], ...)}

@@ -50,18 +56,2 @@ SchemaGetFieldRelationInput = create_model(

# from pydantic import BaseModel, create_model
# from typing import Any, Dict, Type
# def create_pydantic_model_from_class(cls: Type) -> Type[BaseModel]:
# annotations: dict[str, TypeAlias] = getattr(cls, '__annotations__', {})
# fields: dict[str, tuple] = {}
# for field_name, field_type in annotations.items():
# # default = getattr(cls, field_name, ...)
# # fields[field_name] = (field_type, default)
# fields[field_name] = (field_type, None)
# model = create_model(cls.__name__ + 'Schema', **fields)
# return model
from typing import (

@@ -78,14 +68,2 @@ Annotated,

# def parse_string_union_type(text: str):
# """
# Парсит строку вида 'Uom | None' или 'Role | None | Other'
# Возвращает (is_union: bool, list of parts)
# """
# if "|" not in text:
# return False, [text.strip()]
# parts = [p.strip() for p in text.split("|")]
# return True, parts
def replace_custom_types(py_type, class_map: dict[str, Type[BaseModel]]):

@@ -101,15 +79,2 @@ """

if isinstance(py_type, str):
# # 1) проверяем на union-строку
# is_union, parts = parse_string_union_type(py_type)
# if is_union:
# converted = []
# for part in parts:
# if part == "None":
# converted.append(type(None))
# else:
# converted.append(ForwardRef(f"Schema{part}"))
# return Union[tuple(converted)]
# 2) обычная строка: "Uom"

@@ -172,34 +137,2 @@ return ForwardRef(f"Schema{py_type}")

# def convert_field_type(
# py_type, field_value, class_map: dict[str, Type[BaseModel]]
# ):
# """
# Оборачиваем тип в Annotated, если поле кастомное.
# - Сохраняет None для одиночных моделей (Role | None)
# - Сохраняет поведение для списков (list[Rule])
# - Минимальные изменения
# """
# final_type = replace_custom_types(py_type, class_map)
# if field_value is not None:
# # Определяем origin, чтобы проверить, что это не список
# origin = get_origin(final_type)
# # Если это одиночная модель (не список) и поле допускает None — добавляем Optional
# if (
# origin not in (list, List)
# and get_origin(py_type) is UnionType
# and type(None) in get_args(py_type)
# ):
# # role_id: Role | None
# # return Annotated[final_type | None, field_value.__class__]
# return Optional[Annotated[final_type, field_value.__class__]]
# # Иначе обычное поведение (списки и одиночные обязательные поля)
# return Annotated[final_type, field_value.__class__]
# return final_type
def generate_pydantic_models(

@@ -225,8 +158,32 @@ classes: list[type], prefix="Schema"

cls_name = cls.__name__
annotations = getattr(cls, "__annotations__", {})
# Используем get_all_fields() для получения всех полей включая @extend
all_fields = cls.get_all_fields()
# Собираем аннотации из всех классов в MRO + добавленные через @extend
annotations = {}
for klass in reversed(cls.__mro__):
if klass is object:
continue
annotations.update(getattr(klass, "__annotations__", {}))
# Добавляем аннотации для полей из @extend которые могут не иметь __annotations__
for field_name, field_obj in all_fields.items():
if field_name not in annotations:
# Получаем тип из Field объекта
if hasattr(field_obj, "python_type"):
annotations[field_name] = field_obj.python_type
else:
# Fallback - используем Any
annotations[field_name] = Any
model_fields = {}
for name, py_type in annotations.items():
field_value = getattr(cls, name, None)
# Проверяем что это реально поле модели
if name not in all_fields:
continue
field_value = all_fields.get(name)
# Разрешаем ForwardRef или вложенные типы

@@ -259,4 +216,2 @@ final_type = convert_field_type(py_type, field_value, known_models)

model_fields[name] = (final_type, default)
else:
raise ValueError("Found not DotField object")

@@ -272,5 +227,2 @@ # Обновляем модель

# known_models[cls_name].model_fields.update(model_fields)
# known_models[cls_name].model_rebuild(force=True)
# Обновляем forward refs

@@ -282,4 +234,3 @@ _types_namespace = {

model.model_rebuild(force=True, _types_namespace=_types_namespace)
# model.update_forward_refs(**known_models)
return known_models

@@ -297,5 +297,13 @@ """DotModel - main ORM model class."""

def get_fields(cls) -> dict[str, Field]:
"""Возвращает все поля модели, включая унаследованные из миксинов.
Это алиас для get_all_fields() для обратной совместимости.
"""
return cls.get_all_fields()
@classmethod
def get_own_fields(cls) -> dict[str, Field]:
"""Возвращает только собственные поля класса (без унаследованных).
Для получения всех полей включая унаследованные используйте get_all_fields().
Для получения всех полей включая унаследованные используйте get_fields().
"""

@@ -406,3 +414,4 @@ return {

for name, field in cls.get_fields().items()
if field.store and not isinstance(field, Many2one)
if field.store
and not isinstance(field, (Many2one, AttachmentMany2one))
]

@@ -669,10 +678,4 @@

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"]
]
# Вложенная сериализация оставляем как есть
fields_json[field_name] = field
elif mode == JsonMode.FORM:

@@ -679,0 +682,0 @@ # При FORM (get) field это dict с data/fields/total

@@ -13,3 +13,3 @@ """Many2many ORM operations mixin."""

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

@@ -142,3 +142,3 @@ from ..utils import execute_maybe_parallel

if isinstance(req.field, Many2one):
if isinstance(req.field, (Many2one, AttachmentMany2one)):
for rec in records:

@@ -145,0 +145,0 @@ rec_field_raw = getattr(rec, req.field_name)

@@ -398,3 +398,3 @@ """Relations ORM operations mixin."""

attachment_id = await field.relation_table.create(
attachment_payload, session
payload=attachment_payload
)

@@ -401,0 +401,0 @@ setattr(payload, name, attachment_id)

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

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