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.
Files changed (33) hide show
  1. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/PKG-INFO +1 -1
  2. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/__init__.py +1 -1
  3. fastapi_extra-0.5.0/fastapi_extra/_patch.py +172 -0
  4. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/service.py +17 -2
  5. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/response.py +1 -3
  6. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/PKG-INFO +1 -1
  7. fastapi_extra-0.3.6/fastapi_extra/_patch.py +0 -121
  8. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/LICENSE +0 -0
  9. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/README.rst +0 -0
  10. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cache/__init__.py +0 -0
  11. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cache/redis.py +0 -0
  12. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/cursor.pyi +0 -0
  13. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/__init__.py +0 -0
  14. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/model.py +0 -0
  15. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/session.py +0 -0
  16. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/database/sqlmap.py +0 -0
  17. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/dependency.py +0 -0
  18. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/form.py +0 -0
  19. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/cursor.pyx +0 -0
  20. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/routing.pyx +0 -0
  21. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/native/urlparse.pyx +0 -0
  22. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/py.typed +0 -0
  23. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/routing.pyi +0 -0
  24. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/settings.py +0 -0
  25. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/types.py +0 -0
  26. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/urlparse.pyi +0 -0
  27. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra/utils.py +0 -0
  28. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/SOURCES.txt +0 -0
  29. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/dependency_links.txt +0 -0
  30. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/requires.txt +0 -0
  31. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/fastapi_extra.egg-info/top_level.txt +0 -0
  32. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/pyproject.toml +0 -0
  33. {fastapi_extra-0.3.6 → fastapi_extra-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-extra
3
- Version: 0.3.6
3
+ Version: 0.5.0
4
4
  Summary: extra package for fastapi.
5
5
  Author-email: Ziyan Yin <408856732@qq.com>
6
6
  License: BSD-3-Clause
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.6"
1
+ __version__ = "0.5.0"
2
2
 
3
3
 
4
4
  from fastapi import FastAPI
@@ -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: int | str, **kwargs: Any) -> Model | None:
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", str(len(self.body)).encode("latin-1")),
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-extra
3
- Version: 0.3.6
3
+ Version: 0.5.0
4
4
  Summary: extra package for fastapi.
5
5
  Author-email: Ziyan Yin <408856732@qq.com>
6
6
  License: BSD-3-Clause
@@ -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