fastapi-extra 0.2.4__tar.gz → 0.3.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.2.4 → fastapi_extra-0.3.0}/PKG-INFO +3 -3
- fastapi_extra-0.3.0/fastapi_extra/__init__.py +17 -0
- fastapi_extra-0.3.0/fastapi_extra/_patch.py +33 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/cache/__init__.py +1 -3
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/cache/redis.py +9 -8
- fastapi_extra-0.3.0/fastapi_extra/cursor.pyi +8 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/database/__init__.py +1 -6
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/database/model.py +19 -10
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/database/service.py +14 -11
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/database/session.py +18 -13
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/dependency.py +7 -9
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/form.py +6 -4
- fastapi_extra-0.3.0/fastapi_extra/native/urlparse.pyx +50 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/response.py +29 -8
- fastapi_extra-0.3.0/fastapi_extra/routing.pyi +11 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/settings.py +7 -7
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/types.py +6 -5
- fastapi_extra-0.3.0/fastapi_extra/urlparse.pyi +11 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra.egg-info/PKG-INFO +3 -3
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra.egg-info/SOURCES.txt +5 -1
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra.egg-info/requires.txt +2 -2
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/pyproject.toml +4 -3
- fastapi_extra-0.2.4/fastapi_extra/__init__.py +0 -13
- fastapi_extra-0.2.4/fastapi_extra/cursor.pyi +0 -8
- fastapi_extra-0.2.4/fastapi_extra/routing.pyi +0 -16
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/LICENSE +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/README.rst +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/native/cursor.pyx +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/native/routing.pyx +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/py.typed +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra/utils.py +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra.egg-info/dependency_links.txt +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/fastapi_extra.egg-info/top_level.txt +0 -0
- {fastapi_extra-0.2.4 → fastapi_extra-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-extra
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: extra package for fastapi.
|
|
5
5
|
Author-email: Ziyan Yin <408856732@qq.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -18,9 +18,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/x-rst
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: fastapi<0.
|
|
21
|
+
Requires-Dist: fastapi<0.129.0,>=0.128.0
|
|
22
22
|
Requires-Dist: httpx<0.29.0,>=0.28.0
|
|
23
|
-
Requires-Dist: pydantic-settings>=2.
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
24
24
|
Requires-Dist: sqlmodel>=0.0.22
|
|
25
25
|
Provides-Extra: redis
|
|
26
26
|
Requires-Dist: redis; extra == "redis"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi import routing as origin_routing
|
|
6
|
+
from fastapi.dependencies import utils as origin_utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup(app: FastAPI) -> None:
|
|
10
|
+
try:
|
|
11
|
+
from fastapi_extra import _patch
|
|
12
|
+
|
|
13
|
+
_patch.install_routes(app)
|
|
14
|
+
origin_routing.solve_dependencies.__globals__['is_sequence_field'] = _patch.is_sequence_field # type: ignore
|
|
15
|
+
origin_utils.QueryParams.__init__ = _patch.query_params_init # type: ignore
|
|
16
|
+
except ImportError: # pragma: nocover
|
|
17
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2026-01-13"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi._compat import v2
|
|
7
|
+
from starlette import datastructures
|
|
8
|
+
|
|
9
|
+
from fastapi_extra import routing
|
|
10
|
+
from fastapi_extra.urlparse import parse_qsl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def install_routes(app: FastAPI) -> None:
|
|
14
|
+
routing.install(app)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_sequence_field(field: v2.ModelField) -> bool:
|
|
18
|
+
if not hasattr(field, "_is_sequence"):
|
|
19
|
+
setattr(field, "_is_sequence", v2.is_sequence_field(field))
|
|
20
|
+
return getattr(field, "_is_sequence")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def query_params_init(obj: datastructures.QueryParams, *args, **kwargs) -> None:
|
|
24
|
+
value = args[0] if args else []
|
|
25
|
+
|
|
26
|
+
if isinstance(value, bytes):
|
|
27
|
+
super(datastructures.QueryParams, obj).__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
|
|
28
|
+
elif isinstance(value, str):
|
|
29
|
+
super(datastructures.QueryParams, obj).__init__(parse_qsl(value.encode("latin-1"), keep_blank_values=True), **kwargs)
|
|
30
|
+
else:
|
|
31
|
+
super(datastructures.QueryParams, obj).__init__(*args, **kwargs) # type: ignore[arg-type]
|
|
32
|
+
obj._list = [(str(k), str(v)) for k, v in obj._list]
|
|
33
|
+
obj._dict = {str(k): str(v) for k, v in obj._dict.items()}
|
|
@@ -30,18 +30,20 @@ class RedisPool(AbstractComponent):
|
|
|
30
30
|
|
|
31
31
|
def __init__(self):
|
|
32
32
|
self._pool: ConnectionPool | None = None
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
@classmethod
|
|
35
35
|
def setup(cls, **options) -> Self:
|
|
36
36
|
redis = cls()
|
|
37
37
|
redis._pool = ConnectionPool.from_url(
|
|
38
|
-
cls.default_config.url,
|
|
39
|
-
**cls.default_config.model_dump(
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
str(cls.default_config.url),
|
|
39
|
+
**cls.default_config.model_dump(
|
|
40
|
+
exclude_defaults=True, exclude={"url", "connection_kwargs"}
|
|
41
|
+
),
|
|
42
|
+
**cls.default_config.connection_kwargs,
|
|
43
|
+
**options,
|
|
42
44
|
)
|
|
43
45
|
return redis
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
def get_client(self) -> Redis:
|
|
46
48
|
return Redis(connection_pool=self._pool)
|
|
47
49
|
|
|
@@ -50,9 +52,8 @@ class RedisPool(AbstractComponent):
|
|
|
50
52
|
await self._pool.aclose()
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
|
|
54
55
|
async def get_redis(pool: RedisPool) -> AsyncGenerator[Redis, None]:
|
|
55
|
-
async with
|
|
56
|
+
async with pool.get_client() as client:
|
|
56
57
|
yield client
|
|
57
58
|
|
|
58
59
|
|
|
@@ -7,9 +7,4 @@ from fastapi_extra.database.service import ModelService
|
|
|
7
7
|
from fastapi_extra.database.session import DefaultSession as Session
|
|
8
8
|
from fastapi_extra.database.session import SessionFactory
|
|
9
9
|
|
|
10
|
-
__all__ = [
|
|
11
|
-
"SessionFactory",
|
|
12
|
-
"Session",
|
|
13
|
-
"SQLBase",
|
|
14
|
-
"ModelService"
|
|
15
|
-
]
|
|
10
|
+
__all__ = ["SessionFactory", "Session", "SQLBase", "ModelService"]
|
|
@@ -15,8 +15,8 @@ from fastapi_extra.utils import get_machine_seed
|
|
|
15
15
|
|
|
16
16
|
class AutoPK(SQLModel):
|
|
17
17
|
id: int | None = Field(
|
|
18
|
-
default=None,
|
|
19
|
-
title="ID",
|
|
18
|
+
default=None,
|
|
19
|
+
title="ID",
|
|
20
20
|
primary_key=True,
|
|
21
21
|
sa_type=BigInteger,
|
|
22
22
|
sa_column_kwargs={"autoincrement": True},
|
|
@@ -26,9 +26,9 @@ class AutoPK(SQLModel):
|
|
|
26
26
|
|
|
27
27
|
class LocalPK(SQLModel):
|
|
28
28
|
id: Cursor | None = Field(
|
|
29
|
-
default_factory=_Cursor(get_machine_seed()).next_val,
|
|
30
|
-
title="ID",
|
|
31
|
-
primary_key=True,
|
|
29
|
+
default_factory=_Cursor(get_machine_seed()).next_val,
|
|
30
|
+
title="ID",
|
|
31
|
+
primary_key=True,
|
|
32
32
|
sa_type=BigInteger,
|
|
33
33
|
sa_column_kwargs={"autoincrement": False},
|
|
34
34
|
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
@@ -53,7 +53,7 @@ class Versioned(SQLModel):
|
|
|
53
53
|
sa_column_kwargs={"nullable": False, "comment": "VERSION_ID"},
|
|
54
54
|
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
55
55
|
)
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
@declared_attr # type: ignore
|
|
58
58
|
def __mapper_args__(cls) -> dict:
|
|
59
59
|
return {"version_id_col": cls.version_id}
|
|
@@ -61,17 +61,26 @@ class Versioned(SQLModel):
|
|
|
61
61
|
|
|
62
62
|
class Optime(SQLModel):
|
|
63
63
|
create_at: LocalDateTime = Field(
|
|
64
|
-
default_factory=datetime.datetime.now,
|
|
64
|
+
default_factory=datetime.datetime.now,
|
|
65
65
|
title="CREATE_AT",
|
|
66
66
|
sa_type=DateTime,
|
|
67
|
-
sa_column_kwargs={
|
|
67
|
+
sa_column_kwargs={
|
|
68
|
+
"default": func.now(),
|
|
69
|
+
"nullable": False,
|
|
70
|
+
"comment": "CREATE_AT",
|
|
71
|
+
},
|
|
68
72
|
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
69
73
|
)
|
|
70
74
|
update_at: LocalDateTime = Field(
|
|
71
|
-
default_factory=datetime.datetime.now,
|
|
75
|
+
default_factory=datetime.datetime.now,
|
|
72
76
|
title="UPDATE_AT",
|
|
73
77
|
sa_type=DateTime,
|
|
74
|
-
sa_column_kwargs={
|
|
78
|
+
sa_column_kwargs={
|
|
79
|
+
"default": func.now(),
|
|
80
|
+
"onupdate": func.now(),
|
|
81
|
+
"nullable": False,
|
|
82
|
+
"comment": "UPDATE_AT",
|
|
83
|
+
},
|
|
75
84
|
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
76
85
|
)
|
|
77
86
|
|
|
@@ -2,7 +2,7 @@ __author__ = "ziyan.yin"
|
|
|
2
2
|
__date__ = "2025-01-12"
|
|
3
3
|
|
|
4
4
|
from contextvars import ContextVar
|
|
5
|
-
from typing import Any, Generic,
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
6
|
|
|
7
7
|
from fastapi_extra.database.model import SQLModel
|
|
8
8
|
from fastapi_extra.database.session import AsyncSession, DefaultSession
|
|
@@ -13,24 +13,27 @@ Model = TypeVar("Model", bound=SQLModel)
|
|
|
13
13
|
|
|
14
14
|
class ModelService(AbstractService, Generic[Model], abstract=True):
|
|
15
15
|
__slot__ = ()
|
|
16
|
-
__model__: Model
|
|
17
|
-
__session_container__ = ContextVar("__session_container__", default=None)
|
|
18
|
-
|
|
16
|
+
__model__: type[Model]
|
|
17
|
+
__session_container__ = ContextVar[AsyncSession | None]("__session_container__", default=None)
|
|
18
|
+
|
|
19
19
|
@classmethod
|
|
20
|
-
def __class_getitem__(cls, item: type[SQLModel]) ->
|
|
20
|
+
def __class_getitem__(cls, item: type[SQLModel]) -> type["ModelService"]:
|
|
21
21
|
if not issubclass(item, SQLModel):
|
|
22
22
|
raise TypeError(f"type[SQLModel] expected, got {item}")
|
|
23
23
|
if not (table_arg := item.model_config.get("table", None)):
|
|
24
|
-
raise AttributeError(
|
|
25
|
-
|
|
24
|
+
raise AttributeError(
|
|
25
|
+
f"True expected for argument {item.__name__}.model_config.table, got {table_arg}"
|
|
26
|
+
)
|
|
27
|
+
|
|
26
28
|
class SubService(ModelService):
|
|
29
|
+
__slot__ = ()
|
|
27
30
|
__model__ = item
|
|
28
|
-
|
|
31
|
+
|
|
29
32
|
return SubService
|
|
30
33
|
|
|
31
34
|
def __init__(self, session: DefaultSession):
|
|
32
35
|
self.__session_container__.set(session)
|
|
33
|
-
|
|
36
|
+
|
|
34
37
|
@property
|
|
35
38
|
def session(self) -> AsyncSession:
|
|
36
39
|
_session = self.__session_container__.get()
|
|
@@ -45,6 +48,6 @@ class ModelService(AbstractService, Generic[Model], abstract=True):
|
|
|
45
48
|
self.session.add(model)
|
|
46
49
|
await self.session.flush()
|
|
47
50
|
return model
|
|
48
|
-
|
|
49
|
-
async def delete(self, model: Model) ->
|
|
51
|
+
|
|
52
|
+
async def delete(self, model: Model) -> None:
|
|
50
53
|
return await self.session.delete(model)
|
|
@@ -5,7 +5,7 @@ from typing import Annotated, AsyncGenerator, Literal, Self
|
|
|
5
5
|
|
|
6
6
|
from fastapi.params import Depends
|
|
7
7
|
from pydantic import AnyUrl, BaseModel, Field
|
|
8
|
-
from sqlalchemy.ext.asyncio import create_async_engine
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
9
9
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
10
10
|
|
|
11
11
|
from fastapi_extra.dependency import AbstractComponent
|
|
@@ -16,13 +16,16 @@ class DatabaseConfig(BaseModel):
|
|
|
16
16
|
url: AnyUrl
|
|
17
17
|
echo: bool = False
|
|
18
18
|
echo_pool: bool = False
|
|
19
|
-
isolation_level:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
isolation_level: (
|
|
20
|
+
Literal[
|
|
21
|
+
"SERIALIZABLE",
|
|
22
|
+
"REPEATABLE READ",
|
|
23
|
+
"READ COMMITTED",
|
|
24
|
+
"READ UNCOMMITTED",
|
|
25
|
+
"AUTOCOMMIT",
|
|
26
|
+
]
|
|
27
|
+
| None
|
|
28
|
+
) = None
|
|
26
29
|
options: dict = Field(default_factory=dict)
|
|
27
30
|
|
|
28
31
|
|
|
@@ -36,18 +39,20 @@ _settings = DefaultDatabaseSettings() # type: ignore
|
|
|
36
39
|
class SessionFactory(AbstractComponent):
|
|
37
40
|
__slot__ = ("_engine",)
|
|
38
41
|
default_config = _settings.datasource
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
def __init__(self):
|
|
41
|
-
self._engine = None
|
|
42
|
-
|
|
44
|
+
self._engine: AsyncEngine | None = None
|
|
45
|
+
|
|
43
46
|
@classmethod
|
|
44
47
|
def setup(cls, **options) -> Self:
|
|
45
48
|
db = cls()
|
|
46
49
|
db._engine = create_async_engine(
|
|
47
50
|
url=str(cls.default_config.url),
|
|
48
|
-
**cls.default_config.model_dump(
|
|
51
|
+
**cls.default_config.model_dump(
|
|
52
|
+
exclude_defaults=True, exclude={"url", "options"}
|
|
53
|
+
),
|
|
49
54
|
**cls.default_config.options,
|
|
50
|
-
**options
|
|
55
|
+
**options,
|
|
51
56
|
)
|
|
52
57
|
return db
|
|
53
58
|
|
|
@@ -9,23 +9,19 @@ from fastapi import Depends, FastAPI, Request
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def async_wrapper(func: Callable):
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
async def func_wrapper(*args, **kwds):
|
|
14
14
|
return func(*args, **kwds)
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
return update_wrapper(func_wrapper, func)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class DependencyMetaClass(ABCMeta):
|
|
20
20
|
__load__ = None
|
|
21
21
|
__token__ = None
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
def __new__(
|
|
24
|
-
mcs,
|
|
25
|
-
name: str,
|
|
26
|
-
bases: tuple[type, ...],
|
|
27
|
-
attrs: dict,
|
|
28
|
-
abstract: bool = False
|
|
24
|
+
mcs, name: str, bases: tuple[type, ...], attrs: dict, abstract: bool = False
|
|
29
25
|
):
|
|
30
26
|
new_cls = super().__new__(mcs, name, bases, attrs)
|
|
31
27
|
new_cls.__token__ = f"{new_cls.__module__}.{new_cls.__name__}"
|
|
@@ -54,7 +50,9 @@ class AbstractComponent(metaclass=DependencyMetaClass, abstract=True):
|
|
|
54
50
|
@final
|
|
55
51
|
@classmethod
|
|
56
52
|
async def __load__(cls, request: Request) -> Self:
|
|
57
|
-
assert hasattr(
|
|
53
|
+
assert hasattr(
|
|
54
|
+
request.app.state, cls.__token__
|
|
55
|
+
), f"{cls.__name__} must be installed in lifespan"
|
|
58
56
|
return getattr(request.app.state, cls.__token__)
|
|
59
57
|
|
|
60
58
|
async def dispose(self) -> None:
|
|
@@ -16,10 +16,12 @@ class DataRange(BaseModel, Generic[C]):
|
|
|
16
16
|
|
|
17
17
|
class ColumnExpression(BaseModel, Generic[S]):
|
|
18
18
|
column_name: str = Field(title="列名")
|
|
19
|
-
option: Literal["eq", "ne", "gt", "lt", "ge", "le"] = Field(
|
|
19
|
+
option: Literal["eq", "ne", "gt", "lt", "ge", "le"] = Field(
|
|
20
|
+
default="eq", title="逻辑值"
|
|
21
|
+
)
|
|
20
22
|
value: S = Field(title="参考值")
|
|
21
|
-
|
|
22
|
-
@model_validator(mode="after")
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after") # type: ignore
|
|
23
25
|
def validate_value(self):
|
|
24
26
|
if self.value is None and self.option not in ("eq", "ne"):
|
|
25
27
|
raise ValueError("NoneType is not comparable")
|
|
@@ -27,7 +29,7 @@ class ColumnExpression(BaseModel, Generic[S]):
|
|
|
27
29
|
|
|
28
30
|
class WhereClause(BaseModel):
|
|
29
31
|
option: Literal["and", "or"] = Field(default="and", title="关系")
|
|
30
|
-
column_clauses: list[ColumnExpression |
|
|
32
|
+
column_clauses: list["ColumnExpression | WhereClause"]
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class Page(BaseModel, Generic[Schema]):
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__describe__ = ""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from libc.stdlib cimport strtol
|
|
6
|
+
from libc.string cimport memmove, strlen
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
cdef inline size_t _unquote(char* c_string, bint change_plus):
|
|
10
|
+
cdef:
|
|
11
|
+
int i = 0
|
|
12
|
+
char[2] quote
|
|
13
|
+
size_t n = strlen(c_string)
|
|
14
|
+
|
|
15
|
+
while i < n:
|
|
16
|
+
if c_string[i] == '+' and change_plus:
|
|
17
|
+
c_string[i] = ' '
|
|
18
|
+
elif c_string[i] == '%':
|
|
19
|
+
quote[0] = c_string[i + 1]
|
|
20
|
+
quote[1] = c_string[i + 2]
|
|
21
|
+
c_string[i] = strtol(quote, NULL, 16)
|
|
22
|
+
memmove(c_string + i + 1, c_string + i + 3, n - i - 2)
|
|
23
|
+
n -= 2
|
|
24
|
+
i += 1
|
|
25
|
+
return n
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def unquote(val: bytes, encoding: str = "utf-8") -> str:
|
|
29
|
+
return val[:_unquote(val, 0)].decode(encoding)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def unquote_plus(val: bytes, encoding: str = "utf-8") -> str:
|
|
33
|
+
return val[:_unquote(val, 1)].decode(encoding)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_qsl(qs: bytes, keep_blank_values: bool = False) -> list[tuple[str, str]]:
|
|
37
|
+
query_args = qs.split(b'&') if qs else []
|
|
38
|
+
r = []
|
|
39
|
+
for name_value in query_args:
|
|
40
|
+
if not name_value:
|
|
41
|
+
continue
|
|
42
|
+
nv = name_value.split(b'=')
|
|
43
|
+
if len(nv) < 2:
|
|
44
|
+
if not keep_blank_values:
|
|
45
|
+
continue
|
|
46
|
+
nv.append(b'')
|
|
47
|
+
name = unquote_plus(nv[0])
|
|
48
|
+
value = unquote_plus(nv[1])
|
|
49
|
+
r.append((name, value))
|
|
50
|
+
return r
|
|
@@ -3,7 +3,7 @@ __date__ = "2024-12-24"
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import TYPE_CHECKING, Generic
|
|
6
|
+
from typing import TYPE_CHECKING, Generic, Mapping
|
|
7
7
|
|
|
8
8
|
from fastapi.responses import JSONResponse
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
@@ -195,32 +195,53 @@ class APIResult(BaseModel, Generic[T]):
|
|
|
195
195
|
data: T | None = Field(default=None, title="返回数据")
|
|
196
196
|
|
|
197
197
|
if TYPE_CHECKING:
|
|
198
|
+
|
|
198
199
|
@classmethod
|
|
199
200
|
def ok(cls, data: T | None = None) -> "APIResult[T]":
|
|
200
201
|
return APIResult(data=data)
|
|
202
|
+
|
|
201
203
|
else:
|
|
204
|
+
|
|
202
205
|
@classmethod
|
|
203
206
|
def ok(cls, data: T | None = None) -> "APIResponse":
|
|
204
|
-
|
|
207
|
+
model = cls.__new__(cls)
|
|
208
|
+
model.__dict__["data"] = data
|
|
209
|
+
return APIResponse(model)
|
|
205
210
|
|
|
206
211
|
|
|
207
212
|
class APIResponse(JSONResponse):
|
|
208
|
-
|
|
213
|
+
|
|
209
214
|
def render(self, content: APIResult) -> bytes:
|
|
210
|
-
return
|
|
215
|
+
return APIResult.__pydantic_serializer__.to_json(
|
|
211
216
|
content,
|
|
212
217
|
indent=None,
|
|
213
218
|
by_alias=True,
|
|
214
219
|
exclude_defaults=False,
|
|
215
220
|
exclude_none=False,
|
|
216
|
-
exclude_unset=False
|
|
221
|
+
exclude_unset=False,
|
|
217
222
|
)
|
|
223
|
+
|
|
224
|
+
def init_headers(self, headers: Mapping[str, str] | None = None) -> None:
|
|
225
|
+
self.raw_headers = [
|
|
226
|
+
(b"content-length", str(len(self.body)).encode("latin-1")),
|
|
227
|
+
(b"content-type", b"application/json; charset=utf-8"),
|
|
228
|
+
]
|
|
229
|
+
if headers:
|
|
230
|
+
raw_headers = [
|
|
231
|
+
(k.lower().encode("latin-1"), v.encode("latin-1"))
|
|
232
|
+
for k, v in headers.items() if k not in ("content-length", "content-type")
|
|
233
|
+
]
|
|
234
|
+
self.raw_headers.extend(raw_headers)
|
|
235
|
+
|
|
236
|
+
|
|
218
237
|
|
|
219
238
|
|
|
220
239
|
class APIError(Exception):
|
|
221
240
|
__slots__ = ("code", "message")
|
|
222
|
-
|
|
223
|
-
def __init__(
|
|
241
|
+
|
|
242
|
+
def __init__(
|
|
243
|
+
self, result: ResultEnum | None = None, code: str = "00000", message: str = ""
|
|
244
|
+
) -> None:
|
|
224
245
|
if result:
|
|
225
246
|
self.code = result.value[0]
|
|
226
247
|
self.message = message or result.value[1]
|
|
@@ -228,7 +249,7 @@ class APIError(Exception):
|
|
|
228
249
|
self.code = code
|
|
229
250
|
self.message = message
|
|
230
251
|
super().__init__(self)
|
|
231
|
-
|
|
252
|
+
|
|
232
253
|
def __str__(self) -> str:
|
|
233
254
|
return self.message
|
|
234
255
|
|
|
@@ -11,18 +11,18 @@ from pydantic_settings import (BaseSettings, PydanticBaseSettingsSource,
|
|
|
11
11
|
|
|
12
12
|
class Settings(BaseSettings):
|
|
13
13
|
model_config = SettingsConfigDict(
|
|
14
|
-
toml_file=["config.default.toml", "config.custom.toml"],
|
|
14
|
+
toml_file=["config.default.toml", "config.custom.toml"],
|
|
15
15
|
validate_default=False,
|
|
16
|
-
extra="ignore"
|
|
16
|
+
extra="ignore",
|
|
17
17
|
)
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
title: str = "FastAPI"
|
|
20
20
|
version: str = "0.1.0"
|
|
21
21
|
debug: bool = False
|
|
22
22
|
root_path: str = ""
|
|
23
23
|
include_in_schema: bool = True
|
|
24
24
|
mode: Literal["dev", "test", "prod"] = "dev"
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
@classmethod
|
|
27
27
|
def settings_customise_sources(
|
|
28
28
|
cls,
|
|
@@ -33,10 +33,10 @@ class Settings(BaseSettings):
|
|
|
33
33
|
file_secret_settings: PydanticBaseSettingsSource,
|
|
34
34
|
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
|
35
35
|
return (
|
|
36
|
-
TomlConfigSettingsSource(settings_cls),
|
|
37
|
-
env_settings,
|
|
36
|
+
TomlConfigSettingsSource(settings_cls),
|
|
37
|
+
env_settings,
|
|
38
38
|
init_settings,
|
|
39
|
-
file_secret_settings
|
|
39
|
+
file_secret_settings,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
@model_validator(mode="after")
|
|
@@ -9,7 +9,9 @@ from typing import Annotated, Any, TypeVar, Union
|
|
|
9
9
|
from pydantic import BaseModel, PlainSerializer
|
|
10
10
|
from sqlmodel import SQLModel
|
|
11
11
|
|
|
12
|
-
Comparable = Union[
|
|
12
|
+
Comparable = Union[
|
|
13
|
+
int, float, decimal.Decimal, datetime.datetime, datetime.date, datetime.time
|
|
14
|
+
]
|
|
13
15
|
Serializable = Union[Comparable, bool, str, None]
|
|
14
16
|
|
|
15
17
|
|
|
@@ -20,9 +22,8 @@ S = TypeVar("S", bound=Serializable)
|
|
|
20
22
|
Schema = TypeVar("Schema", bound=BaseModel)
|
|
21
23
|
Model = TypeVar("Model", bound=SQLModel)
|
|
22
24
|
|
|
23
|
-
Cursor = Annotated[
|
|
24
|
-
int, PlainSerializer(lambda x: str(x), return_type=str)
|
|
25
|
-
]
|
|
25
|
+
Cursor = Annotated[int, PlainSerializer(lambda x: str(x), return_type=str)]
|
|
26
26
|
LocalDateTime = Annotated[
|
|
27
|
-
datetime.datetime,
|
|
27
|
+
datetime.datetime,
|
|
28
|
+
PlainSerializer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"), return_type=str),
|
|
28
29
|
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__describe__ = ""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def unquote(val: bytes, encoding: str = "utf-8") -> str: ...
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unquote_plus(val: bytes, encoding: str = "utf-8") -> str: ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_qsl(qs: bytes, keep_blank_values: bool = False) -> list[tuple[str, str]]: ...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-extra
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: extra package for fastapi.
|
|
5
5
|
Author-email: Ziyan Yin <408856732@qq.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -18,9 +18,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/x-rst
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: fastapi<0.
|
|
21
|
+
Requires-Dist: fastapi<0.129.0,>=0.128.0
|
|
22
22
|
Requires-Dist: httpx<0.29.0,>=0.28.0
|
|
23
|
-
Requires-Dist: pydantic-settings>=2.
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
24
24
|
Requires-Dist: sqlmodel>=0.0.22
|
|
25
25
|
Provides-Extra: redis
|
|
26
26
|
Requires-Dist: redis; extra == "redis"
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.rst
|
|
3
3
|
pyproject.toml
|
|
4
4
|
fastapi_extra/__init__.py
|
|
5
|
+
fastapi_extra/_patch.py
|
|
5
6
|
fastapi_extra/cursor.pyi
|
|
6
7
|
fastapi_extra/dependency.py
|
|
7
8
|
fastapi_extra/form.py
|
|
@@ -10,6 +11,7 @@ fastapi_extra/response.py
|
|
|
10
11
|
fastapi_extra/routing.pyi
|
|
11
12
|
fastapi_extra/settings.py
|
|
12
13
|
fastapi_extra/types.py
|
|
14
|
+
fastapi_extra/urlparse.pyi
|
|
13
15
|
fastapi_extra/utils.py
|
|
14
16
|
fastapi_extra.egg-info/PKG-INFO
|
|
15
17
|
fastapi_extra.egg-info/SOURCES.txt
|
|
@@ -18,6 +20,7 @@ fastapi_extra.egg-info/requires.txt
|
|
|
18
20
|
fastapi_extra.egg-info/top_level.txt
|
|
19
21
|
fastapi_extra/native/cursor.pyx
|
|
20
22
|
fastapi_extra/native/routing.pyx
|
|
23
|
+
fastapi_extra/native/urlparse.pyx
|
|
21
24
|
fastapi_extra/cache/__init__.py
|
|
22
25
|
fastapi_extra/cache/redis.py
|
|
23
26
|
fastapi_extra/database/__init__.py
|
|
@@ -25,4 +28,5 @@ fastapi_extra/database/model.py
|
|
|
25
28
|
fastapi_extra/database/service.py
|
|
26
29
|
fastapi_extra/database/session.py
|
|
27
30
|
fastapi_extra/native/cursor.pyx
|
|
28
|
-
fastapi_extra/native/routing.pyx
|
|
31
|
+
fastapi_extra/native/routing.pyx
|
|
32
|
+
fastapi_extra/native/urlparse.pyx
|
|
@@ -30,9 +30,9 @@ classifiers = [
|
|
|
30
30
|
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
31
31
|
]
|
|
32
32
|
dependencies = [
|
|
33
|
-
"fastapi>=0.
|
|
33
|
+
"fastapi>=0.128.0,<0.129.0",
|
|
34
34
|
"httpx>=0.28.0,<0.29.0",
|
|
35
|
-
"pydantic-settings>=2.
|
|
35
|
+
"pydantic-settings>=2.12.0",
|
|
36
36
|
"sqlmodel>=0.0.22",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -47,7 +47,8 @@ aiomysql = ["aiomysql"]
|
|
|
47
47
|
packages = ["fastapi_extra", "fastapi_extra.database", "fastapi_extra.cache"]
|
|
48
48
|
ext-modules = [
|
|
49
49
|
{ name = "fastapi_extra.cursor", sources = ["fastapi_extra/native/cursor.pyx"] },
|
|
50
|
-
{ name = "fastapi_extra.routing", sources = ["fastapi_extra/native/routing.pyx"] }
|
|
50
|
+
{ name = "fastapi_extra.routing", sources = ["fastapi_extra/native/routing.pyx"] },
|
|
51
|
+
{ name = "fastapi_extra.urlparse", sources = ["fastapi_extra/native/urlparse.pyx"] }
|
|
51
52
|
]
|
|
52
53
|
|
|
53
54
|
[tool.setuptools.dynamic]
|
|
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
|