Skip to content

Commit c0f376c

Browse files
committed
Fix bugs
1 parent 04cc0f5 commit c0f376c

29 files changed

+1971
-2
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/.python-version
2+
/examples/api_for_tortoise_orm/db.sqlite3
3+
/examples/api_for_tortoise_orm/db.sqlite3-shm
4+
/examples/api_for_tortoise_orm/db.sqlite3-wal

fastapi_rest_jsonapi/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""JSON API utils package."""
2+
3+
from fastapi_rest_jsonapi.api import RoutersJSONAPI
4+
from fastapi_rest_jsonapi.exceptions import BadRequest
5+
from fastapi_rest_jsonapi.filter import json_api_filter
6+
from fastapi_rest_jsonapi.pagination import json_api_pagination
7+
from fastapi_rest_jsonapi.querystring import QueryStringManager
8+
from fastapi_rest_jsonapi.sorting import json_api_sort
9+
from fastapi_rest_jsonapi.splitter import prepare_field_name_for_filtering
10+
11+
__all__ = [
12+
"BadRequest",
13+
"json_api_filter",
14+
"json_api_pagination",
15+
"QueryStringManager",
16+
"json_api_filter",
17+
"prepare_field_name_for_filtering",
18+
"json_api_sort",
19+
"RoutersJSONAPI",
20+
]

fastapi_rest_jsonapi/api.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""JSON API router class."""
2+
3+
from typing import (
4+
Any,
5+
Dict,
6+
List,
7+
Optional,
8+
Type,
9+
Union,
10+
)
11+
12+
from fastapi import APIRouter
13+
from pydantic import BaseModel
14+
15+
from fastapi_rest_jsonapi.exceptions import ExceptionResponseSchema
16+
from fastapi_rest_jsonapi.methods import (
17+
delete_detail_jsonapi,
18+
get_detail_jsonapi,
19+
get_list_jsonapi,
20+
patch_detail_jsonapi,
21+
post_list_jsonapi,
22+
)
23+
24+
JSON_API_RESPONSE_TYPE = Optional[Dict[Union[int, str], Dict[str, Any]]]
25+
26+
27+
class RoutersJSONAPI(object):
28+
"""API Router interface for JSON API endpoints in web-services."""
29+
30+
def __init__( # noqa: WPS211
31+
self,
32+
routers: APIRouter,
33+
path: Union[str, List[str]],
34+
tags: List[str],
35+
class_detail: Any,
36+
class_list: Any,
37+
schema: Type[BaseModel],
38+
type_resource: str,
39+
schema_in_patch: Type[BaseModel],
40+
schema_in_post: Type[BaseModel],
41+
resp_schema_detail: Type[BaseModel],
42+
resp_schema_list: Type[BaseModel],
43+
) -> None:
44+
"""Initialize router items."""
45+
self._routers: APIRouter = routers
46+
self._path: Union[str, List[str]] = path
47+
self._tags: List[str] = tags
48+
self.class_detail: Any = class_detail
49+
self.class_list: Any = class_list
50+
self._type: str = type_resource
51+
self._schema: Type[BaseModel] = schema
52+
self._schema_in_patch: Type[BaseModel] = schema_in_patch
53+
self._schema_in_post: Type[BaseModel] = schema_in_post
54+
self._resp_schema_detail: Type[BaseModel] = resp_schema_detail
55+
self._resp_schema_list: Type[BaseModel] = resp_schema_list
56+
57+
if isinstance(self._path, list):
58+
for i_path in self._path:
59+
self._add_routers(i_path)
60+
else:
61+
self._add_routers(self._path)
62+
63+
def _add_routers(self, path: str):
64+
"""Add new router."""
65+
error_responses: Optional[JSON_API_RESPONSE_TYPE] = {
66+
400: {"model": ExceptionResponseSchema},
67+
401: {"model": ExceptionResponseSchema},
68+
404: {"model": ExceptionResponseSchema},
69+
500: {"model": ExceptionResponseSchema},
70+
}
71+
if hasattr(self.class_list, "get"):
72+
self._routers.get(path, tags=self._tags, response_model=self._resp_schema_list, responses=error_responses,)(
73+
get_list_jsonapi(schema=self._schema, type_=self._type, schema_resp=self._resp_schema_list)(
74+
self.class_list.get
75+
)
76+
)
77+
78+
if hasattr(self.class_list, "post"):
79+
self._routers.post(
80+
path,
81+
tags=self._tags,
82+
response_model=self._resp_schema_detail,
83+
responses=error_responses,
84+
)(
85+
post_list_jsonapi(
86+
schema=self._schema,
87+
schema_in=self._schema_in_post,
88+
type_=self._type,
89+
schema_resp=self._resp_schema_detail,
90+
)(self.class_list.post)
91+
)
92+
93+
if hasattr(self.class_detail, "get"):
94+
self._routers.get(
95+
path + "/{obj_id}",
96+
tags=self._tags,
97+
response_model=self._resp_schema_detail,
98+
responses=error_responses,
99+
)(
100+
get_detail_jsonapi(schema=self._schema, type_=self._type, schema_resp=self._resp_schema_detail)(
101+
self.class_detail.get
102+
)
103+
)
104+
105+
if hasattr(self.class_detail, "patch"):
106+
self._routers.patch(
107+
path + "/{obj_id}",
108+
tags=self._tags,
109+
response_model=self._resp_schema_detail,
110+
responses=error_responses,
111+
)(
112+
patch_detail_jsonapi(
113+
schema=self._schema,
114+
schema_in=self._schema_in_patch,
115+
type_=self._type,
116+
schema_resp=self._resp_schema_detail,
117+
)(self.class_detail.patch)
118+
)
119+
120+
if hasattr(self.class_detail, "delete"):
121+
self._routers.delete(
122+
path + "/{obj_id}",
123+
tags=self._tags,
124+
)(delete_detail_jsonapi(schema=self._schema)(self.class_detail.delete))
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Filters for Clickhouse."""
2+
3+
from typing import (
4+
Any,
5+
Dict,
6+
List,
7+
Type,
8+
)
9+
10+
from pydantic import (
11+
BaseModel,
12+
Field,
13+
)
14+
15+
from fastapi_rest_jsonapi.filter import json_api_filter_converter
16+
from fastapi_rest_jsonapi.querystring import QueryStringManager
17+
18+
19+
def prepare_filter_value_fo_ch(field: Type[Field], field_name: str, type_op: str, value: Any) -> Dict:
20+
"""Prepare filter to Clickhouse request."""
21+
return {
22+
"name": field_name,
23+
"value": value,
24+
"op": type_op,
25+
}
26+
27+
28+
async def json_api_filter_for_clickhouse(
29+
schema: Type[BaseModel],
30+
query_params: QueryStringManager,
31+
) -> List[Dict]:
32+
"""Convert filters for clickhouse."""
33+
filters = await json_api_filter_converter(
34+
schema=schema,
35+
filters=query_params.filters,
36+
conversion_func=prepare_filter_value_fo_ch,
37+
)
38+
return filters

fastapi_rest_jsonapi/data_layers/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Fields package."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Base enum module."""
2+
3+
from fastapi_rest_jsonapi.data_layers.fields.mixins import (
4+
MixinEnum,
5+
MixinIntEnum,
6+
)
7+
8+
9+
class Enum(MixinEnum):
10+
"""
11+
Base enum class.
12+
13+
All used non-integer enumerations must inherit from this class.
14+
"""
15+
16+
pass
17+
18+
19+
class IntEnum(MixinIntEnum):
20+
"""
21+
Base IntEnum class.
22+
23+
All used integer enumerations must inherit from this class.
24+
"""
25+
26+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Enum mixin module."""
2+
3+
from enum import (
4+
Enum,
5+
IntEnum,
6+
)
7+
8+
9+
class MixinEnum(Enum):
10+
"""Extension over enum class from standard library."""
11+
12+
@classmethod
13+
def names(cls):
14+
"""Get all field names."""
15+
return ",".join(field.name for field in cls)
16+
17+
@classmethod
18+
def values(cls):
19+
"""Get all values from Enum."""
20+
return [value for _, value in cls._member_map_.items()]
21+
22+
@classmethod
23+
def keys(cls):
24+
"""Get all field keys from Enum."""
25+
return [key for key, _ in cls._member_map_.items()]
26+
27+
@classmethod
28+
def inverse(cls):
29+
"""Return all inverted items sequence."""
30+
return {value: key for key, value in cls._member_map_.items()}
31+
32+
@classmethod
33+
def value_to_enum(cls, value):
34+
"""Convert value to enum."""
35+
val_to_enum = {value.value: value for _, value in cls._member_map_.items()}
36+
return val_to_enum.get(value)
37+
38+
39+
class MixinIntEnum(IntEnum):
40+
"""
41+
Здесь пришлось дублировать код, чтобы обеспечить совместимость с FastAPI и Pydantic.
42+
43+
Основная проблема - данные либы определяют валидаторы для стандартной библиотеки enum, используя вызов issubclass.
44+
И для стандартного IntEnum есть отдельная ветка issubclass(IntEnum), в которой происходят
45+
специальные преобразования, например, аргументы из запроса конвертируются в тип int.
46+
Поэтому OurEnum(int, Enum) не срабатывает по условию issubclass(obj, IntEnum) и выбираются
47+
неверные валидаторы и конверторы.
48+
А код ниже пришлось задублировать, так как у стандартного Enum есть метакласс, который разрешает только
49+
такую цепочку наследования:
50+
NewEnum(клас_тип, миксин_без_типа_1, ..., миксин_без_типа_n, Enum)
51+
По этому правилу нельзя построить наследование, добавляющее миксин без типа к стандартному IntEnum:
52+
NewEnum(our_mixin, IntEnum), так как IntEnum = (int, Enum)
53+
Поэтому пока остается такое решение до каких-либо исправлений со стороны разработчиков либы,
54+
либо появления более гениальных идей
55+
"""
56+
57+
@classmethod
58+
def names(cls):
59+
"""Get all field names."""
60+
return ",".join(field.name for field in cls)
61+
62+
@classmethod
63+
def values(cls):
64+
"""Get all values from Enum."""
65+
return [value for _, value in cls._member_map_.items()]
66+
67+
@classmethod
68+
def keys(cls):
69+
"""Get all field keys from Enum."""
70+
return [key for key, _ in cls._member_map_.items()]
71+
72+
@classmethod
73+
def inverse(cls):
74+
"""Return all inverted items sequence."""
75+
return {value: key for key, value in cls._member_map_.items()}
76+
77+
@classmethod
78+
def value_to_enum(cls, value):
79+
"""Convert value to enum."""
80+
val_to_enum = {value.value: value for _, value in cls._member_map_.items()}
81+
return val_to_enum.get(value)
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""ORM types enums."""
2+
3+
from fastapi_rest_jsonapi.data_layers.fields.enum import Enum
4+
5+
6+
class DBORMType(str, Enum):
7+
clickhouse = "clickhouse"
8+
tortoise = "tortoise"
9+
filter_event = "filter_event"
10+
11+
12+
class DBORMOperandType(str, Enum):
13+
or_ = "or"
14+
and_ = "and"
15+
not_ = "not"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Exceptions utils package. Contains exception schemas."""
2+
3+
from .base import (
4+
ExceptionResponseSchema,
5+
ExceptionSchema,
6+
ExceptionSourceSchema,
7+
QueryError,
8+
)
9+
from .json_api import (
10+
BadRequest,
11+
HTTPException,
12+
InvalidField,
13+
InvalidFilters,
14+
InvalidInclude,
15+
InvalidSort,
16+
)
17+
18+
__all__ = [
19+
"ExceptionResponseSchema",
20+
"ExceptionSchema",
21+
"ExceptionSourceSchema",
22+
"BadRequest",
23+
"InvalidField",
24+
"InvalidFilters",
25+
"InvalidInclude",
26+
"InvalidSort",
27+
"QueryError",
28+
"HTTPException",
29+
]
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Collection of useful http error for the Api."""
2+
3+
from typing import (
4+
Any,
5+
Dict,
6+
List,
7+
Optional,
8+
)
9+
10+
from pydantic import Field
11+
from pydantic.main import BaseModel
12+
13+
14+
class ExceptionSourceSchema(BaseModel):
15+
"""Source exception schema."""
16+
17+
parameter: Optional[str] = None
18+
pointer: Optional[str] = None
19+
20+
21+
class ExceptionSchema(BaseModel):
22+
"""Exception schema."""
23+
24+
status: str
25+
source: Optional[ExceptionSourceSchema] = None
26+
title: str
27+
detail: Any
28+
29+
30+
class ExceptionResponseSchema(BaseModel):
31+
"""Exception response schema."""
32+
33+
errors: List[ExceptionSchema]
34+
jsonapi: Dict[str, str] = Field(default={"version": "1.0"})
35+
36+
37+
class QueryError(Exception):
38+
"""Query build error."""

0 commit comments

Comments
 (0)