diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index cf591cebf..aba0a1bda 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -17,7 +17,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' strategy: matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12] + python-version: [3.9, "3.10", 3.11, 3.12] fail-fast: false services: mysql: diff --git a/docs/migration.md b/docs/migration.md index 2637ac9d7..02bed0e31 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -57,7 +57,6 @@ engine: Optional[sqlalchemy.engine.Engine] tablename: Optional[str] order_by: Optional[List[str]] abstract: bool -exclude_parent_fields: Optional[List[str]] queryset_class: Type[QuerySet] extra: Extra constraints: Optional[List[ColumnCollectionConstraint]] diff --git a/docs/models/inheritance.md b/docs/models/inheritance.md index cca98ebae..bdecdf333 100644 --- a/docs/models/inheritance.md +++ b/docs/models/inheritance.md @@ -443,105 +443,3 @@ abstract parent model you may lose your data on through table if not careful. That means that each time you define a Child model you need to either manually create the table in the database, or run a migration (with alembic). - -## exclude_parent_fields - -Ormar allows you to skip certain fields in inherited model that are coming from a parent model. - -!!!Note - Note that the same behaviour can be achieved by splitting the model into more abstract models and mixins - which is a preferred way in normal circumstances. - -To skip certain fields from a child model, list all fields that you want to skip in `model.ormar_config.exclude_parent_fields` parameter like follows: - -```python -base_ormar_config = OrmarConfig( - metadata=sa.MetaData(), - database=databases.Database(DATABASE_URL), -) - - -class AuditModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_by: str = ormar.String(max_length=100) - updated_by: str = ormar.String(max_length=100, default="Sam") - - -class DateFieldsModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="creation_date" - ) - updated_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="modification_date" - ) - - -class Category(DateFieldsModel, AuditModel): - ormar_config = base_ormar_config.copy( - tablename="categories", - # set fields that should be skipped - exclude_parent_fields=["updated_by", "updated_date"], - ) - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50, unique=True, index=True) - code: int = ormar.Integer() - -# Note that now the update fields in Category are gone in all places -> ormar fields, pydantic fields and sqlachemy table columns -# so full list of available fields in Category is: ["created_by", "created_date", "id", "name", "code"] -``` - -Note how you simply need to provide field names and it will exclude the parent field regardless of from which parent model the field is coming from. - -!!!Note - Note that if you want to overwrite a field in child model you do not have to exclude it, simply overwrite the field declaration in child model with same field name. - -!!!Warning - Note that this kind of behavior can confuse mypy and static type checkers, yet accessing the non existing fields will fail at runtime. That's why splitting the base classes is preferred. - -The same effect can be achieved by splitting base classes like: - -```python -base_ormar_config = OrmarConfig( - metadata=sa.MetaData(), - database=databases.Database(DATABASE_URL), -) - - -class AuditCreateModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_by: str = ormar.String(max_length=100) - - -class AuditUpdateModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - updated_by: str = ormar.String(max_length=100, default="Sam") - -class CreateDateFieldsModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="creation_date" - ) - -class UpdateDateFieldsModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - updated_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="modification_date" - ) - - -class Category(CreateDateFieldsModel, AuditCreateModel): - ormar_config = base_ormar_config.copy(tablename="categories") - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50, unique=True, index=True) - code: int = ormar.Integer() -``` - -That way you can inherit from both create and update classes if needed, and only one of them otherwise. diff --git a/docs/releases.md b/docs/releases.md index a125a1738..00ceac5e3 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -89,7 +89,6 @@ tablename: Optional[str] order_by: Optional[List[str]] abstract: bool - exclude_parent_fields: Optional[List[str]] queryset_class: Type[QuerySet] extra: Extra constraints: Optional[List[ColumnCollectionConstraint]] diff --git a/ormar/fields/base.py b/ormar/fields/base.py index a55f9a956..8838a0fb3 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -1,4 +1,14 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + Optional, + Type, + Union, + overload, +) import sqlalchemy from pydantic.fields import FieldInfo, _Unset @@ -167,8 +177,23 @@ def default_value(self, use_server: bool = False) -> Optional[Dict]: return dict(default=default) return None + @overload + def get_default( + self, + *, + call_default_factory: Literal[True], + validated_data: Union[dict[str, Any], None] = None, + ) -> Any: ... + + @overload + def get_default(self, *, call_default_factory: Literal[False] = ...) -> Any: ... + def get_default( - self, use_server: bool = False, call_default_factory: bool = True + self, + *, + call_default_factory: bool = True, + validated_data: Union[dict[str, Any], None] = None, + use_server: bool = False, ) -> Any: # noqa CCR001 """ Return default value for a field. @@ -192,7 +217,7 @@ def get_default( call_default_factory=call_default_factory, ) - def _get_default_server_value(self, use_server: bool) -> Any: + def _get_default_server_value(self, use_server: bool) -> Any: # pragma: no cover """ Return default value for a server side if use_server is True """ @@ -266,7 +291,7 @@ def get_column(self, name: str) -> sqlalchemy.Column: :rtype: sqlalchemy.Column """ if self.encrypt_backend == EncryptBackends.NONE: - column = sqlalchemy.Column( + column: sqlalchemy.Column = sqlalchemy.Column( self.db_alias or name, self.column_type, *self.construct_constraints(), @@ -295,7 +320,7 @@ def _get_encrypted_column(self, name: str) -> sqlalchemy.Column: raise ModelDefinitionError( "Primary key field and relations fields" "cannot be encrypted!" ) - column = sqlalchemy.Column( + column: sqlalchemy.Column = sqlalchemy.Column( self.db_alias or name, EncryptedString( _field_type=self, diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 5b0522f51..e968e25b8 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -18,7 +18,7 @@ from typing import Literal # type: ignore except ImportError: # pragma: no cover from typing_extensions import Literal # type: ignore - + try: from typing import Self # type: ignore except ImportError: # pragma: no cover diff --git a/ormar/fields/sqlalchemy_encrypted.py b/ormar/fields/sqlalchemy_encrypted.py index 1ad13f95e..254c657e5 100644 --- a/ormar/fields/sqlalchemy_encrypted.py +++ b/ormar/fields/sqlalchemy_encrypted.py @@ -177,7 +177,7 @@ def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: return encrypted_value def process_result_value(self, value: Any, dialect: Dialect) -> Any: - if value is None: + if value is None: # pragma: no cover return value self._refresh() decrypted_value = self.backend.decrypt(value) diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index f2a8f99f5..2bf253822 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -32,7 +32,7 @@ def load_dialect_impl(self, dialect: Dialect) -> Any: else dialect.type_descriptor(CHAR(32)) ) - def process_bind_param(self, value: uuid.UUID, dialect: Dialect) -> Optional[str]: + def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: if value is None: return value return str(value) if self.uuid_format == "string" else "%.32x" % value.int @@ -40,7 +40,7 @@ def process_bind_param(self, value: uuid.UUID, dialect: Dialect) -> Optional[str def process_result_value( self, value: Optional[str], dialect: Dialect ) -> Optional[uuid.UUID]: - if value is None: + if value is None: # pragma: no cover return value if not isinstance(value, uuid.UUID): return uuid.UUID(value) diff --git a/ormar/models/helpers/__init__.py b/ormar/models/helpers/__init__.py index 5d4c1de74..c85fa2349 100644 --- a/ormar/models/helpers/__init__.py +++ b/ormar/models/helpers/__init__.py @@ -8,7 +8,6 @@ get_potential_fields, get_pydantic_base_orm_config, merge_or_generate_pydantic_config, - remove_excluded_parent_fields, ) from ormar.models.helpers.relations import ( alias_manager, diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index 27629b9fe..683352bc1 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -32,7 +32,9 @@ def create_pydantic_field( model_field.through.model_fields[field_name] = FieldInfo.from_annotated_attribute( annotation=Optional[model], default=None # type: ignore ) - model_field.through.model_rebuild(force=True) + model_field.through.model_rebuild( + force=True, _types_namespace={model_field.owner.__name__: model_field.owner} + ) def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]: @@ -123,20 +125,3 @@ def get_potential_fields(attrs: Union[Dict, MappingProxyType]) -> Dict: or isinstance(v, BaseField) ) } - - -def remove_excluded_parent_fields(model: Type["Model"]) -> None: - """ - Removes pydantic fields that should be excluded from parent models - - :param model: - :type model: Type["Model"] - """ - excludes = {*model.ormar_config.exclude_parent_fields} - { - *model.ormar_config.model_fields.keys() - } - if excludes: - model.model_fields = { - k: v for k, v in model.model_fields.items() if k not in excludes - } - model.model_rebuild(force=True) diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index 8a0b958bf..865731aef 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -1,6 +1,6 @@ import inspect import warnings -from typing import TYPE_CHECKING, Any, List, Optional, Type, Union, cast +from typing import TYPE_CHECKING, Any, ForwardRef, List, Optional, Type, Union, cast from pydantic import BaseModel, create_model, field_serializer from pydantic._internal._decorators import DecoratorInfos @@ -156,7 +156,17 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: add_field_serializer_for_reverse_relations( to_model=model_field.to, related_name=related_name ) - model_field.to.model_rebuild(force=True) + model_field.to.model_rebuild( + force=True, + _types_namespace={ + **{model_field.owner.__name__: model_field.owner}, + **{ + field.to.__name__: field.to + for field in related_model_fields.values() + if field.is_relation and field.to.__class__ != ForwardRef + }, + }, + ) setattr(model_field.to, related_name, RelationDescriptor(name=related_name)) diff --git a/ormar/models/helpers/sqlalchemy.py b/ormar/models/helpers/sqlalchemy.py index 1a4fd974d..f41dbd899 100644 --- a/ormar/models/helpers/sqlalchemy.py +++ b/ormar/models/helpers/sqlalchemy.py @@ -93,7 +93,7 @@ def create_and_append_m2m_fk( ), ) model_field.through.ormar_config.columns.append(column) - model_field.through.ormar_config.table.append_column(column) + model_field.through.ormar_config.table.append_column(column, replace_existing=True) def check_pk_column_validity( diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 849d1882f..112548961 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -51,7 +51,6 @@ populate_config_tablename_columns_and_pk, populate_default_options_values, register_relation_in_alias_manager, - remove_excluded_parent_fields, sqlalchemy_columns_from_model_fields, ) from ormar.models.ormar_config import OrmarConfig @@ -195,7 +194,9 @@ def get_constraint_copy( } checks = (key if isinstance(constraint, key) else None for key in constraints) target_class = next((target for target in checks if target is not None), None) - constructor: Optional[Callable] = constraints.get(target_class) + constructor: Optional[Callable] = ( + constraints.get(target_class) if target_class else None + ) if not constructor: raise ValueError(f"{constraint} must be a ColumnCollectionMixin!") @@ -293,13 +294,12 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 metadata=through_class.ormar_config.metadata, database=through_class.ormar_config.database, abstract=through_class.ormar_config.abstract, - exclude_parent_fields=through_class.ormar_config.exclude_parent_fields, queryset_class=through_class.ormar_config.queryset_class, extra=through_class.ormar_config.extra, constraints=through_class.ormar_config.constraints, order_by=through_class.ormar_config.orders_by, ) - new_config.table = through_class.ormar_config.pkname + new_config.table = through_class.ormar_config.pkname # type: ignore new_config.pkname = through_class.ormar_config.pkname new_config.alias_manager = through_class.ormar_config.alias_manager new_config.signals = through_class.ormar_config.signals @@ -316,7 +316,7 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 # they will be populated later in expanding reverse relation # if hasattr(new_config, "table"): new_config.tablename += "_" + ormar_config.tablename - new_config.table = None + new_config.table = None # type: ignore new_config.model_fields = { name: field for name, field in new_config.model_fields.items() @@ -385,11 +385,6 @@ def copy_data_from_parent_model( # noqa: CCR001 else attrs.get("__name__", "").lower() + "s" ) for field_name, field in base_class.ormar_config.model_fields.items(): - if ( - hasattr(ormar_config, "exclude_parent_fields") - and field_name in ormar_config.exclude_parent_fields - ): - continue if field.is_multi: field = cast(ManyToManyField, field) copy_and_replace_m2m_through_model( @@ -682,7 +677,6 @@ def __new__( # type: ignore # noqa: CCR001 new_model.model_rebuild(force=True) new_model.pk = PkDescriptor(name=new_model.ormar_config.pkname) - remove_excluded_parent_fields(new_model) return new_model diff --git a/ormar/models/mixins/pydantic_mixin.py b/ormar/models/mixins/pydantic_mixin.py index bd2e1a6f3..0e8343f39 100644 --- a/ormar/models/mixins/pydantic_mixin.py +++ b/ormar/models/mixins/pydantic_mixin.py @@ -18,7 +18,6 @@ import pydantic from pydantic import BaseModel from pydantic._internal._decorators import DecoratorInfos -from pydantic.fields import FieldInfo from ormar.fields import BaseField, ForeignKeyField, ManyToManyField from ormar.models.mixins.relation_mixin import RelationMixin # noqa: I100, I202 @@ -30,7 +29,6 @@ class PydanticMixin(RelationMixin): if TYPE_CHECKING: # pragma: no cover __pydantic_decorators__: DecoratorInfos - model_fields: Dict[str, FieldInfo] _skip_ellipsis: Callable _get_not_excluded_fields: Callable @@ -126,7 +124,7 @@ def _determine_pydantic_field_type( relation_map=relation_map, ) elif not field.is_relation: - defaults[name] = cls.model_fields[name].default + defaults[name] = cls.model_fields[name].default # type: ignore target = field.__type__ if target is not None and field.nullable: target = Optional[target] diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index 495f3b9e2..965895046 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -10,6 +10,7 @@ List, Optional, Set, + Union, cast, ) @@ -40,7 +41,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): _bytes_fields: Set[str] __pydantic_core_schema__: CoreSchema __ormar_fields_validators__: Optional[ - Dict[str, SchemaValidator | PluggableSchemaValidator] + Dict[str, Union[SchemaValidator, PluggableSchemaValidator]] ] @classmethod @@ -285,7 +286,7 @@ def _extract_pydantic_fields(cls) -> Any: return cls.__pydantic_core_schema__["schema"]["fields"] elif cls.__pydantic_core_schema__["type"] == "definitions": main_schema = cls.__pydantic_core_schema__["schema"] - if "schema_ref" in main_schema: + if "schema_ref" in main_schema: # pragma: no cover reference_id = main_schema["schema_ref"] return next( ref diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index a4f6ab24e..2143052cc 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -286,7 +286,7 @@ def _process_kwargs(self, kwargs: Dict) -> Tuple[Dict, Dict]: # noqa: CCR001 """ property_fields = self.ormar_config.property_fields model_fields = self.ormar_config.model_fields - pydantic_fields = set(self.model_fields.keys()) + pydantic_fields = set(self.__class__.model_fields.keys()) # remove property fields for prop_filed in property_fields: @@ -517,7 +517,14 @@ def update_forward_refs(cls, **localns: Any) -> None: update_column_definition(model=cls, field=field) populate_config_sqlalchemy_table_if_required(config=cls.ormar_config) # super().update_forward_refs(**localns) - cls.model_rebuild(force=True) + cls.model_rebuild( + force=True, + _types_namespace={ + field.to.__name__: field.to + for field in fields_to_check.values() + if field.is_relation + }, + ) cls.ormar_config.requires_ref_update = False @staticmethod diff --git a/ormar/models/ormar_config.py b/ormar/models/ormar_config.py index e4941b18b..b5af521a4 100644 --- a/ormar/models/ormar_config.py +++ b/ormar/models/ormar_config.py @@ -32,13 +32,12 @@ def __init__( tablename: Optional[str] = None, order_by: Optional[List[str]] = None, abstract: bool = False, - exclude_parent_fields: Optional[List[str]] = None, queryset_class: Type[QuerySet] = QuerySet, extra: Extra = Extra.forbid, constraints: Optional[List[ColumnCollectionConstraint]] = None, ) -> None: self.pkname = None # type: ignore - self.metadata = metadata + self.metadata = metadata # type: ignore self.database = database # type: ignore self.engine = engine # type: ignore self.tablename = tablename # type: ignore @@ -53,10 +52,9 @@ def __init__( self.signals: SignalEmitter = SignalEmitter() self.abstract = abstract self.requires_ref_update: bool = False - self.exclude_parent_fields = exclude_parent_fields or [] self.extra = extra self.queryset_class = queryset_class - self.table: sqlalchemy.Table = None + self.table: sqlalchemy.Table = None # type: ignore def copy( self, @@ -66,7 +64,6 @@ def copy( tablename: Optional[str] = None, order_by: Optional[List[str]] = None, abstract: Optional[bool] = None, - exclude_parent_fields: Optional[List[str]] = None, queryset_class: Optional[Type[QuerySet]] = None, extra: Optional[Extra] = None, constraints: Optional[List[ColumnCollectionConstraint]] = None, @@ -78,7 +75,6 @@ def copy( tablename=tablename, order_by=order_by, abstract=abstract or self.abstract, - exclude_parent_fields=exclude_parent_fields, queryset_class=queryset_class or self.queryset_class, extra=extra or self.extra, constraints=constraints, diff --git a/ormar/queryset/actions/filter_action.py b/ormar/queryset/actions/filter_action.py index f5ab83212..48271608c 100644 --- a/ormar/queryset/actions/filter_action.py +++ b/ormar/queryset/actions/filter_action.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Type -import sqlalchemy +from sqlalchemy import TextClause import ormar # noqa: I100, I202 from ormar.exceptions import QueryDefinitionError @@ -124,7 +124,7 @@ def _prefix_suffix_quote(self) -> None: sufix = "%" if "end" not in self.operator else "" self.filter_value = f"{prefix}{self.filter_value}{sufix}" - def get_text_clause(self) -> sqlalchemy.sql.expression.BinaryExpression: + def get_text_clause(self) -> TextClause: """ Escapes characters if it's required. Substitutes values of the models if value is a ormar Model with its pk value. diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index 0000ac1c2..511c08a2c 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -1,9 +1,10 @@ import itertools from dataclasses import dataclass from enum import Enum -from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Type, Union import sqlalchemy +from sqlalchemy import ColumnElement import ormar # noqa I100 from ormar.queryset.actions.filter_action import FilterAction @@ -100,7 +101,9 @@ def _iter(self) -> Generator: yield from group._iter() yield from self.actions - def _get_text_clauses(self) -> List[sqlalchemy.sql.expression.TextClause]: + def _get_text_clauses( + self, + ) -> List[Union[sqlalchemy.sql.expression.TextClause, ColumnElement[Any]]]: """ Helper to return list of text queries from actions and nested groups :return: list of text queries from actions and nested groups @@ -110,7 +113,7 @@ def _get_text_clauses(self) -> List[sqlalchemy.sql.expression.TextClause]: x.get_text_clause() for x in self.actions ] - def get_text_clause(self) -> sqlalchemy.sql.expression.TextClause: + def get_text_clause(self) -> ColumnElement[bool]: """ Returns all own actions and nested groups conditions compiled and joined inside parentheses. diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 9d7e4d157..983abc7db 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, cast import sqlalchemy -from sqlalchemy import text +from sqlalchemy import TextClause, text import ormar # noqa I100 from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError @@ -17,7 +17,7 @@ class SqlJoin: def __init__( # noqa: CFQ002 self, used_aliases: List, - select_from: sqlalchemy.sql.select, + select_from: sqlalchemy.sql.Select, columns: List[sqlalchemy.Column], excludable: "ExcludableItems", order_columns: Optional[List["OrderAction"]], @@ -101,7 +101,7 @@ def _on_clause( from_column_name: str, to_table_name: str, to_column_name: str, - ) -> text: + ) -> TextClause: """ Receives aliases and names of both ends of the join and combines them into one text clause used in joins. @@ -133,7 +133,7 @@ def _on_clause( return text(f"{left_part}={right_part}") - def build_join(self) -> Tuple[List, sqlalchemy.sql.select, List, Dict]: + def build_join(self) -> Tuple[List, sqlalchemy.sql.Select, List, Dict]: """ Main external access point for building a join. Splits the join definition, updates fields and exclude_fields if needed, @@ -305,7 +305,7 @@ def _process_join(self) -> None: # noqa: CFQ002 self.next_alias, self.to_table ) self.select_from = sqlalchemy.sql.outerjoin( - self.select_from, target_table, on_clause + self.select_from, target_table, on_clause # type: ignore ) self._get_order_bys() @@ -317,8 +317,8 @@ def _process_join(self) -> None: # noqa: CFQ002 use_alias=True, ) self.columns.extend( - self.alias_manager.prefixed_columns( - self.next_alias, target_table, self_related_fields + self.alias_manager.prefixed_columns( # type: ignore + self.next_alias, target_table, self_related_fields # type: ignore ) ) self.used_aliases.append(self.next_alias) diff --git a/ormar/queryset/queries/filter_query.py b/ormar/queryset/queries/filter_query.py index e6b7e828c..804b31d12 100644 --- a/ormar/queryset/queries/filter_query.py +++ b/ormar/queryset/queries/filter_query.py @@ -1,6 +1,7 @@ -from typing import List +from typing import Any, List, Union import sqlalchemy +from sqlalchemy import ColumnElement, Select, TextClause from ormar.queryset.actions.filter_action import FilterAction @@ -16,7 +17,10 @@ def __init__( self.exclude = exclude self.filter_clauses = filter_clauses - def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + def apply( + self, + expr: Select[Any], + ) -> Select[Any]: """ Applies all filter clauses if set. @@ -27,7 +31,9 @@ def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: """ if self.filter_clauses: if len(self.filter_clauses) == 1: - clause = self.filter_clauses[0].get_text_clause() + clause: Union[TextClause, ColumnElement[Any]] = self.filter_clauses[ + 0 + ].get_text_clause() else: clause = sqlalchemy.sql.and_( *[x.get_text_clause() for x in self.filter_clauses] diff --git a/ormar/queryset/queries/limit_query.py b/ormar/queryset/queries/limit_query.py index f4a560f98..954298e6d 100644 --- a/ormar/queryset/queries/limit_query.py +++ b/ormar/queryset/queries/limit_query.py @@ -11,7 +11,7 @@ class LimitQuery: def __init__(self, limit_count: Optional[int]) -> None: self.limit_count = limit_count - def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select: """ Applies the limit clause. diff --git a/ormar/queryset/queries/offset_query.py b/ormar/queryset/queries/offset_query.py index 2970f6ed2..3956ac6f2 100644 --- a/ormar/queryset/queries/offset_query.py +++ b/ormar/queryset/queries/offset_query.py @@ -11,7 +11,7 @@ class OffsetQuery: def __init__(self, query_offset: Optional[int]) -> None: self.query_offset = query_offset - def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select: """ Applies the offset clause. diff --git a/ormar/queryset/queries/order_query.py b/ormar/queryset/queries/order_query.py index 45157490d..68fc3df33 100644 --- a/ormar/queryset/queries/order_query.py +++ b/ormar/queryset/queries/order_query.py @@ -11,7 +11,7 @@ class OrderQuery: def __init__(self, sorted_orders: Dict) -> None: self.sorted_orders = sorted_orders - def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: + def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select: """ Applies all order_by clauses if set. diff --git a/ormar/queryset/queries/query.py b/ormar/queryset/queries/query.py index 22c465e71..25bd6349a 100644 --- a/ormar/queryset/queries/query.py +++ b/ormar/queryset/queries/query.py @@ -1,7 +1,7 @@ -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union import sqlalchemy -from sqlalchemy import Table, text +from sqlalchemy import Column, Select, Table, TextClause from sqlalchemy.sql import Join import ormar # noqa I100 @@ -42,9 +42,9 @@ def __init__( # noqa CFQ002 self.used_aliases: List[str] = [] self.select_from: Union[Join, Table, List[str]] = [] - self.columns = [sqlalchemy.Column] + self.columns: List[Column] = [] self.order_columns = order_bys - self.sorted_orders: Dict[OrderAction, text] = {} + self.sorted_orders: Dict[OrderAction, TextClause] = {} self._init_sorted_orders() self.limit_raw_sql = limit_raw_sql @@ -55,7 +55,7 @@ def _init_sorted_orders(self) -> None: """ if self.order_columns: for clause in self.order_columns: - self.sorted_orders[clause] = None + self.sorted_orders[clause] = None # type: ignore def apply_order_bys_for_primary_model(self) -> None: # noqa: CCR001 """ @@ -97,7 +97,7 @@ def _pagination_query_required(self) -> bool: and self._select_related ) - def build_select_expression(self) -> sqlalchemy.sql.select: + def build_select_expression(self) -> sqlalchemy.sql.Select: """ Main entry point from outside (after proper initialization). @@ -113,7 +113,7 @@ def build_select_expression(self) -> sqlalchemy.sql.select: self_related_fields = self.model_cls.own_table_columns( model=self.model_cls, excludable=self.excludable, use_alias=True ) - self.columns = self.model_cls.ormar_config.alias_manager.prefixed_columns( + self.columns = self.model_cls.ormar_config.alias_manager.prefixed_columns( # type: ignore "", self.table, self_related_fields ) self.apply_order_bys_for_primary_model() @@ -127,7 +127,7 @@ def build_select_expression(self) -> sqlalchemy.sql.select: remainder = related_models[related] sql_join = SqlJoin( used_aliases=self.used_aliases, - select_from=self.select_from, + select_from=self.select_from, # type: ignore columns=self.columns, excludable=self.excludable, order_columns=self.order_columns, @@ -143,7 +143,7 @@ def build_select_expression(self) -> sqlalchemy.sql.select: self.select_from, self.columns, self.sorted_orders, - ) = sql_join.build_join() + ) = sql_join.build_join() # type: ignore if self._pagination_query_required(): limit_qry, on_clause = self._build_pagination_condition() @@ -151,7 +151,7 @@ def build_select_expression(self) -> sqlalchemy.sql.select: self.select_from, limit_qry, on_clause ) - expr = sqlalchemy.sql.select(self.columns) + expr = sqlalchemy.sql.select(*self.columns) expr = expr.select_from(self.select_from) expr = self._apply_expression_modifiers(expr) @@ -191,8 +191,8 @@ def _build_pagination_condition( elif order.get_field_name_text() == pk_aliased_name: maxes[pk_aliased_name] = order.get_text_clause() - limit_qry = sqlalchemy.sql.select([qry_text]) - limit_qry = limit_qry.select_from(self.select_from) + limit_qry: Select[Any] = sqlalchemy.sql.select(qry_text) + limit_qry = limit_qry.select_from(self.select_from) # type: ignore limit_qry = FilterQuery(filter_clauses=self.filter_clauses).apply(limit_qry) limit_qry = FilterQuery( filter_clauses=self.exclude_clauses, exclude=True @@ -202,15 +202,15 @@ def _build_pagination_condition( limit_qry = limit_qry.order_by(order_by) limit_qry = LimitQuery(limit_count=self.limit_count).apply(limit_qry) limit_qry = OffsetQuery(query_offset=self.query_offset).apply(limit_qry) - limit_qry = limit_qry.alias("limit_query") + limit_qry = limit_qry.alias("limit_query") # type: ignore on_clause = sqlalchemy.text( f"limit_query.{pk_alias}={self.table.name}.{pk_alias}" ) - return limit_qry, on_clause + return limit_qry, on_clause # type: ignore def _apply_expression_modifiers( - self, expr: sqlalchemy.sql.select - ) -> sqlalchemy.sql.select: + self, expr: sqlalchemy.sql.Select + ) -> sqlalchemy.sql.Select: """ Receives the select query (might be join) and applies: * Filter clauses diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index f17a2f3e4..4203c6bdd 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -20,15 +20,6 @@ import sqlalchemy from sqlalchemy import bindparam -try: - from sqlalchemy.engine import LegacyRow -except ImportError: # pragma: no cover - if TYPE_CHECKING: - - class LegacyRow(dict): # type: ignore - pass - - import ormar # noqa I100 from ormar import MultipleMatches, NoMatch from ormar.exceptions import ( @@ -269,7 +260,7 @@ def build_select_expression( limit: Optional[int] = None, offset: Optional[int] = None, order_bys: Optional[List] = None, - ) -> sqlalchemy.sql.select: + ) -> sqlalchemy.sql.Select: """ Constructs the actual database query used in the QuerySet. If any of the params is not passed the QuerySet own value is used. @@ -628,9 +619,7 @@ async def values( model_cls=self.model_cls, # type: ignore exclude_through=exclude_through, ) - column_map = alias_resolver.resolve_columns( - columns_names=list(cast(LegacyRow, rows[0]).keys()) - ) + column_map = alias_resolver.resolve_columns(columns_names=list(rows[0].keys())) # type: ignore result = [ {column_map.get(k): v for k, v in dict(x).items() if k in column_map} for x in rows @@ -705,11 +694,11 @@ async def count(self, distinct: bool = True) -> int: :rtype: int """ expr = self.build_select_expression().alias("subquery_for_count") - expr = sqlalchemy.func.count().select().select_from(expr) + expr = sqlalchemy.func.count().select().select_from(expr) # type: ignore if distinct: pk_column_name = self.model.get_column_alias(self.model_config.pkname) - expr_distinct = expr.group_by(pk_column_name).alias("subquery_for_group") - expr = sqlalchemy.func.count().select().select_from(expr_distinct) + expr_distinct = expr.group_by(pk_column_name).alias("subquery_for_group") # type: ignore + expr = sqlalchemy.func.count().select().select_from(expr_distinct) # type: ignore return await self.database.fetch_val(expr) async def _query_aggr_function(self, func_name: str, columns: List) -> Any: @@ -724,7 +713,7 @@ async def _query_aggr_function(self, func_name: str, columns: List) -> Any: ) select_columns = [x.apply_func(func, use_label=True) for x in select_actions] expr = self.build_select_expression().alias(f"subquery_for_{func_name}") - expr = sqlalchemy.select(select_columns).select_from(expr) + expr = sqlalchemy.select(*select_columns).select_from(expr) # type: ignore # print("\n", expr.compile(compile_kwargs={"literal_binds": True})) result = await self.database.fetch_one(expr) return dict(result) if len(result) > 1 else result[0] # type: ignore @@ -805,7 +794,7 @@ async def update(self, each: bool = False, **kwargs: Any) -> int: updates = self.model.translate_columns_to_aliases(updates) expr = FilterQuery(filter_clauses=self.filter_clauses).apply( - self.table.update().values(**updates) + self.table.update().values(**updates) # type: ignore ) expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply( expr @@ -834,7 +823,7 @@ async def delete(self, *args: Any, each: bool = False, **kwargs: Any) -> int: "If you want to delete all rows use delete(each=True)" ) expr = FilterQuery(filter_clauses=self.filter_clauses).apply( - self.table.delete() + self.table.delete() # type: ignore ) expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply( expr @@ -1247,7 +1236,7 @@ async def bulk_update( # noqa: CCR001 ) # databases bind params only where query is passed as string # otherwise it just passes all data to values and results in unconsumed columns - expr = str(expr) + expr = str(expr) # type: ignore await self.database.execute_many(expr, ready_objects) for obj in objects: diff --git a/ormar/relations/alias_manager.py b/ormar/relations/alias_manager.py index 0355f3430..a0e9ece37 100644 --- a/ormar/relations/alias_manager.py +++ b/ormar/relations/alias_manager.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import sqlalchemy -from sqlalchemy import text +from sqlalchemy import Label +from sqlalchemy.sql.selectable import NamedFromClause if TYPE_CHECKING: # pragma: no cover from ormar import Model @@ -35,7 +36,7 @@ class AliasManager: def __init__(self) -> None: self._aliases_new: Dict[str, str] = dict() self._reversed_aliases: Dict[str, str] = dict() - self._prefixed_tables: Dict[str, text] = dict() + self._prefixed_tables: Dict[str, NamedFromClause] = dict() def __contains__(self, item: str) -> bool: return self._aliases_new.__contains__(item) @@ -60,7 +61,7 @@ def reversed_aliases(self) -> Dict: @staticmethod def prefixed_columns( alias: str, table: sqlalchemy.Table, fields: Optional[List] = None - ) -> List[text]: + ) -> List[Label[Any]]: """ Creates a list of aliases sqlalchemy text clauses from string alias and sqlalchemy.Table. @@ -90,7 +91,9 @@ def prefixed_columns( ) return [column.label(f"{alias}{column.name}") for column in all_columns] - def prefixed_table_name(self, alias: str, table: sqlalchemy.Table) -> text: + def prefixed_table_name( + self, alias: str, table: sqlalchemy.Table + ) -> NamedFromClause: """ Creates text clause with table name with aliased name. diff --git a/poetry.lock b/poetry.lock index 21d6c2458..19c95832c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,5 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + [[package]] name = "aiomysql" @@ -7,7 +8,7 @@ description = "MySQL driver for asyncio." optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"mysql\"" +markers = "extra == \"mysql\" or extra == \"all\"" files = [ {file = "aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a"}, {file = "aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67"}, @@ -47,7 +48,7 @@ description = "asyncio bridge to the standard sqlite3 module" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"sqlite\"" +markers = "extra == \"sqlite\" or extra == \"all\"" files = [ {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, @@ -60,6 +61,18 @@ typing_extensions = ">=4.0" dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] +[[package]] +name = "annotated-doc" +version = "0.0.4" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -72,9 +85,6 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "anyio" version = "4.5.2" @@ -113,23 +123,6 @@ files = [ [package.dependencies] sniffio = "*" -[[package]] -name = "astunparse" -version = "1.6.3" -description = "An AST unparser for Python" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "python_version < \"3.9\"" -files = [ - {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, - {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, -] - -[package.dependencies] -six = ">=1.6.1,<2.0" -wheel = ">=0.23.0,<1.0" - [[package]] name = "async-timeout" version = "4.0.3" @@ -137,7 +130,7 @@ description = "Timeout context manager for asyncio programs" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"aiopg\" or extra == \"all\" or (extra == \"all\" or extra == \"postgres\" or extra == \"postgresql\" or extra == \"aiopg\") and python_version < \"3.11.0\"" +markers = "(extra == \"postgresql\" or extra == \"postgres\" or extra == \"all\") and python_version < \"3.11.0\" or extra == \"aiopg\" or extra == \"all\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -150,7 +143,7 @@ description = "An asyncio PostgreSQL driver" optional = true python-versions = ">=3.8.0" groups = ["main"] -markers = "extra == \"all\" or extra == \"postgres\" or extra == \"postgresql\"" +markers = "extra == \"postgresql\" or extra == \"postgres\" or extra == \"all\"" files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -223,9 +216,6 @@ files = [ {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] @@ -364,7 +354,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] -markers = {main = "(extra == \"all\" or extra == \"crypto\") and platform_python_implementation != \"PyPy\""} +markers = {main = "(extra == \"crypto\" or extra == \"all\") and platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -634,7 +624,7 @@ description = "cryptography is a package which provides cryptographic recipes an optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"crypto\"" +markers = "extra == \"crypto\" or extra == \"all\"" files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -680,18 +670,18 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "databases" -version = "0.7.0" +version = "0.9.0" description = "Async database support for Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "databases-0.7.0-py3-none-any.whl", hash = "sha256:cf5da4b8a3e3cd038c459529725ebb64931cbbb7a091102664f20ef8f6cefd0d"}, - {file = "databases-0.7.0.tar.gz", hash = "sha256:ea2d419d3d2eb80595b7ceb8f282056f080af62efe2fb9bcd83562f93ec4b674"}, + {file = "databases-0.9.0-py3-none-any.whl", hash = "sha256:9ee657c9863b34f8d3a06c06eafbe1bda68af2a434b56996312edf1f1c0b6297"}, + {file = "databases-0.9.0.tar.gz", hash = "sha256:d2f259677609bf187737644c95fa41701072e995dfeb8d2882f335795c5b61b0"}, ] [package.dependencies] -sqlalchemy = ">=1.4.42,<1.5" +sqlalchemy = ">=2.0.7" [package.extras] aiomysql = ["aiomysql"] @@ -798,25 +788,27 @@ typing-extensions = "*" [[package]] name = "fastapi" -version = "0.118.0" +version = "0.128.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855"}, - {file = "fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8"}, + {file = "fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d"}, + {file = "fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a"}, ] [package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.49.0" +annotated-doc = ">=0.0.2" +pydantic = ">=2.7.0" +starlette = ">=0.40.0,<0.51.0" typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "filelock" @@ -954,7 +946,6 @@ files = [ ] [package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" [[package]] @@ -1564,7 +1555,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"orjson\"" +markers = "extra == \"orjson\" or extra == \"all\"" files = [ {file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"}, @@ -1746,7 +1737,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"aiopg\" or extra == \"all\" or extra == \"postgres\" or extra == \"postgresql\"" +markers = "extra == \"aiopg\" or extra == \"all\" or extra == \"postgresql\" or extra == \"postgres\"" files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -1841,128 +1832,137 @@ files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -markers = {main = "(extra == \"all\" or extra == \"crypto\") and platform_python_implementation != \"PyPy\""} +markers = {main = "(extra == \"crypto\" or extra == \"all\") and platform_python_implementation != \"PyPy\""} [[package]] name = "pydantic" -version = "2.8.2" +version = "2.11.9" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"}, + {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -2033,7 +2033,7 @@ description = "Pure Python MySQL Driver" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"mysql\"" +markers = "extra == \"mysql\" or extra == \"all\"" files = [ {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, @@ -2178,19 +2178,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "python_version < \"3.9\"" -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -2425,24 +2412,24 @@ files = [ [[package]] name = "setuptools" -version = "75.3.0" +version = "78.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, + {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] -core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2470,81 +2457,99 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.54" +version = "2.0.44" description = "Database Abstraction Library" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "SQLAlchemy-1.4.54-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:af00236fe21c4d4f4c227b6ccc19b44c594160cc3ff28d104cdce85855369277"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1183599e25fa38a1a322294b949da02b4f0da13dbc2688ef9dbe746df573f8a6"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1990d5a6a5dc358a0894c8ca02043fb9a5ad9538422001fb2826e91c50f1d539"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14b3f4783275339170984cadda66e3ec011cce87b405968dc8d51cf0f9997b0d"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b24364150738ce488333b3fb48bfa14c189a66de41cd632796fbcacb26b4585"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-win32.whl", hash = "sha256:a8a72259a1652f192c68377be7011eac3c463e9892ef2948828c7d58e4829988"}, - {file = "SQLAlchemy-1.4.54-cp310-cp310-win_amd64.whl", hash = "sha256:b67589f7955924865344e6eacfdcf70675e64f36800a576aa5e961f0008cde2a"}, - {file = "SQLAlchemy-1.4.54-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b05e0626ec1c391432eabb47a8abd3bf199fb74bfde7cc44a26d2b1b352c2c6e"}, - {file = "SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13e91d6892b5fcb94a36ba061fb7a1f03d0185ed9d8a77c84ba389e5bb05e936"}, - {file = "SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb59a11689ff3c58e7652260127f9e34f7f45478a2f3ef831ab6db7bcd72108f"}, - {file = "SQLAlchemy-1.4.54-cp311-cp311-win32.whl", hash = "sha256:1390ca2d301a2708fd4425c6d75528d22f26b8f5cbc9faba1ddca136671432bc"}, - {file = "SQLAlchemy-1.4.54-cp311-cp311-win_amd64.whl", hash = "sha256:2b37931eac4b837c45e2522066bda221ac6d80e78922fb77c75eb12e4dbcdee5"}, - {file = "SQLAlchemy-1.4.54-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3f01c2629a7d6b30d8afe0326b8c649b74825a0e1ebdcb01e8ffd1c920deb07d"}, - {file = "SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c24dd161c06992ed16c5e528a75878edbaeced5660c3db88c820f1f0d3fe1f4"}, - {file = "SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5e0d47d619c739bdc636bbe007da4519fc953393304a5943e0b5aec96c9877c"}, - {file = "SQLAlchemy-1.4.54-cp312-cp312-win32.whl", hash = "sha256:12bc0141b245918b80d9d17eca94663dbd3f5266ac77a0be60750f36102bbb0f"}, - {file = "SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl", hash = "sha256:f941aaf15f47f316123e1933f9ea91a6efda73a161a6ab6046d1cde37be62c88"}, - {file = "SQLAlchemy-1.4.54-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a41611835010ed4ea4c7aed1da5b58aac78ee7e70932a91ed2705a7b38e40f52"}, - {file = "SQLAlchemy-1.4.54-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8c1b9ecaf9f2590337d5622189aeb2f0dbc54ba0232fa0856cf390957584a9"}, - {file = "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de620f978ca273ce027769dc8db7e6ee72631796187adc8471b3c76091b809e"}, - {file = "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a2530400a6e7e68fd1552a55515de6a4559122e495f73554a51cedafc11669"}, - {file = "SQLAlchemy-1.4.54-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cf7076c8578b3de4e43a046cc7a1af8466e1c3f5e64167189fe8958a4f9c02"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:f1e1b92ee4ee9ffc68624ace218b89ca5ca667607ccee4541a90cc44999b9aea"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41cffc63c7c83dfc30c4cab5b4308ba74440a9633c4509c51a0c52431fb0f8ab"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5933c45d11cbd9694b1540aa9076816cc7406964c7b16a380fd84d3a5fe3241"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cafe0ba3a96d0845121433cffa2b9232844a2609fce694fcc02f3f31214ece28"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a19f816f4702d7b1951d7576026c7124b9bfb64a9543e571774cf517b7a50b29"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-win32.whl", hash = "sha256:76c2ba7b5a09863d0a8166fbc753af96d561818c572dbaf697c52095938e7be4"}, - {file = "SQLAlchemy-1.4.54-cp37-cp37m-win_amd64.whl", hash = "sha256:a86b0e4be775902a5496af4fb1b60d8a2a457d78f531458d294360b8637bb014"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:a49730afb716f3f675755afec109895cab95bc9875db7ffe2e42c1b1c6279482"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26e78444bc77d089e62874dc74df05a5c71f01ac598010a327881a48408d0064"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02d2ecb9508f16ab9c5af466dfe5a88e26adf2e1a8d1c56eb616396ccae2c186"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:394b0135900b62dbf63e4809cdc8ac923182af2816d06ea61cd6763943c2cc05"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed3576675c187e3baa80b02c4c9d0edfab78eff4e89dd9da736b921333a2432"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-win32.whl", hash = "sha256:fc9ffd9a38e21fad3e8c5a88926d57f94a32546e937e0be46142b2702003eba7"}, - {file = "SQLAlchemy-1.4.54-cp38-cp38-win_amd64.whl", hash = "sha256:a01bc25eb7a5688656c8770f931d5cb4a44c7de1b3cec69b84cc9745d1e4cc10"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0b76bbb1cbae618d10679be8966f6d66c94f301cfc15cb49e2f2382563fb6efb"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb2886c0be2c6c54d0651d5a61c29ef347e8eec81fd83afebbf7b59b80b7393"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:954816850777ac234a4e32b8c88ac1f7847088a6e90cfb8f0e127a1bf3feddff"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d83cd1cc03c22d922ec94d0d5f7b7c96b1332f5e122e81b1a61fb22da77879a"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1576fba3616f79496e2f067262200dbf4aab1bb727cd7e4e006076686413c80c"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-win32.whl", hash = "sha256:3112de9e11ff1957148c6de1df2bc5cc1440ee36783412e5eedc6f53638a577d"}, - {file = "SQLAlchemy-1.4.54-cp39-cp39-win_amd64.whl", hash = "sha256:6da60fb24577f989535b8fc8b2ddc4212204aaf02e53c4c7ac94ac364150ed08"}, - {file = "sqlalchemy-1.4.54.tar.gz", hash = "sha256:4470fbed088c35dc20b78a39aaf4ae54fe81790c783b3264872a0224f437c31a"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-win32.whl", hash = "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74"}, + {file = "SQLAlchemy-2.0.44-cp37-cp37m-win_amd64.whl", hash = "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-win32.whl", hash = "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013"}, + {file = "sqlalchemy-2.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"}, + {file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"}, + {file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = ">=1", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\""} +typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql (>=0.2.0) ; python_version >= \"3\"", "greenlet (!=0.4.17) ; python_version >= \"3\""] -aiosqlite = ["aiosqlite ; python_version >= \"3\"", "greenlet (!=0.4.17) ; python_version >= \"3\"", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17) ; python_version >= \"3\""] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4) ; python_version >= \"3\"", "greenlet (!=0.4.17) ; python_version >= \"3\""] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2) ; python_version >= \"3\"", "mariadb (>=1.0.1,!=1.1.2) ; python_version >= \"3\""] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] -mssql-pymssql = ["pymssql", "pymssql"] -mssql-pyodbc = ["pyodbc", "pyodbc"] -mypy = ["mypy (>=0.910) ; python_version >= \"3\"", "sqlalchemy2-stubs"] -mysql = ["mysqlclient (>=1.4.0) ; python_version >= \"3\"", "mysqlclient (>=1.4.0,<2) ; python_version < \"3\""] -mysql-connector = ["mysql-connector-python", "mysql-connector-python"] -oracle = ["cx_oracle (>=7) ; python_version >= \"3\"", "cx_oracle (>=7,<8) ; python_version < \"3\""] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg ; python_version >= \"3\"", "asyncpg ; python_version >= \"3\"", "greenlet (!=0.4.17) ; python_version >= \"3\"", "greenlet (!=0.4.17) ; python_version >= \"3\""] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0) ; python_version >= \"3\"", "pg8000 (>=1.16.6,!=1.29.0) ; python_version >= \"3\""] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql (<1) ; python_version < \"3\"", "pymysql ; python_version >= \"3\""] -sqlcipher = ["sqlcipher3_binary ; python_version >= \"3\""] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" @@ -2731,6 +2736,21 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" version = "2.2.3" @@ -2833,22 +2853,6 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] -[[package]] -name = "wheel" -version = "0.45.1" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.9\"" -files = [ - {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, - {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=65)"] - [[package]] name = "yappi" version = "1.6.10" @@ -2951,5 +2955,5 @@ sqlite = ["aiosqlite"] [metadata] lock-version = "2.1" -python-versions = "^3.8.0" -content-hash = "5a37d0c0688852b52dacb54451c3c9088b011b7193bd10d0086b34022888762b" +python-versions = "^3.9.0" +content-hash = "182dc321326869e9567074df357cd198a1811eeeedce980e3d64f38ee578064c" diff --git a/pyproject.toml b/pyproject.toml index ae5ba1d9b..276ca2708 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,10 +42,10 @@ classifiers = [ [tool.poetry.dependencies] -python = "^3.8.0" -databases = "^0.7.0" -pydantic = ">=2.5.3,<2.9.0" -SQLAlchemy = "^1.4.42" +python = "^3.9.0" +databases = "^0.9.0" +pydantic = "^2.11.9" +SQLAlchemy = {version = "^2.0.40", extras = ["asyncio"]} cryptography = { version = ">=41.0.3,<44.0.0", optional = true } # Async database drivers aiosqlite = { version = ">=0.19,<0.21", optional = true } @@ -56,7 +56,6 @@ asyncpg = { version = ">=0.28,<0.31", optional = true } psycopg2-binary = { version = "^2.9.1", optional = true } mysqlclient = { version = "^2.1.0", optional = true } PyMySQL = { version = "^1.1.0", optional = true } -setuptools = "^75.1.0" [tool.poetry.dependencies.orjson] @@ -90,11 +89,14 @@ pytest = ">=7.4.4,<9.0.0" pytest-cov = ">=4,<6" codecov = "^2.1.13" pytest-asyncio = ">=0.21,<0.24" -fastapi = ">=0.109.1,<0.118.1" +fastapi = ">=0.125.0" + black = "^24.1.0" ruff = ">=0.5.1,<0.8.3" +setuptools = "^78.1.1" + # types mypy = "^1.8.0" types-ujson = "^5.7.0" @@ -104,7 +106,7 @@ types-enum34 = "^1.1.1" types-cryptography = "^3.3.23" types-orjson = "^3.6.1" types-aiofiles = "^23.2.0" -types-requests = "^2.31.0" +types-requests = "^2.32.0" types-toml = "^0.10.8" # Documentation diff --git a/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py b/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py index cb2ce9470..7171970ac 100644 --- a/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py +++ b/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py @@ -79,7 +79,7 @@ class User2(ormar.Model): first_name: str = ormar.String(max_length=255) last_name: str = ormar.String(max_length=255) category: str = ormar.String(max_length=255, nullable=True) - timestamp: datetime.datetime = pydantic.Field(default=datetime.datetime.now) + timestamp: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now) create_test_database = init_tests(base_ormar_config) diff --git a/tests/test_fastapi/test_fastapi_usage.py b/tests/test_fastapi/test_fastapi_usage.py index ec32de1e2..ab065bba4 100644 --- a/tests/test_fastapi/test_fastapi_usage.py +++ b/tests/test_fastapi/test_fastapi_usage.py @@ -47,6 +47,13 @@ async def test_read_main(): assert response.json() == { "category": { "id": None, + "items": [ + { + "category": {"id": None, "name": "test cat"}, + "id": 1, + "name": "test", + } + ], "name": "test cat", }, "id": 1, diff --git a/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py b/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py deleted file mode 100644 index a083a29cc..000000000 --- a/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py +++ /dev/null @@ -1,134 +0,0 @@ -import datetime - -import ormar -import pytest - -from tests.lifespan import init_tests -from tests.settings import create_config - -base_ormar_config = create_config() - - -class User(ormar.Model): - ormar_config = base_ormar_config.copy(tablename="users") - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50, unique=True, index=True) - - -class RelationalAuditModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_by: User = ormar.ForeignKey(User, nullable=False) - updated_by: User = ormar.ForeignKey(User, nullable=False) - - -class AuditModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_by: str = ormar.String(max_length=100) - updated_by: str = ormar.String(max_length=100, default="Sam") - - -class DateFieldsModel(ormar.Model): - ormar_config = base_ormar_config.copy(abstract=True) - - created_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="creation_date" - ) - updated_date: datetime.datetime = ormar.DateTime( - default=datetime.datetime.now, name="modification_date" - ) - - -class Category(DateFieldsModel, AuditModel): - ormar_config = base_ormar_config.copy( - tablename="categories", - exclude_parent_fields=["updated_by", "updated_date"], - ) - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50, unique=True, index=True) - code: int = ormar.Integer() - - -class Item(DateFieldsModel, AuditModel): - ormar_config = base_ormar_config.copy( - tablename="items", - exclude_parent_fields=["updated_by", "updated_date"], - ) - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50, unique=True, index=True) - code: int = ormar.Integer() - updated_by: str = ormar.String(max_length=100, default="Bob") - - -class Gun(RelationalAuditModel, DateFieldsModel): - ormar_config = base_ormar_config.copy( - tablename="guns", - exclude_parent_fields=["updated_by"], - ) - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=50) - - -create_test_database = init_tests(base_ormar_config) - - -def test_model_definition(): - model_fields = Category.ormar_config.model_fields - sqlalchemy_columns = Category.ormar_config.table.c - pydantic_columns = Category.model_fields - assert "updated_by" not in model_fields - assert "updated_by" not in sqlalchemy_columns - assert "updated_by" not in pydantic_columns - assert "updated_date" not in model_fields - assert "updated_date" not in sqlalchemy_columns - assert "updated_date" not in pydantic_columns - - assert "updated_by" not in Gun.ormar_config.model_fields - assert "updated_by" not in Gun.ormar_config.table.c - assert "updated_by" not in Gun.model_fields - - -@pytest.mark.asyncio -async def test_model_works_as_expected(): - async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - test = await Category(name="Cat", code=2, created_by="Joe").save() - assert test.created_date is not None - - test2 = await Category.objects.get(pk=test.pk) - assert test2.name == "Cat" - assert test2.created_by == "Joe" - - -@pytest.mark.asyncio -async def test_exclude_with_redefinition(): - async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - test = await Item(name="Item", code=3, created_by="Anna").save() - assert test.created_date is not None - assert test.updated_by == "Bob" - - test2 = await Item.objects.get(pk=test.pk) - assert test2.name == "Item" - assert test2.code == 3 - - -@pytest.mark.asyncio -async def test_exclude_with_relation(): - async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - user = await User(name="Michail Kalasznikow").save() - test = await Gun(name="AK47", created_by=user).save() - assert test.created_date is not None - - with pytest.raises(AttributeError): - assert test.updated_by - - test2 = await Gun.objects.select_related("created_by").get(pk=test.pk) - assert test2.name == "AK47" - assert test2.created_by.name == "Michail Kalasznikow" diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py index aa4d7756b..0c27f70f9 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py @@ -508,4 +508,4 @@ def test_custom_config() -> None: def test_get_constraint_copy() -> None: with pytest.raises(ValueError): - get_constraint_copy("INVALID CONSTRAINT") + get_constraint_copy("INVALID CONSTRAINT") # type: ignore diff --git a/tests/test_model_definition/test_fields_access.py b/tests/test_model_definition/test_fields_access.py index 538f8258c..bfcc52f8b 100644 --- a/tests/test_model_definition/test_fields_access.py +++ b/tests/test_model_definition/test_fields_access.py @@ -168,7 +168,27 @@ def test_combining_groups_together(): category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix assert group_str == ( f"((product.name LIKE '%Test%') " - f"OR (({price_list_prefix}_price_lists.name LIKE 'Aa%') " + f"OR ({price_list_prefix}_price_lists.name LIKE 'Aa%') " + f"OR ({category_prefix}_categories.name IN ('Toys', 'Books')))" + ) + + group = (Product.name % "Test") & ( + (Product.category.price_lists.name.startswith("Aa")) + | (Product.category.name << (["Toys", "Books"])) + ) + group.resolve(model_cls=Product) + assert len(group._nested_groups) == 2 + assert len(group._nested_groups[1]._nested_groups) == 2 + group_str = str( + group.get_text_clause().compile(compile_kwargs={"literal_binds": True}) + ) + price_list_prefix = ( + group._nested_groups[1]._nested_groups[0].actions[0].table_prefix + ) + category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix + assert group_str == ( + f"((product.name LIKE '%Test%') " + f"AND (({price_list_prefix}_price_lists.name LIKE 'Aa%') " f"OR ({category_prefix}_categories.name IN ('Toys', 'Books'))))" ) diff --git a/tests/test_model_definition/test_model_definition.py b/tests/test_model_definition/test_model_definition.py index f8457c06f..9056d0ec6 100644 --- a/tests/test_model_definition/test_model_definition.py +++ b/tests/test_model_definition/test_model_definition.py @@ -166,7 +166,7 @@ def test_primary_key_access_and_setting(example): def test_pydantic_model_is_created(example): assert issubclass(example.__class__, pydantic.BaseModel) - assert all([field in example.model_fields for field in fields_to_check]) + assert all([field in example.__class__.model_fields for field in fields_to_check]) assert example.test == 1 diff --git a/tests/test_model_definition/test_pydantic_only_fields.py b/tests/test_model_definition/test_pydantic_only_fields.py index 49800b015..9dffa5a27 100644 --- a/tests/test_model_definition/test_pydantic_only_fields.py +++ b/tests/test_model_definition/test_pydantic_only_fields.py @@ -16,7 +16,7 @@ class Album(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) - timestamp: datetime.datetime = pydantic.Field(default=None) + timestamp: datetime.datetime = pydantic.Field(default=None) # type: ignore @computed_field def name10(self) -> str: diff --git a/tests/test_queries/test_selecting_subset_of_columns.py b/tests/test_queries/test_selecting_subset_of_columns.py index d7b943981..51d842ce3 100644 --- a/tests/test_queries/test_selecting_subset_of_columns.py +++ b/tests/test_queries/test_selecting_subset_of_columns.py @@ -1,4 +1,3 @@ -import asyncio import itertools from typing import List, Optional @@ -57,15 +56,9 @@ class Car(ormar.Model): create_test_database = init_tests(base_ormar_config) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - @pytest_asyncio.fixture(autouse=True, scope="module") -async def sample_data(event_loop, create_test_database): +@pytest.mark.usefixtures("create_test_database") +async def sample_data(): async with base_ormar_config.database: nick1 = await NickNames.objects.create(name="Nippon", is_lame=False) nick2 = await NickNames.objects.create(name="EroCherry", is_lame=True) diff --git a/tests/test_queries/test_values_and_values_list.py b/tests/test_queries/test_values_and_values_list.py index c090e4ae5..76eb4d3cc 100644 --- a/tests/test_queries/test_values_and_values_list.py +++ b/tests/test_queries/test_values_and_values_list.py @@ -1,4 +1,3 @@ -import asyncio from typing import List, Optional import ormar @@ -47,15 +46,9 @@ class Post(ormar.Model): create_test_database = init_tests(base_ormar_config) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - @pytest_asyncio.fixture(autouse=True, scope="module") -async def sample_data(event_loop, create_test_database): +@pytest.mark.usefixtures("create_test_database") +async def sample_data(): async with base_ormar_config.database: creator = await User(name="Anonymous").save() admin = await Role(name="admin").save() diff --git a/tests/test_relations/test_prefetch_related_with_same_models.py b/tests/test_relations/test_prefetch_related_with_same_models.py index 259163b64..cbec28a23 100644 --- a/tests/test_relations/test_prefetch_related_with_same_models.py +++ b/tests/test_relations/test_prefetch_related_with_same_models.py @@ -75,12 +75,12 @@ async def test_prefetch_related_with_same_model_relations() -> None: prefetch_result = await Book.objects.prefetch_related( ["authors", "co_authors"] ).all() - prefetch_dict_result = [x.dict() for x in prefetch_result if x.id == 1][0] + prefetch_dict_result = [x.model_dump() for x in prefetch_result if x.id == 1][0] select_result = await Book.objects.select_related( ["authors", "co_authors"] ).all() select_dict_result = [ - x.dict( + x.model_dump( exclude={ "authors": {"bookauthor": ...}, "co_authors": {"bookcoauthor": ...}, @@ -107,7 +107,7 @@ async def test_prefetch_related_with_self_referencing() -> None: select_result = await SelfRef.objects.select_related( ["main_child", "children"] ).get(name="Main") - print(select_result.json(indent=4)) + print(select_result.model_dump_json(indent=4)) prefetch_result = await SelfRef.objects.prefetch_related( ["main_child", "children"] diff --git a/tests/test_signals/test_signals.py b/tests/test_signals/test_signals.py index 7a9f7e0e2..0d515b0b7 100644 --- a/tests/test_signals/test_signals.py +++ b/tests/test_signals/test_signals.py @@ -55,6 +55,7 @@ async def cleanup(): yield async with base_ormar_config.database: await AuditLog.objects.delete(each=True) + await Album.objects.delete(each=True) def test_passing_not_callable(): @@ -79,297 +80,286 @@ def test_invalid_signal(): @pytest.mark.asyncio async def test_signal_functions(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - @pre_save(Album) - async def before_save(sender, instance, **kwargs): + @pre_save(Album) + async def before_save(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_SAVE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @post_save(Album) + async def after_save(sender, instance, **kwargs): + await AuditLog( + event_type=f"POST_SAVE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @pre_update(Album) + async def before_update(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_UPDATE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @post_update(Album) + async def after_update(sender, instance, **kwargs): + await AuditLog( + event_type=f"POST_UPDATE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @pre_delete(Album) + async def before_delete(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_DELETE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @post_delete(Album) + async def after_delete(sender, instance, **kwargs): + await AuditLog( + event_type=f"POST_DELETE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() + + @post_bulk_update(Album) + async def after_bulk_update(sender, instances, **kwargs): + for it in instances: await AuditLog( - event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.model_dump_json(), + event_type=f"BULK_POST_UPDATE_{sender.get_name()}", + event_log=it.model_dump_json(), ).save() - @post_save(Album) - async def after_save(sender, instance, **kwargs): - await AuditLog( - event_type=f"POST_SAVE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + album = await Album.objects.create(name="Venice") - @pre_update(Album) - async def before_update(sender, instance, **kwargs): - await AuditLog( - event_type=f"PRE_UPDATE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + audits = await AuditLog.objects.all() + assert len(audits) == 2 + assert audits[0].event_type == "PRE_SAVE_album" + assert audits[0].event_log.get("name") == album.name + assert audits[1].event_type == "POST_SAVE_album" + assert audits[1].event_log.get("id") == album.pk - @post_update(Album) - async def after_update(sender, instance, **kwargs): - await AuditLog( - event_type=f"POST_UPDATE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + album = await Album(name="Rome").save() + audits = await AuditLog.objects.all() + assert len(audits) == 4 + assert audits[2].event_type == "PRE_SAVE_album" + assert audits[2].event_log.get("name") == album.name + assert audits[3].event_type == "POST_SAVE_album" + assert audits[3].event_log.get("id") == album.pk - @pre_delete(Album) - async def before_delete(sender, instance, **kwargs): - await AuditLog( - event_type=f"PRE_DELETE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + album.is_best_seller = True + await album.update() - @post_delete(Album) - async def after_delete(sender, instance, **kwargs): - await AuditLog( - event_type=f"POST_DELETE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all() + assert len(audits) == 2 + assert audits[0].event_type == "PRE_UPDATE_album" + assert audits[0].event_log.get("name") == album.name + assert audits[1].event_type == "POST_UPDATE_album" + assert audits[1].event_log.get("is_best_seller") == album.is_best_seller + + album.signals.pre_update.disconnect(before_update) + album.signals.post_update.disconnect(after_update) + + album.is_best_seller = False + await album.update() + + audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all() + assert len(audits) == 2 + + await album.delete() + audits = await AuditLog.objects.filter(event_type__contains="DELETE").all() + assert len(audits) == 2 + assert audits[0].event_type == "PRE_DELETE_album" + assert ( + audits[0].event_log.get("id") == audits[1].event_log.get("id") == album.id + ) + assert audits[1].event_type == "POST_DELETE_album" + + album.signals.pre_delete.disconnect(before_delete) + album.signals.post_delete.disconnect(after_delete) + album.signals.pre_save.disconnect(before_save) + album.signals.post_save.disconnect(after_save) + + albums = await Album.objects.all() + assert len(albums) + + for album in albums: + album.play_count = 1 - @post_bulk_update(Album) - async def after_bulk_update(sender, instances, **kwargs): - for it in instances: - await AuditLog( - event_type=f"BULK_POST_UPDATE_{sender.get_name()}", - event_log=it.model_dump_json(), - ).save() - - album = await Album.objects.create(name="Venice") - - audits = await AuditLog.objects.all() - assert len(audits) == 2 - assert audits[0].event_type == "PRE_SAVE_album" - assert audits[0].event_log.get("name") == album.name - assert audits[1].event_type == "POST_SAVE_album" - assert audits[1].event_log.get("id") == album.pk - - album = await Album(name="Rome").save() - audits = await AuditLog.objects.all() - assert len(audits) == 4 - assert audits[2].event_type == "PRE_SAVE_album" - assert audits[2].event_log.get("name") == album.name - assert audits[3].event_type == "POST_SAVE_album" - assert audits[3].event_log.get("id") == album.pk - - album.is_best_seller = True - await album.update() - - audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all() - assert len(audits) == 2 - assert audits[0].event_type == "PRE_UPDATE_album" - assert audits[0].event_log.get("name") == album.name - assert audits[1].event_type == "POST_UPDATE_album" - assert audits[1].event_log.get("is_best_seller") == album.is_best_seller - - album.signals.pre_update.disconnect(before_update) - album.signals.post_update.disconnect(after_update) - - album.is_best_seller = False - await album.update() - - audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all() - assert len(audits) == 2 - - await album.delete() - audits = await AuditLog.objects.filter(event_type__contains="DELETE").all() - assert len(audits) == 2 - assert audits[0].event_type == "PRE_DELETE_album" - assert ( - audits[0].event_log.get("id") - == audits[1].event_log.get("id") - == album.id - ) - assert audits[1].event_type == "POST_DELETE_album" - - album.signals.pre_delete.disconnect(before_delete) - album.signals.post_delete.disconnect(after_delete) - album.signals.pre_save.disconnect(before_save) - album.signals.post_save.disconnect(after_save) - - albums = await Album.objects.all() - assert len(albums) - - for album in albums: - album.play_count = 1 - - await Album.objects.bulk_update(albums) - - cnt = await AuditLog.objects.filter( - event_type__contains="BULK_POST" - ).count() - assert cnt == len(albums) - - album.signals.bulk_post_update.disconnect(after_bulk_update) + await Album.objects.bulk_update(albums) + + cnt = await AuditLog.objects.filter(event_type__contains="BULK_POST").count() + assert cnt == len(albums) + + album.signals.bulk_post_update.disconnect(after_bulk_update) @pytest.mark.asyncio async def test_multiple_signals(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - @pre_save(Album) - async def before_save(sender, instance, **kwargs): - await AuditLog( - event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + @pre_save(Album) + async def before_save(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_SAVE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() - @pre_save(Album) - async def before_save2(sender, instance, **kwargs): - await AuditLog( - event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + @pre_save(Album) + async def before_save2(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_SAVE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() - album = await Album.objects.create(name="Miami") - audits = await AuditLog.objects.all() - assert len(audits) == 2 - assert audits[0].event_type == "PRE_SAVE_album" - assert audits[0].event_log.get("name") == album.name - assert audits[1].event_type == "PRE_SAVE_album" - assert audits[1].event_log.get("name") == album.name + album = await Album.objects.create(name="Miami") + audits = await AuditLog.objects.all() + assert len(audits) == 2 + assert audits[0].event_type == "PRE_SAVE_album" + assert audits[0].event_log.get("name") == album.name + assert audits[1].event_type == "PRE_SAVE_album" + assert audits[1].event_log.get("name") == album.name - album.signals.pre_save.disconnect(before_save) - album.signals.pre_save.disconnect(before_save2) + album.signals.pre_save.disconnect(before_save) + album.signals.pre_save.disconnect(before_save2) @pytest.mark.asyncio async def test_static_methods_as_signals(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - class AlbumAuditor: - event_type = "ALBUM_INSTANCE" + class AlbumAuditor: + event_type = "ALBUM_INSTANCE" - @staticmethod - @pre_save(Album) - async def before_save(sender, instance, **kwargs): - await AuditLog( - event_type=f"{AlbumAuditor.event_type}_SAVE", - event_log=instance.model_dump_json(), - ).save() + @staticmethod + @pre_save(Album) + async def before_save(sender, instance, **kwargs): + await AuditLog( + event_type=f"{AlbumAuditor.event_type}_SAVE", + event_log=instance.model_dump_json(), + ).save() - album = await Album.objects.create(name="Colorado") - audits = await AuditLog.objects.all() - assert len(audits) == 1 - assert audits[0].event_type == "ALBUM_INSTANCE_SAVE" - assert audits[0].event_log.get("name") == album.name + album = await Album.objects.create(name="Colorado") + audits = await AuditLog.objects.all() + assert len(audits) == 1 + assert audits[0].event_type == "ALBUM_INSTANCE_SAVE" + assert audits[0].event_log.get("name") == album.name - album.signals.pre_save.disconnect(AlbumAuditor.before_save) + album.signals.pre_save.disconnect(AlbumAuditor.before_save) @pytest.mark.asyncio async def test_methods_as_signals(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - class AlbumAuditor: - def __init__(self): - self.event_type = "ALBUM_INSTANCE" + class AlbumAuditor: + def __init__(self): + self.event_type = "ALBUM_INSTANCE" - async def before_save(self, sender, instance, **kwargs): - await AuditLog( - event_type=f"{self.event_type}_SAVE", - event_log=instance.model_dump_json(), - ).save() + async def before_save(self, sender, instance, **kwargs): + await AuditLog( + event_type=f"{self.event_type}_SAVE", + event_log=instance.model_dump_json(), + ).save() - auditor = AlbumAuditor() - pre_save(Album)(auditor.before_save) + auditor = AlbumAuditor() + pre_save(Album)(auditor.before_save) - album = await Album.objects.create(name="San Francisco") - audits = await AuditLog.objects.all() - assert len(audits) == 1 - assert audits[0].event_type == "ALBUM_INSTANCE_SAVE" - assert audits[0].event_log.get("name") == album.name + album = await Album.objects.create(name="San Francisco") + audits = await AuditLog.objects.all() + assert len(audits) == 1 + assert audits[0].event_type == "ALBUM_INSTANCE_SAVE" + assert audits[0].event_log.get("name") == album.name - album.signals.pre_save.disconnect(auditor.before_save) + album.signals.pre_save.disconnect(auditor.before_save) @pytest.mark.asyncio async def test_multiple_senders_signal(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - @pre_save([Album, Cover]) - async def before_save(sender, instance, **kwargs): - await AuditLog( - event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.model_dump_json(), - ).save() + @pre_save([Album, Cover]) + async def before_save(sender, instance, **kwargs): + await AuditLog( + event_type=f"PRE_SAVE_{sender.get_name()}", + event_log=instance.model_dump_json(), + ).save() - cover = await Cover(title="Blue").save() - album = await Album.objects.create(name="San Francisco", cover=cover) + cover = await Cover(title="Blue").save() + album = await Album.objects.create(name="San Francisco", cover=cover) - audits = await AuditLog.objects.all() - assert len(audits) == 2 - assert audits[0].event_type == "PRE_SAVE_cover" - assert audits[0].event_log.get("title") == cover.title - assert audits[1].event_type == "PRE_SAVE_album" - assert audits[1].event_log.get("cover") == album.cover.model_dump( - exclude={"albums"} - ) + audits = await AuditLog.objects.all() + assert len(audits) == 2 + assert audits[0].event_type == "PRE_SAVE_cover" + assert audits[0].event_log.get("title") == cover.title + assert audits[1].event_type == "PRE_SAVE_album" + assert audits[1].event_log.get("cover") == album.cover.model_dump( + exclude={"albums"} + ) - album.signals.pre_save.disconnect(before_save) - cover.signals.pre_save.disconnect(before_save) + album.signals.pre_save.disconnect(before_save) + cover.signals.pre_save.disconnect(before_save) @pytest.mark.asyncio async def test_modifing_the_instance(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - @pre_update(Album) - async def before_update(sender, instance, **kwargs): - if instance.play_count > 50 and not instance.is_best_seller: - instance.is_best_seller = True + @pre_update(Album) + async def before_update(sender, instance, **kwargs): + if instance.play_count > 50 and not instance.is_best_seller: + instance.is_best_seller = True - # here album.play_count ans is_best_seller get default values - album = await Album.objects.create(name="Venice") - assert not album.is_best_seller - assert album.play_count == 0 + # here album.play_count ans is_best_seller get default values + album = await Album.objects.create(name="Venice") + assert not album.is_best_seller + assert album.play_count == 0 - album.play_count = 30 - # here a trigger is called but play_count is too low - await album.update() - assert not album.is_best_seller + album.play_count = 30 + # here a trigger is called but play_count is too low + await album.update() + assert not album.is_best_seller - album.play_count = 60 - await album.update() - assert album.is_best_seller - album.signals.pre_update.disconnect(before_update) + album.play_count = 60 + await album.update() + assert album.is_best_seller + album.signals.pre_update.disconnect(before_update) @pytest.mark.asyncio async def test_custom_signal(cleanup): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - - async def after_update(sender, instance, **kwargs): - if instance.play_count > 50 and not instance.is_best_seller: - instance.is_best_seller = True - elif instance.play_count < 50 and instance.is_best_seller: - instance.is_best_seller = False - await instance.update() - - Album.ormar_config.signals.custom.connect(after_update) - - # here album.play_count ans is_best_seller get default values - album = await Album.objects.create(name="Venice") - assert not album.is_best_seller - assert album.play_count == 0 - - album.play_count = 30 - # here a trigger is called but play_count is too low - await album.update() - assert not album.is_best_seller - - album.play_count = 60 - await album.update() - assert not album.is_best_seller - await Album.ormar_config.signals.custom.send(sender=Album, instance=album) - assert album.is_best_seller - - album.play_count = 30 - await album.update() - assert album.is_best_seller - await Album.ormar_config.signals.custom.send(sender=Album, instance=album) - assert not album.is_best_seller - - Album.ormar_config.signals.custom.disconnect(after_update) + + async def after_update(sender, instance, **kwargs): + if instance.play_count > 50 and not instance.is_best_seller: + instance.is_best_seller = True + elif instance.play_count < 50 and instance.is_best_seller: + instance.is_best_seller = False + await instance.update() + + Album.ormar_config.signals.custom.connect(after_update) + + # here album.play_count and is_best_seller get default values + album = await Album.objects.create(name="Venice") + assert not album.is_best_seller + assert album.play_count == 0 + + album.play_count = 30 + # here a trigger is called but play_count is too low + await album.update() + assert not album.is_best_seller + + album.play_count = 60 + await album.update() + assert not album.is_best_seller + await Album.ormar_config.signals.custom.send(sender=Album, instance=album) + assert album.is_best_seller + + album.play_count = 30 + await album.update() + assert album.is_best_seller + await Album.ormar_config.signals.custom.send(sender=Album, instance=album) + assert not album.is_best_seller + + Album.ormar_config.signals.custom.disconnect(after_update) diff --git a/tests/test_signals/test_signals_for_relations.py b/tests/test_signals/test_signals_for_relations.py index 2ac2e23d3..f43ac11df 100644 --- a/tests/test_signals/test_signals_for_relations.py +++ b/tests/test_signals/test_signals_for_relations.py @@ -56,143 +56,143 @@ async def cleanup(): yield async with base_ormar_config.database: await AuditLog.objects.delete(each=True) + await Album.objects.delete(each=True) @pytest.mark.asyncio async def test_relation_signal_functions(): async with base_ormar_config.database: - async with base_ormar_config.database.transaction(force_rollback=True): - - @pre_relation_add([Album, Cover, Artist]) - async def before_relation_add( - sender, instance, child, relation_name, passed_kwargs, **kwargs - ): - await AuditLog.objects.create( - event_type="RELATION_PRE_ADD", - event_log=dict( - class_affected=sender.get_name(), - parent_id=instance.pk, - child_id=child.pk, - relation_name=relation_name, - kwargs=passed_kwargs, - ), - ) - - passed_kwargs.pop("dummy", None) - - @post_relation_add([Album, Cover, Artist]) - async def after_relation_add( - sender, instance, child, relation_name, passed_kwargs, **kwargs - ): - await AuditLog.objects.create( - event_type="RELATION_POST_ADD", - event_log=dict( - class_affected=sender.get_name(), - parent_id=instance.pk, - child_id=child.pk, - relation_name=relation_name, - kwargs=passed_kwargs, - ), - ) - - @pre_relation_remove([Album, Cover, Artist]) - async def before_relation_remove( - sender, instance, child, relation_name, **kwargs - ): - await AuditLog.objects.create( - event_type="RELATION_PRE_REMOVE", - event_log=dict( - class_affected=sender.get_name(), - parent_id=instance.pk, - child_id=child.pk, - relation_name=relation_name, - kwargs=kwargs, - ), - ) - - @post_relation_remove([Album, Cover, Artist]) - async def after_relation_remove( - sender, instance, child, relation_name, **kwargs - ): - await AuditLog.objects.create( - event_type="RELATION_POST_REMOVE", - event_log=dict( - class_affected=sender.get_name(), - parent_id=instance.pk, - child_id=child.pk, - relation_name=relation_name, - kwargs=kwargs, - ), - ) - - cover = await Cover(title="New").save() - artist = await Artist(name="Artist").save() - album = await Album(title="New Album").save() - - await cover.albums.add(album, index=0) - log = await AuditLog.objects.get(event_type="RELATION_PRE_ADD") - assert log.event_log.get("parent_id") == cover.pk - assert log.event_log.get("child_id") == album.pk - assert log.event_log.get("relation_name") == "albums" - assert log.event_log.get("kwargs") == dict(index=0) - - log2 = await AuditLog.objects.get(event_type="RELATION_POST_ADD") - assert log2.event_log.get("parent_id") == cover.pk - assert log2.event_log.get("child_id") == album.pk - assert log2.event_log.get("relation_name") == "albums" - assert log2.event_log.get("kwargs") == dict(index=0) - - await album.artists.add(artist, dummy="test") - - log3 = await AuditLog.objects.filter( - event_type="RELATION_PRE_ADD", id__gt=log2.pk - ).get() - assert log3.event_log.get("parent_id") == album.pk - assert log3.event_log.get("child_id") == artist.pk - assert log3.event_log.get("relation_name") == "artists" - assert log3.event_log.get("kwargs") == dict(dummy="test") - - log4 = await AuditLog.objects.get( - event_type="RELATION_POST_ADD", id__gt=log3.pk + + @pre_relation_add([Album, Cover, Artist]) + async def before_relation_add( + sender, instance, child, relation_name, passed_kwargs, **kwargs + ): + await AuditLog.objects.create( + event_type="RELATION_PRE_ADD", + event_log=dict( + class_affected=sender.get_name(), + parent_id=instance.pk, + child_id=child.pk, + relation_name=relation_name, + kwargs=passed_kwargs, + ), + ) + + passed_kwargs.pop("dummy", None) + + @post_relation_add([Album, Cover, Artist]) + async def after_relation_add( + sender, instance, child, relation_name, passed_kwargs, **kwargs + ): + await AuditLog.objects.create( + event_type="RELATION_POST_ADD", + event_log=dict( + class_affected=sender.get_name(), + parent_id=instance.pk, + child_id=child.pk, + relation_name=relation_name, + kwargs=passed_kwargs, + ), ) - assert log4.event_log.get("parent_id") == album.pk - assert log4.event_log.get("child_id") == artist.pk - assert log4.event_log.get("relation_name") == "artists" - assert log4.event_log.get("kwargs") == dict() - - assert album.cover == cover - assert len(album.artists) == 1 - - await cover.albums.remove(album) - log = await AuditLog.objects.get(event_type="RELATION_PRE_REMOVE") - assert log.event_log.get("parent_id") == cover.pk - assert log.event_log.get("child_id") == album.pk - assert log.event_log.get("relation_name") == "albums" - assert log.event_log.get("kwargs") == dict() - - log2 = await AuditLog.objects.get(event_type="RELATION_POST_REMOVE") - assert log2.event_log.get("parent_id") == cover.pk - assert log2.event_log.get("child_id") == album.pk - assert log2.event_log.get("relation_name") == "albums" - assert log2.event_log.get("kwargs") == dict() - - await album.artists.remove(artist) - log3 = await AuditLog.objects.filter( - event_type="RELATION_PRE_REMOVE", id__gt=log2.pk - ).get() - assert log3.event_log.get("parent_id") == album.pk - assert log3.event_log.get("child_id") == artist.pk - assert log3.event_log.get("relation_name") == "artists" - assert log3.event_log.get("kwargs") == dict() - - log4 = await AuditLog.objects.get( - event_type="RELATION_POST_REMOVE", id__gt=log3.pk + + @pre_relation_remove([Album, Cover, Artist]) + async def before_relation_remove( + sender, instance, child, relation_name, **kwargs + ): + await AuditLog.objects.create( + event_type="RELATION_PRE_REMOVE", + event_log=dict( + class_affected=sender.get_name(), + parent_id=instance.pk, + child_id=child.pk, + relation_name=relation_name, + kwargs=kwargs, + ), ) - assert log4.event_log.get("parent_id") == album.pk - assert log4.event_log.get("child_id") == artist.pk - assert log4.event_log.get("relation_name") == "artists" - assert log4.event_log.get("kwargs") == dict() - - await album.load_all() - assert len(album.artists) == 0 - assert album.cover is None + + @post_relation_remove([Album, Cover, Artist]) + async def after_relation_remove( + sender, instance, child, relation_name, **kwargs + ): + await AuditLog.objects.create( + event_type="RELATION_POST_REMOVE", + event_log=dict( + class_affected=sender.get_name(), + parent_id=instance.pk, + child_id=child.pk, + relation_name=relation_name, + kwargs=kwargs, + ), + ) + + cover = await Cover(title="New").save() + artist = await Artist(name="Artist").save() + album = await Album(title="New Album").save() + + await cover.albums.add(album, index=0) + log = await AuditLog.objects.get(event_type="RELATION_PRE_ADD") + assert log.event_log.get("parent_id") == cover.pk + assert log.event_log.get("child_id") == album.pk + assert log.event_log.get("relation_name") == "albums" + assert log.event_log.get("kwargs") == dict(index=0) + + log2 = await AuditLog.objects.get(event_type="RELATION_POST_ADD") + assert log2.event_log.get("parent_id") == cover.pk + assert log2.event_log.get("child_id") == album.pk + assert log2.event_log.get("relation_name") == "albums" + assert log2.event_log.get("kwargs") == dict(index=0) + + await album.artists.add(artist, dummy="test") + + log3 = await AuditLog.objects.filter( + event_type="RELATION_PRE_ADD", id__gt=log2.pk + ).get() + assert log3.event_log.get("parent_id") == album.pk + assert log3.event_log.get("child_id") == artist.pk + assert log3.event_log.get("relation_name") == "artists" + assert log3.event_log.get("kwargs") == dict(dummy="test") + + log4 = await AuditLog.objects.get( + event_type="RELATION_POST_ADD", id__gt=log3.pk + ) + assert log4.event_log.get("parent_id") == album.pk + assert log4.event_log.get("child_id") == artist.pk + assert log4.event_log.get("relation_name") == "artists" + assert log4.event_log.get("kwargs") == dict() + + assert album.cover == cover + assert len(album.artists) == 1 + + await cover.albums.remove(album) + log = await AuditLog.objects.get(event_type="RELATION_PRE_REMOVE") + assert log.event_log.get("parent_id") == cover.pk + assert log.event_log.get("child_id") == album.pk + assert log.event_log.get("relation_name") == "albums" + assert log.event_log.get("kwargs") == dict() + + log2 = await AuditLog.objects.get(event_type="RELATION_POST_REMOVE") + assert log2.event_log.get("parent_id") == cover.pk + assert log2.event_log.get("child_id") == album.pk + assert log2.event_log.get("relation_name") == "albums" + assert log2.event_log.get("kwargs") == dict() + + await album.artists.remove(artist) + log3 = await AuditLog.objects.filter( + event_type="RELATION_PRE_REMOVE", id__gt=log2.pk + ).get() + assert log3.event_log.get("parent_id") == album.pk + assert log3.event_log.get("child_id") == artist.pk + assert log3.event_log.get("relation_name") == "artists" + assert log3.event_log.get("kwargs") == dict() + + log4 = await AuditLog.objects.get( + event_type="RELATION_POST_REMOVE", id__gt=log3.pk + ) + assert log4.event_log.get("parent_id") == album.pk + assert log4.event_log.get("child_id") == artist.pk + assert log4.event_log.get("relation_name") == "artists" + assert log4.event_log.get("kwargs") == dict() + + await album.load_all() + assert len(album.artists) == 0 + assert album.cover is None