Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 53 additions & 36 deletions fastapi_jsonapi/querystring.py
Original file line number Diff line number Diff line change
@@ -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 (
TYPE_CHECKING,
Expand All @@ -7,7 +8,6 @@
List,
Optional,
Type,
Union,
)
from urllib.parse import unquote

Expand All @@ -22,17 +22,18 @@
)
from starlette.datastructures import QueryParams

from fastapi_jsonapi.api import RoutersJSONAPI
from fastapi_jsonapi.exceptions import (
BadRequest,
InvalidField,
InvalidFilters,
InvalidInclude,
InvalidSort,
InvalidType,
)
from fastapi_jsonapi.schema import (
get_model_field,
get_relationships,
get_schema_from_type,
)
from fastapi_jsonapi.splitter import SPLIT_REL

Expand Down Expand Up @@ -89,33 +90,45 @@ def __init__(self, request: Request) -> None:
self.MAX_INCLUDE_DEPTH: int = self.config.get("MAX_INCLUDE_DEPTH", 3)
self.headers: HeadersQueryStringManager = HeadersQueryStringManager(**dict(self.request.headers))

def _get_key_values(self, name: str) -> Dict[str, Union[List[str], str]]:
def _extract_item_key(self, key: str) -> str:
try:
key_start = key.index("[") + 1
key_end = key.index("]")
return key[key_start:key_end]
except Exception:
msg = "Parse error"
raise BadRequest(msg, parameter=key)

def _get_unique_key_values(self, name: str) -> Dict[str, str]:
"""
Return a dict containing key / values items for a given key, used for items like filters, page, etc.

:param name: name of the querystring parameter
:return: a dict of key / values items
:raises BadRequest: if an error occurred while parsing the querystring.
"""
results: Dict[str, Union[List[str], str]] = {}
results = {}

for raw_key, value in self.qs.multi_items():
key = unquote(raw_key)
try:
if not key.startswith(name):
continue
if not key.startswith(name):
continue

key_start = key.index("[") + 1
key_end = key.index("]")
item_key = key[key_start:key_end]
item_key = self._extract_item_key(key)
results[item_key] = value

if "," in value:
results.update({item_key: value.split(",")})
else:
results.update({item_key: value})
except Exception:
msg = "Parse error"
raise BadRequest(msg, parameter=key)
return results

def _get_multiple_key_values(self, name: str) -> Dict[str, List]:
results = defaultdict(list)

for raw_key, value in self.qs.multi_items():
key = unquote(raw_key)
if not key.startswith(name):
continue

item_key = self._extract_item_key(key)
results[item_key].extend(value.split(","))

return results

Expand All @@ -134,7 +147,7 @@ def querystring(self) -> Dict[str, str]:
return {
key: value
for (key, value) in self.qs.multi_items()
if key.startswith(self.managed_keys) or self._get_key_values("filter[")
if key.startswith(self.managed_keys) or self._get_unique_key_values("filter[")
}

@property
Expand All @@ -159,8 +172,8 @@ def filters(self) -> List[dict]:
raise InvalidFilters(msg)

results.extend(loaded_filters)
if self._get_key_values("filter["):
results.extend(self._simple_filters(self._get_key_values("filter[")))
if filter_key_values := self._get_unique_key_values("filter["):
results.extend(self._simple_filters(filter_key_values))
return results

@cached_property
Expand All @@ -186,7 +199,7 @@ def pagination(self) -> PaginationQueryStringManager:
:raises BadRequest: if the client is not allowed to disable pagination.
"""
# check values type
pagination_data: Dict[str, Union[List[str], str]] = self._get_key_values("page")
pagination_data: Dict[str, str] = self._get_unique_key_values("page")
pagination = PaginationQueryStringManager(**pagination_data)
if pagination_data.get("size") is None:
pagination.size = None
Expand All @@ -199,8 +212,6 @@ def pagination(self) -> PaginationQueryStringManager:

return pagination

# TODO: finally use this! upgrade Sqlachemy Data Layer
# and add to all views (get list/detail, create, patch)
@property
def fields(self) -> Dict[str, List[str]]:
"""
Expand All @@ -216,26 +227,32 @@ def fields(self) -> Dict[str, List[str]]:

:raises InvalidField: if result field not in schema.
"""
if self.request.method != "GET":
msg = "attribute 'fields' allowed only for GET-method"
raise InvalidField(msg)
fields = self._get_key_values("fields")
for key, value in fields.items():
if not isinstance(value, list):
value = [value] # noqa: PLW2901
fields[key] = value
fields = self._get_multiple_key_values("fields")
for resource_type, field_names in fields.items():
# TODO: we have registry for models (BaseModel)
# TODO: create `type to schemas` registry
schema: Type[BaseModel] = get_schema_from_type(key, self.app)
for field in value:
if field not in schema.__fields__:

if resource_type not in RoutersJSONAPI.all_jsonapi_routers:
msg = f"Application has no resource with type {resource_type!r}"
raise InvalidType(msg)

schema: Type[BaseModel] = self._get_schema(resource_type)

for field_name in field_names:
if field_name == "":
continue

if field_name not in schema.__fields__:
msg = "{schema} has no attribute {field}".format(
schema=schema.__name__,
field=field,
field=field_name,
)
raise InvalidField(msg)

return fields
return {resource_type: set(field_names) for resource_type, field_names in fields.items()}

def _get_schema(self, resource_type: str) -> Type[BaseModel]:
return RoutersJSONAPI.all_jsonapi_routers[resource_type]._schema

def get_sorts(self, schema: Type["TypeSchema"]) -> List[Dict[str, str]]:
"""
Expand Down
11 changes: 7 additions & 4 deletions fastapi_jsonapi/views/detail_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BaseJSONAPIItemInSchema,
JSONAPIResultDetailSchema,
)
from fastapi_jsonapi.views.utils import handle_jsonapi_fields
from fastapi_jsonapi.views.view_base import ViewBase

if TYPE_CHECKING:
Expand All @@ -34,22 +35,24 @@ async def handle_get_resource_detail(
self,
object_id: Union[int, str],
**extra_view_deps,
):
) -> Union[JSONAPIResultDetailSchema, Dict]:
dl: "BaseDataLayer" = await self.get_data_layer(extra_view_deps)

view_kwargs = {dl.url_id_field: object_id}
db_object = await dl.get_object(view_kwargs=view_kwargs, qs=self.query_params)

return self._build_detail_response(db_object)
response = self._build_detail_response(db_object)
return handle_jsonapi_fields(response, self.query_params, self.jsonapi)

async def handle_update_resource(
self,
obj_id: str,
data_update: BaseJSONAPIItemInSchema,
**extra_view_deps,
) -> JSONAPIResultDetailSchema:
) -> Union[JSONAPIResultDetailSchema, Dict]:
dl: "BaseDataLayer" = await self.get_data_layer(extra_view_deps)
return await self.process_update_object(dl=dl, obj_id=obj_id, data_update=data_update)
response = await self.process_update_object(dl=dl, obj_id=obj_id, data_update=data_update)
return handle_jsonapi_fields(response, self.query_params, self.jsonapi)

async def process_update_object(
self,
Expand Down
16 changes: 10 additions & 6 deletions fastapi_jsonapi/views/list_view.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
from typing import TYPE_CHECKING, Any, Dict
from typing import TYPE_CHECKING, Any, Dict, Union

from fastapi_jsonapi.schema import (
BaseJSONAPIItemInSchema,
JSONAPIResultDetailSchema,
JSONAPIResultListSchema,
)
from fastapi_jsonapi.views.utils import handle_jsonapi_fields
from fastapi_jsonapi.views.view_base import ViewBase

if TYPE_CHECKING:
Expand Down Expand Up @@ -34,21 +35,23 @@ async def get_data_layer(
) -> "BaseDataLayer":
return await self.get_data_layer_for_list(extra_view_deps)

async def handle_get_resource_list(self, **extra_view_deps) -> JSONAPIResultListSchema:
async def handle_get_resource_list(self, **extra_view_deps) -> Union[JSONAPIResultListSchema, Dict]:
dl: "BaseDataLayer" = await self.get_data_layer(extra_view_deps)
query_params = self.query_params
count, items_from_db = await dl.get_collection(qs=query_params)
total_pages = self._calculate_total_pages(count)

return self._build_list_response(items_from_db, count, total_pages)
response = self._build_list_response(items_from_db, count, total_pages)
return handle_jsonapi_fields(response, query_params, self.jsonapi)

async def handle_post_resource_list(
self,
data_create: BaseJSONAPIItemInSchema,
**extra_view_deps,
) -> JSONAPIResultDetailSchema:
) -> Union[JSONAPIResultDetailSchema, Dict]:
dl: "BaseDataLayer" = await self.get_data_layer(extra_view_deps)
return await self.process_create_object(dl=dl, data_create=data_create)
response = await self.process_create_object(dl=dl, data_create=data_create)
return handle_jsonapi_fields(response, self.query_params, self.jsonapi)

async def process_create_object(self, dl: "BaseDataLayer", data_create: BaseJSONAPIItemInSchema):
created_object = await dl.create_object(data_create=data_create, view_kwargs={})
Expand All @@ -68,4 +71,5 @@ async def handle_delete_resource_list(self, **extra_view_deps) -> JSONAPIResultL

await dl.delete_objects(items_from_db, {})

return self._build_list_response(items_from_db, count, total_pages)
response = self._build_list_response(items_from_db, count, total_pages)
return handle_jsonapi_fields(response, self.query_params, self.jsonapi)
Loading