diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6090575..846d7639 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,12 +10,12 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/psf/black - rev: "23.3.0" + rev: "24.4.2" hooks: - id: black - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.8" + rev: "v0.4.10" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes] diff --git a/docs/changelog.rst b/docs/changelog.rst index 461bc508..8860c204 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,20 @@ Changelog ######### +**2.8.0** +********* + +Performance improvements +======================== + +* added simple cache to SchemaBuilder by `@CosmoV`_ in `#87 `_ + +Authors +""""""" + +* `@CosmoV`_ + + **2.7.0** ********* diff --git a/examples/api_for_sqlalchemy/main.py b/examples/api_for_sqlalchemy/main.py index 92256676..0a080a33 100644 --- a/examples/api_for_sqlalchemy/main.py +++ b/examples/api_for_sqlalchemy/main.py @@ -3,6 +3,7 @@ In module placed db initialization functions, app factory. """ + import sys from pathlib import Path diff --git a/examples/api_for_sqlalchemy/models/schemas/__init__.py b/examples/api_for_sqlalchemy/models/schemas/__init__.py index a04a683b..e56fb50c 100644 --- a/examples/api_for_sqlalchemy/models/schemas/__init__.py +++ b/examples/api_for_sqlalchemy/models/schemas/__init__.py @@ -1,6 +1,5 @@ """schemas package.""" - from .child import ( ChildInSchema, ChildPatchSchema, diff --git a/examples/api_for_sqlalchemy/models/schemas/post_comment.py b/examples/api_for_sqlalchemy/models/schemas/post_comment.py index f9dc908f..055b78d1 100644 --- a/examples/api_for_sqlalchemy/models/schemas/post_comment.py +++ b/examples/api_for_sqlalchemy/models/schemas/post_comment.py @@ -1,4 +1,5 @@ """Post Comment schemas module.""" + from datetime import datetime from typing import TYPE_CHECKING diff --git a/examples/api_for_sqlalchemy/models/schemas/user.py b/examples/api_for_sqlalchemy/models/schemas/user.py index 80ecbdf2..07615051 100644 --- a/examples/api_for_sqlalchemy/models/schemas/user.py +++ b/examples/api_for_sqlalchemy/models/schemas/user.py @@ -1,4 +1,5 @@ """User schemas module.""" + from __future__ import annotations from datetime import datetime diff --git a/examples/api_for_sqlalchemy/models/user.py b/examples/api_for_sqlalchemy/models/user.py index bd088bfe..92cc9679 100644 --- a/examples/api_for_sqlalchemy/models/user.py +++ b/examples/api_for_sqlalchemy/models/user.py @@ -1,4 +1,5 @@ """User model.""" + from __future__ import annotations from sqlalchemy import Column, Integer, String diff --git a/examples/api_for_sqlalchemy/models/user_bio.py b/examples/api_for_sqlalchemy/models/user_bio.py index 7fd458f5..de09b20e 100644 --- a/examples/api_for_sqlalchemy/models/user_bio.py +++ b/examples/api_for_sqlalchemy/models/user_bio.py @@ -1,4 +1,5 @@ """User Bio model.""" + from typing import Dict, List from sqlalchemy import JSON, Column, ForeignKey, Integer, String diff --git a/examples/api_for_tortoise_orm/main.py b/examples/api_for_tortoise_orm/main.py index a87eb383..26668566 100644 --- a/examples/api_for_tortoise_orm/main.py +++ b/examples/api_for_tortoise_orm/main.py @@ -3,6 +3,7 @@ In module placed db initialization functions, app factory. """ + import sys from pathlib import Path diff --git a/examples/api_for_tortoise_orm/models/pydantic/__init__.py b/examples/api_for_tortoise_orm/models/pydantic/__init__.py index 3a2443b8..47e0b868 100644 --- a/examples/api_for_tortoise_orm/models/pydantic/__init__.py +++ b/examples/api_for_tortoise_orm/models/pydantic/__init__.py @@ -1,6 +1,5 @@ """W-mount schemas package.""" - from .user import ( UserPatchSchema, UserSchema, diff --git a/examples/api_for_tortoise_orm/models/tortoise/user.py b/examples/api_for_tortoise_orm/models/tortoise/user.py index 211f813c..7f17a6cb 100644 --- a/examples/api_for_tortoise_orm/models/tortoise/user.py +++ b/examples/api_for_tortoise_orm/models/tortoise/user.py @@ -1,6 +1,5 @@ """User model.""" - from tortoise import ( fields, models, diff --git a/fastapi_jsonapi/VERSION b/fastapi_jsonapi/VERSION index 24ba9a38..834f2629 100644 --- a/fastapi_jsonapi/VERSION +++ b/fastapi_jsonapi/VERSION @@ -1 +1 @@ -2.7.0 +2.8.0 diff --git a/fastapi_jsonapi/__init__.py b/fastapi_jsonapi/__init__.py index a9d73a18..ebb190a8 100644 --- a/fastapi_jsonapi/__init__.py +++ b/fastapi_jsonapi/__init__.py @@ -1,4 +1,5 @@ """JSON API utils package.""" + from pathlib import Path from fastapi import FastAPI diff --git a/fastapi_jsonapi/api.py b/fastapi_jsonapi/api.py index 93ea4606..94d5bbba 100644 --- a/fastapi_jsonapi/api.py +++ b/fastapi_jsonapi/api.py @@ -1,4 +1,5 @@ """JSON API router class.""" + from enum import Enum, auto from inspect import Parameter, Signature, signature from typing import ( @@ -78,6 +79,7 @@ def __init__( pagination_default_offset: Optional[int] = None, pagination_default_limit: Optional[int] = None, methods: Iterable[str] = (), + max_cache_size: int = 0, ) -> None: """ Initialize router items. @@ -127,7 +129,7 @@ def __init__( self.pagination_default_number: Optional[int] = pagination_default_number self.pagination_default_offset: Optional[int] = pagination_default_offset self.pagination_default_limit: Optional[int] = pagination_default_limit - self.schema_builder = SchemaBuilder(resource_type=resource_type) + self.schema_builder = SchemaBuilder(resource_type=resource_type, max_cache_size=max_cache_size) dto = self.schema_builder.create_schemas( schema=schema, diff --git a/fastapi_jsonapi/data_layers/filtering/sqlalchemy.py b/fastapi_jsonapi/data_layers/filtering/sqlalchemy.py index a768f5bb..d8fbe605 100644 --- a/fastapi_jsonapi/data_layers/filtering/sqlalchemy.py +++ b/fastapi_jsonapi/data_layers/filtering/sqlalchemy.py @@ -1,4 +1,5 @@ """Helper to create sqlalchemy filters according to filter querystring parameter""" + import inspect import logging from collections.abc import Sequence diff --git a/fastapi_jsonapi/data_layers/filtering/tortoise_operation.py b/fastapi_jsonapi/data_layers/filtering/tortoise_operation.py index 83266c1b..316a8f78 100644 --- a/fastapi_jsonapi/data_layers/filtering/tortoise_operation.py +++ b/fastapi_jsonapi/data_layers/filtering/tortoise_operation.py @@ -1,6 +1,7 @@ """ Previously used: '__' """ + from typing import Protocol @@ -292,8 +293,7 @@ def type_op_ilike(field_name: str, type_op: str) -> str: class ProcessTypeOperationFieldName(Protocol): - def __call__(self, field_name: str, type_op: str) -> str: - ... + def __call__(self, field_name: str, type_op: str) -> str: ... filters_dict: dict[str, ProcessTypeOperationFieldName] = { diff --git a/fastapi_jsonapi/data_layers/sorting/sqlalchemy.py b/fastapi_jsonapi/data_layers/sorting/sqlalchemy.py index 63632480..2e32da16 100644 --- a/fastapi_jsonapi/data_layers/sorting/sqlalchemy.py +++ b/fastapi_jsonapi/data_layers/sorting/sqlalchemy.py @@ -1,4 +1,5 @@ """Helper to create sqlalchemy sortings according to filter querystring parameter""" + from typing import Any, List, Tuple, Type, Union from pydantic.fields import ModelField diff --git a/fastapi_jsonapi/data_layers/sqla_orm.py b/fastapi_jsonapi/data_layers/sqla_orm.py index 3854605c..381c33dd 100644 --- a/fastapi_jsonapi/data_layers/sqla_orm.py +++ b/fastapi_jsonapi/data_layers/sqla_orm.py @@ -1,4 +1,5 @@ """This module is a CRUD interface between resource managers and the sqlalchemy ORM""" + import logging from typing import TYPE_CHECKING, Any, Iterable, List, Literal, Optional, Tuple, Type, Union diff --git a/fastapi_jsonapi/querystring.py b/fastapi_jsonapi/querystring.py index c68bbba4..ba1dfd0c 100644 --- a/fastapi_jsonapi/querystring.py +++ b/fastapi_jsonapi/querystring.py @@ -1,4 +1,5 @@ """Helper to deal with querystring parameters according to jsonapi specification.""" + from collections import defaultdict from functools import cached_property from typing import ( diff --git a/fastapi_jsonapi/schema.py b/fastapi_jsonapi/schema.py index d9bf31d6..aad7d9a9 100644 --- a/fastapi_jsonapi/schema.py +++ b/fastapi_jsonapi/schema.py @@ -3,6 +3,7 @@ Pydantic (for FastAPI). """ + from typing import ( TYPE_CHECKING, Dict, diff --git a/fastapi_jsonapi/schema_builder.py b/fastapi_jsonapi/schema_builder.py index 3db08eeb..b5893b90 100644 --- a/fastapi_jsonapi/schema_builder.py +++ b/fastapi_jsonapi/schema_builder.py @@ -1,5 +1,7 @@ """JSON API schemas builder class.""" + from dataclasses import dataclass +from functools import lru_cache from typing import ( Any, Callable, @@ -122,8 +124,16 @@ class SchemaBuilder: def __init__( self, resource_type: str, + max_cache_size: int = 0, ): self._resource_type = resource_type + self._init_cache(max_cache_size) + + def _init_cache(self, max_cache_size: int): + # TODO: remove crutch + self._get_info_from_schema_for_building_cached = lru_cache(maxsize=max_cache_size)( + self._get_info_from_schema_for_building_cached, + ) def _create_schemas_objects_list(self, schema: Type[BaseModel]) -> Type[JSONAPIResultListSchema]: object_jsonapi_list_schema, list_jsonapi_schema = self.build_list_schemas(schema) @@ -187,7 +197,7 @@ def build_schema_in( ) -> Tuple[Type[BaseJSONAPIDataInSchema], Type[BaseJSONAPIItemInSchema]]: base_schema_name = schema_in.__name__.removesuffix("Schema") + schema_name_suffix - dto = self._get_info_from_schema_for_building( + dto = self._get_info_from_schema_for_building_wrapper( base_name=base_schema_name, schema=schema_in, non_optional_relationships=non_optional_relationships, @@ -258,6 +268,40 @@ def build_list_schemas( includes=includes, ) + def _get_info_from_schema_for_building_cached( + self, + base_name: str, + schema: Type[BaseModel], + includes: Iterable[str], + non_optional_relationships: bool, + ): + return self._get_info_from_schema_for_building( + base_name=base_name, + schema=schema, + includes=includes, + non_optional_relationships=non_optional_relationships, + ) + + def _get_info_from_schema_for_building_wrapper( + self, + base_name: str, + schema: Type[BaseModel], + includes: Iterable[str] = not_passed, + non_optional_relationships: bool = False, + ): + """ + Wrapper function for return cached schema result + """ + if includes is not not_passed: + includes = tuple(includes) + + return self._get_info_from_schema_for_building_cached( + base_name=base_name, + schema=schema, + includes=includes, + non_optional_relationships=non_optional_relationships, + ) + def _get_info_from_schema_for_building( self, base_name: str, @@ -494,7 +538,7 @@ def create_jsonapi_object_schemas( if includes is not not_passed: includes = set(includes) - dto = self._get_info_from_schema_for_building( + dto = self._get_info_from_schema_for_building_wrapper( base_name=base_name, schema=schema, includes=includes, diff --git a/fastapi_jsonapi/signature.py b/fastapi_jsonapi/signature.py index 2d4fd88f..ad09c3f2 100644 --- a/fastapi_jsonapi/signature.py +++ b/fastapi_jsonapi/signature.py @@ -1,4 +1,5 @@ """Functions for extracting and updating signatures.""" + import inspect import logging from enum import Enum diff --git a/poetry.lock b/poetry.lock index 632f64e7..fbb8abbf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -111,36 +111,33 @@ files = [ [[package]] name = "black" -version = "23.3.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -150,11 +147,11 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -1476,28 +1473,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.1.8" +version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7de792582f6e490ae6aef36a58d85df9f7a0cfd1b0d4fe6b4fb51803a3ac96fa"}, - {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8e3255afd186c142eef4ec400d7826134f028a85da2146102a1172ecc7c3696"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff78a7583020da124dd0deb835ece1d87bb91762d40c514ee9b67a087940528b"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd8ee69b02e7bdefe1e5da2d5b6eaaddcf4f90859f00281b2333c0e3a0cc9cd6"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a05b0ddd7ea25495e4115a43125e8a7ebed0aa043c3d432de7e7d6e8e8cd6448"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e6f08ca730f4dc1b76b473bdf30b1b37d42da379202a059eae54ec7fc1fbcfed"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f35960b02df6b827c1b903091bb14f4b003f6cf102705efc4ce78132a0aa5af3"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d076717c67b34c162da7c1a5bda16ffc205e0e0072c03745275e7eab888719f"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a21ab023124eafb7cef6d038f835cb1155cd5ea798edd8d9eb2f8b84be07d9"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ce697c463458555027dfb194cb96d26608abab920fa85213deb5edf26e026664"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db6cedd9ffed55548ab313ad718bc34582d394e27a7875b4b952c2d29c001b26"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:05ffe9dbd278965271252704eddb97b4384bf58b971054d517decfbf8c523f05"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5daaeaf00ae3c1efec9742ff294b06c3a2a9db8d3db51ee4851c12ad385cda30"}, - {file = "ruff-0.1.8-py3-none-win32.whl", hash = "sha256:e49fbdfe257fa41e5c9e13c79b9e79a23a79bd0e40b9314bc53840f520c2c0b3"}, - {file = "ruff-0.1.8-py3-none-win_amd64.whl", hash = "sha256:f41f692f1691ad87f51708b823af4bb2c5c87c9248ddd3191c8f088e66ce590a"}, - {file = "ruff-0.1.8-py3-none-win_arm64.whl", hash = "sha256:aa8ee4f8440023b0a6c3707f76cadce8657553655dcbb5fc9b2f9bb9bee389f6"}, - {file = "ruff-0.1.8.tar.gz", hash = "sha256:f7ee467677467526cfe135eab86a40a0e8db43117936ac4f9b469ce9cdb3fb62"}, + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] [[package]] @@ -2072,4 +2069,4 @@ tortoise = ["tortoise-orm"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "7b85a97137b5a7a8983babc8127252ede873a1993e986f43228c1ae210eb0ab8" +content-hash = "2f2720bcb66e0d0a310d86457cf66d0040a309377652865ee912a1ab3fa224c5" diff --git a/pyproject.toml b/pyproject.toml index 5ef57294..8f7d15b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,8 +97,8 @@ asyncpg = "0.28.0" [tool.poetry.group.lint.dependencies] -black = "^23.3.0" -ruff = "^0.1.8" +black = "^24.4.2" +ruff = "^0.4.10" mypy = "^1.4.1" sqlalchemy-stubs = "^0.4" pre-commit = "^3.3.3" diff --git a/tests/fixtures/app.py b/tests/fixtures/app.py index b0a68075..70998e58 100644 --- a/tests/fixtures/app.py +++ b/tests/fixtures/app.py @@ -232,10 +232,11 @@ def build_app_custom( resource_type: str = "misc", class_list: Type[ListViewBase] = ListViewBaseGeneric, class_detail: Type[DetailViewBase] = DetailViewBaseGeneric, + max_cache_size: int = 0, ) -> FastAPI: router: APIRouter = APIRouter() - RoutersJSONAPI( + jsonapi_routers = RoutersJSONAPI( router=router, path=path, tags=["Misc"], @@ -246,6 +247,7 @@ def build_app_custom( schema_in_patch=schema_in_patch, schema_in_post=schema_in_post, model=model, + max_cache_size=max_cache_size, ) app = build_app_plain() @@ -254,6 +256,9 @@ def build_app_custom( atomic = AtomicOperations() app.include_router(atomic.router, prefix="") init(app) + + app.jsonapi_routers = jsonapi_routers + return app diff --git a/tests/test_api/test_api_sqla_with_includes.py b/tests/test_api/test_api_sqla_with_includes.py index 82f3941d..476f7c3a 100644 --- a/tests/test_api/test_api_sqla_with_includes.py +++ b/tests/test_api/test_api_sqla_with_includes.py @@ -6,6 +6,7 @@ from itertools import chain, zip_longest from json import dumps, loads from typing import Dict, List, Literal, Set, Tuple +from unittest.mock import call, patch from uuid import UUID, uuid4 import pytest @@ -20,6 +21,7 @@ from starlette.datastructures import QueryParams from fastapi_jsonapi.api import RoutersJSONAPI +from fastapi_jsonapi.schema_builder import SchemaBuilder from fastapi_jsonapi.views.view_base import ViewBase from tests.common import is_postgres_tests from tests.fixtures.app import build_alphabet_app, build_app_custom @@ -52,6 +54,8 @@ CustomUUIDItemAttributesSchema, PostAttributesBaseSchema, PostCommentAttributesBaseSchema, + PostCommentSchema, + PostSchema, SelfRelationshipAttributesSchema, SelfRelationshipSchema, UserAttributesBaseSchema, @@ -360,6 +364,215 @@ async def test_select_custom_fields_for_includes_without_requesting_includes( "meta": {"count": 1, "totalPages": 1}, } + def _get_clear_mock_calls(self, mock_obj) -> list[call]: + mock_calls = mock_obj.mock_calls + return [call_ for call_ in mock_calls if call_ not in [call.__len__(), call.__str__()]] + + def _prepare_info_schema_calls_to_assert(self, mock_calls) -> list[call]: + calls_to_check = [] + for wrapper_call in mock_calls: + kwargs = wrapper_call.kwargs + kwargs["includes"] = sorted(kwargs["includes"], key=lambda x: x) + + calls_to_check.append( + call( + *wrapper_call.args, + **kwargs, + ), + ) + + return sorted( + calls_to_check, + key=lambda x: (x.kwargs["base_name"], x.kwargs["includes"]), + ) + + async def test_check_get_info_schema_cache( + self, + user_1: User, + ): + resource_type = "user_with_cache" + with suppress(KeyError): + RoutersJSONAPI.all_jsonapi_routers.pop(resource_type) + + app_with_cache = build_app_custom( + model=User, + schema=UserSchema, + schema_in_post=UserInSchemaAllowIdOnPost, + schema_in_patch=UserPatchSchema, + resource_type=resource_type, + # set cache size to enable caching + max_cache_size=128, + ) + + target_func_name = "_get_info_from_schema_for_building" + url = app_with_cache.url_path_for(f"get_{resource_type}_list") + params = { + "include": "posts,posts.comments", + } + + expected_len_with_cache = 6 + expected_len_without_cache = 10 + + with patch.object( + SchemaBuilder, + target_func_name, + wraps=app_with_cache.jsonapi_routers.schema_builder._get_info_from_schema_for_building, + ) as wrapped_func: + async with AsyncClient(app=app_with_cache, base_url="http://test") as client: + response = await client.get(url, params=params) + assert response.status_code == status.HTTP_200_OK, response.text + + calls_to_check = self._prepare_info_schema_calls_to_assert(self._get_clear_mock_calls(wrapped_func)) + + # there are no duplicated calls + assert calls_to_check == sorted( + [ + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts"], + non_optional_relationships=False, + ), + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts", "posts.comments"], + non_optional_relationships=False, + ), + call( + base_name="PostSchema", + schema=PostSchema, + includes=[], + non_optional_relationships=False, + ), + call( + base_name="PostSchema", + schema=PostSchema, + includes=["comments"], + non_optional_relationships=False, + ), + call( + base_name="PostCommentSchema", + schema=PostCommentSchema, + includes=[], + non_optional_relationships=False, + ), + call( + base_name="PostCommentSchema", + schema=PostCommentSchema, + includes=["posts"], + non_optional_relationships=False, + ), + ], + key=lambda x: (x.kwargs["base_name"], x.kwargs["includes"]), + ) + assert wrapped_func.call_count == expected_len_with_cache + + response = await client.get(url, params=params) + assert response.status_code == status.HTTP_200_OK, response.text + + # there are no new calls + assert wrapped_func.call_count == expected_len_with_cache + + resource_type = "user_without_cache" + with suppress(KeyError): + RoutersJSONAPI.all_jsonapi_routers.pop(resource_type) + + app_without_cache = build_app_custom( + model=User, + schema=UserSchema, + schema_in_post=UserInSchemaAllowIdOnPost, + schema_in_patch=UserPatchSchema, + resource_type=resource_type, + max_cache_size=0, + ) + + with patch.object( + SchemaBuilder, + target_func_name, + wraps=app_without_cache.jsonapi_routers.schema_builder._get_info_from_schema_for_building, + ) as wrapped_func: + async with AsyncClient(app=app_without_cache, base_url="http://test") as client: + response = await client.get(url, params=params) + assert response.status_code == status.HTTP_200_OK, response.text + + calls_to_check = self._prepare_info_schema_calls_to_assert(self._get_clear_mock_calls(wrapped_func)) + + # there are duplicated calls + assert calls_to_check == sorted( + [ + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts"], + non_optional_relationships=False, + ), + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts"], + non_optional_relationships=False, + ), # duplicate + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts", "posts.comments"], + non_optional_relationships=False, + ), + call( + base_name="PostSchema", + schema=PostSchema, + includes=[], + non_optional_relationships=False, + ), + call( + base_name="PostSchema", + schema=PostSchema, + includes=[], + non_optional_relationships=False, + ), # duplicate + call( + base_name="PostSchema", + schema=PostSchema, + includes=[], + non_optional_relationships=False, + ), # duplicate + call( + base_name="PostSchema", + schema=PostSchema, + includes=["comments"], + non_optional_relationships=False, + ), + call( + base_name="PostSchema", + schema=PostSchema, + includes=["comments"], + non_optional_relationships=False, + ), # duplicate + call( + base_name="PostCommentSchema", + schema=PostCommentSchema, + includes=[], + non_optional_relationships=False, + ), + call( + base_name="PostCommentSchema", + schema=PostCommentSchema, + includes=["posts"], + non_optional_relationships=False, + ), # duplicate + ], + key=lambda x: (x.kwargs["base_name"], x.kwargs["includes"]), + ) + + assert wrapped_func.call_count == expected_len_without_cache + + response = await client.get(url, params=params) + assert response.status_code == status.HTTP_200_OK, response.text + + # there are new calls + assert wrapped_func.call_count == expected_len_without_cache * 2 + class TestCreatePostAndComments: async def test_get_posts_with_users( @@ -371,6 +584,13 @@ async def test_get_posts_with_users( user_1_posts: List[Post], user_2_posts: List[Post], ): + call( + base_name="UserSchema", + schema=UserSchema, + includes=["posts"], + non_optional_relationships=False, + on_optional_relationships=False, + ) url = app.url_path_for("get_post_list") url = f"{url}?include=user" response = await client.get(url) @@ -986,14 +1206,16 @@ def prepare_expected_includes( }, }, "comments": { - "data": [ - { - "id": str(user_2_comment_for_one_u1_post.id), - "type": "post_comment", - }, - ] - if p.id == user_2_comment_for_one_u1_post.post_id - else [], + "data": ( + [ + { + "id": str(user_2_comment_for_one_u1_post.id), + "type": "post_comment", + }, + ] + if p.id == user_2_comment_for_one_u1_post.post_id + else [] + ), }, }, } @@ -1419,7 +1641,8 @@ async def test_create_with_relationship_to_the_same_table(self): response_json = res.json() assert response_json["data"] - assert (parent_object_id := response_json["data"].get("id")) + parent_object_id = response_json["data"].get("id") + assert parent_object_id assert response_json == { "data": { "attributes": { @@ -1453,7 +1676,8 @@ async def test_create_with_relationship_to_the_same_table(self): response_json = res.json() assert response_json["data"] - assert (child_object_id := response_json["data"].get("id")) + child_object_id = response_json["data"].get("id") + assert child_object_id assert res.json() == { "data": { "attributes": {"name": "child"}, @@ -1508,7 +1732,8 @@ class ContainsTimestampAttrsSchema(BaseModel): assert res.status_code == status.HTTP_201_CREATED, res.text response_json = res.json() - assert (entity_id := response_json["data"]["id"]) + entity_id = response_json["data"]["id"] + assert entity_id assert response_json == { "meta": None, "jsonapi": {"version": "1.0"}, @@ -2645,7 +2870,8 @@ async def test_filter_by_null( response_json = response.json() - assert len(data := response_json["data"]) == 1 + data = response_json["data"] + assert len(data) == 1 assert data[0]["id"] == str(target_user.id) assert data[0]["attributes"]["email"] == target_user.email