From 54bf6ba88f0fb9587fc35577b9feaddf73195e65 Mon Sep 17 00:00:00 2001 From: AngusWG <740713651@qq.com> Date: Tue, 20 Apr 2021 17:27:48 +0800 Subject: [PATCH 01/16] Feature: Support simple type field matching query --- fastapi_crudrouter/core/sqlalchemy.py | 78 ++++++++++++++++++++++----- setup.cfg | 2 + tests/dev.requirements.txt | 1 + 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index fa26f4a..384bc61 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -1,6 +1,9 @@ -from typing import Any, Callable, List, Type, Generator, Optional, Union +import typing +from datetime import datetime +from typing import Any, Callable, Dict, Generator, List +from typing import Optional, Type, Union, get_type_hints -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, Request from . import CRUDGenerator, NOT_FOUND, _utils from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA @@ -16,11 +19,41 @@ sqlalchemy_installed = True Session = Callable[..., Generator[Session, Any, None]] +FILTER = Dict[str, Optional[Union[int, str, datetime, None]]] + + +def schemas_args_factory(schema: Optional[Type[SCHEMA]]) -> Any: + """ + Created the schema dependency to be used in the router + """ + schema_typing = get_type_hints(schema) + _str = "{}: Optional[{}] = None" + args = ( + _str.format(k, v.__name__) + for k, v in schema_typing.items() + if v.__module__ != "typing" + ) + args_str: str = ",".join(args) + return_str = ", ".join( + [ + "{}={}".format(k, k) + for k, v in schema_typing.items() + if v.__module__ != "typing" + ] + ) + + func_code = "def tmp_function({0}) -> FILTER:return dict({1})" + func_code = func_code.format(args_str, return_str) + local_var = {"datetime": datetime, "FILTER": FILTER, "typing": typing} + exec(func_code, globals(), local_var) + tmp_function = local_var["tmp_function"] + return tmp_function + class SQLAlchemyCRUDRouter(CRUDGenerator[SCHEMA]): def __init__( self, - schema: Type[SCHEMA], + schema: Optional[Type[SCHEMA]], db_model: Model, db: "Session", create_schema: Optional[Type[SCHEMA]] = None, @@ -39,7 +72,14 @@ def __init__( assert ( sqlalchemy_installed ), "SQLAlchemy must be installed to use the SQLAlchemyCRUDRouter." - + if schema is None: + try: + from pydantic_sqlalchemy import sqlalchemy_to_pydantic + except ImportError: + raise ValueError( + "Schema should not be None,or installed pydantic_sqlalchemy" + ) + schema = sqlalchemy_to_pydantic(db_model) self.db_model = db_model self.db_func = db self._pk: str = db_model.__table__.primary_key.columns.keys()[0] @@ -65,21 +105,33 @@ def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., List[Model]]: def route( db: Session = Depends(self.db_func), pagination: PAGINATION = self.pagination, + condition: FILTER = Depends(schemas_args_factory(self.schema)), + request: Request = Request(scope={"type": "http"}), ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") + effective_filter = { + k: v for k, v in condition.items() if k in request.query_params.keys() + } - db_models: List[Model] = ( - db.query(self.db_model).limit(limit).offset(skip).all() - ) + query = db.query(self.db_model).filter_by(**effective_filter) + db_models: List[Model] = query.limit(limit).offset(skip).all() return db_models return route def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Model]: def route( - item_id: self._pk_type, db: Session = Depends(self.db_func) # type: ignore + item_id: Optional[self._pk_type] = None, # type: ignore + db: Session = Depends(self.db_func), + condition: FILTER = Depends(schemas_args_factory(self.schema)), + request: Request = Request(scope={"type": "http"}), ) -> Model: - model: Model = db.query(self.db_model).get(item_id) + effective_filter = { + k: v for k, v in condition.items() if k in request.query_params.keys() + } + if item_id: + effective_filter[self._pk] = item_id + model: Model = db.query(self.db_model).filter_by(**effective_filter).first() if model: return model @@ -112,7 +164,7 @@ def route( db: Session = Depends(self.db_func), ) -> Model: try: - db_model: Model = self._get_one()(item_id, db) + db_model: Model = self._get_one()(item_id, db, {}) for key, value in model.dict(exclude={self._pk}).items(): if hasattr(db_model, key): @@ -133,7 +185,9 @@ def route(db: Session = Depends(self.db_func)) -> List[Model]: db.query(self.db_model).delete() db.commit() - return self._get_all()(db=db, pagination={"skip": 0, "limit": None}) + return self._get_all()( + db=db, pagination={"skip": 0, "limit": None}, condition={} + ) return route @@ -141,7 +195,7 @@ def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Model]: def route( item_id: self._pk_type, db: Session = Depends(self.db_func) # type: ignore ) -> Model: - db_model: Model = self._get_one()(item_id, db) + db_model: Model = self._get_one()(item_id, db, {}) db.delete(db_model) db.commit() diff --git a/setup.cfg b/setup.cfg index a2081ed..2614d23 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,4 +34,6 @@ ignore_missing_imports = True [mypy-uvicorn.*] ignore_missing_imports = True +[mypy-pydantic_sqlalchemy.*] +ignore_missing_imports = True diff --git a/tests/dev.requirements.txt b/tests/dev.requirements.txt index c2b285b..0f16498 100644 --- a/tests/dev.requirements.txt +++ b/tests/dev.requirements.txt @@ -9,6 +9,7 @@ databases aiosqlite sqlalchemy==1.3.22 sqlalchemy_utils==0.36.8 +pydantic_sqlalchemy # Testing pytest From c0de284b59ecb38c1bb5ce4ee9d93addfe0fc98e Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 00:46:36 -0700 Subject: [PATCH 02/16] :white_check_mark: Added Simple Filter Test --- tests/test_query_params.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/test_query_params.py diff --git a/tests/test_query_params.py b/tests/test_query_params.py new file mode 100644 index 0000000..fd495a2 --- /dev/null +++ b/tests/test_query_params.py @@ -0,0 +1,15 @@ +from tests.test_router import test_post, test_get + + +def test_simple(client): + test_post(client) + test_post(client, expected_length=2) + test_post(client, expected_length=3) + + basic_potato = dict(thickness=0.24, mass=1.2, color="Red", type="Mini") + test_post(client, model=basic_potato, expected_length=1) + + data = test_get(client, params={'color': 'Red'}) + assert len(data) == 1, data + + From b87eafc81b0d107fbd9facb2646068e0a680edec Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 01:02:17 -0700 Subject: [PATCH 03/16] :white_check_mark: Added Simple Filter Test --- tests/test_query_params.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/test_query_params.py b/tests/test_query_params.py index fd495a2..c9b74a3 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -1,15 +1,29 @@ from tests.test_router import test_post, test_get +def insert(_client): + test_post(_client) + test_post(_client, expected_length=2) + test_post(_client, expected_length=3) + + test_post(_client, model=dict(thickness=0.24, mass=1.1, color="red", type="Large"), expected_length=4) + test_post(_client, model=dict(thickness=0.10, mass=1.9, color="red", type="Small"), expected_length=5) + + def test_simple(client): - test_post(client) - test_post(client, expected_length=2) - test_post(client, expected_length=3) + insert(client) + + test_get(client, params={'color': 'red'}, expected_length=2) + test_get(client, params={'color': 'blue'}, expected_length=0) + test_get(client, params={'type': 'Large'}, expected_length=1) + test_get(client, params={'thickness': 0.24}, expected_length=4) - basic_potato = dict(thickness=0.24, mass=1.2, color="Red", type="Mini") - test_post(client, model=basic_potato, expected_length=1) - data = test_get(client, params={'color': 'Red'}) - assert len(data) == 1, data +def test_two_params(client): + insert(client) + test_get(client, params={'color': 'red', 'type': 'Large'}, expected_length=1) + test_get(client, params={'color': 'red', 'type': 'Small'}, expected_length=1) + test_get(client, params={'color': 'blue', 'type': 'Small'}, expected_length=0) + test_get(client, params={'thickness': 0.24, 'mass': 1.2}, expected_length=3) From a723acedd7641c83ba3d3ec0aa4d2af5e55a3ea0 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 01:02:47 -0700 Subject: [PATCH 04/16] :white_check_mark: Added Simple Filter Test --- tests/test_query_params.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/test_query_params.py b/tests/test_query_params.py index c9b74a3..9ec34ba 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -6,24 +6,31 @@ def insert(_client): test_post(_client, expected_length=2) test_post(_client, expected_length=3) - test_post(_client, model=dict(thickness=0.24, mass=1.1, color="red", type="Large"), expected_length=4) - test_post(_client, model=dict(thickness=0.10, mass=1.9, color="red", type="Small"), expected_length=5) + test_post( + _client, + model=dict(thickness=0.24, mass=1.1, color="red", type="Large"), + expected_length=4, + ) + test_post( + _client, + model=dict(thickness=0.10, mass=1.9, color="red", type="Small"), + expected_length=5, + ) def test_simple(client): insert(client) - test_get(client, params={'color': 'red'}, expected_length=2) - test_get(client, params={'color': 'blue'}, expected_length=0) - test_get(client, params={'type': 'Large'}, expected_length=1) - test_get(client, params={'thickness': 0.24}, expected_length=4) + test_get(client, params={"color": "red"}, expected_length=2) + test_get(client, params={"color": "blue"}, expected_length=0) + test_get(client, params={"type": "Large"}, expected_length=1) + test_get(client, params={"thickness": 0.24}, expected_length=4) def test_two_params(client): insert(client) - test_get(client, params={'color': 'red', 'type': 'Large'}, expected_length=1) - test_get(client, params={'color': 'red', 'type': 'Small'}, expected_length=1) - test_get(client, params={'color': 'blue', 'type': 'Small'}, expected_length=0) - test_get(client, params={'thickness': 0.24, 'mass': 1.2}, expected_length=3) - + test_get(client, params={"color": "red", "type": "Large"}, expected_length=1) + test_get(client, params={"color": "red", "type": "Small"}, expected_length=1) + test_get(client, params={"color": "blue", "type": "Small"}, expected_length=0) + test_get(client, params={"thickness": 0.24, "mass": 1.2}, expected_length=3) From 30323313308d2395b98e5561e8d784b721320775 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 09:49:09 -0700 Subject: [PATCH 05/16] :sparkles: Implemented Query Filter as a Dependency --- fastapi_crudrouter/core/_base.py | 4 +- fastapi_crudrouter/core/_utils.py | 34 +++++++++++++- fastapi_crudrouter/core/sqlalchemy.py | 68 ++++----------------------- setup.cfg | 4 -- tests/dev.requirements.txt | 1 - 5 files changed, 46 insertions(+), 65 deletions(-) diff --git a/fastapi_crudrouter/core/_base.py b/fastapi_crudrouter/core/_base.py index 9b3067c..2d4303e 100644 --- a/fastapi_crudrouter/core/_base.py +++ b/fastapi_crudrouter/core/_base.py @@ -4,7 +4,7 @@ from fastapi.types import DecoratedCallable from ._types import T, DEPENDENCIES -from ._utils import pagination_factory, schema_factory +from ._utils import pagination_factory, schema_factory, query_factory NOT_FOUND = HTTPException(404, "Item not found") @@ -34,6 +34,8 @@ def __init__( self.schema = schema self.pagination = pagination_factory(max_limit=paginate) + self.filter = query_factory(self.schema) + self._pk: str = self._pk if hasattr(self, "_pk") else "id" self.create_schema = ( create_schema diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index c42e197..4fdc298 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -3,9 +3,10 @@ from fastapi import Depends, HTTPException from pydantic import create_model -from ._types import PAGINATION, PYDANTIC_SCHEMA +from core._types import PAGINATION, PYDANTIC_SCHEMA T = TypeVar("T", bound=PYDANTIC_SCHEMA) +FILTER_TYPES = [int, float, bool, str] def get_pk_type(schema: Type[PYDANTIC_SCHEMA], pk_field: str) -> Any: @@ -71,3 +72,34 @@ def pagination(skip: int = 0, limit: Optional[int] = max_limit) -> PAGINATION: return {"skip": skip, "limit": limit} return Depends(pagination) + + +def query_factory(schema: Type[T]) -> Any: + field_names = schema.__fields__.keys() + + _str = "{}: Optional[{}] = None" + args_str = ", ".join( + [ + _str.format(name, field.type_.__name__) + for name, field in schema.__fields__.items() + if field.type_ in FILTER_TYPES + ] + ) + + _str = "{}={}" + return_str = ", ".join( + [ + _str.format(name, field.name) + for name, field in schema.__fields__.items() + if field.type_ in FILTER_TYPES + ] + ) + + filter_func_src = f""" +def filter_func({args_str}): + ret = dict({return_str}) + return {{k:v for k, v in ret.items() if v is not None}} +""" + + exec(filter_func_src, globals(), locals()) + return Depends(locals().get("filter_func")) diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 384bc61..1ce20a1 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -1,9 +1,8 @@ -import typing from datetime import datetime from typing import Any, Callable, Dict, Generator, List -from typing import Optional, Type, Union, get_type_hints +from typing import Optional, Type, Union -from fastapi import Depends, HTTPException, Request +from fastapi import Depends, HTTPException from . import CRUDGenerator, NOT_FOUND, _utils from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA @@ -22,38 +21,10 @@ FILTER = Dict[str, Optional[Union[int, str, datetime, None]]] -def schemas_args_factory(schema: Optional[Type[SCHEMA]]) -> Any: - """ - Created the schema dependency to be used in the router - """ - schema_typing = get_type_hints(schema) - _str = "{}: Optional[{}] = None" - args = ( - _str.format(k, v.__name__) - for k, v in schema_typing.items() - if v.__module__ != "typing" - ) - args_str: str = ",".join(args) - return_str = ", ".join( - [ - "{}={}".format(k, k) - for k, v in schema_typing.items() - if v.__module__ != "typing" - ] - ) - - func_code = "def tmp_function({0}) -> FILTER:return dict({1})" - func_code = func_code.format(args_str, return_str) - local_var = {"datetime": datetime, "FILTER": FILTER, "typing": typing} - exec(func_code, globals(), local_var) - tmp_function = local_var["tmp_function"] - return tmp_function - - class SQLAlchemyCRUDRouter(CRUDGenerator[SCHEMA]): def __init__( self, - schema: Optional[Type[SCHEMA]], + schema: Type[SCHEMA], db_model: Model, db: "Session", create_schema: Optional[Type[SCHEMA]] = None, @@ -72,14 +43,7 @@ def __init__( assert ( sqlalchemy_installed ), "SQLAlchemy must be installed to use the SQLAlchemyCRUDRouter." - if schema is None: - try: - from pydantic_sqlalchemy import sqlalchemy_to_pydantic - except ImportError: - raise ValueError( - "Schema should not be None,or installed pydantic_sqlalchemy" - ) - schema = sqlalchemy_to_pydantic(db_model) + self.db_model = db_model self.db_func = db self._pk: str = db_model.__table__.primary_key.columns.keys()[0] @@ -105,15 +69,10 @@ def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., List[Model]]: def route( db: Session = Depends(self.db_func), pagination: PAGINATION = self.pagination, - condition: FILTER = Depends(schemas_args_factory(self.schema)), - request: Request = Request(scope={"type": "http"}), + filter_: FILTER = self.filter, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") - effective_filter = { - k: v for k, v in condition.items() if k in request.query_params.keys() - } - - query = db.query(self.db_model).filter_by(**effective_filter) + query = db.query(self.db_model).filter_by(**filter_) db_models: List[Model] = query.limit(limit).offset(skip).all() return db_models @@ -123,15 +82,8 @@ def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Model]: def route( item_id: Optional[self._pk_type] = None, # type: ignore db: Session = Depends(self.db_func), - condition: FILTER = Depends(schemas_args_factory(self.schema)), - request: Request = Request(scope={"type": "http"}), ) -> Model: - effective_filter = { - k: v for k, v in condition.items() if k in request.query_params.keys() - } - if item_id: - effective_filter[self._pk] = item_id - model: Model = db.query(self.db_model).filter_by(**effective_filter).first() + model: Model = db.query(self.db_model).get(item_id) if model: return model @@ -164,7 +116,7 @@ def route( db: Session = Depends(self.db_func), ) -> Model: try: - db_model: Model = self._get_one()(item_id, db, {}) + db_model: Model = self._get_one()(item_id, db) for key, value in model.dict(exclude={self._pk}).items(): if hasattr(db_model, key): @@ -186,7 +138,7 @@ def route(db: Session = Depends(self.db_func)) -> List[Model]: db.commit() return self._get_all()( - db=db, pagination={"skip": 0, "limit": None}, condition={} + db=db, pagination={"skip": 0, "limit": None}, filter_={} ) return route @@ -195,7 +147,7 @@ def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Model]: def route( item_id: self._pk_type, db: Session = Depends(self.db_func) # type: ignore ) -> Model: - db_model: Model = self._get_one()(item_id, db, {}) + db_model: Model = self._get_one()(item_id, db) db.delete(db_model) db.commit() diff --git a/setup.cfg b/setup.cfg index 2614d23..02371af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,7 +33,3 @@ ignore_missing_imports = True [mypy-uvicorn.*] ignore_missing_imports = True - -[mypy-pydantic_sqlalchemy.*] -ignore_missing_imports = True - diff --git a/tests/dev.requirements.txt b/tests/dev.requirements.txt index 0f16498..c2b285b 100644 --- a/tests/dev.requirements.txt +++ b/tests/dev.requirements.txt @@ -9,7 +9,6 @@ databases aiosqlite sqlalchemy==1.3.22 sqlalchemy_utils==0.36.8 -pydantic_sqlalchemy # Testing pytest From d561a03a25f6be9a7c228faee9a2245ef1915558 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 10:31:47 -0700 Subject: [PATCH 06/16] :sparkles: Filter working for databases router --- fastapi_crudrouter/core/_types.py | 3 ++- fastapi_crudrouter/core/databases.py | 6 +++++- fastapi_crudrouter/core/sqlalchemy.py | 7 ++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/fastapi_crudrouter/core/_types.py b/fastapi_crudrouter/core/_types.py index 959d5fe..d0ae694 100644 --- a/fastapi_crudrouter/core/_types.py +++ b/fastapi_crudrouter/core/_types.py @@ -1,9 +1,10 @@ -from typing import Dict, TypeVar, Optional, Sequence +from typing import Dict, TypeVar, Optional, Sequence, Union from fastapi.params import Depends from pydantic import BaseModel PAGINATION = Dict[str, Optional[int]] +FILTER = Dict[str, Optional[Union[int, float, str, bool]]] PYDANTIC_SCHEMA = BaseModel T = TypeVar("T", bound=BaseModel) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index a761278..16f7916 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -12,7 +12,7 @@ from fastapi import HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import PAGINATION, PYDANTIC_SCHEMA, DEPENDENCIES +from ._types import PAGINATION, PYDANTIC_SCHEMA, DEPENDENCIES, FILTER try: from sqlalchemy.sql.schema import Table @@ -75,10 +75,14 @@ def __init__( def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route( pagination: PAGINATION = self.pagination, + filter_: FILTER = self.filter, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") query = self.table.select().limit(limit).offset(skip) + for col, val in filter_.items(): + query = query.where(self.table.c[col] == val) + return await self.db.fetch_all(query) return route diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 1ce20a1..00d260a 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -1,11 +1,10 @@ -from datetime import datetime -from typing import Any, Callable, Dict, Generator, List +from typing import Any, Callable, Generator, List from typing import Optional, Type, Union from fastapi import Depends, HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER try: from sqlalchemy.orm import Session @@ -18,8 +17,6 @@ sqlalchemy_installed = True Session = Callable[..., Generator[Session, Any, None]] -FILTER = Dict[str, Optional[Union[int, str, datetime, None]]] - class SQLAlchemyCRUDRouter(CRUDGenerator[SCHEMA]): def __init__( From 7ab0e58a8f3658c178b31a6d2ea4dd618a9cd922 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 21 Apr 2021 10:58:24 -0700 Subject: [PATCH 07/16] :sparkles: Filter working for tortoise --- fastapi_crudrouter/core/_utils.py | 6 +++--- fastapi_crudrouter/core/databases.py | 4 +++- fastapi_crudrouter/core/tortoise.py | 13 ++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index 4fdc298..1b83e40 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -1,6 +1,6 @@ -from typing import Optional, Type, TypeVar, Any +from typing import Optional, Type, TypeVar, Any, List # noqa: F401 -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, Query # noqa: F401 from pydantic import create_model from core._types import PAGINATION, PYDANTIC_SCHEMA @@ -77,7 +77,7 @@ def pagination(skip: int = 0, limit: Optional[int] = max_limit) -> PAGINATION: def query_factory(schema: Type[T]) -> Any: field_names = schema.__fields__.keys() - _str = "{}: Optional[{}] = None" + _str = "{}: Optional[{}] = Query(None)" args_str = ", ".join( [ _str.format(name, field.type_.__name__) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index 16f7916..24c72b7 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -133,7 +133,9 @@ async def route() -> List[Model]: query = self.table.delete() await self.db.execute(query=query) - return await self._get_all()(pagination={"skip": 0, "limit": None}) + return await self._get_all()( + pagination={"skip": 0, "limit": None}, filter_={} + ) return route diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index 23c3926..b5ad044 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Type, cast, Coroutine, Optional, Union from . import CRUDGenerator, NOT_FOUND -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER try: from tortoise.models import Model @@ -11,7 +11,6 @@ else: tortoise_installed = True - CALLABLE = Callable[..., Coroutine[Any, Any, Model]] CALLABLE_LIST = Callable[..., Coroutine[Any, Any, List[Model]]] @@ -58,9 +57,11 @@ def __init__( ) def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: - async def route(pagination: PAGINATION = self.pagination) -> List[Model]: + async def route( + pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter + ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.db_model.all().offset(cast(int, skip)) + query = self.db_model.filter(**filter_).offset(cast(int, skip)) if limit: query = query.limit(limit) return await query @@ -101,7 +102,9 @@ async def route( def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route() -> List[Model]: await self.db_model.all().delete() - return await self._get_all()(pagination={"skip": 0, "limit": None}) + return await self._get_all()( + pagination={"skip": 0, "limit": None}, filter_={} + ) return route From d94179e49a2a2c5db198d6b5b561bf84843e2289 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 22 Apr 2021 00:41:38 -0700 Subject: [PATCH 08/16] :sparkles: Filter working for Ormar --- fastapi_crudrouter/core/_utils.py | 21 +++++++++++++++------ fastapi_crudrouter/core/ormar.py | 11 +++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index 1b83e40..93b8db0 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -3,10 +3,16 @@ from fastapi import Depends, HTTPException, Query # noqa: F401 from pydantic import create_model -from core._types import PAGINATION, PYDANTIC_SCHEMA +from ._types import PAGINATION, PYDANTIC_SCHEMA T = TypeVar("T", bound=PYDANTIC_SCHEMA) -FILTER_TYPES = [int, float, bool, str] +FILTER_MAPPING = { + "int": int, + "float": float, + "bool": bool, + "str": str, + "ConstrainedStrValue": str, +} def get_pk_type(schema: Type[PYDANTIC_SCHEMA], pk_field: str) -> Any: @@ -75,14 +81,16 @@ def pagination(skip: int = 0, limit: Optional[int] = max_limit) -> PAGINATION: def query_factory(schema: Type[T]) -> Any: - field_names = schema.__fields__.keys() + """ + Dynamically builds a Fastapi query dependency based on all available field in the + """ _str = "{}: Optional[{}] = Query(None)" args_str = ", ".join( [ - _str.format(name, field.type_.__name__) + _str.format(name, FILTER_MAPPING[field.type_.__name__].__name__) for name, field in schema.__fields__.items() - if field.type_ in FILTER_TYPES + if field.type_.__name__ in FILTER_MAPPING ] ) @@ -91,7 +99,7 @@ def query_factory(schema: Type[T]) -> Any: [ _str.format(name, field.name) for name, field in schema.__fields__.items() - if field.type_ in FILTER_TYPES + if field.type_.__name__ in FILTER_MAPPING ] ) @@ -102,4 +110,5 @@ def filter_func({args_str}): """ exec(filter_func_src, globals(), locals()) + print(filter_func_src) return Depends(locals().get("filter_func")) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index 4586b5b..f394315 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -12,7 +12,7 @@ from fastapi import HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import DEPENDENCIES, PAGINATION +from ._types import DEPENDENCIES, PAGINATION, FILTER try: from ormar import Model, NoMatch @@ -68,10 +68,11 @@ def __init__( def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route( - pagination: PAGINATION = self.pagination, + pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.schema.objects.offset(cast(int, skip)) + query = self.schema.objects.filter(**filter_).offset(cast(int, skip)) + if limit: query = query.limit(limit) return await query.all() @@ -122,7 +123,9 @@ async def route( def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route() -> List[Optional[Model]]: await self.schema.objects.delete(each=True) - return await self._get_all()(pagination={"skip": 0, "limit": None}) + return await self._get_all()( + pagination={"skip": 0, "limit": None}, filter_={} + ) return route From b00f252655df337aefe80a46e7afe1e7793d0426 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 22 Apr 2021 01:03:50 -0700 Subject: [PATCH 09/16] :sparkles: Implement Filters for memory --- fastapi_crudrouter/core/mem.py | 31 ++++++++++++++++++++++++------- fastapi_crudrouter/core/ormar.py | 2 +- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/fastapi_crudrouter/core/mem.py b/fastapi_crudrouter/core/mem.py index d4e13c1..b0dae14 100644 --- a/fastapi_crudrouter/core/mem.py +++ b/fastapi_crudrouter/core/mem.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Type, cast, Optional, Union from . import CRUDGenerator, NOT_FOUND -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER CALLABLE = Callable[..., SCHEMA] CALLABLE_LIST = Callable[..., List[SCHEMA]] @@ -44,15 +44,14 @@ def __init__( self._id = 1 def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: - def route(pagination: PAGINATION = self.pagination) -> List[SCHEMA]: + def route( + pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter + ) -> List[SCHEMA]: skip, limit = pagination.get("skip"), pagination.get("limit") skip = cast(int, skip) - return ( - self.models[skip:] - if limit is None - else self.models[skip : skip + limit] - ) + models = self._get_filtered_list(self.models, filter_) + return models[skip:] if limit is None else models[skip : skip + limit] return route @@ -112,3 +111,21 @@ def _get_next_id(self) -> int: self._id += 1 return id_ + + @staticmethod + def _get_filtered_list(models: List[SCHEMA], filters_: FILTER) -> List[SCHEMA]: + if not filters_: + return models + + return [ + model + for model in models + if MemoryCRUDRouter._check_filters(model, filters_) + ] + + @staticmethod + def _check_filters(model: SCHEMA, filters_: FILTER) -> bool: + for k, v in filters_.items(): + if getattr(model, k) != v: + return False + return True diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index f394315..e586c92 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -71,7 +71,7 @@ async def route( pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.schema.objects.filter(**filter_).offset(cast(int, skip)) + query = self.schema.objects.filter(**filter_).offset(cast(int, skip)) # type: ignore if limit: query = query.limit(limit) From 7352f3d1552ceb0e170c0ba37c46b931d4ca64be Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 22 Apr 2021 01:05:17 -0700 Subject: [PATCH 10/16] :broom: Clean Up --- fastapi_crudrouter/core/_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index 93b8db0..d06fbf6 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -110,5 +110,4 @@ def filter_func({args_str}): """ exec(filter_func_src, globals(), locals()) - print(filter_func_src) return Depends(locals().get("filter_func")) From aa017f5c78cf66189d78494fae720b53f0bd799f Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 22 Apr 2021 01:10:34 -0700 Subject: [PATCH 11/16] :broom: Clean Up --- fastapi_crudrouter/core/ormar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index e586c92..44f0da2 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -71,7 +71,9 @@ async def route( pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.schema.objects.filter(**filter_).offset(cast(int, skip)) # type: ignore + query = self.schema.objects.filter(**filter_).offset( + cast(int, skip) + ) # type: ignore if limit: query = query.limit(limit) From 39b87758c01aa83cea14abd73111129e4a02d23f Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 22 Apr 2021 01:14:46 -0700 Subject: [PATCH 12/16] :broom: Clean Up linting --- fastapi_crudrouter/core/ormar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index 44f0da2..faaa97e 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -71,9 +71,9 @@ async def route( pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.schema.objects.filter(**filter_).offset( + query = self.schema.objects.filter(**filter_).offset( # type: ignore cast(int, skip) - ) # type: ignore + ) if limit: query = query.limit(limit) From ddbad47d84abfe8168b37a29cf6a81541cff1110 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 28 Apr 2021 23:27:54 -0700 Subject: [PATCH 13/16] :sparkles: Added Sort and Direction Deps --- fastapi_crudrouter/core/_base.py | 3 ++- fastapi_crudrouter/core/_types.py | 1 + fastapi_crudrouter/core/_utils.py | 16 ++++++++++++++-- fastapi_crudrouter/core/databases.py | 3 ++- fastapi_crudrouter/core/mem.py | 6 ++++-- fastapi_crudrouter/core/ormar.py | 6 ++++-- fastapi_crudrouter/core/sqlalchemy.py | 3 ++- fastapi_crudrouter/core/tortoise.py | 6 ++++-- 8 files changed, 33 insertions(+), 11 deletions(-) diff --git a/fastapi_crudrouter/core/_base.py b/fastapi_crudrouter/core/_base.py index 2d4303e..a218cc3 100644 --- a/fastapi_crudrouter/core/_base.py +++ b/fastapi_crudrouter/core/_base.py @@ -4,7 +4,7 @@ from fastapi.types import DecoratedCallable from ._types import T, DEPENDENCIES -from ._utils import pagination_factory, schema_factory, query_factory +from ._utils import pagination_factory, schema_factory, query_factory, sort_factory NOT_FOUND = HTTPException(404, "Item not found") @@ -35,6 +35,7 @@ def __init__( self.schema = schema self.pagination = pagination_factory(max_limit=paginate) self.filter = query_factory(self.schema) + self.sort = sort_factory(self.schema) self._pk: str = self._pk if hasattr(self, "_pk") else "id" self.create_schema = ( diff --git a/fastapi_crudrouter/core/_types.py b/fastapi_crudrouter/core/_types.py index d0ae694..a88ecee 100644 --- a/fastapi_crudrouter/core/_types.py +++ b/fastapi_crudrouter/core/_types.py @@ -5,6 +5,7 @@ PAGINATION = Dict[str, Optional[int]] FILTER = Dict[str, Optional[Union[int, float, str, bool]]] +SORT = Dict[str, str] PYDANTIC_SCHEMA = BaseModel T = TypeVar("T", bound=BaseModel) diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index d06fbf6..a4fd50e 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -3,7 +3,7 @@ from fastapi import Depends, HTTPException, Query # noqa: F401 from pydantic import create_model -from ._types import PAGINATION, PYDANTIC_SCHEMA +from ._types import PAGINATION, PYDANTIC_SCHEMA, SORT, FILTER # noqa: F401 T = TypeVar("T", bound=PYDANTIC_SCHEMA) FILTER_MAPPING = { @@ -104,10 +104,22 @@ def query_factory(schema: Type[T]) -> Any: ) filter_func_src = f""" -def filter_func({args_str}): +def filter_func({args_str}) -> FILTER: ret = dict({return_str}) return {{k:v for k, v in ret.items() if v is not None}} """ exec(filter_func_src, globals(), locals()) return Depends(locals().get("filter_func")) + + +def sort_factory(schema: Type[T]) -> Any: + fields = [field.name for field in schema.__fields__.values()] + + def sort_func( + sort_: str = Query(None, alias="sort", enum=fields), + direction: str = Query(None, enum=["asc", "desc"]), + ) -> SORT: + return {"sort": sort_, "direction": direction} + + return Depends(sort_func) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index 24c72b7..b66542c 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -12,7 +12,7 @@ from fastapi import HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import PAGINATION, PYDANTIC_SCHEMA, DEPENDENCIES, FILTER +from ._types import PAGINATION, PYDANTIC_SCHEMA, DEPENDENCIES, FILTER, SORT try: from sqlalchemy.sql.schema import Table @@ -76,6 +76,7 @@ def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route( pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter, + sort_: SORT = self.sort, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") diff --git a/fastapi_crudrouter/core/mem.py b/fastapi_crudrouter/core/mem.py index b0dae14..21b0b1a 100644 --- a/fastapi_crudrouter/core/mem.py +++ b/fastapi_crudrouter/core/mem.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Type, cast, Optional, Union from . import CRUDGenerator, NOT_FOUND -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER, SORT CALLABLE = Callable[..., SCHEMA] CALLABLE_LIST = Callable[..., List[SCHEMA]] @@ -45,7 +45,9 @@ def __init__( def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: def route( - pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter + pagination: PAGINATION = self.pagination, + filter_: FILTER = self.filter, + sort_: SORT = self.sort, ) -> List[SCHEMA]: skip, limit = pagination.get("skip"), pagination.get("limit") skip = cast(int, skip) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index faaa97e..da15a9c 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -12,7 +12,7 @@ from fastapi import HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import DEPENDENCIES, PAGINATION, FILTER +from ._types import DEPENDENCIES, PAGINATION, FILTER, SORT try: from ormar import Model, NoMatch @@ -68,7 +68,9 @@ def __init__( def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route( - pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter + pagination: PAGINATION = self.pagination, + filter_: FILTER = self.filter, + sort_: SORT = self.sort, ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") query = self.schema.objects.filter(**filter_).offset( # type: ignore diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 00d260a..fdaa61b 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -4,7 +4,7 @@ from fastapi import Depends, HTTPException from . import CRUDGenerator, NOT_FOUND, _utils -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER, SORT try: from sqlalchemy.orm import Session @@ -67,6 +67,7 @@ def route( db: Session = Depends(self.db_func), pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter, + sort_: SORT = self.sort, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") query = db.query(self.db_model).filter_by(**filter_) diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index b5ad044..199bb3d 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Type, cast, Coroutine, Optional, Union from . import CRUDGenerator, NOT_FOUND -from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER +from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA, FILTER, SORT try: from tortoise.models import Model @@ -58,7 +58,9 @@ def __init__( def _get_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route( - pagination: PAGINATION = self.pagination, filter_: FILTER = self.filter + pagination: PAGINATION = self.pagination, + filter_: FILTER = self.filter, + sort_: SORT = self.sort, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") query = self.db_model.filter(**filter_).offset(cast(int, skip)) From 4d6c1bafac9344073550184bad9bd39914d438f9 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Thu, 29 Apr 2021 00:11:28 -0700 Subject: [PATCH 14/16] :sparkles: OrderBy (SQLA) + Simple Test --- fastapi_crudrouter/core/_utils.py | 3 ++- fastapi_crudrouter/core/sqlalchemy.py | 6 ++++++ tests/test_query_params.py | 25 ++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/fastapi_crudrouter/core/_utils.py b/fastapi_crudrouter/core/_utils.py index a4fd50e..bffe81b 100644 --- a/fastapi_crudrouter/core/_utils.py +++ b/fastapi_crudrouter/core/_utils.py @@ -120,6 +120,7 @@ def sort_func( sort_: str = Query(None, alias="sort", enum=fields), direction: str = Query(None, enum=["asc", "desc"]), ) -> SORT: - return {"sort": sort_, "direction": direction} + ret = {"sort": sort_, "reverse": direction == "desc"} + return {k: v for k, v in ret.items() if v} # type: ignore return Depends(sort_func) diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index fdaa61b..293a7e7 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -71,6 +71,12 @@ def route( ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") query = db.query(self.db_model).filter_by(**filter_) + + if sort_: + field = getattr(self.db_model, sort_.get("sort", self._pk)) + order = field.desc() if sort_.get("reverse", False) else field + query = query.order_by(order) + db_models: List[Model] = query.limit(limit).offset(skip).all() return db_models diff --git a/tests/test_query_params.py b/tests/test_query_params.py index 9ec34ba..a047021 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -1,3 +1,5 @@ +from operator import itemgetter + from tests.test_router import test_post, test_get @@ -16,12 +18,17 @@ def insert(_client): model=dict(thickness=0.10, mass=1.9, color="red", type="Small"), expected_length=5, ) + test_post( + _client, + model=dict(thickness=0.25, mass=1.9, color="red", type="Medium"), + expected_length=6, + ) def test_simple(client): insert(client) - test_get(client, params={"color": "red"}, expected_length=2) + test_get(client, params={"color": "red"}, expected_length=3) test_get(client, params={"color": "blue"}, expected_length=0) test_get(client, params={"type": "Large"}, expected_length=1) test_get(client, params={"thickness": 0.24}, expected_length=4) @@ -34,3 +41,19 @@ def test_two_params(client): test_get(client, params={"color": "red", "type": "Small"}, expected_length=1) test_get(client, params={"color": "blue", "type": "Small"}, expected_length=0) test_get(client, params={"thickness": 0.24, "mass": 1.2}, expected_length=3) + + +def test_sort_asc(client): + insert(client) + + data1 = test_get(client, params={"color": "red", "sort": "thickness"}, expected_length=3) + assert data1 == sorted(data1, key=itemgetter("thickness")) + + +def test_sort_desc(client): + insert(client) + + data = test_get(client, params={"color": "red", "sort": "thickness", "direction": "desc"}, expected_length=3) + assert data == sorted(data, key=itemgetter("thickness"), reverse=True) + + From 9b0a416e378d46ac7d3656912ecd541d90aa2471 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Sat, 8 May 2021 10:23:09 -0700 Subject: [PATCH 15/16] :sparkles: Sort added for ORMAR --- fastapi_crudrouter/core/databases.py | 2 +- fastapi_crudrouter/core/ormar.py | 19 +++++++++++-------- fastapi_crudrouter/core/sqlalchemy.py | 2 +- fastapi_crudrouter/core/tortoise.py | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index b66542c..91c4c51 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -135,7 +135,7 @@ async def route() -> List[Model]: await self.db.execute(query=query) return await self._get_all()( - pagination={"skip": 0, "limit": None}, filter_={} + pagination={"skip": 0, "limit": None}, filter_={}, sort_={} ) return route diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index da15a9c..1304276 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -41,7 +41,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, - **kwargs: Any + **kwargs: Any, ) -> None: assert ormar_installed, "Ormar must be installed to use the OrmarCRUDRouter." @@ -61,7 +61,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, - **kwargs + **kwargs, ) self._INTEGRITY_ERROR = self._get_integrity_error_type() @@ -73,12 +73,15 @@ async def route( sort_: SORT = self.sort, ) -> List[Optional[Model]]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.schema.objects.filter(**filter_).offset( # type: ignore - cast(int, skip) - ) + query = self.schema.objects.filter(**filter_) # type: ignore + + if sort_: + field = sort_.get("sort", self._pk) + order = f"-{field}" if sort_.get("reverse", False) else field + query = query.order_by(order) + + query = query.limit(limit).offset(cast(int, skip)) # type: ignore - if limit: - query = query.limit(limit) return await query.all() return route @@ -128,7 +131,7 @@ def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route() -> List[Optional[Model]]: await self.schema.objects.delete(each=True) return await self._get_all()( - pagination={"skip": 0, "limit": None}, filter_={} + pagination={"skip": 0, "limit": None}, filter_={}, sort_={} ) return route diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 293a7e7..b11fe06 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -142,7 +142,7 @@ def route(db: Session = Depends(self.db_func)) -> List[Model]: db.commit() return self._get_all()( - db=db, pagination={"skip": 0, "limit": None}, filter_={} + db=db, pagination={"skip": 0, "limit": None}, filter_={}, sort_={} ) return route diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index 199bb3d..046da6d 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -105,7 +105,7 @@ def _delete_all(self, *args: Any, **kwargs: Any) -> CALLABLE_LIST: async def route() -> List[Model]: await self.db_model.all().delete() return await self._get_all()( - pagination={"skip": 0, "limit": None}, filter_={} + pagination={"skip": 0, "limit": None}, filter_={}, sort_={} ) return route From bcc029ecb8ba468ae984490d6980847035889988 Mon Sep 17 00:00:00 2001 From: Adam Watkins Date: Wed, 23 Jun 2021 22:45:09 -0700 Subject: [PATCH 16/16] :sparkles: Sort added for Mem, Databases, and Tortoise --- fastapi_crudrouter/core/databases.py | 8 +++++++- fastapi_crudrouter/core/mem.py | 15 ++++++++++++++- fastapi_crudrouter/core/tortoise.py | 6 ++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index 91c4c51..a8fab6a 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -16,6 +16,7 @@ try: from sqlalchemy.sql.schema import Table + from sqlalchemy import desc from databases.core import Database except ImportError: databases_installed = False @@ -79,8 +80,13 @@ async def route( sort_: SORT = self.sort, ) -> List[Model]: skip, limit = pagination.get("skip"), pagination.get("limit") - query = self.table.select().limit(limit).offset(skip) + + if sort_: + field = getattr(self.table.c, sort_.get("sort", self._pk)) + order = desc(field) if sort_.get("reverse", False) else field + query = query.order_by(order) + for col, val in filter_.items(): query = query.where(self.table.c[col] == val) diff --git a/fastapi_crudrouter/core/mem.py b/fastapi_crudrouter/core/mem.py index 21b0b1a..0e41326 100644 --- a/fastapi_crudrouter/core/mem.py +++ b/fastapi_crudrouter/core/mem.py @@ -53,6 +53,8 @@ def route( skip = cast(int, skip) models = self._get_filtered_list(self.models, filter_) + models = self._get_sorted_list(models, sort_) + return models[skip:] if limit is None else models[skip : skip + limit] return route @@ -70,7 +72,7 @@ def route(item_id: int) -> SCHEMA: def _create(self, *args: Any, **kwargs: Any) -> CALLABLE: def route(model: self.create_schema) -> SCHEMA: # type: ignore model_dict = model.dict() - model_dict["id"] = self._get_next_id() + model_dict[self._pk] = self._get_next_id() ready_model = self.schema(**model_dict) self.models.append(ready_model) return ready_model @@ -114,6 +116,17 @@ def _get_next_id(self) -> int: return id_ + def _get_sorted_list(self, models: List[SCHEMA], sort_: SORT) -> List[SCHEMA]: + if not sort_: + return models + + field = sort_.get("sort", self._pk) + models.sort( + reverse=bool(sort_.get("reverse", False)), key=lambda x: getattr(x, field) # type: ignore + ) + + return models + @staticmethod def _get_filtered_list(models: List[SCHEMA], filters_: FILTER) -> List[SCHEMA]: if not filters_: diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index 046da6d..675131c 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -66,6 +66,12 @@ async def route( query = self.db_model.filter(**filter_).offset(cast(int, skip)) if limit: query = query.limit(limit) + + if sort_: + field = sort_.get("sort", self._pk) + order = "-" + field if sort_.get("reverse", False) else field + query = query.order_by(order) + return await query return route