From 6e04628a0ab582bad40ab626aea11bcd1c26e625 Mon Sep 17 00:00:00 2001 From: collerek Date: Thu, 8 Jan 2026 22:08:07 +0700 Subject: [PATCH 1/5] support pydantic 2.11.9 - and remove the exclude parent fields - wip --- docs/migration.md | 1 - docs/models/inheritance.md | 102 ---- docs/releases.md | 1 - ormar/fields/model_fields.py | 2 +- ormar/models/helpers/__init__.py | 1 - ormar/models/helpers/pydantic.py | 21 +- ormar/models/helpers/relations.py | 14 +- ormar/models/helpers/sqlalchemy.py | 2 +- ormar/models/metaclass.py | 8 - ormar/models/newbasemodel.py | 9 +- ormar/models/ormar_config.py | 4 - ormar/queryset/queries/limit_query.py | 2 +- ormar/queryset/queries/offset_query.py | 2 +- ormar/queryset/queries/order_query.py | 2 +- ormar/queryset/queries/query.py | 4 +- ormar/queryset/queryset.py | 12 +- ormar/relations/alias_manager.py | 8 +- poetry.lock | 457 +++++++++--------- pyproject.toml | 8 +- ...est_docs_with_multiple_relations_to_one.py | 2 +- tests/test_fastapi/test_fastapi_usage.py | 7 + ...est_excluding_parent_fields_inheritance.py | 134 ----- .../test_fields_access.py | 22 +- .../test_pydantic_only_fields.py | 2 +- 24 files changed, 293 insertions(+), 534 deletions(-) delete mode 100644 tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py 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/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/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..e80177b71 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 @@ -293,7 +292,6 @@ 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, @@ -385,11 +383,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 +675,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/newbasemodel.py b/ormar/models/newbasemodel.py index a4f6ab24e..5ba44d1c8 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -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..f2dd164d6 100644 --- a/ormar/models/ormar_config.py +++ b/ormar/models/ormar_config.py @@ -32,7 +32,6 @@ 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, @@ -53,7 +52,6 @@ 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 @@ -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/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..5da1e4d4a 100644 --- a/ormar/queryset/queries/query.py +++ b/ormar/queryset/queries/query.py @@ -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,7 +191,7 @@ 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 = sqlalchemy.sql.select(qry_text) limit_qry = limit_qry.select_from(self.select_from) limit_qry = FilterQuery(filter_clauses=self.filter_clauses).apply(limit_qry) limit_qry = FilterQuery( diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index f17a2f3e4..4e8be5b22 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -20,14 +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 @@ -629,7 +621,7 @@ async def values( exclude_through=exclude_through, ) column_map = alias_resolver.resolve_columns( - columns_names=list(cast(LegacyRow, rows[0]).keys()) + columns_names=list(rows[0].keys()) ) result = [ {column_map.get(k): v for k, v in dict(x).items() if k in column_map} @@ -724,7 +716,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) # 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 diff --git a/ormar/relations/alias_manager.py b/ormar/relations/alias_manager.py index 0355f3430..fcd705f20 100644 --- a/ormar/relations/alias_manager.py +++ b/ormar/relations/alias_manager.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import sqlalchemy -from sqlalchemy import text +from sqlalchemy import TextClause if TYPE_CHECKING: # pragma: no cover from ormar import Model @@ -35,7 +35,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, TextClause] = dict() def __contains__(self, item: str) -> bool: return self._aliases_new.__contains__(item) @@ -60,7 +60,7 @@ def reversed_aliases(self) -> Dict: @staticmethod def prefixed_columns( alias: str, table: sqlalchemy.Table, fields: Optional[List] = None - ) -> List[text]: + ) -> List[TextClause]: """ Creates a list of aliases sqlalchemy text clauses from string alias and sqlalchemy.Table. @@ -90,7 +90,7 @@ 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) -> TextClause: """ Creates text clause with table name with aliased name. diff --git a/poetry.lock b/poetry.lock index 5634485c7..8a9e8233f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 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 +7,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 +47,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"}, @@ -72,9 +72,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 +110,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 +117,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\") 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 +130,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 +203,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 +341,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 +611,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 +657,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"] @@ -953,7 +930,6 @@ files = [ ] [package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" [[package]] @@ -1563,7 +1539,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"}, @@ -1745,7 +1721,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"}, @@ -1840,128 +1816,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] @@ -2032,7 +2017,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"}, @@ -2177,19 +2162,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" @@ -2469,81 +2441,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" @@ -2730,6 +2720,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" @@ -2832,22 +2837,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" @@ -2950,5 +2939,5 @@ sqlite = ["aiosqlite"] [metadata] lock-version = "2.1" -python-versions = "^3.8.0" -content-hash = "0eea148afbc886b147f5369d2cbc09c854b9bcf041fb784173bea4b055ac8208" +python-versions = "^3.9.0" +content-hash = "0ca17d73a2e9ea468935509647503d4e841ec3138b7f5d9cee374c365e31ef6d" diff --git a/pyproject.toml b/pyproject.toml index b330077de..94bb82ead 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 } diff --git a/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py index 43df07240..7c95248c8 100644 --- a/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py +++ b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py @@ -66,4 +66,4 @@ async def test_all_endpoints(): raw_names_w_o_modules = [x.split("__")[-1] for x in components.keys()] assert all(x in raw_names_w_o_modules for x in ["CA", "CB1", "CB2"]) pk_onlys = [x for x in list(raw_names_w_o_modules) if x.startswith("PkOnly")] - assert len(pk_onlys) == 4 + assert len(pk_onlys) == 2 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_model_definition/test_fields_access.py b/tests/test_model_definition/test_fields_access.py index 538f8258c..3bd2fa0a6 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_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: From 12ae0f5a6b86ede1662fbd3462ffe580250c67ad Mon Sep 17 00:00:00 2001 From: collerek Date: Fri, 9 Jan 2026 17:33:53 +0700 Subject: [PATCH 2/5] fix all test passing - wip --- ormar/fields/base.py | 4 +- ormar/models/mixins/pydantic_mixin.py | 4 +- ormar/models/newbasemodel.py | 2 +- ormar/models/ormar_config.py | 2 +- ormar/queryset/join.py | 8 +- ormar/queryset/queries/query.py | 6 +- ormar/queryset/queryset.py | 7 +- .../test_excluding_fields_in_fastapi.py | 2 +- .../test_fields_access.py | 4 +- .../test_model_definition.py | 2 +- .../test_selecting_subset_of_columns.py | 11 +- .../test_values_and_values_list.py | 11 +- .../test_prefetch_related_with_same_models.py | 6 +- tests/test_signals/test_signals.py | 458 +++++++++--------- .../test_signals_for_relations.py | 266 +++++----- 15 files changed, 382 insertions(+), 411 deletions(-) diff --git a/ormar/fields/base.py b/ormar/fields/base.py index a55f9a956..2a4192cd2 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -266,7 +266,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 +295,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/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/newbasemodel.py b/ormar/models/newbasemodel.py index 5ba44d1c8..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: diff --git a/ormar/models/ormar_config.py b/ormar/models/ormar_config.py index f2dd164d6..38c11ff7b 100644 --- a/ormar/models/ormar_config.py +++ b/ormar/models/ormar_config.py @@ -37,7 +37,7 @@ def __init__( 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 diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 9d7e4d157..1b67b13fb 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, diff --git a/ormar/queryset/queries/query.py b/ormar/queryset/queries/query.py index 5da1e4d4a..1075c8374 100644 --- a/ormar/queryset/queries/query.py +++ b/ormar/queryset/queries/query.py @@ -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). @@ -209,8 +209,8 @@ def _build_pagination_condition( return limit_qry, on_clause 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 4e8be5b22..a0bd921e2 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -20,7 +20,6 @@ import sqlalchemy from sqlalchemy import bindparam - import ormar # noqa I100 from ormar import MultipleMatches, NoMatch from ormar.exceptions import ( @@ -261,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. @@ -620,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(rows[0].keys()) - ) + column_map = alias_resolver.resolve_columns(columns_names=list(rows[0].keys())) result = [ {column_map.get(k): v for k, v in dict(x).items() if k in column_map} for x in rows 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_model_definition/test_fields_access.py b/tests/test_model_definition/test_fields_access.py index 3bd2fa0a6..bfcc52f8b 100644 --- a/tests/test_model_definition/test_fields_access.py +++ b/tests/test_model_definition/test_fields_access.py @@ -173,8 +173,8 @@ def test_combining_groups_together(): ) group = (Product.name % "Test") & ( - (Product.category.price_lists.name.startswith("Aa")) - | (Product.category.name << (["Toys", "Books"])) + (Product.category.price_lists.name.startswith("Aa")) + | (Product.category.name << (["Toys", "Books"])) ) group.resolve(model_cls=Product) assert len(group._nested_groups) == 2 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_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 From 9a5496feb7618fdb30229f61e66bd65be6a91aef Mon Sep 17 00:00:00 2001 From: collerek Date: Fri, 9 Jan 2026 18:28:37 +0700 Subject: [PATCH 3/5] fix some lint and bump some dependencies --- ormar/fields/base.py | 29 ++++++++++- ormar/fields/sqlalchemy_uuid.py | 2 +- ormar/models/metaclass.py | 8 +-- ormar/models/ormar_config.py | 2 +- ormar/queryset/actions/filter_action.py | 4 +- ormar/queryset/clause.py | 7 ++- ormar/queryset/join.py | 6 +-- ormar/queryset/queries/filter_query.py | 12 +++-- ormar/queryset/queries/query.py | 24 ++++----- ormar/queryset/queryset.py | 16 +++--- ormar/relations/alias_manager.py | 11 ++-- poetry.lock | 50 ++++++++++++------- pyproject.toml | 9 ++-- ...est_docs_with_multiple_relations_to_one.py | 2 +- .../test_inheritance_concrete.py | 2 +- 15 files changed, 119 insertions(+), 65 deletions(-) diff --git a/ormar/fields/base.py b/ormar/fields/base.py index 2a4192cd2..635454c91 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: 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: dict[str, Any] | None = None, + use_server: bool = False, ) -> Any: # noqa CCR001 """ Return default value for a field. diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index f2a8f99f5..ef88e2a68 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 diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index e80177b71..112548961 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -194,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!") @@ -297,7 +299,7 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 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 @@ -314,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() diff --git a/ormar/models/ormar_config.py b/ormar/models/ormar_config.py index 38c11ff7b..b5af521a4 100644 --- a/ormar/models/ormar_config.py +++ b/ormar/models/ormar_config.py @@ -54,7 +54,7 @@ def __init__( self.requires_ref_update: bool = False self.extra = extra self.queryset_class = queryset_class - self.table: sqlalchemy.Table = None + self.table: sqlalchemy.Table = None # type: ignore def copy( self, 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..509f25b23 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Type 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[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 1b67b13fb..983abc7db 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -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..a468d1e47 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 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: 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/query.py b/ormar/queryset/queries/query.py index 1075c8374..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 """ @@ -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() @@ -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,11 +202,11 @@ 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 diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index a0bd921e2..4203c6bdd 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -619,7 +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(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 @@ -694,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: @@ -713,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 @@ -794,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 @@ -823,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 @@ -1236,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 fcd705f20..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 TextClause +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, TextClause] = 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[TextClause]: + ) -> 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) -> TextClause: + 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 8a9e8233f..fc917e7e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -60,6 +60,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" @@ -775,24 +787,26 @@ typing-extensions = "*" [[package]] name = "fastapi" -version = "0.115.6" +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.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, - {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, + {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.42.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.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "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.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +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)", "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" @@ -2396,24 +2410,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" @@ -2940,4 +2954,4 @@ sqlite = ["aiosqlite"] [metadata] lock-version = "2.1" python-versions = "^3.9.0" -content-hash = "0ca17d73a2e9ea468935509647503d4e841ec3138b7f5d9cee374c365e31ef6d" +content-hash = "182dc321326869e9567074df357cd198a1811eeeedce980e3d64f38ee578064c" diff --git a/pyproject.toml b/pyproject.toml index 94bb82ead..bbc9898bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.9.0" databases = "^0.9.0" -pydantic = "2.11.9" +pydantic = "^2.11.9" SQLAlchemy = {version = "^2.0.40", extras = ["asyncio"]} cryptography = { version = ">=41.0.3,<44.0.0", optional = true } # Async database drivers @@ -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,13 @@ 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.115.7" +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 +105,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_fastapi/test_docs_with_multiple_relations_to_one.py b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py index 7c95248c8..43df07240 100644 --- a/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py +++ b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py @@ -66,4 +66,4 @@ async def test_all_endpoints(): raw_names_w_o_modules = [x.split("__")[-1] for x in components.keys()] assert all(x in raw_names_w_o_modules for x in ["CA", "CB1", "CB2"]) pk_onlys = [x for x in list(raw_names_w_o_modules) if x.startswith("PkOnly")] - assert len(pk_onlys) == 2 + assert len(pk_onlys) == 4 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 From 9fda54aa0707eb5a8ed87c8be68f5d63f84cd779 Mon Sep 17 00:00:00 2001 From: collerek Date: Fri, 9 Jan 2026 18:39:33 +0700 Subject: [PATCH 4/5] fix some types to support 3.9, remove 3.8 from workflow --- .github/workflows/test-package.yml | 2 +- ormar/fields/base.py | 6 +++--- ormar/models/mixins/save_mixin.py | 5 +++-- ormar/queryset/clause.py | 4 ++-- ormar/queryset/queries/filter_query.py | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 49c4f0cc2..2697e7dec 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/ormar/fields/base.py b/ormar/fields/base.py index 635454c91..8838a0fb3 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -182,7 +182,7 @@ def get_default( self, *, call_default_factory: Literal[True], - validated_data: dict[str, Any] | None = None, + validated_data: Union[dict[str, Any], None] = None, ) -> Any: ... @overload @@ -192,7 +192,7 @@ def get_default( self, *, call_default_factory: bool = True, - validated_data: dict[str, Any] | None = None, + validated_data: Union[dict[str, Any], None] = None, use_server: bool = False, ) -> Any: # noqa CCR001 """ @@ -217,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 """ 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/queryset/clause.py b/ormar/queryset/clause.py index 509f25b23..511c08a2c 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -1,7 +1,7 @@ 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 @@ -103,7 +103,7 @@ def _iter(self) -> Generator: def _get_text_clauses( self, - ) -> List[sqlalchemy.sql.expression.TextClause | ColumnElement[Any]]: + ) -> 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 diff --git a/ormar/queryset/queries/filter_query.py b/ormar/queryset/queries/filter_query.py index a468d1e47..804b31d12 100644 --- a/ormar/queryset/queries/filter_query.py +++ b/ormar/queryset/queries/filter_query.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Union import sqlalchemy from sqlalchemy import ColumnElement, Select, TextClause @@ -31,7 +31,7 @@ def apply( """ if self.filter_clauses: if len(self.filter_clauses) == 1: - clause: TextClause | ColumnElement[Any] = self.filter_clauses[ + clause: Union[TextClause, ColumnElement[Any]] = self.filter_clauses[ 0 ].get_text_clause() else: From 62479b4737de56145b53ea41a78c7a2f623061b5 Mon Sep 17 00:00:00 2001 From: collerek Date: Fri, 9 Jan 2026 18:44:04 +0700 Subject: [PATCH 5/5] fix coverage --- ormar/fields/sqlalchemy_encrypted.py | 2 +- ormar/fields/sqlalchemy_uuid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 ef88e2a68..2bf253822 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -40,7 +40,7 @@ def process_bind_param(self, value: Any, 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)