fastapi-extra 0.3.6__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/PKG-INFO +1 -1
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/__init__.py +1 -1
- fastapi_extra-0.5.0/fastapi_extra/_patch.py +172 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/service.py +17 -2
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/response.py +1 -3
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/PKG-INFO +1 -1
- fastapi_extra-0.3.6/fastapi_extra/_patch.py +0 -121
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/LICENSE +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/README.rst +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cache/__init__.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cache/redis.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cursor.pyi +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/__init__.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/model.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/session.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/sqlmap.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/dependency.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/form.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/cursor.pyx +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/routing.pyx +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/urlparse.pyx +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/py.typed +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/routing.pyi +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/settings.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/types.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/urlparse.pyi +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/utils.py +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/SOURCES.txt +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/dependency_links.txt +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/requires.txt +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/top_level.txt +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/pyproject.toml +0 -0
- {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/setup.cfg +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2026-01-13"
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, params
|
|
7
|
+
from fastapi._compat import (ModelField, get_cached_model_fields,
|
|
8
|
+
lenient_issubclass, shared)
|
|
9
|
+
from fastapi.dependencies import utils
|
|
10
|
+
from pydantic import BaseModel, Json
|
|
11
|
+
from pydantic.fields import FieldInfo
|
|
12
|
+
from starlette import datastructures
|
|
13
|
+
|
|
14
|
+
from fastapi_extra import routing
|
|
15
|
+
from fastapi_extra.urlparse import parse_qsl
|
|
16
|
+
|
|
17
|
+
_ID_MAP: dict[int, bool] = {}
|
|
18
|
+
_ALIAS_MAP: dict[int, str] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def install_routes(app: FastAPI) -> None:
|
|
22
|
+
routing.install(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_sequence_field(annotation: type[Any] | None) -> bool:
|
|
26
|
+
_id = id(annotation)
|
|
27
|
+
if _id not in _ID_MAP:
|
|
28
|
+
_ID_MAP[_id] = shared.field_annotation_is_sequence(annotation)
|
|
29
|
+
return _ID_MAP[_id]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_json_field(field_info: FieldInfo):
|
|
33
|
+
_id = id(field_info)
|
|
34
|
+
if _id not in _ID_MAP:
|
|
35
|
+
_ID_MAP[_id] = any(type(item) is Json for item in field_info.metadata)
|
|
36
|
+
return _ID_MAP[_id]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_multidict_value(
|
|
40
|
+
field: ModelField,
|
|
41
|
+
values: Mapping[str, Any],
|
|
42
|
+
alias: str,
|
|
43
|
+
is_json: bool,
|
|
44
|
+
is_sequence: bool
|
|
45
|
+
) -> Any:
|
|
46
|
+
if (not is_json) and is_sequence and hasattr(values, "getlist"):
|
|
47
|
+
value = values.getlist(alias) # pyright: ignore[reportAttributeAccessIssue]
|
|
48
|
+
else:
|
|
49
|
+
value = values.get(alias, None)
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
(
|
|
53
|
+
value == ""
|
|
54
|
+
and isinstance(field.field_info, params.Form)
|
|
55
|
+
)
|
|
56
|
+
or (
|
|
57
|
+
is_sequence
|
|
58
|
+
and len(value) == 0
|
|
59
|
+
)
|
|
60
|
+
):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def request_params_to_args(
|
|
67
|
+
fields: Sequence[ModelField],
|
|
68
|
+
received_params: Mapping[str, Any] | datastructures.QueryParams | datastructures.Headers,
|
|
69
|
+
) -> tuple[dict[str, Any], list[Any]]:
|
|
70
|
+
values: dict[str, Any] = {}
|
|
71
|
+
errors: list[dict[str, Any]] = []
|
|
72
|
+
|
|
73
|
+
if not fields:
|
|
74
|
+
return values, errors
|
|
75
|
+
|
|
76
|
+
first_field = fields[0]
|
|
77
|
+
|
|
78
|
+
is_multidict = hasattr(received_params, "getlist")
|
|
79
|
+
is_headers = isinstance(received_params, datastructures.Headers)
|
|
80
|
+
|
|
81
|
+
# =========================================================================
|
|
82
|
+
# 分支 A:单模型解析 (例如使用 Pydantic 接收整个 Query 或是 Header 结构)
|
|
83
|
+
# =========================================================================
|
|
84
|
+
if len(fields) == 1 and lenient_issubclass(
|
|
85
|
+
first_field.field_info.annotation, BaseModel
|
|
86
|
+
):
|
|
87
|
+
fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)
|
|
88
|
+
default_convert_underscores = getattr(
|
|
89
|
+
first_field.field_info, "convert_underscores", True
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
params_to_process: dict[str, Any] = {}
|
|
93
|
+
processed_keys = set()
|
|
94
|
+
|
|
95
|
+
for field in fields_to_extract:
|
|
96
|
+
field_alias = utils.get_validation_alias(field)
|
|
97
|
+
alias = field_alias
|
|
98
|
+
|
|
99
|
+
if is_headers:
|
|
100
|
+
convert_underscores = getattr(
|
|
101
|
+
field.field_info, "convert_underscores", default_convert_underscores
|
|
102
|
+
)
|
|
103
|
+
if convert_underscores and alias == field.name:
|
|
104
|
+
alias = alias.replace("_", "-")
|
|
105
|
+
|
|
106
|
+
# 提前准备属性判断
|
|
107
|
+
is_json = is_json_field(field.field_info)
|
|
108
|
+
is_sequence = is_sequence_field(field.field_info.annotation)
|
|
109
|
+
|
|
110
|
+
value = _get_multidict_value(
|
|
111
|
+
field, received_params, alias=alias, is_json=is_json, is_sequence=is_sequence
|
|
112
|
+
)
|
|
113
|
+
if value is not None:
|
|
114
|
+
params_to_process[field_alias] = value
|
|
115
|
+
processed_keys.add(alias)
|
|
116
|
+
|
|
117
|
+
# 补全未在模型中显式定义的其他传入参数
|
|
118
|
+
for key in received_params.keys():
|
|
119
|
+
if key not in processed_keys:
|
|
120
|
+
if is_multidict:
|
|
121
|
+
value = received_params.getlist(key) # type: ignore
|
|
122
|
+
params_to_process[key] = value[0] if isinstance(value, list) and len(value) == 1 else value
|
|
123
|
+
else:
|
|
124
|
+
params_to_process[key] = received_params.get(key)
|
|
125
|
+
|
|
126
|
+
field_info = first_field.field_info
|
|
127
|
+
assert isinstance(field_info, params.Param), "Params must be subclasses of Param"
|
|
128
|
+
|
|
129
|
+
v_, errors_ = first_field.validate(params_to_process, values, loc=(field_info.in_.value,))
|
|
130
|
+
return {first_field.name: v_}, errors_
|
|
131
|
+
|
|
132
|
+
# =========================================================================
|
|
133
|
+
# 分支 B:多参数平铺解析 (Fast Path - 大部分常规路由走这里)
|
|
134
|
+
# =========================================================================
|
|
135
|
+
else:
|
|
136
|
+
for field in fields:
|
|
137
|
+
field_alias = utils.get_validation_alias(field)
|
|
138
|
+
field_info = field.field_info
|
|
139
|
+
|
|
140
|
+
# 属性判断预提取与缓存
|
|
141
|
+
is_json = is_json_field(field_info)
|
|
142
|
+
is_sequence = is_sequence_field(field_info.annotation)
|
|
143
|
+
|
|
144
|
+
value = _get_multidict_value(
|
|
145
|
+
field, received_params, alias=field_alias, is_json=is_json, is_sequence=is_sequence
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert isinstance(field_info, params.Param), "Params must be subclasses of Param"
|
|
149
|
+
loc = (field_info.in_.value, field_alias)
|
|
150
|
+
|
|
151
|
+
v_, errors_ = utils._validate_value_with_model_field(
|
|
152
|
+
field=field, value=value, values=values, loc=loc
|
|
153
|
+
)
|
|
154
|
+
if errors_:
|
|
155
|
+
errors.extend(errors_)
|
|
156
|
+
else:
|
|
157
|
+
values[field.name] = v_
|
|
158
|
+
|
|
159
|
+
return values, errors
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def query_params_init(obj: datastructures.QueryParams, *args, **kwargs) -> None:
|
|
163
|
+
value = args[0] if args else []
|
|
164
|
+
|
|
165
|
+
if isinstance(value, bytes):
|
|
166
|
+
super(datastructures.QueryParams, obj).__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
|
|
167
|
+
elif isinstance(value, str):
|
|
168
|
+
super(datastructures.QueryParams, obj).__init__(parse_qsl(value.encode("latin-1"), keep_blank_values=True), **kwargs)
|
|
169
|
+
else:
|
|
170
|
+
super(datastructures.QueryParams, obj).__init__(*args, **kwargs) # type: ignore[arg-type]
|
|
171
|
+
obj._list = [(str(k), str(v)) for k, v in obj._list]
|
|
172
|
+
obj._dict = {str(k): str(v) for k, v in obj._dict.items()}
|
|
@@ -2,13 +2,18 @@ __author__ = "ziyan.yin"
|
|
|
2
2
|
__date__ = "2025-01-12"
|
|
3
3
|
|
|
4
4
|
from contextvars import ContextVar
|
|
5
|
-
from typing import Any, Generic, TypeVar
|
|
5
|
+
from typing import Any, Generic, Sequence, TypeVar
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ColumnExpressionArgument
|
|
8
|
+
from sqlmodel import select
|
|
6
9
|
|
|
7
10
|
from fastapi_extra.database.model import SQLModel
|
|
8
11
|
from fastapi_extra.database.session import AsyncSession, DefaultSession
|
|
9
12
|
from fastapi_extra.dependency import AbstractService
|
|
10
13
|
|
|
11
14
|
Model = TypeVar("Model", bound=SQLModel)
|
|
15
|
+
ID = int | str
|
|
16
|
+
PK = ID | tuple[ID] | dict[str, ID]
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
class ModelService(AbstractService, Generic[Model], abstract=True):
|
|
@@ -40,14 +45,24 @@ class ModelService(AbstractService, Generic[Model], abstract=True):
|
|
|
40
45
|
assert _session is not None, "Session is not initialized"
|
|
41
46
|
return _session
|
|
42
47
|
|
|
43
|
-
async def get(self, ident:
|
|
48
|
+
async def get(self, ident: PK, **kwargs: Any) -> Model | None:
|
|
44
49
|
return await self.session.get(self.__model__, ident, **kwargs)
|
|
50
|
+
|
|
51
|
+
async def get_list(self, *clause: ColumnExpressionArgument[bool] | bool) -> Sequence[Model]:
|
|
52
|
+
return (await self.session.exec(select(self.__model__).where(*clause))).all()
|
|
45
53
|
|
|
46
54
|
async def create_model(self, **kwargs: Any) -> Model:
|
|
47
55
|
model = self.__model__.model_validate(kwargs)
|
|
48
56
|
self.session.add(model)
|
|
49
57
|
await self.session.flush()
|
|
50
58
|
return model
|
|
59
|
+
|
|
60
|
+
async def update_model(self, model: Model, **kwargs: Any) -> Model:
|
|
61
|
+
for key, value in kwargs.items():
|
|
62
|
+
if key in model.__fields__:
|
|
63
|
+
setattr(model, key, value)
|
|
64
|
+
await self.session.flush()
|
|
65
|
+
return model
|
|
51
66
|
|
|
52
67
|
async def delete(self, model: Model) -> None:
|
|
53
68
|
return await self.session.delete(model)
|
|
@@ -223,7 +223,7 @@ class APIResponse(JSONResponse):
|
|
|
223
223
|
|
|
224
224
|
def init_headers(self, headers: Mapping[str, str] | None = None) -> None:
|
|
225
225
|
self.raw_headers = [
|
|
226
|
-
(b"content-length",
|
|
226
|
+
(b"content-length", f"{len(self.body)}".encode("latin-1")),
|
|
227
227
|
(b"content-type", b"application/json; charset=utf-8"),
|
|
228
228
|
]
|
|
229
229
|
if headers:
|
|
@@ -234,8 +234,6 @@ class APIResponse(JSONResponse):
|
|
|
234
234
|
self.raw_headers.extend(raw_headers)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
237
|
class APIError(Exception):
|
|
240
238
|
__slots__ = ("code", "message")
|
|
241
239
|
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
__author__ = "ziyan.yin"
|
|
2
|
-
__date__ = "2026-01-13"
|
|
3
|
-
|
|
4
|
-
import functools
|
|
5
|
-
from typing import Any, Mapping, Sequence, Union
|
|
6
|
-
|
|
7
|
-
from fastapi import FastAPI, params
|
|
8
|
-
from fastapi._compat import (ModelField, get_cached_model_fields,
|
|
9
|
-
lenient_issubclass, shared)
|
|
10
|
-
from fastapi.dependencies import utils
|
|
11
|
-
from pydantic import BaseModel
|
|
12
|
-
from starlette import datastructures
|
|
13
|
-
|
|
14
|
-
from fastapi_extra import routing
|
|
15
|
-
from fastapi_extra.urlparse import parse_qsl
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def install_routes(app: FastAPI) -> None:
|
|
19
|
-
routing.install(app)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@functools.lru_cache
|
|
23
|
-
def is_sequence_field(annotation: type[Any]) -> bool:
|
|
24
|
-
return shared.field_annotation_is_sequence(annotation)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def request_params_to_args(
|
|
28
|
-
fields: Sequence[ModelField],
|
|
29
|
-
received_params: Union[Mapping[str, Any], datastructures.QueryParams, datastructures.Headers],
|
|
30
|
-
) -> tuple[dict[str, Any], list[Any]]:
|
|
31
|
-
values: dict[str, Any] = {}
|
|
32
|
-
errors: list[dict[str, Any]] = []
|
|
33
|
-
|
|
34
|
-
if not fields:
|
|
35
|
-
return values, errors
|
|
36
|
-
|
|
37
|
-
first_field = fields[0]
|
|
38
|
-
fields_to_extract = fields
|
|
39
|
-
single_not_embedded_field = False
|
|
40
|
-
default_convert_underscores = True
|
|
41
|
-
if len(fields) == 1 and lenient_issubclass(
|
|
42
|
-
first_field.field_info.annotation, BaseModel
|
|
43
|
-
):
|
|
44
|
-
fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)
|
|
45
|
-
single_not_embedded_field = True
|
|
46
|
-
# If headers are in a Pydantic model, the way to disable convert_underscores
|
|
47
|
-
# would be with Header(convert_underscores=False) at the Pydantic model level
|
|
48
|
-
default_convert_underscores = getattr(
|
|
49
|
-
first_field.field_info, "convert_underscores", True
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
params_to_process: dict[str, Any] = {}
|
|
53
|
-
|
|
54
|
-
processed_keys = set()
|
|
55
|
-
|
|
56
|
-
for field in fields_to_extract:
|
|
57
|
-
alias = field_alias = utils.get_validation_alias(field)
|
|
58
|
-
if isinstance(received_params, datastructures.Headers):
|
|
59
|
-
# Handle fields extracted from a Pydantic Model for a header, each field
|
|
60
|
-
# doesn't have a FieldInfo of type Header with the default convert_underscores=True
|
|
61
|
-
convert_underscores = getattr(
|
|
62
|
-
field.field_info, "convert_underscores", default_convert_underscores
|
|
63
|
-
)
|
|
64
|
-
if convert_underscores and alias == field.name:
|
|
65
|
-
alias = alias.replace("_", "-")
|
|
66
|
-
value = utils._get_multidict_value(field, received_params, alias=alias)
|
|
67
|
-
if value is not None:
|
|
68
|
-
params_to_process[field_alias] = value
|
|
69
|
-
processed_keys.add(alias)
|
|
70
|
-
|
|
71
|
-
for key in received_params.keys():
|
|
72
|
-
if key not in processed_keys:
|
|
73
|
-
if hasattr(received_params, "getlist"):
|
|
74
|
-
value = received_params.getlist(key) # type: ignore
|
|
75
|
-
if isinstance(value, list) and (len(value) == 1):
|
|
76
|
-
params_to_process[key] = value[0]
|
|
77
|
-
else:
|
|
78
|
-
params_to_process[key] = value
|
|
79
|
-
else:
|
|
80
|
-
params_to_process[key] = received_params.get(key)
|
|
81
|
-
|
|
82
|
-
if single_not_embedded_field:
|
|
83
|
-
field_info = first_field.field_info
|
|
84
|
-
assert isinstance(field_info, params.Param), (
|
|
85
|
-
"Params must be subclasses of Param"
|
|
86
|
-
)
|
|
87
|
-
loc: tuple[str, ...] = (field_info.in_.value,)
|
|
88
|
-
v_, errors_ = utils._validate_value_with_model_field(
|
|
89
|
-
field=first_field, value=params_to_process, values=values, loc=loc
|
|
90
|
-
)
|
|
91
|
-
return {first_field.name: v_}, errors_
|
|
92
|
-
|
|
93
|
-
for field in fields:
|
|
94
|
-
field_alias = utils.get_validation_alias(field)
|
|
95
|
-
value = params_to_process.get(field_alias, None)
|
|
96
|
-
field_info = field.field_info
|
|
97
|
-
assert isinstance(field_info, params.Param), (
|
|
98
|
-
"Params must be subclasses of Param"
|
|
99
|
-
)
|
|
100
|
-
loc = (field_info.in_.value, field_alias)
|
|
101
|
-
v_, errors_ = utils._validate_value_with_model_field(
|
|
102
|
-
field=field, value=value, values=values, loc=loc
|
|
103
|
-
)
|
|
104
|
-
if errors_:
|
|
105
|
-
errors.extend(errors_)
|
|
106
|
-
else:
|
|
107
|
-
values[field.name] = v_
|
|
108
|
-
return values, errors
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def query_params_init(obj: datastructures.QueryParams, *args, **kwargs) -> None:
|
|
112
|
-
value = args[0] if args else []
|
|
113
|
-
|
|
114
|
-
if isinstance(value, bytes):
|
|
115
|
-
super(datastructures.QueryParams, obj).__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
|
|
116
|
-
elif isinstance(value, str):
|
|
117
|
-
super(datastructures.QueryParams, obj).__init__(parse_qsl(value.encode("latin-1"), keep_blank_values=True), **kwargs)
|
|
118
|
-
else:
|
|
119
|
-
super(datastructures.QueryParams, obj).__init__(*args, **kwargs) # type: ignore[arg-type]
|
|
120
|
-
obj._list = [(str(k), str(v)) for k, v in obj._list]
|
|
121
|
-
obj._dict = {str(k): str(v) for k, v in obj._dict.items()}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|