diff --git a/fastapi_rest_jsonapi/__init__.py b/fastapi_rest_jsonapi/__init__.py index ee03dfc..76b90a6 100644 --- a/fastapi_rest_jsonapi/__init__.py +++ b/fastapi_rest_jsonapi/__init__.py @@ -4,4 +4,12 @@ from fastapi_rest_jsonapi.data import DataLayer, SQLAlchemyDataLayer -__all__ = ["RestAPI", "Schema", "Resource", "ResourceList", "ResourceDetail", "DataLayer", "SQLAlchemyDataLayer"] +__all__ = [ + "RestAPI", + "Schema", + "Resource", + "ResourceList", + "ResourceDetail", + "DataLayer", + "SQLAlchemyDataLayer", +] diff --git a/fastapi_rest_jsonapi/common/exceptions.py b/fastapi_rest_jsonapi/common/exceptions.py index 85a5875..7898872 100644 --- a/fastapi_rest_jsonapi/common/exceptions.py +++ b/fastapi_rest_jsonapi/common/exceptions.py @@ -16,6 +16,12 @@ def __init__(self, relationship: str): self.message = f"Unknown relationship: {relationship}" +class UnprocessableEntityException(RestAPIException): + def __init__(self, entity: str): + self.status = status.HTTP_422_UNPROCESSABLE_ENTITY + self.message = f"Unprocessable entity: {entity}" + + class UnknownTypeException(RestAPIException): def __init__(self, type_: str): self.status = status.HTTP_400_BAD_REQUEST diff --git a/fastapi_rest_jsonapi/data/data_layer.py b/fastapi_rest_jsonapi/data/data_layer.py index d21ad29..9ebfb7c 100644 --- a/fastapi_rest_jsonapi/data/data_layer.py +++ b/fastapi_rest_jsonapi/data/data_layer.py @@ -8,12 +8,18 @@ class DataLayer(metaclass=ABCMeta): @abstractmethod def get( - self, sorts: list[Sort] = None, fields: list[Field] = None, page: Page = None, includes: list[Include] = None + self, + sorts: list[Sort] = None, + fields: list[Field] = None, + page: Page = None, + includes: list[Include] = None, ) -> list: raise NotImplementedError @abstractmethod - def get_one(self, id_: int, fields: list[Field] = None, includes: list[Include] = None) -> object: + def get_one( + self, id_: int, fields: list[Field] = None, includes: list[Include] = None + ) -> object: raise NotImplementedError @abstractmethod diff --git a/fastapi_rest_jsonapi/data/sqlachemy_data_layer.py b/fastapi_rest_jsonapi/data/sqlachemy_data_layer.py index 9cd1aeb..14f229b 100644 --- a/fastapi_rest_jsonapi/data/sqlachemy_data_layer.py +++ b/fastapi_rest_jsonapi/data/sqlachemy_data_layer.py @@ -1,4 +1,5 @@ from math import ceil +from fastapi_rest_jsonapi.common.exceptions import UnprocessableEntityException import sqlalchemy from fastapi_rest_jsonapi.common.exceptions import ( @@ -30,23 +31,35 @@ def __get_model_for_type(self, type_: str): return class_ raise UnknownTypeException(type_) - def __get_relationships_and_properties_for_model(self, model) -> tuple[list[str], list[str]]: + def __get_relationships_and_properties_for_model( + self, model + ) -> tuple[list[str], list[str]]: relationships = [] properties = [] for field_name, field_attr in model.__dict__.items(): - if type(getattr(field_attr, "property", False)) is sqlalchemy.orm.relationships.RelationshipProperty: + if ( + type(getattr(field_attr, "property", False)) + is sqlalchemy.orm.relationships.RelationshipProperty + ): relationships.append(field_name) elif type(getattr(field_attr, "property", False)): properties.append(field_name) return relationships, properties - def __get_fields_for_type(self, type_: str, fields: list[Field]) -> tuple[list[str], list[str]]: + def __get_fields_for_type( + self, type_: str, fields: list[Field] + ) -> tuple[list[str], list[str]]: type_model = self.__get_model_for_type(type_) - type_relationship_fields, type_properties_fields = self.__get_relationships_and_properties_for_model(type_model) + ( + type_relationship_fields, + type_properties_fields, + ) = self.__get_relationships_and_properties_for_model(type_model) fields_ = list(filter(lambda f: f.type == type_, fields or [])) fields_properties = filter(lambda f: f.field in type_properties_fields, fields_) - fields_relationships = filter(lambda f: f.field in type_relationship_fields, fields_) + fields_relationships = filter( + lambda f: f.field in type_relationship_fields, fields_ + ) fields_properties = map(lambda f: f.field, fields_properties) fields_relationships = map(lambda f: f.field, fields_relationships) return list(fields_properties), list(fields_relationships) @@ -75,10 +88,16 @@ def __paginate_query(self, query: Query, page: Page) -> Query: page.max_number = ceil(total / page.size) return query.offset(page.size * (page.number - 1)).limit(page.size) - def __include_and_field_query(self, query: Query, includes: list[Include], fields: list[Field]) -> Query: + def __include_and_field_query( + self, query: Query, includes: list[Include], fields: list[Field] + ) -> Query: processed_fields = [] - fields_properties, fields_relationship = self.__get_fields_for_type(self.current_tablename, fields) - query = query.options(load_only(*fields_properties)) if fields_properties else query + fields_properties, fields_relationship = self.__get_fields_for_type( + self.current_tablename, fields + ) + query = ( + query.options(load_only(*fields_properties)) if fields_properties else query + ) processed_fields.extend(fields_properties) processed_fields.extend(fields_relationship) @@ -91,12 +110,18 @@ def __include_and_field_query(self, query: Query, includes: list[Include], field joined_load_func = joined_load_func.load_only(*fields_) query = query.options(joined_load_func) - if processed_diff := set(processed_fields) ^ set([x.field for x in fields or []]): + if processed_diff := set(processed_fields) ^ set( + [x.field for x in fields or []] + ): raise UnknownRelationshipException(", ".join(processed_diff)) return query def get( - self, sorts: list[Sort] = None, fields: list[Field] = None, page: Page = None, includes: list[Include] = None + self, + sorts: list[Sort] = None, + fields: list[Field] = None, + page: Page = None, + includes: list[Include] = None, ) -> list: query: Query = self.session.query(self.model) query = self.__include_and_field_query(query, includes, fields) @@ -104,7 +129,9 @@ def get( query = self.__paginate_query(query, page) return query.all() - def get_one(self, id_: int, fields: list[Field] = None, includes: list[Include] = None) -> object: + def get_one( + self, id_: int, fields: list[Field] = None, includes: list[Include] = None + ) -> object: query: Query = self.session.query(self.model) query = self.__include_and_field_query(query, includes, fields) query = query.filter(self.model.id == id_) @@ -130,5 +157,9 @@ def update_one(self, id_: int, **kwargs) -> object: def create_one(self, **kwargs) -> object: obj = self.model(**kwargs) self.session.add(obj) - self.session.commit() - return obj + try: + self.session.commit() + return obj + except Exception: + self.session.rollback() + raise UnprocessableEntityException(str(kwargs)) diff --git a/fastapi_rest_jsonapi/request/page.py b/fastapi_rest_jsonapi/request/page.py index a89bfaa..5a6d5c6 100644 --- a/fastapi_rest_jsonapi/request/page.py +++ b/fastapi_rest_jsonapi/request/page.py @@ -6,7 +6,13 @@ class Page: - def __init__(self, url: URL, query_params: Optional[dict], number: int, size: Optional[int] = None) -> None: + def __init__( + self, + url: URL, + query_params: Optional[dict], + number: int, + size: Optional[int] = None, + ) -> None: self.url = url self.query_params = query_params self.number = number @@ -19,9 +25,18 @@ def is_paginated(self): def __get_query_params_as_dict(self) -> dict: if not self.query_params: return {} - non_iterable_query_params = {k: v for k, v in self.query_params.items() if v and not isinstance(v, list)} - iterable_query_params = {k: v for k, v in self.query_params.items() if v and k not in non_iterable_query_params} - iterable_query_params = {v.split("=")[0]: v.split("=")[1] for v in chain(*iterable_query_params.values())} + non_iterable_query_params = { + k: v for k, v in self.query_params.items() if v and not isinstance(v, list) + } + iterable_query_params = { + k: v + for k, v in self.query_params.items() + if v and k not in non_iterable_query_params + } + iterable_query_params = { + v.split("=")[0]: v.split("=")[1] + for v in chain(*iterable_query_params.values()) + } return non_iterable_query_params | iterable_query_params def get_self_link(self) -> str: diff --git a/fastapi_rest_jsonapi/resource/resource_detail.py b/fastapi_rest_jsonapi/resource/resource_detail.py index a56da48..d3c98ff 100644 --- a/fastapi_rest_jsonapi/resource/resource_detail.py +++ b/fastapi_rest_jsonapi/resource/resource_detail.py @@ -3,9 +3,11 @@ from fastapi import status from fastapi_rest_jsonapi.common import Methods +from fastapi_rest_jsonapi.common.exceptions import UnprocessableEntityException from fastapi_rest_jsonapi.resource import Resource from fastapi_rest_jsonapi.request.request_context import RequestContext from fastapi_rest_jsonapi.response.response import Response +from marshmallow import ValidationError class ResourceDetail(Resource, ABC): @@ -13,12 +15,19 @@ class ResourceDetail(Resource, ABC): @staticmethod def get(cls: Resource, request_ctx: RequestContext): - obj = cls.data_layer.get_one(request_ctx.path_parameters.id, request_ctx.fields, request_ctx.includes) + obj = cls.data_layer.get_one( + request_ctx.path_parameters.id, request_ctx.fields, request_ctx.includes + ) if obj is None: return Response(request_ctx, status_code=status.HTTP_404_NOT_FOUND) return Response( request_ctx, - content=cls.schema().dump(includes=request_ctx.includes, fields=request_ctx.fields, obj=obj, many=False), + content=cls.schema().dump( + includes=request_ctx.includes, + fields=request_ctx.fields, + obj=obj, + many=False, + ), ) @staticmethod @@ -30,7 +39,15 @@ def delete(cls: Resource, request_ctx: RequestContext): @staticmethod def patch(cls: Resource, request_ctx: RequestContext): - is_updated = cls.data_layer.update_one(request_ctx.path_parameters.id, **request_ctx.body) is not None + try: + data = cls.schema().load(request_ctx.body) + except ValidationError: + raise UnprocessableEntityException(str(request_ctx.body)) + + is_updated = ( + cls.data_layer.update_one(request_ctx.path_parameters.id, **data) + is not None + ) if is_updated: return Response(request_ctx, status_code=status.HTTP_204_NO_CONTENT) return Response(request_ctx, status_code=status.HTTP_404_NOT_FOUND) diff --git a/fastapi_rest_jsonapi/resource/resource_list.py b/fastapi_rest_jsonapi/resource/resource_list.py index 95be4a5..170ca52 100644 --- a/fastapi_rest_jsonapi/resource/resource_list.py +++ b/fastapi_rest_jsonapi/resource/resource_list.py @@ -3,8 +3,10 @@ from fastapi import status from fastapi_rest_jsonapi.resource import Resource from fastapi_rest_jsonapi.common.methods import Methods +from fastapi_rest_jsonapi.common.exceptions import UnprocessableEntityException from fastapi_rest_jsonapi.request.request_context import RequestContext from fastapi_rest_jsonapi.response.response import Response +from marshmallow import ValidationError DEFAULT_PAGE_SIZE = 30 @@ -20,15 +22,39 @@ def get(cls: Resource, request_ctx: RequestContext): if request_ctx_page.size is None: request_ctx_page.size = cls.page_size - objects = cls.data_layer.get(request_ctx.sorts, request_ctx.fields, request_ctx_page, request_ctx.includes) - content = cls.schema().dump(includes=request_ctx.includes, fields=request_ctx.fields, obj=objects, many=True) + objects = cls.data_layer.get( + request_ctx.sorts, + request_ctx.fields, + request_ctx_page, + request_ctx.includes, + ) + content = cls.schema().dump( + includes=request_ctx.includes, + fields=request_ctx.fields, + obj=objects, + many=True, + ) return Response(request_ctx, content=content) @staticmethod def post(cls: Resource, request_ctx: RequestContext): - created = cls.data_layer.create_one(**request_ctx.body) - if created is None: - return Response(request_ctx, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + try: + data = cls.schema().load(request_ctx.body) + except ValidationError: + raise UnprocessableEntityException(str(request_ctx.body)) - content = cls.schema().dump(includes=request_ctx.fields, fields=request_ctx.fields, obj=created, many=False) - return Response(request_ctx, content=content, status_code=status.HTTP_201_CREATED) + created = cls.data_layer.create_one(**data) + if created is None: + return Response( + request_ctx, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + content = cls.schema().dump( + includes=request_ctx.fields, + fields=request_ctx.fields, + obj=created, + many=False, + ) + return Response( + request_ctx, content=content, status_code=status.HTTP_201_CREATED + ) diff --git a/fastapi_rest_jsonapi/rest_api.py b/fastapi_rest_jsonapi/rest_api.py index e42bd47..e2cea40 100644 --- a/fastapi_rest_jsonapi/rest_api.py +++ b/fastapi_rest_jsonapi/rest_api.py @@ -60,7 +60,9 @@ def __get_response_model(self, resource: Resource, method: str) -> BaseModel: is_detail_resource_ = is_detail_resource(resource) model_suffix = "detail" if is_detail_resource_ else "list" # For some reasons, FastAPI does not allow to use the same name for the response model - response_model = create_model(f"{schema.__type__}-{method}-{model_suffix}", **fields) + response_model = create_model( + f"{schema.__type__}-{method}-{model_suffix}", **fields + ) if is_detail_resource_: return response_model return List[response_model] @@ -74,9 +76,7 @@ def __get_path_parameters_model(self, resource: Resource, method: str) -> BaseMo def __get_endpoint_summary(self, resource: Resource, method: str) -> str: is_detail_resource_ = is_detail_resource(resource) schema_type = resource.schema.__type__ - return ( - f"{method} {'a' if is_detail_resource_ else 'multiple'} {schema_type}{'' if is_detail_resource_ else 's'}" - ) + return f"{method} {'a' if is_detail_resource_ else 'multiple'} {schema_type}{'' if is_detail_resource_ else 's'}" def __get_query_parameters_dict(self, request: Request) -> dict: request_query_params_dict = request.query_params._dict @@ -89,9 +89,16 @@ def __get_query_params_with_brackets(query_param_name: str): if request_query_param := request_query_params_dict.get(query_param_name): return request_query_param.split("&") - return [f"{k}={v}" for k, v in request_query_params_dict.items() if query_param_name in k] + return [ + f"{k}={v}" + for k, v in request_query_params_dict.items() + if query_param_name in k + ] - return {parameter: __get_query_params_with_brackets(parameter) for parameter in RestAPI.QUERY_PARAMETER_KEYS} + return { + parameter: __get_query_params_with_brackets(parameter) + for parameter in RestAPI.QUERY_PARAMETER_KEYS + } def __override_swagger_doc(self): def __generate_field_parameter(field_name: str) -> dict: @@ -104,10 +111,15 @@ def __generate_field_parameter(field_name: str) -> dict: openapi = self.app.openapi() for resource, resource_url in self.registered_resources: - if Methods.GET.value not in resource.methods or is_detail_resource(resource): + if Methods.GET.value not in resource.methods or is_detail_resource( + resource + ): continue openapi["paths"][resource_url]["get"]["parameters"] = [ - *[__generate_field_parameter(field_name) for field_name in RestAPI.QUERY_PARAMETER_KEYS] + *[ + __generate_field_parameter(field_name) + for field_name in RestAPI.QUERY_PARAMETER_KEYS + ] ] def endpoint_wrapper(self, resource: Resource, method: str): @@ -131,7 +143,9 @@ def endpoint(request: Request, path_parameters, body): def wrapper( request: Request, - path_parameters: self.__get_path_parameters_model(resource, method) = Depends(), + path_parameters: self.__get_path_parameters_model( + resource, method + ) = Depends(), body: Optional[dict] = Body(default=None), ): return endpoint(request, path_parameters, body) @@ -140,7 +154,9 @@ def wrapper( def wrapper( request: Request, - path_parameters: self.__get_path_parameters_model(resource, method) = Depends(), + path_parameters: self.__get_path_parameters_model( + resource, method + ) = Depends(), ): return endpoint(request, path_parameters, None) diff --git a/fastapi_rest_jsonapi/schema/schema.py b/fastapi_rest_jsonapi/schema/schema.py index c987c76..3f28d92 100644 --- a/fastapi_rest_jsonapi/schema/schema.py +++ b/fastapi_rest_jsonapi/schema/schema.py @@ -2,7 +2,10 @@ from marshmallow import class_registry from marshmallow_jsonapi import Schema as MarshmallowSchema from marshmallow_jsonapi.fields import Relationship as MarshmallowRelationship -from fastapi_rest_jsonapi.common.exceptions import UnknownFieldException, UnknownSchemaException +from fastapi_rest_jsonapi.common.exceptions import ( + UnknownFieldException, + UnknownSchemaException, +) from fastapi_rest_jsonapi.request.field import Field from fastapi_rest_jsonapi.request.include import Include from fastapi_rest_jsonapi.schema.fields import Relationship @@ -16,10 +19,13 @@ def __init_subclass__(cls, *_, **__): def __get_schema_class_from_field(self, field: str) -> type["Schema"]: if field not in self.declared_fields: raise UnknownFieldException(field) - field_schema_type: str = self.declared_fields[field].__dict__["_Relationship__schema"] + field_schema_type: str = self.declared_fields[field].__dict__[ + "_Relationship__schema" + ] field_schema_cls: list[type[Schema]] = next( filter( - lambda schema_class: getattr(schema_class[0], "__type__", None) == field_schema_type, + lambda schema_class: getattr(schema_class[0], "__type__", None) + == field_schema_type, class_registry._registry.values(), ), None, @@ -37,7 +43,10 @@ def __get_relationship_fields(self, fields: list[Field]) -> list[Field]: for field in fields: field_ = field.field if declared_field := self.declared_fields.get(field_): - if type(declared_field) is MarshmallowRelationship or type(declared_field) is Relationship: + if ( + type(declared_field) is MarshmallowRelationship + or type(declared_field) is Relationship + ): relationship_fields.append(field) return relationship_fields @@ -92,10 +101,20 @@ def __handle_includes_and_relationships( ) obj_dump["relationships"][field] = field_relationship - def dump(self, includes: list[Include], fields: list[Field], obj: Any, many: Optional[bool] = None): + def dump( + self, + includes: list[Include], + fields: list[Field], + obj: Any, + many: Optional[bool] = None, + ): current_type_fields = [x for x in fields or [] if x.type == self.__type__] relationship_fields = self.__get_relationship_fields(current_type_fields) - only = [x.field for x in current_type_fields] + ["id"] if current_type_fields else None + only = ( + [x.field for x in current_type_fields] + ["id"] + if current_type_fields + else None + ) obj_dump = self.__class__(only=only)._dump(obj, many=many) if includes: obj_dump["included"] = [] @@ -103,11 +122,23 @@ def dump(self, includes: list[Include], fields: list[Field], obj: Any, many: Opt if many: for idx in range(len(obj)): self.__handle_includes_and_relationships( - obj_dump["data"][idx], obj_dump, obj[idx], relationship_fields, includes, only, fields + obj_dump["data"][idx], + obj_dump, + obj[idx], + relationship_fields, + includes, + only, + fields, ) else: self.__handle_includes_and_relationships( - obj_dump["data"], obj_dump, obj, relationship_fields, includes, only, fields + obj_dump["data"], + obj_dump, + obj, + relationship_fields, + includes, + only, + fields, ) return obj_dump diff --git a/setup.py b/setup.py index 0795f60..62a1ba0 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import io from setuptools import setup, find_packages -__version__ = "0.8" +__version__ = "0.8.1" def get_requirements(file_name): diff --git a/tests/test_sqlalchemy_datalayer/conftest.py b/tests/test_sqlalchemy_datalayer/conftest.py index 3601dce..5810c58 100644 --- a/tests/test_sqlalchemy_datalayer/conftest.py +++ b/tests/test_sqlalchemy_datalayer/conftest.py @@ -202,7 +202,9 @@ class UserDetail(ResourceDetail): @fixture(autouse=True) -def register_schema_routes(rest_api: RestAPI, user_list, user_detail, article_list, article_detail): +def register_schema_routes( + rest_api: RestAPI, user_list, user_detail, article_list, article_detail +): rest_api.register(user_list, "/users") rest_api.register(user_detail, "/users/{id}") rest_api.register(article_list, "/articles") @@ -244,7 +246,9 @@ def authors(session: Session, author_model, comment_count): def comments(session: Session, authors, articles, comment_model, comment_count): comments = [] for i in range(comment_count): - comment = comment_model(text=f"comment {i}", author_id=authors[i].id, article_id=articles[i // 2].id) + comment = comment_model( + text=f"comment {i}", author_id=authors[i].id, article_id=articles[i // 2].id + ) comments.append(comment) session.add(comment) session.commit() diff --git a/tests/test_sqlalchemy_datalayer/test_create.py b/tests/test_sqlalchemy_datalayer/test_create.py new file mode 100644 index 0000000..051c733 --- /dev/null +++ b/tests/test_sqlalchemy_datalayer/test_create.py @@ -0,0 +1,15 @@ +from fastapi import status +from fastapi.testclient import TestClient + + +def test_create_uniq_key(client: TestClient, users): + response = client.post( + "/users", + json={ + "data": { + "type": "user", + "attributes": {"name": "John", "age": 25, "id": users[0].id}, + } + }, + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY diff --git a/tests/test_sqlalchemy_datalayer/test_fields.py b/tests/test_sqlalchemy_datalayer/test_fields.py index 8b1e523..037b740 100644 --- a/tests/test_sqlalchemy_datalayer/test_fields.py +++ b/tests/test_sqlalchemy_datalayer/test_fields.py @@ -6,13 +6,17 @@ def test_simple_fields(client: TestClient, users, generate_data): response: Response = client.get("/users?field%5Buser%5D=age") assert response.status_code == status.HTTP_200_OK - assert response.json() == {"data": [generate_data(user, only_fields=["age"]) for user in users]} + assert response.json() == { + "data": [generate_data(user, only_fields=["age"]) for user in users] + } def test_two_fields(client: TestClient, users, generate_data): response: Response = client.get("/users?field%5Buser%5D=age,name") assert response.status_code == status.HTTP_200_OK - assert response.json() == {"data": [generate_data(user, only_fields=["age", "name"]) for user in users]} + assert response.json() == { + "data": [generate_data(user, only_fields=["age", "name"]) for user in users] + } def test_non_existing_field(client: TestClient, users): diff --git a/tests/test_sqlalchemy_datalayer/test_include.py b/tests/test_sqlalchemy_datalayer/test_include.py index d559265..961ddf8 100644 --- a/tests/test_sqlalchemy_datalayer/test_include.py +++ b/tests/test_sqlalchemy_datalayer/test_include.py @@ -11,11 +11,23 @@ def test_simple_include(client: TestClient, articles, comments): "type": "article", "id": 1, "attributes": {"name": "Article 0", "price": 0}, - "relationships": {"comments": {"data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}]}}, + "relationships": { + "comments": { + "data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}] + } + }, }, "included": [ - {"type": "comment", "id": 1, "attributes": {"text": "comment 0", "author_id": 1}}, - {"type": "comment", "id": 2, "attributes": {"text": "comment 1", "author_id": 2}}, + { + "type": "comment", + "id": 1, + "attributes": {"text": "comment 0", "author_id": 1}, + }, + { + "type": "comment", + "id": 2, + "attributes": {"text": "comment 1", "author_id": 2}, + }, ], } @@ -29,97 +41,253 @@ def test_include_list(client: TestClient, articles, comments): "type": "article", "attributes": {"name": "Article 0", "price": 0}, "id": 1, - "relationships": {"comments": {"data": [{"type": "comment", "id": 1}, {"type": "comment", "id": 2}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 1}, + {"type": "comment", "id": 2}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 1", "price": 1}, "id": 2, - "relationships": {"comments": {"data": [{"type": "comment", "id": 3}, {"type": "comment", "id": 4}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 3}, + {"type": "comment", "id": 4}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 2", "price": 2}, "id": 3, - "relationships": {"comments": {"data": [{"type": "comment", "id": 5}, {"type": "comment", "id": 6}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 5}, + {"type": "comment", "id": 6}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 3", "price": 3}, "id": 4, - "relationships": {"comments": {"data": [{"type": "comment", "id": 7}, {"type": "comment", "id": 8}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 7}, + {"type": "comment", "id": 8}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 4", "price": 4}, "id": 5, - "relationships": {"comments": {"data": [{"type": "comment", "id": 9}, {"type": "comment", "id": 10}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 9}, + {"type": "comment", "id": 10}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 5", "price": 5}, "id": 6, - "relationships": {"comments": {"data": [{"type": "comment", "id": 11}, {"type": "comment", "id": 12}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 11}, + {"type": "comment", "id": 12}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 6", "price": 6}, "id": 7, - "relationships": {"comments": {"data": [{"type": "comment", "id": 13}, {"type": "comment", "id": 14}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 13}, + {"type": "comment", "id": 14}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 7", "price": 7}, "id": 8, - "relationships": {"comments": {"data": [{"type": "comment", "id": 15}, {"type": "comment", "id": 16}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 15}, + {"type": "comment", "id": 16}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 8", "price": 8}, "id": 9, - "relationships": {"comments": {"data": [{"type": "comment", "id": 17}, {"type": "comment", "id": 18}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 17}, + {"type": "comment", "id": 18}, + ] + } + }, }, { "type": "article", "attributes": {"name": "Article 9", "price": 9}, "id": 10, - "relationships": {"comments": {"data": [{"type": "comment", "id": 19}, {"type": "comment", "id": 20}]}}, + "relationships": { + "comments": { + "data": [ + {"type": "comment", "id": 19}, + {"type": "comment", "id": 20}, + ] + } + }, }, ], "included": [ - {"type": "comment", "attributes": {"text": "comment 0", "author_id": 1}, "id": 1}, - {"type": "comment", "attributes": {"text": "comment 1", "author_id": 2}, "id": 2}, - {"type": "comment", "attributes": {"text": "comment 2", "author_id": 3}, "id": 3}, - {"type": "comment", "attributes": {"text": "comment 3", "author_id": 4}, "id": 4}, - {"type": "comment", "attributes": {"text": "comment 4", "author_id": 5}, "id": 5}, - {"type": "comment", "attributes": {"text": "comment 5", "author_id": 6}, "id": 6}, - {"type": "comment", "attributes": {"text": "comment 6", "author_id": 7}, "id": 7}, - {"type": "comment", "attributes": {"text": "comment 7", "author_id": 8}, "id": 8}, - {"type": "comment", "attributes": {"text": "comment 8", "author_id": 9}, "id": 9}, - {"type": "comment", "attributes": {"text": "comment 9", "author_id": 10}, "id": 10}, - {"type": "comment", "attributes": {"text": "comment 10", "author_id": 11}, "id": 11}, - {"type": "comment", "attributes": {"text": "comment 11", "author_id": 12}, "id": 12}, - {"type": "comment", "attributes": {"text": "comment 12", "author_id": 13}, "id": 13}, - {"type": "comment", "attributes": {"text": "comment 13", "author_id": 14}, "id": 14}, - {"type": "comment", "attributes": {"text": "comment 14", "author_id": 15}, "id": 15}, - {"type": "comment", "attributes": {"text": "comment 15", "author_id": 16}, "id": 16}, - {"type": "comment", "attributes": {"text": "comment 16", "author_id": 17}, "id": 17}, - {"type": "comment", "attributes": {"text": "comment 17", "author_id": 18}, "id": 18}, - {"type": "comment", "attributes": {"text": "comment 18", "author_id": 19}, "id": 19}, - {"type": "comment", "attributes": {"text": "comment 19", "author_id": 20}, "id": 20}, + { + "type": "comment", + "attributes": {"text": "comment 0", "author_id": 1}, + "id": 1, + }, + { + "type": "comment", + "attributes": {"text": "comment 1", "author_id": 2}, + "id": 2, + }, + { + "type": "comment", + "attributes": {"text": "comment 2", "author_id": 3}, + "id": 3, + }, + { + "type": "comment", + "attributes": {"text": "comment 3", "author_id": 4}, + "id": 4, + }, + { + "type": "comment", + "attributes": {"text": "comment 4", "author_id": 5}, + "id": 5, + }, + { + "type": "comment", + "attributes": {"text": "comment 5", "author_id": 6}, + "id": 6, + }, + { + "type": "comment", + "attributes": {"text": "comment 6", "author_id": 7}, + "id": 7, + }, + { + "type": "comment", + "attributes": {"text": "comment 7", "author_id": 8}, + "id": 8, + }, + { + "type": "comment", + "attributes": {"text": "comment 8", "author_id": 9}, + "id": 9, + }, + { + "type": "comment", + "attributes": {"text": "comment 9", "author_id": 10}, + "id": 10, + }, + { + "type": "comment", + "attributes": {"text": "comment 10", "author_id": 11}, + "id": 11, + }, + { + "type": "comment", + "attributes": {"text": "comment 11", "author_id": 12}, + "id": 12, + }, + { + "type": "comment", + "attributes": {"text": "comment 12", "author_id": 13}, + "id": 13, + }, + { + "type": "comment", + "attributes": {"text": "comment 13", "author_id": 14}, + "id": 14, + }, + { + "type": "comment", + "attributes": {"text": "comment 14", "author_id": 15}, + "id": 15, + }, + { + "type": "comment", + "attributes": {"text": "comment 15", "author_id": 16}, + "id": 16, + }, + { + "type": "comment", + "attributes": {"text": "comment 16", "author_id": 17}, + "id": 17, + }, + { + "type": "comment", + "attributes": {"text": "comment 17", "author_id": 18}, + "id": 18, + }, + { + "type": "comment", + "attributes": {"text": "comment 18", "author_id": 19}, + "id": 19, + }, + { + "type": "comment", + "attributes": {"text": "comment 19", "author_id": 20}, + "id": 20, + }, ], } def test_simple_include_with_fields(client: TestClient, articles, comments): - response: Response = client.get("/articles/1?include=comments&fields[comment]=text&fields[article]=name,comments") + response: Response = client.get( + "/articles/1?include=comments&fields[comment]=text&fields[article]=name,comments" + ) assert response.status_code == status.HTTP_200_OK assert response.json() == { "data": { "type": "article", "id": 1, "attributes": {"name": "Article 0"}, - "relationships": {"comments": {"data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}]}}, + "relationships": { + "comments": { + "data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}] + } + }, }, "included": [ {"type": "comment", "id": 1, "attributes": {"text": "comment 0"}}, @@ -128,8 +296,12 @@ def test_simple_include_with_fields(client: TestClient, articles, comments): } -def test_simple_include_with_fields_no_relationship(client: TestClient, articles, comments): - response: Response = client.get("/articles/1?include=comments&fields[comment]=text&fields[article]=name") +def test_simple_include_with_fields_no_relationship( + client: TestClient, articles, comments +): + response: Response = client.get( + "/articles/1?include=comments&fields[comment]=text&fields[article]=name" + ) assert response.status_code == status.HTTP_200_OK assert response.json() == { "data": { @@ -152,13 +324,21 @@ def test_simple_include_with_fields_no_include(client: TestClient, articles, com "type": "article", "id": 1, "attributes": {"name": "Article 0"}, - "relationships": {"comments": {"data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}]}}, + "relationships": { + "comments": { + "data": [{"id": 1, "type": "comment"}, {"id": 2, "type": "comment"}] + } + }, }, } -def test_simple_include_with_fields_no_include_bad_field(client: TestClient, articles, comments): - response: Response = client.get("/articles/1?fields[comment]=text&fields[article]=name,comments") +def test_simple_include_with_fields_no_include_bad_field( + client: TestClient, articles, comments +): + response: Response = client.get( + "/articles/1?fields[comment]=text&fields[article]=name,comments" + ) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/tests/test_sqlalchemy_datalayer/test_pagination.py b/tests/test_sqlalchemy_datalayer/test_pagination.py index 3660a8a..8c93e44 100644 --- a/tests/test_sqlalchemy_datalayer/test_pagination.py +++ b/tests/test_sqlalchemy_datalayer/test_pagination.py @@ -5,7 +5,11 @@ def generate_links( - current_page_number: int, current_page_size: int, has_prev_page: bool, has_next_page: bool, user_count: int + current_page_number: int, + current_page_size: int, + has_prev_page: bool, + has_next_page: bool, + user_count: int, ) -> dict: last_page = ceil(user_count / current_page_size) return { @@ -21,7 +25,9 @@ def generate_links( } -def test_simple_default_pagination_first_page(client: TestClient, users, generate_data, user_count): +def test_simple_default_pagination_first_page( + client: TestClient, users, generate_data, user_count +): response: Response = client.get("/users?page[number]=1&page[size]=30") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -30,7 +36,9 @@ def test_simple_default_pagination_first_page(client: TestClient, users, generat } -def test_simple_default_pagination_second_page(client: TestClient, users, generate_data, user_count): +def test_simple_default_pagination_second_page( + client: TestClient, users, generate_data, user_count +): response: Response = client.get("/users?page[number]=2&page[size]=30") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -48,4 +56,7 @@ def test_disable_pagination(client: TestClient, users, generate_data): def test_wrong_page(client: TestClient, users, generate_data, user_count): response: Response = client.get("/users?page[number]=424242&page[size]=30") assert response.status_code == status.HTTP_200_OK - assert response.json() == {"data": [], "links": generate_links(424242, 30, True, False, user_count)} + assert response.json() == { + "data": [], + "links": generate_links(424242, 30, True, False, user_count), + } diff --git a/tests/test_sqlalchemy_datalayer/test_simple_schema.py b/tests/test_sqlalchemy_datalayer/test_simple_schema.py index 9ce5e7e..d4c2ce1 100644 --- a/tests/test_sqlalchemy_datalayer/test_simple_schema.py +++ b/tests/test_sqlalchemy_datalayer/test_simple_schema.py @@ -60,23 +60,39 @@ def test_simple_delete_multiple_users_object_not_found(client: TestClient, users def test_simple_update(client: TestClient, user): - response: Response = client.patch(f"/users/{user.id}", json={"name": "New user name", "age": 24}) + response: Response = client.patch( + f"/users/{user.id}", + json={ + "data": {"type": "user", "attributes": {"name": "New user name", "age": 24}} + }, + ) + assert response.status_code == status.HTTP_204_NO_CONTENT def test_simple_update_multiple_users(client: TestClient, users): random_user = random.choice(users) - response: Response = client.patch(f"/users/{random_user.id}", json={"name": "New user name", "age": 24}) + response: Response = client.patch( + f"/users/{random_user.id}", + json={ + "data": {"type": "user", "attributes": {"name": "New user name", "age": 24}} + }, + ) assert response.status_code == status.HTTP_204_NO_CONTENT def test_simple_create_user(client: TestClient): - response: Response = client.post("/users", json={"name": "New user name", "age": 42}) + response: Response = client.post( + "/users", + json={ + "data": {"type": "user", "attributes": {"name": "New user name", "age": 24}} + }, + ) assert response.status_code == status.HTTP_201_CREATED assert response.json() == { "data": { "type": "user", "id": response.json()["data"]["id"], - "attributes": {"name": "New user name", "age": 42}, + "attributes": {"name": "New user name", "age": 24}, } }