Skip to content

Commit

Permalink
Merge pull request #105 from JakNowy/feat/simplified-endpoints
Browse files Browse the repository at this point in the history
Simplified endpoints
  • Loading branch information
igorbenav authored Jun 28, 2024
2 parents e675910 + dc6b50b commit fd20036
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 32 deletions.
12 changes: 11 additions & 1 deletion docs/advanced/crud.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ items = await item_crud.get_multi(
- __in - included in
- __not_in - not included in

### OR parameter filters
### OR clauses

More complex OR filters are supported. They must be passed as dictionary, where each key is a library-supported operator to be used in OR expression and values is what get's passed as the parameter.

Expand All @@ -147,6 +147,16 @@ items = await item_crud.get_multi(
)
```

### AND clauses
And clauses can be achieved by chaining multiple filters together.

# Fetch items priced under $20 and over 2 years of warranty.
items = await item_crud.get_multi(
db=db,
price__lt=20,
warranty_years__gt=2,
)


#### Counting Records

Expand Down
17 changes: 17 additions & 0 deletions docs/advanced/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ FastCRUD automates the creation of CRUD (Create, Read, Update, Delete) endpoints
"items_per_page": 3
}
```
!!! WARNING

_read_paginated endpoint is getting deprecated and mixed into _read_items in the next major release.
Please use _read_items with optional page and items_per_pagequery params instead, to achieve pagination as before.
Simple _read_items behaviour persists with no breaking changes.

Read items paginated:
curl -X 'GET' 'http://localhost:8000/users/get_multi?page=2&itemsPerPage=10' -H 'accept: application/json'

Read items unpaginated:
url -X 'GET' 'http://localhost:8000/users/get_multi?offset=0&limit=100' -H 'accept: application/json'

### Update

Expand Down Expand Up @@ -226,6 +237,12 @@ app.include_router(endpoint_creator.router)

You only need to pass the names of the endpoints you want to change in the endpoint_names dict.

!!! WARNING

default_endpoint_names for EndpointCreator are going to be changed to empty strings in the next major release.
See: https://github.com/igorbenav/fastcrud/issues/67


## Extending EndpointCreator

You can create a subclass of `EndpointCreator` and override or add new methods to define custom routes. Here's an example:
Expand Down
82 changes: 59 additions & 23 deletions fastcrud/endpoint/endpoint_creator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from typing import Type, TypeVar, Optional, Callable, Sequence, Union
from enum import Enum

Expand Down Expand Up @@ -254,6 +255,15 @@ def __init__(
"read_paginated": "get_paginated",
}
self.endpoint_names = {**self.default_endpoint_names, **(endpoint_names or {})}
if self.endpoint_names == self.default_endpoint_names:
warnings.warn(
"Old default_endpoint_names are getting deprecated. "
"Default values are going to be replaced by empty strings, "
"resulting in plain endpoint names. "
"For details see:"
" https://github.com/igorbenav/fastcrud/issues/67",
DeprecationWarning
)
if filter_config:
if isinstance(filter_config, dict):
filter_config = FilterConfig(**filter_config)
Expand Down Expand Up @@ -310,17 +320,40 @@ def _read_items(self):

async def endpoint(
db: AsyncSession = Depends(self.session),
offset: int = Query(0),
limit: int = Query(100),
page: Optional[int] = Query(
None, alias="page", description="Page number"
),
items_per_page: Optional[int] = Query(
None, alias="itemsPerPage", description="Number of items per page"
),
filters: dict = Depends(dynamic_filters),
):
return await self.crud.get_multi(db, offset=offset, limit=limit, **filters)
if not (page and items_per_page):
return await self.crud.get_multi(db, offset=0, limit=100,
**filters)

offset = compute_offset(page=page, items_per_page=items_per_page)
crud_data = await self.crud.get_multi(
db, offset=offset, limit=items_per_page, **filters
)

return paginated_response(
crud_data=crud_data, page=page, items_per_page=items_per_page
) # pragma: no cover

return endpoint

def _read_paginated(self):
"""Creates an endpoint for reading multiple items from the database with pagination."""
dynamic_filters = _create_dynamic_filters(self.filter_config, self.column_types)
warnings.warn(
"_read_paginated endpoint is getting deprecated and mixed "
"into _read_items in the next major release. "
"Please use _read_items with optional page and items_per_page "
"query params instead, to achieve pagination as before."
"Simple _read_items behaviour persists with no breaking changes.",
DeprecationWarning
)

async def endpoint(
db: AsyncSession = Depends(self.session),
Expand All @@ -332,6 +365,10 @@ async def endpoint(
),
filters: dict = Depends(dynamic_filters),
):
if not (page and items_per_page):
return await self.crud.get_multi(db, offset=0, limit=100,
**filters)

offset = compute_offset(page=page, items_per_page=items_per_page)
crud_data = await self.crud.get_multi(
db, offset=offset, limit=items_per_page, **filters
Expand Down Expand Up @@ -384,11 +421,19 @@ async def endpoint(db: AsyncSession = Depends(self.session), **pkeys):

return endpoint

def _get_endpoint_name(self, operation: str) -> str:
"""Get the endpoint name for a given CRUD operation, using defaults if not overridden by the user."""
return self.endpoint_names.get(
def _get_endpoint_path(self, operation: str):
endpoint_name = self.endpoint_names.get(
operation, self.default_endpoint_names.get(operation, operation)
)
path = f"{self.path}/{endpoint_name}" if endpoint_name else self.path

if operation in {'read', 'update', 'delete', 'db_delete'}:
_primary_keys_path_suffix = "/".join(
f"{{{n}}}" for n in self.primary_key_names
)
path = f'{path}/{_primary_keys_path_suffix}'

return path

def add_routes_to_router(
self,
Expand Down Expand Up @@ -493,12 +538,9 @@ def get_current_user(...):
if self.delete_schema:
delete_description = "Soft delete a"

_primary_keys_path_suffix = "/".join(f"{{{n}}}" for n in self.primary_key_names)

if ("create" in included_methods) and ("create" not in deleted_methods):
endpoint_name = self._get_endpoint_name("create")
self.router.add_api_route(
f"{self.path}/{endpoint_name}",
self._get_endpoint_path(operation='create'),
self._create_item(),
methods=["POST"],
include_in_schema=self.include_in_schema,
Expand All @@ -508,10 +550,8 @@ def get_current_user(...):
)

if ("read" in included_methods) and ("read" not in deleted_methods):
endpoint_name = self._get_endpoint_name("read")

self.router.add_api_route(
f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
self._get_endpoint_path(operation='read'),
self._read_item(),
methods=["GET"],
include_in_schema=self.include_in_schema,
Expand All @@ -521,9 +561,8 @@ def get_current_user(...):
)

if ("read_multi" in included_methods) and ("read_multi" not in deleted_methods):
endpoint_name = self._get_endpoint_name("read_multi")
self.router.add_api_route(
f"{self.path}/{endpoint_name}",
self._get_endpoint_path(operation='read_multi'),
self._read_items(),
methods=["GET"],
include_in_schema=self.include_in_schema,
Expand All @@ -535,9 +574,8 @@ def get_current_user(...):
if ("read_paginated" in included_methods) and (
"read_paginated" not in deleted_methods
):
endpoint_name = self._get_endpoint_name("read_paginated")
self.router.add_api_route(
f"{self.path}/{endpoint_name}",
self._get_endpoint_path(operation='read_paginated'),
self._read_paginated(),
methods=["GET"],
include_in_schema=self.include_in_schema,
Expand All @@ -547,9 +585,8 @@ def get_current_user(...):
)

if ("update" in included_methods) and ("update" not in deleted_methods):
endpoint_name = self._get_endpoint_name("update")
self.router.add_api_route(
f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
self._get_endpoint_path(operation='update'),
self._update_item(),
methods=["PATCH"],
include_in_schema=self.include_in_schema,
Expand All @@ -559,9 +596,9 @@ def get_current_user(...):
)

if ("delete" in included_methods) and ("delete" not in deleted_methods):
endpoint_name = self._get_endpoint_name("delete")
path = self._get_endpoint_path(operation='delete')
self.router.add_api_route(
f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
path,
self._delete_item(),
methods=["DELETE"],
include_in_schema=self.include_in_schema,
Expand All @@ -575,9 +612,8 @@ def get_current_user(...):
and ("db_delete" not in deleted_methods)
and self.delete_schema
):
endpoint_name = self._get_endpoint_name("db_delete")
self.router.add_api_route(
f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
self._get_endpoint_path(operation='db_delete'),
self._db_delete(),
methods=["DELETE"],
include_in_schema=self.include_in_schema,
Expand Down
36 changes: 28 additions & 8 deletions tests/sqlalchemy/endpoint/test_endpoint_custom_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
from fastcrud import crud_router


@pytest.mark.parametrize('custom_endpoint_names, endpoint_paths', [
(
{"create": "", "read": "", "read_multi": ""},
["/test_custom_names", "/test_custom_names",
"/test_custom_names"]
),
(
{"create": "add", "read": "fetch", "read_multi": "fetch_multi"},
["/test_custom_names/add", "/test_custom_names/fetch",
"/test_custom_names/fetch_multi"]),
])
@pytest.mark.asyncio
async def test_endpoint_custom_names(
client: TestClient,
Expand All @@ -12,16 +23,13 @@ async def test_endpoint_custom_names(
test_model,
create_schema,
update_schema,
custom_endpoint_names,
endpoint_paths,
):
for item in test_data:
async_session.add(test_model(**item))
await async_session.commit()

custom_endpoint_names = {
"create": "add",
"read": "fetch",
}

custom_router = crud_router(
session=lambda: async_session,
model=test_model,
Expand All @@ -34,19 +42,31 @@ async def test_endpoint_custom_names(

client.app.include_router(custom_router)

create_path, read_path, read_multi_path = endpoint_paths

create_response = client.post(
"/test_custom_names/add", json={"name": "Custom Endpoint Item", "tier_id": 1}
create_path, json={"name": "Custom Endpoint Item", "tier_id": 1}
)

assert (
create_response.status_code == 200
), "Failed to create item with custom endpoint name"

item_id = create_response.json()["id"]

fetch_response = client.get(f"/test_custom_names/fetch/{item_id}")
fetch_response = client.get(f'{read_path}/{item_id}')
assert (
fetch_response.status_code == 200
), "Failed to fetch item with custom endpoint name"
assert (
fetch_response.json()["id"] == item_id
), "Fetched item ID does not match created item ID"
), (f"Fetched item ID does not match created item ID:"
f" {fetch_response.json()['id']} != {item_id}")

fetch_multi_response = client.get(read_multi_path)
assert (
fetch_multi_response.status_code == 200
), "Failed to fetch multi items with custom endpoint name"
assert (
len(fetch_multi_response.json()['data']) == 12
), "Fetched item list has incorrect length"

0 comments on commit fd20036

Please sign in to comment.