Fix for prefetch related (#1275)

* fix prefetch related merging same relations refering to the same children models

* change to List for p3.8

* adapt refactored prefetch query from abandoned composite_key branch and make sure new test passes

* remove unused code, add missing test for prefetch related with self reference models
This commit is contained in:
collerek
2024-03-24 00:00:51 +01:00
committed by GitHub
parent 1ed0d5a87f
commit 52d992d8c7
13 changed files with 875 additions and 831 deletions

View File

@ -364,6 +364,64 @@ class ForeignKeyField(BaseField):
prefix = "to_" if self.self_reference else ""
return self.through_relation_name or f"{prefix}{self.owner.get_name()}"
def get_filter_clause_target(self) -> Type["Model"]:
return self.to
def get_model_relation_fields(self, use_alias: bool = False) -> str:
"""
Extract names of the database columns or model fields that are connected
with given relation based on use_alias switch and which side of the relation
the current field is - reverse or normal.
:param use_alias: use db names aliases or model fields
:type use_alias: bool
:return: name or names of the related columns/ fields
:rtype: Union[str, List[str]]
"""
if use_alias:
return self._get_model_relation_fields_alias()
return self._get_model_relation_fields_name()
def _get_model_relation_fields_name(self) -> str:
if self.virtual:
return self.owner.ormar_config.pkname
return self.name
def _get_model_relation_fields_alias(self) -> str:
if self.virtual:
return self.owner.ormar_config.model_fields[
self.owner.ormar_config.pkname
].get_alias()
return self.get_alias()
def get_related_field_alias(self) -> str:
"""
Extract names of the related database columns or that are connected
with given relation based to use as a target in filter clause.
:return: name or names of the related columns/ fields
:rtype: Union[str, Dict[str, str]]
"""
if self.virtual:
field_name = self.get_related_name()
field = self.to.ormar_config.model_fields[field_name]
return field.get_alias()
target_field = self.to.get_column_alias(self.to.ormar_config.pkname)
return target_field
def get_related_field_name(self) -> Union[str, List[str]]:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:return: name(s) of the field
:rtype: Union[str, List[str]]
"""
if self.virtual:
return self.get_related_name()
return self.to.ormar_config.pkname
def _evaluate_forward_ref(
self, globalns: Any, localns: Any, is_through: bool = False
) -> None:

View File

@ -268,6 +268,51 @@ class ManyToManyField( # type: ignore
"""
return self.through
def get_filter_clause_target(self) -> Type["Model"]:
return self.through
def get_model_relation_fields(self, use_alias: bool = False) -> str:
"""
Extract names of the database columns or model fields that are connected
with given relation based on use_alias switch.
:param use_alias: use db names aliases or model fields
:type use_alias: bool
:return: name or names of the related columns/ fields
:rtype: Union[str, List[str]]
"""
pk_field = self.owner.ormar_config.model_fields[self.owner.ormar_config.pkname]
result = pk_field.get_alias() if use_alias else pk_field.name
return result
def get_related_field_alias(self) -> str:
"""
Extract names of the related database columns or that are connected
with given relation based to use as a target in filter clause.
:return: name or names of the related columns/ fields
:rtype: Union[str, Dict[str, str]]
"""
if self.self_reference and self.self_reference_primary == self.name:
field_name = self.default_target_field_name()
else:
field_name = self.default_source_field_name()
sub_field = self.through.ormar_config.model_fields[field_name]
return sub_field.get_alias()
def get_related_field_name(self) -> Union[str, List[str]]:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:return: name(s) of the field
:rtype: Union[str, List[str]]
"""
if self.self_reference and self.self_reference_primary == self.name:
return self.default_target_field_name()
return self.default_source_field_name()
def create_default_through_model(self) -> None:
"""
Creates default empty through model if no additional fields are required.

View File

@ -8,14 +8,12 @@ it became quite complicated over time.
from ormar.models.mixins.alias_mixin import AliasMixin
from ormar.models.mixins.excludable_mixin import ExcludableMixin
from ormar.models.mixins.merge_mixin import MergeModelMixin
from ormar.models.mixins.prefetch_mixin import PrefetchQueryMixin
from ormar.models.mixins.pydantic_mixin import PydanticMixin
from ormar.models.mixins.save_mixin import SavePrepareMixin
__all__ = [
"MergeModelMixin",
"AliasMixin",
"PrefetchQueryMixin",
"SavePrepareMixin",
"ExcludableMixin",
"PydanticMixin",

View File

@ -1,123 +0,0 @@
from typing import TYPE_CHECKING, Callable, Dict, List, Tuple, Type, cast
from ormar.models.mixins.relation_mixin import RelationMixin
if TYPE_CHECKING: # pragma: no cover
from ormar.fields import ForeignKeyField, ManyToManyField
class PrefetchQueryMixin(RelationMixin):
"""
Used in PrefetchQuery to extract ids and names of models to prefetch.
"""
if TYPE_CHECKING: # pragma no cover
from ormar import Model
get_name: Callable # defined in NewBaseModel
@staticmethod
def get_clause_target_and_filter_column_name(
parent_model: Type["Model"],
target_model: Type["Model"],
reverse: bool,
related: str,
) -> Tuple[Type["Model"], str]:
"""
Returns Model on which query clause should be performed and name of the column.
:param parent_model: related model that the relation lead to
:type parent_model: Type[Model]
:param target_model: model on which query should be performed
:type target_model: Type[Model]
:param reverse: flag if the relation is reverse
:type reverse: bool
:param related: name of the relation field
:type related: str
:return: Model on which query clause should be performed and name of the column
:rtype: Tuple[Type[Model], str]
"""
if reverse:
field_name = parent_model.ormar_config.model_fields[
related
].get_related_name()
field = target_model.ormar_config.model_fields[field_name]
if field.is_multi:
field = cast("ManyToManyField", field)
field_name = field.default_target_field_name()
sub_field = field.through.ormar_config.model_fields[field_name]
return field.through, sub_field.get_alias()
return target_model, field.get_alias()
target_field = target_model.get_column_alias(target_model.ormar_config.pkname)
return target_model, target_field
@staticmethod
def get_column_name_for_id_extraction(
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool
) -> str:
"""
Returns name of the column that should be used to extract ids from model.
Depending on the relation side it's either primary key column of parent model
or field name specified by related parameter.
:param parent_model: model from which id column should be extracted
:type parent_model: Type[Model]
:param reverse: flag if the relation is reverse
:type reverse: bool
:param related: name of the relation field
:type related: str
:param use_raw: flag if aliases or field names should be used
:type use_raw: bool
:return:
:rtype:
"""
if reverse:
column_name = parent_model.ormar_config.pkname
return (
parent_model.get_column_alias(column_name) if use_raw else column_name
)
column = parent_model.ormar_config.model_fields[related]
return column.get_alias() if use_raw else column.name
@classmethod
def get_related_field_name(cls, target_field: "ForeignKeyField") -> str:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:param target_field: relation field that should be used in prefetch
:type target_field: Type[BaseField]
:return: name of the field
:rtype: str
"""
if target_field.is_multi:
return cls.get_name()
if target_field.virtual:
return target_field.get_related_name()
return target_field.to.ormar_config.pkname
@classmethod
def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List:
"""
Returns list of related fields names that should be followed to prefetch related
models from.
List of models is translated into dict to assure each model is extracted only
once in one query, that's why this function accepts prefetch_dict not list.
Only relations from current model are returned.
:param prefetch_dict: dictionary of fields to extract
:type prefetch_dict: Dict
:return: list of fields names to extract
:rtype: List
"""
related_to_extract = []
if prefetch_dict and prefetch_dict is not Ellipsis:
related_to_extract = [
related
for related in cls.extract_related_names()
if related in prefetch_dict
]
return related_to_extract

View File

@ -1,14 +1,12 @@
from ormar.models.mixins import (
ExcludableMixin,
MergeModelMixin,
PrefetchQueryMixin,
PydanticMixin,
SavePrepareMixin,
)
class ModelTableProxy(
PrefetchQueryMixin,
MergeModelMixin,
SavePrepareMixin,
ExcludableMixin,

View File

@ -25,7 +25,6 @@ import typing_extensions
import ormar # noqa I100
from ormar.exceptions import ModelError, ModelPersistenceError
from ormar.fields import BaseField
from ormar.fields.foreign_key import ForeignKeyField
from ormar.fields.parsers import decode_bytes, encode_json
from ormar.models.helpers import register_relation_in_alias_manager
@ -1167,18 +1166,3 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
f"model without pk set!"
)
return self_fields
def get_relation_model_id(self, target_field: "BaseField") -> Optional[int]:
"""
Returns an id of the relation side model to use in prefetch query.
:param target_field: field with relation definition
:type target_field: "BaseField"
:return: value of pk if set
:rtype: Optional[int]
"""
if target_field.virtual or target_field.is_multi:
return self.pk
related_name = target_field.name
related_model = getattr(self, related_name)
return None if not related_model else related_model.pk

File diff suppressed because it is too large Load Diff

View File

@ -172,7 +172,7 @@ class QuerySet(Generic[T]):
select_related=self._select_related,
orders_by=self.order_bys,
)
return await query.prefetch_related(models=models, rows=rows) # type: ignore
return await query.prefetch_related(models=models) # type: ignore
async def _process_query_result_rows(self, rows: List) -> List["T"]:
"""

View File

@ -6,7 +6,6 @@ from typing import (
Dict,
List,
Optional,
Sequence,
Set,
Tuple,
Type,
@ -42,7 +41,7 @@ def check_node_not_dict_or_not_last_node(
def translate_list_to_dict( # noqa: CCR001
list_to_trans: Union[List, Set], is_order: bool = False
list_to_trans: Union[List, Set], default: Any = ...
) -> Dict:
"""
Splits the list of strings by '__' and converts them to dictionary with nested
@ -53,6 +52,8 @@ def translate_list_to_dict( # noqa: CCR001
:param list_to_trans: input list
:type list_to_trans: Union[List, Set]
:param default: value to use as a default value
:type default: Any
:param is_order: flag if change affects order_by clauses are they require special
default value with sort order.
:type is_order: bool
@ -63,14 +64,7 @@ def translate_list_to_dict( # noqa: CCR001
for path in list_to_trans:
current_level = new_dict
parts = path.split("__")
def_val: Any = ...
if is_order:
if parts[0][0] == "-":
def_val = "desc"
parts[0] = parts[0][1:]
else:
def_val = "asc"
def_val: Any = default
for ind, part in enumerate(parts):
is_last = ind == len(parts) - 1
if check_node_not_dict_or_not_last_node(
@ -189,78 +183,6 @@ def update_dict_from_list(curr_dict: Dict, list_to_update: Union[List, Set]) ->
return updated_dict
def extract_nested_models( # noqa: CCR001
model: "Model", model_type: Type["Model"], select_dict: Dict, extracted: Dict
) -> None:
"""
Iterates over model relations and extracts all nested models from select_dict and
puts them in corresponding list under relation name in extracted dict.keys
Basically flattens all relation to dictionary of all related models, that can be
used on several models and extract all of their children into dictionary of lists
witch children models.
Goes also into nested relations if needed (specified in select_dict).
:param model: parent Model
:type model: Model
:param model_type: parent model class
:type model_type: Type[Model]
:param select_dict: dictionary of related models from select_related
:type select_dict: Dict
:param extracted: dictionary with already extracted models
:type extracted: Dict
"""
follow = [rel for rel in model_type.extract_related_names() if rel in select_dict]
for related in follow:
child = getattr(model, related)
if not child:
continue
target_model = model_type.ormar_config.model_fields[related].to
if isinstance(child, list):
extracted.setdefault(target_model.get_name(), []).extend(child)
if select_dict[related] is not Ellipsis:
for sub_child in child:
extract_nested_models(
sub_child, target_model, select_dict[related], extracted
)
else:
extracted.setdefault(target_model.get_name(), []).append(child)
if select_dict[related] is not Ellipsis:
extract_nested_models(
child, target_model, select_dict[related], extracted
)
def extract_models_to_dict_of_lists(
model_type: Type["Model"],
models: Sequence["Model"],
select_dict: Dict,
extracted: Optional[Dict] = None,
) -> Dict:
"""
Receives a list of models and extracts all of the children and their children
into dictionary of lists with children models, flattening the structure to one dict
with all children models under their relation keys.
:param model_type: parent model class
:type model_type: Type[Model]
:param models: list of models from which related models should be extracted.
:type models: List[Model]
:param select_dict: dictionary of related models from select_related
:type select_dict: Dict
:param extracted: dictionary with already extracted models
:type extracted: Dict
:return: dictionary of lists f related models
:rtype: Dict
"""
if not extracted:
extracted = dict()
for model in models:
extract_nested_models(model, model_type, select_dict, extracted)
return extracted
def get_relationship_alias_model_and_str(
source_model: Type["Model"], related_parts: List
) -> Tuple[str, Type["Model"], str, bool]:

150
poetry.lock generated
View File

@ -1,9 +1,10 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "aiomysql"
version = "0.2.0"
description = "MySQL driver for asyncio."
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -22,6 +23,7 @@ sa = ["sqlalchemy (>=1.3,<1.4)"]
name = "aiopg"
version = "1.4.0"
description = "Postgres integration with asyncio."
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -40,6 +42,7 @@ sa = ["sqlalchemy[postgresql-psycopg2binary] (>=1.3,<1.5)"]
name = "aiosqlite"
version = "0.19.0"
description = "asyncio bridge to the standard sqlite3 module"
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -55,6 +58,7 @@ docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
name = "annotated-types"
version = "0.6.0"
description = "Reusable constraint types to use with typing.Annotated"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -69,6 +73,7 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
name = "anyio"
version = "4.3.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -91,6 +96,7 @@ trio = ["trio (>=0.23)"]
name = "asgi-lifespan"
version = "2.1.0"
description = "Programmatic startup/shutdown of ASGI apps."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -105,6 +111,7 @@ sniffio = "*"
name = "astunparse"
version = "1.6.3"
description = "An AST unparser for Python"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -120,6 +127,7 @@ wheel = ">=0.23.0,<1.0"
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -131,6 +139,7 @@ files = [
name = "asyncpg"
version = "0.28.0"
description = "An asyncio PostgreSQL driver"
category = "main"
optional = true
python-versions = ">=3.7.0"
files = [
@ -184,6 +193,7 @@ test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"]
name = "babel"
version = "2.14.0"
description = "Internationalization utilities"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -201,6 +211,7 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
name = "black"
version = "24.3.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -247,6 +258,7 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -258,6 +270,7 @@ files = [
name = "cffi"
version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -322,6 +335,7 @@ pycparser = "*"
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -333,6 +347,7 @@ files = [
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
@ -432,6 +447,7 @@ files = [
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -446,6 +462,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "codecov"
version = "2.1.13"
description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -461,6 +478,7 @@ requests = ">=2.7.9"
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -472,6 +490,7 @@ files = [
name = "coverage"
version = "7.4.4"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -539,6 +558,7 @@ toml = ["tomli"]
name = "cryptography"
version = "42.0.5"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -593,6 +613,7 @@ test-randomorder = ["pytest-randomly"]
name = "databases"
version = "0.7.0"
description = "Async database support for Python."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -617,6 +638,7 @@ sqlite = ["aiosqlite"]
name = "dataclasses"
version = "0.6"
description = "A backport of the dataclasses module for Python 3.6"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -628,6 +650,7 @@ files = [
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -639,6 +662,7 @@ files = [
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -649,10 +673,27 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "faker"
version = "24.3.0"
description = "Faker is a Python package that generates fake data for you."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "Faker-24.3.0-py3-none-any.whl", hash = "sha256:9978025e765ba79f8bf6154c9630a9c2b7f9c9b0f175d4ad5e04b19a82a8d8d6"},
{file = "Faker-24.3.0.tar.gz", hash = "sha256:5fb5aa9749d09971e04a41281ae3ceda9414f683d4810a694f8a8eebb8f9edec"},
]
[package.dependencies]
python-dateutil = ">=2.4"
typing-extensions = {version = ">=3.10.0.1", markers = "python_version <= \"3.8\""}
[[package]]
name = "fastapi"
version = "0.109.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -672,6 +713,7 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)"
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -688,6 +730,7 @@ typing = ["typing-extensions (>=4.8)"]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
category = "dev"
optional = false
python-versions = "*"
files = [
@ -705,6 +748,7 @@ dev = ["flake8", "markdown", "twine", "wheel"]
name = "greenlet"
version = "3.0.3"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -776,6 +820,7 @@ test = ["objgraph", "psutil"]
name = "griffe"
version = "0.42.1"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -791,6 +836,7 @@ colorama = ">=0.4"
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -802,6 +848,7 @@ files = [
name = "httpcore"
version = "0.17.3"
description = "A minimal low-level HTTP client."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -813,16 +860,17 @@ files = [
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
sniffio = "==1.*"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.24.1"
description = "The next generation HTTP client."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -838,14 +886,15 @@ sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.35"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -860,6 +909,7 @@ license = ["ukkonen"]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -871,6 +921,7 @@ files = [
name = "importlib-metadata"
version = "7.1.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -890,6 +941,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)",
name = "importlib-resources"
version = "6.4.0"
description = "Read resources from Python packages"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -908,6 +960,7 @@ testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "p
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -919,6 +972,7 @@ files = [
name = "jinja2"
version = "3.1.3"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -936,6 +990,7 @@ i18n = ["Babel (>=2.7)"]
name = "markdown"
version = "3.6"
description = "Python implementation of John Gruber's Markdown."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -954,6 +1009,7 @@ testing = ["coverage", "pyyaml"]
name = "markupsafe"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1023,6 +1079,7 @@ files = [
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1034,6 +1091,7 @@ files = [
name = "mike"
version = "2.0.0"
description = "Manage multiple versions of your MkDocs-powered documentation"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1058,6 +1116,7 @@ test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"]
name = "mkdocs"
version = "1.5.3"
description = "Project documentation with Markdown."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1089,6 +1148,7 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
name = "mkdocs-autorefs"
version = "1.0.1"
description = "Automatically link across pages in MkDocs."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1105,6 +1165,7 @@ mkdocs = ">=1.1"
name = "mkdocs-gen-files"
version = "0.5.0"
description = "MkDocs plugin to programmatically generate documentation pages during the build"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1119,6 +1180,7 @@ mkdocs = ">=1.0.3"
name = "mkdocs-literate-nav"
version = "0.6.1"
description = "MkDocs plugin to specify the navigation in Markdown instead of YAML"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1133,6 +1195,7 @@ mkdocs = ">=1.0.3"
name = "mkdocs-material"
version = "9.2.8"
description = "Documentation that simply works"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1157,6 +1220,7 @@ requests = ">=2.31,<3.0"
name = "mkdocs-material-extensions"
version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1168,6 +1232,7 @@ files = [
name = "mkdocs-section-index"
version = "0.3.8"
description = "MkDocs plugin to allow clickable sections that lead to an index page"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1182,6 +1247,7 @@ mkdocs = ">=1.2"
name = "mkdocstrings"
version = "0.22.0"
description = "Automatic documentation from sources, for MkDocs."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1209,6 +1275,7 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
name = "mkdocstrings-python"
version = "1.8.0"
description = "A Python handler for mkdocstrings."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1224,6 +1291,7 @@ mkdocstrings = ">=0.20"
name = "mypy"
version = "1.9.0"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1271,6 +1339,7 @@ reports = ["lxml"]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1282,6 +1351,7 @@ files = [
name = "mysqlclient"
version = "2.2.4"
description = "Python interface to MySQL"
category = "main"
optional = true
python-versions = ">=3.8"
files = [
@ -1300,6 +1370,7 @@ files = [
name = "nest-asyncio"
version = "1.6.0"
description = "Patch asyncio to allow nested event loops"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -1311,6 +1382,7 @@ files = [
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@ -1325,6 +1397,7 @@ setuptools = "*"
name = "orjson"
version = "3.9.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
category = "main"
optional = true
python-versions = ">=3.8"
files = [
@ -1384,6 +1457,7 @@ files = [
name = "packaging"
version = "24.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1395,6 +1469,7 @@ files = [
name = "paginate"
version = "0.5.6"
description = "Divides large result sets into pages for easier browsing"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1405,6 +1480,7 @@ files = [
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1416,6 +1492,7 @@ files = [
name = "platformdirs"
version = "4.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1431,6 +1508,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-
name = "pluggy"
version = "1.4.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1446,6 +1524,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "pre-commit"
version = "2.21.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1464,6 +1543,7 @@ virtualenv = ">=20.10.0"
name = "psycopg2-binary"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -1545,6 +1625,7 @@ files = [
name = "py-cpuinfo"
version = "9.0.0"
description = "Get CPU info with pure Python"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1556,6 +1637,7 @@ files = [
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -1567,6 +1649,7 @@ files = [
name = "pydantic"
version = "2.5.3"
description = "Data validation using Python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1586,6 +1669,7 @@ email = ["email-validator (>=2.0.0)"]
name = "pydantic-core"
version = "2.14.6"
description = ""
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1703,6 +1787,7 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
name = "pydantic-extra-types"
version = "2.6.0"
description = "Extra Pydantic types."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1720,6 +1805,7 @@ all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)",
name = "pygments"
version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1735,6 +1821,7 @@ windows-terminal = ["colorama (>=0.4.6)"]
name = "pymdown-extensions"
version = "10.7.1"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1753,6 +1840,7 @@ extra = ["pygments (>=2.12)"]
name = "pymysql"
version = "1.1.0"
description = "Pure Python MySQL Driver"
category = "main"
optional = true
python-versions = ">=3.7"
files = [
@ -1768,6 +1856,7 @@ rsa = ["cryptography"]
name = "pyparsing"
version = "3.1.2"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
files = [
@ -1782,6 +1871,7 @@ diagrams = ["jinja2", "railroad-diagrams"]
name = "pytest"
version = "7.4.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1804,6 +1894,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pytest-asyncio"
version = "0.21.1"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1822,6 +1913,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
name = "pytest-benchmark"
version = "4.0.0"
description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1842,6 +1934,7 @@ histogram = ["pygal", "pygaljs"]
name = "pytest-codspeed"
version = "2.2.1"
description = "Pytest plugin to create CodSpeed benchmarks"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1864,6 +1957,7 @@ test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"]
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1882,6 +1976,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
@ -1896,6 +1991,7 @@ six = ">=1.5"
name = "pytz"
version = "2024.1"
description = "World timezone definitions, modern and historical"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1907,6 +2003,7 @@ files = [
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1915,7 +2012,6 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@ -1923,16 +2019,8 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@ -1949,7 +2037,6 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@ -1957,7 +2044,6 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@ -1967,6 +2053,7 @@ files = [
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1981,6 +2068,7 @@ pyyaml = "*"
name = "regex"
version = "2023.12.25"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2083,6 +2171,7 @@ files = [
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2104,6 +2193,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "ruff"
version = "0.0.275"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2130,6 +2220,7 @@ files = [
name = "setuptools"
version = "69.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2146,6 +2237,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@ -2157,6 +2249,7 @@ files = [
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2168,6 +2261,7 @@ files = [
name = "sqlalchemy"
version = "1.4.52"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
@ -2220,7 +2314,7 @@ files = [
]
[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 = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""}
[package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
@ -2247,6 +2341,7 @@ sqlcipher = ["sqlcipher3_binary"]
name = "starlette"
version = "0.36.3"
description = "The little ASGI library that shines."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2265,6 +2360,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2276,6 +2372,7 @@ files = [
name = "types-aiofiles"
version = "23.2.0.20240311"
description = "Typing stubs for aiofiles"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2287,6 +2384,7 @@ files = [
name = "types-cryptography"
version = "3.3.23.2"
description = "Typing stubs for cryptography"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2298,6 +2396,7 @@ files = [
name = "types-enum34"
version = "1.1.8"
description = "Typing stubs for enum34"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2309,6 +2408,7 @@ files = [
name = "types-ipaddress"
version = "1.0.8"
description = "Typing stubs for ipaddress"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2320,6 +2420,7 @@ files = [
name = "types-orjson"
version = "3.6.2"
description = "Typing stubs for orjson"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2331,6 +2432,7 @@ files = [
name = "types-pkg-resources"
version = "0.1.3"
description = "Typing stubs for pkg_resources"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2342,6 +2444,7 @@ files = [
name = "types-pymysql"
version = "1.1.0.1"
description = "Typing stubs for PyMySQL"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2353,6 +2456,7 @@ files = [
name = "types-requests"
version = "2.31.0.20240311"
description = "Typing stubs for requests"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2367,6 +2471,7 @@ urllib3 = ">=2"
name = "types-toml"
version = "0.10.8.20240310"
description = "Typing stubs for toml"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2378,6 +2483,7 @@ files = [
name = "types-ujson"
version = "5.9.0.0"
description = "Typing stubs for ujson"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2389,6 +2495,7 @@ files = [
name = "typing-extensions"
version = "4.10.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2400,6 +2507,7 @@ files = [
name = "urllib3"
version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2417,6 +2525,7 @@ zstd = ["zstandard (>=0.18.0)"]
name = "verspec"
version = "0.1.0"
description = "Flexible version handling"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -2431,6 +2540,7 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"]
name = "virtualenv"
version = "20.25.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2451,6 +2561,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
name = "watchdog"
version = "3.0.0"
description = "Filesystem events monitoring"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2490,6 +2601,7 @@ watchmedo = ["PyYAML (>=3.10)"]
name = "wheel"
version = "0.43.0"
description = "A built-package format for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2504,6 +2616,7 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
name = "yappi"
version = "1.6.0"
description = "Yet Another Python Profiler"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -2566,6 +2679,7 @@ test = ["gevent (>=20.6.2)"]
name = "zipp"
version = "3.18.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2590,4 +2704,4 @@ sqlite = ["aiosqlite"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0"
content-hash = "57525b944571556e307f2795f2d74c63017b4e3fbc10a82f30f323f91f8163e8"
content-hash = "69ac3f442f88e777aeb77154e45fdd3d000cf3eedcfaca1d6b82e2fd568ceb44"

View File

@ -128,6 +128,7 @@ pydantic-extra-types = "^2.5.0"
watchdog = "<4.0.0"
pytest-codspeed = "^2.2.0"
mike = "^2.0.0"
faker = "^24.3.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -0,0 +1,119 @@
from random import randint
from typing import ForwardRef, Optional
import ormar
import pytest
from faker import Faker
from ormar.relations.relation_proxy import RelationProxy
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
fake = Faker()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=256)
class BookAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="book_authors")
id: int = ormar.Integer(primary_key=True)
class BookCoAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="book_co_authors")
id: int = ormar.Integer(primary_key=True)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=256)
description: Optional[str] = ormar.String(max_length=256, nullable=True)
authors: RelationProxy[Author] = ormar.ManyToMany(
Author, related_name="author_books", through=BookAuthor
)
co_authors: RelationProxy[Author] = ormar.ManyToMany(
Author, related_name="co_author_books", through=BookCoAuthor
)
class SelfRef(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="selfrefs")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
main_child = ormar.ForeignKey(to=ForwardRef("SelfRef"), related_name="parent")
children: RelationProxy["SelfRef"] = ormar.ManyToMany(ForwardRef("SelfRef"))
SelfRef.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_prefetch_related_with_same_model_relations() -> None:
async with base_ormar_config.database:
for _ in range(6):
await Author.objects.create(name=fake.name())
book = await Book.objects.create(name=fake.sentence(nb_words=randint(1, 4)))
for i in range(1, 3):
await book.authors.add(await Author.objects.get(id=i))
for i in range(3, 6):
await book.co_authors.add(await Author.objects.get(id=i))
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]
select_result = await Book.objects.select_related(
["authors", "co_authors"]
).all()
select_dict_result = [
x.dict(
exclude={
"authors": {"bookauthor": ...},
"co_authors": {"bookcoauthor": ...},
}
)
for x in select_result
if x.id == 1
][0]
assert prefetch_dict_result == select_dict_result
@pytest.mark.asyncio
async def test_prefetch_related_with_self_referencing() -> None:
async with base_ormar_config.database:
main_child = await SelfRef.objects.create(name="MainChild")
main = await SelfRef.objects.create(name="Main", main_child=main_child)
child1 = await SelfRef.objects.create(name="Child1")
child2 = await SelfRef.objects.create(name="Child2")
await main.children.add(child1)
await main.children.add(child2)
select_result = await SelfRef.objects.select_related(
["main_child", "children"]
).get(name="Main")
print(select_result.json(indent=4))
prefetch_result = await SelfRef.objects.prefetch_related(
["main_child", "children"]
).get(name="Main")
assert prefetch_result.main_child.name == main_child.name
assert len(prefetch_result.children) == 2
assert prefetch_result.children[0].name == child1.name
assert prefetch_result.children[1].name == child2.name

View File

@ -1,5 +1,3 @@
import ormar
from ormar.queryset.queries.prefetch_query import sort_models
from ormar.queryset.utils import (
subtract_dict,
translate_list_to_dict,
@ -7,7 +5,6 @@ from ormar.queryset.utils import (
update_dict_from_list,
)
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
@ -172,39 +169,3 @@ def test_subtracting_with_set_and_dict():
}
test = subtract_dict(curr_dict, dict_to_update)
assert test == {"translation": {"translations": {"language": Ellipsis}}}
class SortModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="sorts")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
sort_order: int = ormar.Integer()
def test_sorting_models():
models = [
SortModel(id=1, name="Alice", sort_order=0),
SortModel(id=2, name="Al", sort_order=1),
SortModel(id=3, name="Zake", sort_order=1),
SortModel(id=4, name="Will", sort_order=0),
SortModel(id=5, name="Al", sort_order=2),
SortModel(id=6, name="Alice", sort_order=2),
]
orders_by = {"name": "asc", "none": {}, "sort_order": "desc"}
models = sort_models(models, orders_by)
assert models[5].name == "Zake"
assert models[0].name == "Al"
assert models[1].name == "Al"
assert [model.id for model in models] == [5, 2, 6, 1, 4, 3]
orders_by = {"name": "asc", "none": set("aa"), "id": "asc"}
models = sort_models(models, orders_by)
assert [model.id for model in models] == [2, 5, 1, 6, 4, 3]
orders_by = {"sort_order": "asc", "none": ..., "id": "asc", "uu": 2, "aa": None}
models = sort_models(models, orders_by)
assert [model.id for model in models] == [1, 4, 2, 3, 5, 6]
create_test_database = init_tests(base_ormar_config)