From 9bb80b4d1ef0bedbf37dbaa9d8957727b92fef51 Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock Date: Wed, 26 Oct 2022 18:23:57 +0200 Subject: [PATCH 1/6] Add parameter to customize item id --- fastapi_crudrouter/core/databases.py | 2 ++ fastapi_crudrouter/core/gino_starlette.py | 2 ++ fastapi_crudrouter/core/mem.py | 2 ++ fastapi_crudrouter/core/ormar.py | 2 ++ fastapi_crudrouter/core/sqlalchemy.py | 2 ++ fastapi_crudrouter/core/tortoise.py | 4 +++- 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index 7ea3c711..e4ffc99f 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -56,6 +56,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -81,6 +82,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/gino_starlette.py b/fastapi_crudrouter/core/gino_starlette.py index d07d893e..c1b10d17 100644 --- a/fastapi_crudrouter/core/gino_starlette.py +++ b/fastapi_crudrouter/core/gino_starlette.py @@ -41,6 +41,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert gino_installed, "Gino must be installed to use the GinoCRUDRouter." @@ -63,6 +64,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/mem.py b/fastapi_crudrouter/core/mem.py index d4e13c11..bbcbed13 100644 --- a/fastapi_crudrouter/core/mem.py +++ b/fastapi_crudrouter/core/mem.py @@ -22,6 +22,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: super().__init__( @@ -37,6 +38,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index 99952600..148b5603 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -42,6 +42,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ormar_installed, "Ormar must be installed to use the OrmarCRUDRouter." @@ -62,6 +63,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 58270f34..dc278970 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -39,6 +39,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -63,6 +64,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index 52972a48..a068a9f7 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, List, Type, cast, Coroutine, Optional, Union +from typing import Any, Callable, ItemsView, List, Type, cast, Coroutine, Optional, Union from . import CRUDGenerator, NOT_FOUND from ._types import DEPENDENCIES, PAGINATION, PYDANTIC_SCHEMA as SCHEMA @@ -32,6 +32,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -54,6 +55,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) From 0d81fa52f32c2ae576b797769acf43913ace991b Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock Date: Wed, 26 Oct 2022 18:40:05 +0200 Subject: [PATCH 2/6] Add missing parameter to base --- fastapi_crudrouter/core/_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fastapi_crudrouter/core/_base.py b/fastapi_crudrouter/core/_base.py index e45d33fe..c827e35a 100644 --- a/fastapi_crudrouter/core/_base.py +++ b/fastapi_crudrouter/core/_base.py @@ -30,6 +30,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any, ) -> None: @@ -46,6 +47,7 @@ def __init__( if update_schema else schema_factory(self.schema, pk_field_name=self._pk, name="Update") ) + item_id_path = f"/{{{item_id_parameter_name}}}" prefix = str(prefix if prefix else self.schema.__name__).lower() prefix = self._base_path + prefix.strip("/") @@ -85,7 +87,7 @@ def __init__( if get_one_route: self._add_api_route( - "/{item_id}", + item_id_path, self._get_one(), methods=["GET"], response_model=self.schema, @@ -96,7 +98,7 @@ def __init__( if update_route: self._add_api_route( - "/{item_id}", + item_id_path, self._update(), methods=["PUT"], response_model=self.schema, @@ -107,7 +109,7 @@ def __init__( if delete_one_route: self._add_api_route( - "/{item_id}", + item_id_path, self._delete_one(), methods=["DELETE"], response_model=self.schema, From 89863978ed3966b511025f2282000209b99158c6 Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock Date: Wed, 26 Oct 2022 18:53:30 +0200 Subject: [PATCH 3/6] Add tests for customizable item id feature --- tests/conftest.py | 10 +++++++ tests/implementations/__init__.py | 1 + tests/implementations/sqlalchemy_.py | 40 ++++++++++++++++++++++++++++ tests/test_custom_item_id.py | 34 +++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 tests/test_custom_item_id.py diff --git a/tests/conftest.py b/tests/conftest.py index 74ac91a2..daacc235 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,3 +68,13 @@ def string_pk_client(request): ) def integrity_errors_client(request): yield from yield_test_client(request.param(), request.param) + + +@pytest.fixture( + params=[ + sqlalchemy_implementation_custom_item_id, + ], + scope="function", +) +def custom_item_id_client(request): + yield from yield_test_client(request.param(), request.param) \ No newline at end of file diff --git a/tests/implementations/__init__.py b/tests/implementations/__init__.py index 0c311095..ab5d8d38 100644 --- a/tests/implementations/__init__.py +++ b/tests/implementations/__init__.py @@ -21,6 +21,7 @@ sqlalchemy_implementation_custom_ids, sqlalchemy_implementation_integrity_errors, sqlalchemy_implementation_string_pk, + sqlalchemy_implementation_custom_item_id, DSN_LIST, ) diff --git a/tests/implementations/sqlalchemy_.py b/tests/implementations/sqlalchemy_.py index e2295ab8..e76d1c7c 100644 --- a/tests/implementations/sqlalchemy_.py +++ b/tests/implementations/sqlalchemy_.py @@ -168,3 +168,43 @@ class CarrotModel(Base): ) return app + + +def sqlalchemy_implementation_custom_item_id(): + app, engine, Base, session = _setup_base_app() + + class PotatoModel(Base): + __tablename__ = "potatoes" + id = Column(Integer, primary_key=True, index=True) + thickness = Column(Float) + mass = Column(Float) + color = Column(String, unique=True) + type = Column(String) + + class CarrotModel(Base): + __tablename__ = "carrots" + id = Column(Integer, primary_key=True, index=True) + length = Column(Float) + color = Column(String) + + Base.metadata.create_all(bind=engine) + app.include_router( + SQLAlchemyCRUDRouter( + schema=Potato, + db_model=PotatoModel, + db=session, + prefix="potato", + item_id_parameter_name="potato_id", + ) + ) + app.include_router( + SQLAlchemyCRUDRouter( + schema=Carrot, + db_model=CarrotModel, + db=session, + prefix="carrot", + item_id_parameter_name="carrot_id" + ) + ) + + return app diff --git a/tests/test_custom_item_id.py b/tests/test_custom_item_id.py new file mode 100644 index 00000000..dfa6281c --- /dev/null +++ b/tests/test_custom_item_id.py @@ -0,0 +1,34 @@ +from . import test_router +from pytest import mark + +from tests import CUSTOM_TAGS + +potato_type = dict(name="russet", origin="Canada") + +PATHS = ["/potato", "/carrot"] + +class TestOpenAPISpec: + def test_schema_exists(self, custom_item_id_client): + res = custom_item_id_client.get("/openapi.json") + assert res.status_code == 200 + + return res + + @mark.parametrize("path", PATHS) + def test_response_types(self, custom_item_id_client, path): + schema = self.test_schema_exists(custom_item_id_client).json() + paths = schema["paths"] + + for method in ["get", "post", "delete"]: + assert "200" in paths[path][method]["responses"] + + assert "422" in paths[path]["post"]["responses"] + + if path == "/potato": + item_path = path + "/{potato_id}" + else: + item_path = path + "/{carrot_id}" + for method in ["get", "put", "delete"]: + assert "200" in paths[item_path][method]["responses"] + assert "404" in paths[item_path][method]["responses"] + assert "422" in paths[item_path][method]["responses"] \ No newline at end of file From 16b7dce3b1850ae3596f140236ac07e5bcfc7ec7 Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock Date: Wed, 26 Oct 2022 18:57:11 +0200 Subject: [PATCH 4/6] Add documentation --- docs/en/docs/routing.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/en/docs/routing.md b/docs/en/docs/routing.md index 58dca3be..4688d1be 100644 --- a/docs/en/docs/routing.md +++ b/docs/en/docs/routing.md @@ -28,6 +28,20 @@ the [SQLAlchemyCRUDRouter](backends/sqlalchemy.md) will use the model's table na You are also able to set custom prefixes with the `prefix` kwarg when creating your CRUDRouter. This can be done like so: `router = CRUDRouter(model=mymodel, prefix='carrot')` +## Custom item_id + +If you don't want the default `item_id` as your path parameter name for the item id you can use set the `item_id_parameter_name` key when creating a new CRUDRouter. This will change the parameter name in the OpenAPI specification for you. + +```python +SQLAlchemyCRUDRouter( + schema=Potato, + db_model=PotatoModel, + db=session, + prefix="potato", + item_id_parameter_name="potato_id", +) +``` + ## Disabling Routes Routes can be disabled from generating with a key word argument (kwarg) when creating your CRUDRouter. The valid kwargs are shown below. From efa96fa549a93aaf3925cc4be8a33b2558f8144c Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock <35262568+nikstuckenbrock@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:23:14 +0200 Subject: [PATCH 5/6] Remove unused import of ItemsView --- fastapi_crudrouter/core/tortoise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index a068a9f7..b3ff5b5b 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, ItemsView, List, Type, cast, Coroutine, Optional, Union +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 adfd2f050eb46afc76dee7b6f25886a7940cf07f Mon Sep 17 00:00:00 2001 From: Nik Stuckenbrock <35262568+nikstuckenbrock@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:24:13 +0200 Subject: [PATCH 6/6] Remove unused imports of CUSTOM_TAGS --- tests/test_custom_item_id.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_custom_item_id.py b/tests/test_custom_item_id.py index dfa6281c..81a5137a 100644 --- a/tests/test_custom_item_id.py +++ b/tests/test_custom_item_id.py @@ -1,7 +1,6 @@ from . import test_router from pytest import mark -from tests import CUSTOM_TAGS potato_type = dict(name="russet", origin="Canada") @@ -31,4 +30,4 @@ def test_response_types(self, custom_item_id_client, path): for method in ["get", "put", "delete"]: assert "200" in paths[item_path][method]["responses"] assert "404" in paths[item_path][method]["responses"] - assert "422" in paths[item_path][method]["responses"] \ No newline at end of file + assert "422" in paths[item_path][method]["responses"]