fastapi-extra 0.1.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.1.0/LICENSE +27 -0
- fastapi_extra-0.1.0/PKG-INFO +24 -0
- fastapi_extra-0.1.0/README.rst +0 -0
- fastapi_extra-0.1.0/fastapi_extra/__init__.py +13 -0
- fastapi_extra-0.1.0/fastapi_extra/databases.py +96 -0
- fastapi_extra-0.1.0/fastapi_extra/form.py +30 -0
- fastapi_extra-0.1.0/fastapi_extra/model.py +73 -0
- fastapi_extra-0.1.0/fastapi_extra/native/cursor.pyx +44 -0
- fastapi_extra-0.1.0/fastapi_extra/native/routing.pyx +163 -0
- fastapi_extra-0.1.0/fastapi_extra/response.py +217 -0
- fastapi_extra-0.1.0/fastapi_extra/settings.py +36 -0
- fastapi_extra-0.1.0/fastapi_extra/types.py +16 -0
- fastapi_extra-0.1.0/fastapi_extra/utils.py +18 -0
- fastapi_extra-0.1.0/fastapi_extra.egg-info/PKG-INFO +24 -0
- fastapi_extra-0.1.0/fastapi_extra.egg-info/SOURCES.txt +20 -0
- fastapi_extra-0.1.0/fastapi_extra.egg-info/dependency_links.txt +1 -0
- fastapi_extra-0.1.0/fastapi_extra.egg-info/requires.txt +4 -0
- fastapi_extra-0.1.0/fastapi_extra.egg-info/top_level.txt +1 -0
- fastapi_extra-0.1.0/pyproject.toml +47 -0
- fastapi_extra-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2024 NoLongerCoding
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fastapi-extra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: extra package for fastapi.
|
|
5
|
+
Author-email: Ziyan Yin <408856732@qq.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Topic :: Internet
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/x-rst
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastapi<0.116.0,>=0.115.0
|
|
22
|
+
Requires-Dist: httpx<0.29.0,>=0.28.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.7.0
|
|
24
|
+
Requires-Dist: sqlmodel>=0.0.22
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def install():
|
|
5
|
+
try:
|
|
6
|
+
from fastapi import routing
|
|
7
|
+
|
|
8
|
+
from fastapi_extra import routing as native_routing # type: ignore
|
|
9
|
+
|
|
10
|
+
routing.APIRouter = native_routing.BaseRouter # type: ignore
|
|
11
|
+
|
|
12
|
+
except ImportError: # pragma: nocover
|
|
13
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-26"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
6
|
+
|
|
7
|
+
from fastapi.params import Depends
|
|
8
|
+
from pydantic import AnyUrl, BaseModel
|
|
9
|
+
from sqlalchemy import Engine, NullPool
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession
|
|
12
|
+
from sqlalchemy.orm import Session as _Session
|
|
13
|
+
from sqlalchemy.util import _concurrency_py3k
|
|
14
|
+
from sqlmodel import create_engine
|
|
15
|
+
|
|
16
|
+
from fastapi_extra.settings import Settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseConfig(BaseModel):
|
|
20
|
+
url: AnyUrl
|
|
21
|
+
echo: bool = False
|
|
22
|
+
echo_pool: bool = False
|
|
23
|
+
isolation_level: Literal[
|
|
24
|
+
"SERIALIZABLE",
|
|
25
|
+
"REPEATABLE READ",
|
|
26
|
+
"READ COMMITTED",
|
|
27
|
+
"READ UNCOMMITTED",
|
|
28
|
+
"AUTOCOMMIT",
|
|
29
|
+
] | None = None
|
|
30
|
+
max_overflow: int = 10
|
|
31
|
+
pool_pre_ping: bool = False
|
|
32
|
+
pool_size: int = 5
|
|
33
|
+
pool_recycle: int = -1
|
|
34
|
+
pool_timeout: int = 30
|
|
35
|
+
pool_use_lifo: bool = False
|
|
36
|
+
query_cache_size: int = 500
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DatabaseSettings(Settings):
|
|
40
|
+
datasources: dict[str, DatabaseConfig]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_settings = DatabaseSettings() # type: ignore
|
|
44
|
+
_engines: dict[str, Engine] = {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_engine(name: str = "default", **kw: Any) -> Engine:
|
|
48
|
+
if name in _engines:
|
|
49
|
+
return _engines[name]
|
|
50
|
+
if name in _settings.datasources:
|
|
51
|
+
config = _settings.datasources[name]
|
|
52
|
+
_engines[name] = create_engine(
|
|
53
|
+
url=str(config.url),
|
|
54
|
+
**config.model_dump(exclude_defaults=True, exclude={"url"}),
|
|
55
|
+
**kw
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return _engines[name]
|
|
59
|
+
|
|
60
|
+
raise KeyError(f"cannot find datasources.{name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def shutdown() -> None:
|
|
64
|
+
for engine in _engines.values():
|
|
65
|
+
await _concurrency_py3k.greenlet_spawn(engine.dispose)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SessionFactory(Depends):
|
|
69
|
+
__slots__ = ("engine", )
|
|
70
|
+
datasource: str = "default"
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
super().__init__()
|
|
74
|
+
if _settings.mode == "test":
|
|
75
|
+
self.engine = load_engine(self.datasource, poolclass=NullPool)
|
|
76
|
+
else:
|
|
77
|
+
self.engine = load_engine(self.datasource)
|
|
78
|
+
self.dependency = self
|
|
79
|
+
|
|
80
|
+
def __call__(self):
|
|
81
|
+
with _Session(self.engine) as session:
|
|
82
|
+
yield session
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncSessionFactory(SessionFactory):
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__()
|
|
89
|
+
self.engine = AsyncEngine(self.engine)
|
|
90
|
+
|
|
91
|
+
async def __call__(self):
|
|
92
|
+
async with _AsyncSession(self.engine) as session:
|
|
93
|
+
yield session
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
AsyncSession = Annotated[_AsyncSession, AsyncSessionFactory()]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-24"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import Generic, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
from fastapi_extra.types import C, S
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataRange(BaseModel, Generic[C]):
|
|
13
|
+
start: C | None = Field(default=None, title="起始")
|
|
14
|
+
end: C | None = Field(default=None, title="终止")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColumnExpression(BaseModel, Generic[S]):
|
|
18
|
+
column_name: str = Field(title="列名")
|
|
19
|
+
option: Literal["eq", "ne", "gt", "lt", "ge", "le"] = Field(default="eq", title="逻辑值")
|
|
20
|
+
value: S = Field(title="参考值")
|
|
21
|
+
|
|
22
|
+
@model_validator(mode="after")
|
|
23
|
+
def validate_value(self):
|
|
24
|
+
if self.value is None and self.option not in ("eq", "ne"):
|
|
25
|
+
raise ValueError("NoneType is not comparable")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WhereClause(BaseModel):
|
|
29
|
+
option: Literal["and", "or"] = Field(default="and", title="关系")
|
|
30
|
+
column_clauses: list[ColumnExpression | "WhereClause"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-25"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
from pydantic import PlainSerializer
|
|
9
|
+
from sqlalchemy import BigInteger, DateTime, Integer, SmallInteger, func
|
|
10
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
11
|
+
from sqlmodel import Field, SQLModel
|
|
12
|
+
|
|
13
|
+
from fastapi_extra.cursor import Cursor as _Cursor # type: ignore
|
|
14
|
+
from fastapi_extra.utils import get_machine_seed
|
|
15
|
+
|
|
16
|
+
Cursor = Annotated[
|
|
17
|
+
int, PlainSerializer(lambda x: str(x), return_type=str)
|
|
18
|
+
]
|
|
19
|
+
LocalDateTime = Annotated[
|
|
20
|
+
datetime.datetime, PlainSerializer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"), return_type=str)
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SQLBase(SQLModel):
|
|
25
|
+
id: int | None = Field(
|
|
26
|
+
default_factory=lambda: None,
|
|
27
|
+
title="ID",
|
|
28
|
+
primary_key=True
|
|
29
|
+
)
|
|
30
|
+
create_at: LocalDateTime = Field(
|
|
31
|
+
default_factory=datetime.datetime.now,
|
|
32
|
+
title="CREATE_AT",
|
|
33
|
+
sa_type=DateTime,
|
|
34
|
+
sa_column_kwargs={"default": func.now(), "nullable": False, "comment": "CREATE_AT"},
|
|
35
|
+
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
36
|
+
)
|
|
37
|
+
update_at: LocalDateTime = Field(
|
|
38
|
+
default_factory=datetime.datetime.now,
|
|
39
|
+
title="UPDATE_AT",
|
|
40
|
+
sa_type=DateTime,
|
|
41
|
+
sa_column_kwargs={"default": func.now(), "onupdate": func.now(), "nullable": False, "comment": "UPDATE_AT"},
|
|
42
|
+
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LocalPK(SQLBase):
|
|
47
|
+
id: Cursor | None = Field(
|
|
48
|
+
default_factory=_Cursor(get_machine_seed()).next_val, title="ID", primary_key=True, sa_type=BigInteger
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Deleted(SQLBase):
|
|
53
|
+
deleted: int = Field(
|
|
54
|
+
default=0,
|
|
55
|
+
title="DELETED",
|
|
56
|
+
sa_type=SmallInteger,
|
|
57
|
+
sa_column_kwargs={"nullable": False, "comment": "DELETED"},
|
|
58
|
+
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Versioned(SQLBase):
|
|
63
|
+
version_id: int = Field(
|
|
64
|
+
default=0,
|
|
65
|
+
title="VERSION_ID",
|
|
66
|
+
sa_type=Integer,
|
|
67
|
+
sa_column_kwargs={"nullable": False, "comment": "VERSION_ID"},
|
|
68
|
+
schema_extra={"json_schema_extra": {"readOnly": True}},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@declared_attr # type: ignore
|
|
72
|
+
def __mapper_args__(cls) -> dict:
|
|
73
|
+
return {"version_id_col": "version_id"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
cimport cython
|
|
2
|
+
from cpython cimport time
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
cdef int _sequence_length = 10
|
|
8
|
+
cdef double _start_point = datetime.datetime(2020, 1, 1).timestamp()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@cython.no_gc
|
|
12
|
+
cdef class Cursor:
|
|
13
|
+
__slots__ = "cursor", "last_point"
|
|
14
|
+
cdef:
|
|
15
|
+
int cursor
|
|
16
|
+
int seed
|
|
17
|
+
long long last_point
|
|
18
|
+
|
|
19
|
+
def __init__(self, seed: int):
|
|
20
|
+
self.seed = seed
|
|
21
|
+
if self.seed > 16:
|
|
22
|
+
self.seed %= 16
|
|
23
|
+
self.cursor = 0
|
|
24
|
+
self.last_point = 0
|
|
25
|
+
|
|
26
|
+
cdef inline long long fetch(self) nogil:
|
|
27
|
+
cdef:
|
|
28
|
+
long long count = 0
|
|
29
|
+
long long point = int((time.time() - _start_point) * 100)
|
|
30
|
+
|
|
31
|
+
if self.last_point == point:
|
|
32
|
+
count = self.cursor + 1
|
|
33
|
+
if count >= (1 << _sequence_length):
|
|
34
|
+
return 0
|
|
35
|
+
else:
|
|
36
|
+
self.last_point = point
|
|
37
|
+
self.cursor = count
|
|
38
|
+
return (point << (_sequence_length + 4)) + (self.seed << _sequence_length) + count
|
|
39
|
+
|
|
40
|
+
def next_val(self) -> str:
|
|
41
|
+
index = self.fetch()
|
|
42
|
+
while index == 0:
|
|
43
|
+
index = self.fetch()
|
|
44
|
+
return str(index)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__describe__ = ""
|
|
3
|
+
|
|
4
|
+
cimport cython
|
|
5
|
+
|
|
6
|
+
from typing import MutableMapping
|
|
7
|
+
|
|
8
|
+
from basex.common import strings
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
from starlette import _utils as starlette_utils
|
|
11
|
+
from starlette.datastructures import URL
|
|
12
|
+
from starlette.responses import RedirectResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_route_path(scope: MutableMapping) -> str:
|
|
16
|
+
root_path = scope.get("root_path", "")
|
|
17
|
+
route_path = scope["path"].removeprefix(root_path)
|
|
18
|
+
return route_path
|
|
19
|
+
|
|
20
|
+
starlette_utils.get_route_path = get_route_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cython.no_gc
|
|
24
|
+
cdef class RouteNode:
|
|
25
|
+
cdef readonly:
|
|
26
|
+
list routes
|
|
27
|
+
dict leaves
|
|
28
|
+
unicode prefix
|
|
29
|
+
|
|
30
|
+
def __cinit__(self, prefix):
|
|
31
|
+
self.prefix = prefix
|
|
32
|
+
self.routes = []
|
|
33
|
+
self.leaves = {}
|
|
34
|
+
|
|
35
|
+
def add_route(self, route):
|
|
36
|
+
self.routes.append(route)
|
|
37
|
+
|
|
38
|
+
def add_leaf(self, node):
|
|
39
|
+
if node.prefix in self.leaves:
|
|
40
|
+
raise KeyError(node.prefix)
|
|
41
|
+
else:
|
|
42
|
+
self.leaves[node.prefix] = node
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
cdef list change_path_to_ranks(unicode path):
|
|
46
|
+
ranks = path.lstrip('/').split('/')
|
|
47
|
+
return ranks
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
cdef void add_route(unicode path, object root, object route):
|
|
51
|
+
current_node = root
|
|
52
|
+
ranks = change_path_to_ranks(path)
|
|
53
|
+
for r in ranks:
|
|
54
|
+
if r.find('{') >= 0 and r.find('}') > 0:
|
|
55
|
+
break
|
|
56
|
+
if not r:
|
|
57
|
+
continue
|
|
58
|
+
if r in current_node.leaves:
|
|
59
|
+
current_node = current_node.leaves[r]
|
|
60
|
+
else:
|
|
61
|
+
next_node = RouteNode.__new__(RouteNode, r)
|
|
62
|
+
current_node.add_leaf(next_node)
|
|
63
|
+
current_node = next_node
|
|
64
|
+
current_node.add_route(route)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
cdef list find_routes(unicode path, RouteNode root):
|
|
68
|
+
current_node = root
|
|
69
|
+
ranks = change_path_to_ranks(path)
|
|
70
|
+
|
|
71
|
+
routes = []
|
|
72
|
+
routes += current_node.routes
|
|
73
|
+
for r in ranks:
|
|
74
|
+
if not r:
|
|
75
|
+
continue
|
|
76
|
+
if r in current_node.leaves:
|
|
77
|
+
current_node = current_node.leaves[r]
|
|
78
|
+
routes += current_node.routes
|
|
79
|
+
continue
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
routes.reverse()
|
|
83
|
+
return routes
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_super_router = APIRouter
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BaseRouter(_super_router):
|
|
90
|
+
|
|
91
|
+
def __init__(self, *args, **kwargs):
|
|
92
|
+
super().__init__(*args, **kwargs)
|
|
93
|
+
self.root_node = RouteNode.__new__(RouteNode, '')
|
|
94
|
+
for route in self.routes:
|
|
95
|
+
add_route(route.path, self.root_node, route)
|
|
96
|
+
|
|
97
|
+
def add_route(self, path, endpoint, **kwargs):
|
|
98
|
+
super().add_route(path, endpoint, **kwargs)
|
|
99
|
+
add_route(self.routes[-1].path, self.root_node, self.routes[-1])
|
|
100
|
+
|
|
101
|
+
def add_api_route(self, path, endpoint, **kwargs):
|
|
102
|
+
super().add_api_route(path)
|
|
103
|
+
add_route(self.routes[-1].path, self.root_node, self.routes[-1])
|
|
104
|
+
|
|
105
|
+
def add_websocket_route(self, path, endpoint, **kwargs):
|
|
106
|
+
super().add_websocket_route(path, endpoint, **kwargs)
|
|
107
|
+
add_route(self.routes[-1].path, self.root_node, self.routes[-1])
|
|
108
|
+
|
|
109
|
+
def add_api_websocket_route(self, path, endpoint, **kwargs):
|
|
110
|
+
super().add_api_websocket_route(path, endpoint, **kwargs)
|
|
111
|
+
add_route(self.routes[-1].path, self.root_node, self.routes[-1])
|
|
112
|
+
|
|
113
|
+
def mount(self, path, app, **kwargs):
|
|
114
|
+
super().mount(path, app, **kwargs)
|
|
115
|
+
add_route(self.routes[-1].path, self.root_node, self.routes[-1])
|
|
116
|
+
|
|
117
|
+
async def __call__(self, scope, receive, send):
|
|
118
|
+
assert scope["type"] in ("http", "websocket", "lifespan")
|
|
119
|
+
|
|
120
|
+
if "router" not in scope:
|
|
121
|
+
scope["router"] = self
|
|
122
|
+
|
|
123
|
+
if scope["type"] == "lifespan":
|
|
124
|
+
await self.lifespan(scope, receive, send)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
partial = None
|
|
128
|
+
|
|
129
|
+
route_path = get_route_path(scope)
|
|
130
|
+
matched_routes = find_routes(route_path, self.root_node)
|
|
131
|
+
|
|
132
|
+
for route in matched_routes:
|
|
133
|
+
match, child_scope = route.matches(scope)
|
|
134
|
+
if match.value == 2:
|
|
135
|
+
scope.update(child_scope)
|
|
136
|
+
await route.handle(scope, receive, send)
|
|
137
|
+
return
|
|
138
|
+
elif match.value == 1 and partial is None:
|
|
139
|
+
partial = route
|
|
140
|
+
partial_scope = child_scope
|
|
141
|
+
|
|
142
|
+
if partial is not None:
|
|
143
|
+
scope.update(partial_scope)
|
|
144
|
+
await partial.handle(scope, receive, send)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if scope["type"] == "http" and self.redirect_slashes and route_path != "/":
|
|
149
|
+
redirect_scope = dict(scope)
|
|
150
|
+
if route_path.endswith("/"):
|
|
151
|
+
redirect_scope["path"] = redirect_scope["path"].rstrip("/")
|
|
152
|
+
else:
|
|
153
|
+
redirect_scope["path"] = redirect_scope["path"] + "/"
|
|
154
|
+
|
|
155
|
+
for route in matched_routes:
|
|
156
|
+
match, child_scope = route.matches(redirect_scope)
|
|
157
|
+
if match.value != 0:
|
|
158
|
+
redirect_url = URL(scope=redirect_scope)
|
|
159
|
+
response = RedirectResponse(url=str(redirect_url))
|
|
160
|
+
await response(scope, receive, send)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
await self.default(scope, receive, send)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-24"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Generic
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from fastapi_extra.types import T
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# alibaba result enum
|
|
14
|
+
class ResultEnum(Enum):
|
|
15
|
+
SUCCESS = ("00000", "OK")
|
|
16
|
+
FAILED = ("99999", "系统异常")
|
|
17
|
+
A0001 = ("A0001", "用户端错误")
|
|
18
|
+
A0100 = ("A0100", "用户注册错误")
|
|
19
|
+
A0101 = ("A0101", "用户未同意隐私协议")
|
|
20
|
+
A0102 = ("A0102", "注册国家或地区受限")
|
|
21
|
+
A0110 = ("A0110", "用户名校验失败")
|
|
22
|
+
A0111 = ("A0111", "用户名已存在")
|
|
23
|
+
A0112 = ("A0112", "用户名包含敏感词")
|
|
24
|
+
A0113 = ("A0113", "用户名包含特殊字符")
|
|
25
|
+
A0120 = ("A0120", "密码校验失败")
|
|
26
|
+
A0121 = ("A0121", "密码长度不够")
|
|
27
|
+
A0122 = ("A0122", "密码强度不够")
|
|
28
|
+
A0130 = ("A0130", "校验码输入错误")
|
|
29
|
+
A0131 = ("A0131", "短信校验码输入错误")
|
|
30
|
+
A0132 = ("A0132", "邮件校验码输入错误")
|
|
31
|
+
A0133 = ("A0133", "语音校验码输入错误")
|
|
32
|
+
A0140 = ("A0140", "用户证件异常")
|
|
33
|
+
A0141 = ("A0141", "用户证件类型未选择")
|
|
34
|
+
A0142 = ("A0142", "大陆身份证编号校验非法")
|
|
35
|
+
A0143 = ("A0143", "护照编号校验非法")
|
|
36
|
+
A0144 = ("A0144", "军官证编号校验非法")
|
|
37
|
+
A0150 = ("A0150", "用户基本信息校验失败")
|
|
38
|
+
A0151 = ("A0151", "手机格式校验失败")
|
|
39
|
+
A0152 = ("A0152", "地址格式校验失败")
|
|
40
|
+
A0153 = ("A0153", "邮箱格式校验失败")
|
|
41
|
+
A0200 = ("A0200", "用户登录异常")
|
|
42
|
+
A0201 = ("A0201", "用户账户不存在")
|
|
43
|
+
A0202 = ("A0202", "用户账户被冻结")
|
|
44
|
+
A0203 = ("A0203", "用户账户已作废")
|
|
45
|
+
A0210 = ("A0210", "用户密码错误")
|
|
46
|
+
A0211 = ("A0211", "用户输入密码错误次数超限")
|
|
47
|
+
A0220 = ("A0220", "用户身份校验失败")
|
|
48
|
+
A0221 = ("A0221", "用户指纹识别失败")
|
|
49
|
+
A0222 = ("A0222", "用户面容识别失败")
|
|
50
|
+
A0223 = ("A0223", "用户未获得第三方登录授权")
|
|
51
|
+
A0230 = ("A0230", "用户登录已过期")
|
|
52
|
+
A0240 = ("A0240", "用户验证码错误")
|
|
53
|
+
A0241 = ("A0241", "用户验证码尝试次数超限")
|
|
54
|
+
A0300 = ("A0300", "访问权限异常")
|
|
55
|
+
A0301 = ("A0301", "访问未授权")
|
|
56
|
+
A0302 = ("A0302", "正在授权中")
|
|
57
|
+
A0303 = ("A0303", "用户授权申请被拒绝")
|
|
58
|
+
A0310 = ("A0310", "因访问对象隐私设置被拦截")
|
|
59
|
+
A0311 = ("A0311", "授权已过期")
|
|
60
|
+
A0312 = ("A0312", "无权限使用 API")
|
|
61
|
+
A0320 = ("A0320", "用户访问被拦截")
|
|
62
|
+
A0321 = ("A0321", "黑名单用户")
|
|
63
|
+
A0322 = ("A0322", "账号被冻结")
|
|
64
|
+
A0323 = ("A0323", "非法 IP 地址")
|
|
65
|
+
A0324 = ("A0324", "网关访问受限")
|
|
66
|
+
A0325 = ("A0325", "地域黑名单")
|
|
67
|
+
A0330 = ("A0330", "服务已欠费")
|
|
68
|
+
A0340 = ("A0340", "用户签名异常")
|
|
69
|
+
A0341 = ("A0341", "RSA 签名错误")
|
|
70
|
+
A0400 = ("A0400", "用户请求参数错误")
|
|
71
|
+
A0401 = ("A0401", "包含非法恶意跳转链接")
|
|
72
|
+
A0402 = ("A0402", "无效的用户输入")
|
|
73
|
+
A0410 = ("A0410", "请求必填参数为空")
|
|
74
|
+
A0411 = ("A0411", "用户订单号为空")
|
|
75
|
+
A0412 = ("A0412", "订购数量为空")
|
|
76
|
+
A0413 = ("A0413", "缺少时间戳参数")
|
|
77
|
+
A0414 = ("A0414", "非法的时间戳参数")
|
|
78
|
+
A0420 = ("A0420", "请求参数值超出允许的范围")
|
|
79
|
+
A0421 = ("A0421", "参数格式不匹配")
|
|
80
|
+
A0422 = ("A0422", "地址不在服务范围")
|
|
81
|
+
A0423 = ("A0423", "时间不在服务范围")
|
|
82
|
+
A0424 = ("A0424", "金额超出限制")
|
|
83
|
+
A0425 = ("A0425", "数量超出限制")
|
|
84
|
+
A0426 = ("A0426", "请求批量处理总个数超出限制")
|
|
85
|
+
A0427 = ("A0427", "请求 JSON 解析失败")
|
|
86
|
+
A0430 = ("A0430", "用户输入内容非法")
|
|
87
|
+
A0431 = ("A0431", "包含违禁敏感词")
|
|
88
|
+
A0432 = ("A0432", "图片包含违禁信息")
|
|
89
|
+
A0433 = ("A0433", "文件侵犯版权")
|
|
90
|
+
A0440 = ("A0440", "用户操作异常")
|
|
91
|
+
A0441 = ("A0441", "用户支付超时")
|
|
92
|
+
A0442 = ("A0442", "确认订单超时")
|
|
93
|
+
A0443 = ("A0443", "订单已关闭")
|
|
94
|
+
A0500 = ("A0500", "用户请求服务异常")
|
|
95
|
+
A0501 = ("A0501", "请求次数超出限制")
|
|
96
|
+
A0502 = ("A0502", "请求并发数超出限制")
|
|
97
|
+
A0503 = ("A0503", "用户操作请等待")
|
|
98
|
+
A0504 = ("A0504", "WebSocket 连接异常")
|
|
99
|
+
A0505 = ("A0505", "WebSocket 连接断开")
|
|
100
|
+
A0506 = ("A0506", "用户重复请求")
|
|
101
|
+
A0600 = ("A0600", "用户资源异常")
|
|
102
|
+
A0601 = ("A0601", "账户余额不足")
|
|
103
|
+
A0602 = ("A0602", "用户磁盘空间不足")
|
|
104
|
+
A0603 = ("A0603", "用户内存空间不足")
|
|
105
|
+
A0604 = ("A0604", "用户 OSS 容量不足")
|
|
106
|
+
A0605 = ("A0605", "用户配额已用光")
|
|
107
|
+
A0700 = ("A0700", "用户上传文件异常")
|
|
108
|
+
A0701 = ("A0701", "用户上传文件类型不匹配")
|
|
109
|
+
A0702 = ("A0702", "用户上传文件太大")
|
|
110
|
+
A0703 = ("A0703", "用户上传图片太大")
|
|
111
|
+
A0704 = ("A0704", "用户上传视频太大")
|
|
112
|
+
A0705 = ("A0705", "用户上传压缩文件太大")
|
|
113
|
+
A0800 = ("A0800", "用户当前版本异常")
|
|
114
|
+
A0801 = ("A0801", "用户安装版本与系统不匹配")
|
|
115
|
+
A0802 = ("A0802", "用户安装版本过低")
|
|
116
|
+
A0803 = ("A0803", "用户安装版本过高")
|
|
117
|
+
A0804 = ("A0804", "用户安装版本已过期")
|
|
118
|
+
A0805 = ("A0805", "用户 API 请求版本不匹配")
|
|
119
|
+
A0806 = ("A0806", "用户 API 请求版本过高")
|
|
120
|
+
A0807 = ("A0807", "用户 API 请求版本过低")
|
|
121
|
+
A0900 = ("A0900", "用户隐私未授权")
|
|
122
|
+
A0901 = ("A0901", "用户隐私未签署")
|
|
123
|
+
A0902 = ("A0902", "用户摄像头未授权")
|
|
124
|
+
A0903 = ("A0903", "用户相机未授权")
|
|
125
|
+
A0904 = ("A0904", "用户图片库未授权")
|
|
126
|
+
A0905 = ("A0905", "用户文件未授权")
|
|
127
|
+
A0906 = ("A0906", "用户位置信息未授权")
|
|
128
|
+
A0907 = ("A0907", "用户通讯录未授权")
|
|
129
|
+
A1000 = ("A1000", "用户设备异常")
|
|
130
|
+
A1001 = ("A1001", "用户相机异常")
|
|
131
|
+
A1002 = ("A1002", "用户麦克风异常")
|
|
132
|
+
A1003 = ("A1003", "用户听筒异常")
|
|
133
|
+
A1004 = ("A1004", "用户扬声器异常")
|
|
134
|
+
A1005 = ("A1005", "用户 GPS 定位异常")
|
|
135
|
+
B0001 = ("B0001", "系统执行出错")
|
|
136
|
+
B0100 = ("B0100", "系统执行超时")
|
|
137
|
+
B0101 = ("B0101", "系统订单处理超时")
|
|
138
|
+
B0200 = ("B0200", "系统容灾功能被触发")
|
|
139
|
+
B0210 = ("B0210", "系统限流")
|
|
140
|
+
B0220 = ("B0220", "系统功能降级")
|
|
141
|
+
B0300 = ("B0300", "系统资源异常")
|
|
142
|
+
B0310 = ("B0310", "系统资源耗尽")
|
|
143
|
+
B0311 = ("B0311", "系统磁盘空间耗尽")
|
|
144
|
+
B0312 = ("B0312", "系统内存耗尽")
|
|
145
|
+
B0313 = ("B0313", "文件句柄耗尽")
|
|
146
|
+
B0314 = ("B0314", "系统连接池耗尽")
|
|
147
|
+
B0315 = ("B0315", "系统线程池耗尽")
|
|
148
|
+
B0320 = ("B0320", "系统资源访问异常")
|
|
149
|
+
B0321 = ("B0321", "系统读取磁盘文件失败")
|
|
150
|
+
C0001 = ("C0001", "调用第三方服务出错")
|
|
151
|
+
C0100 = ("C0100", "中间件服务出错")
|
|
152
|
+
C0110 = ("C0110", "RPC 服务出错")
|
|
153
|
+
C0111 = ("C0111", "RPC 服务未找到")
|
|
154
|
+
C0112 = ("C0112", "RPC 服务未注册")
|
|
155
|
+
C0113 = ("C0113", "接口不存在")
|
|
156
|
+
C0120 = ("C0120", "消息服务出错")
|
|
157
|
+
C0121 = ("C0121", "消息投递出错")
|
|
158
|
+
C0122 = ("C0122", "消息消费出错")
|
|
159
|
+
C0123 = ("C0123", "消息订阅出错")
|
|
160
|
+
C0124 = ("C0124", "消息分组未查到")
|
|
161
|
+
C0130 = ("C0130", "缓存服务出错")
|
|
162
|
+
C0131 = ("C0131", "key 长度超过限制")
|
|
163
|
+
C0132 = ("C0132", "value 长度超过限制")
|
|
164
|
+
C0133 = ("C0133", "存储容量已满")
|
|
165
|
+
C0134 = ("C0134", "不支持的数据格式")
|
|
166
|
+
C0140 = ("C0140", "配置服务出错")
|
|
167
|
+
C0150 = ("C0150", "网络资源服务出错")
|
|
168
|
+
C0151 = ("C0151", "VPN 服务出错")
|
|
169
|
+
C0152 = ("C0152", "CDN 服务出错")
|
|
170
|
+
C0153 = ("C0153", "域名解析服务出错")
|
|
171
|
+
C0154 = ("C0154", "网关服务出错")
|
|
172
|
+
C0200 = ("C0200", "第三方系统执行超时")
|
|
173
|
+
C0210 = ("C0210", "RPC 执行超时")
|
|
174
|
+
C0220 = ("C0220", "消息投递超时")
|
|
175
|
+
C0230 = ("C0230", "缓存服务超时")
|
|
176
|
+
C0240 = ("C0240", "配置服务超时")
|
|
177
|
+
C0250 = ("C0250", "数据库服务超时")
|
|
178
|
+
C0300 = ("C0300", "数据库服务出错")
|
|
179
|
+
C0311 = ("C0311", "表不存在")
|
|
180
|
+
C0312 = ("C0312", "列不存在")
|
|
181
|
+
C0321 = ("C0321", "多表关联中存在多个相同名称的列")
|
|
182
|
+
C0331 = ("C0331", "数据库死锁")
|
|
183
|
+
C0341 = ("C0341", "主键冲突")
|
|
184
|
+
C0400 = ("C0400", "第三方容灾系统被触发")
|
|
185
|
+
C0401 = ("C0401", "第三方系统限流")
|
|
186
|
+
C0402 = ("C0402", "第三方功能降级")
|
|
187
|
+
C0500 = ("C0500", "通知服务出错")
|
|
188
|
+
C0501 = ("C0501", "短信提醒服务失败")
|
|
189
|
+
C0502 = ("C0502", "语音提醒服务失败")
|
|
190
|
+
C0503 = ("C0503", "邮件提醒服务失败")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class APIResult(BaseModel, Generic[T]):
|
|
194
|
+
data: T | None = Field(default=None, title="返回数据")
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def ok(cls, data: T | None = None) -> "APIResult[T]":
|
|
198
|
+
return APIResult(data=data)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class APIError(Exception):
|
|
202
|
+
__slots__ = ("code", "message")
|
|
203
|
+
|
|
204
|
+
def __init__(self, result: ResultEnum | None = None, code: str = "00000", message: str = "") -> None:
|
|
205
|
+
if result:
|
|
206
|
+
self.code = result.value[0]
|
|
207
|
+
self.message = message or result.value[1]
|
|
208
|
+
else:
|
|
209
|
+
self.code = code
|
|
210
|
+
self.message = message
|
|
211
|
+
super().__init__(self)
|
|
212
|
+
|
|
213
|
+
def __str__(self) -> str:
|
|
214
|
+
return self.message
|
|
215
|
+
|
|
216
|
+
def __repr__(self) -> str:
|
|
217
|
+
return f"[{self.code}]{self}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-26"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Tuple, Type
|
|
6
|
+
|
|
7
|
+
from pydantic_settings import (BaseSettings, PydanticBaseSettingsSource,
|
|
8
|
+
SettingsConfigDict, TomlConfigSettingsSource)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Settings(BaseSettings):
|
|
12
|
+
model_config = SettingsConfigDict(
|
|
13
|
+
toml_file=["config.default.toml", "config.custom.toml"],
|
|
14
|
+
validate_default=False
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
title: str = ""
|
|
18
|
+
version: str = "0.1.0"
|
|
19
|
+
debug: bool = False
|
|
20
|
+
mode: Literal["dev", "test", "prod"] = "dev"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def settings_customise_sources(
|
|
24
|
+
cls,
|
|
25
|
+
settings_cls: Type[BaseSettings],
|
|
26
|
+
init_settings: PydanticBaseSettingsSource,
|
|
27
|
+
env_settings: PydanticBaseSettingsSource,
|
|
28
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
29
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
30
|
+
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
|
31
|
+
return (
|
|
32
|
+
TomlConfigSettingsSource(settings_cls),
|
|
33
|
+
env_settings,
|
|
34
|
+
init_settings,
|
|
35
|
+
file_secret_settings
|
|
36
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-25"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import decimal
|
|
7
|
+
from typing import Any, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
Comparable = Union[int, float, decimal.Decimal, datetime.datetime, datetime.date, datetime.time]
|
|
10
|
+
Serializable = Union[Comparable, bool, str, None]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound=Any)
|
|
14
|
+
E = TypeVar("E", bound=Exception)
|
|
15
|
+
C = TypeVar("C", bound=Comparable)
|
|
16
|
+
S = TypeVar("S", bound=Serializable)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__author__ = "ziyan.yin"
|
|
2
|
+
__date__ = "2024-12-25"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_machine_seed() -> int:
|
|
9
|
+
"""获取生成id的机器码, 可以覆盖该方法替换
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
int: 机器码
|
|
13
|
+
"""
|
|
14
|
+
ppid, pid = os.getppid(), os.getpid()
|
|
15
|
+
try:
|
|
16
|
+
return (pid - ppid) % 0x10
|
|
17
|
+
except ValueError:
|
|
18
|
+
return 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fastapi-extra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: extra package for fastapi.
|
|
5
|
+
Author-email: Ziyan Yin <408856732@qq.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Topic :: Internet
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/x-rst
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fastapi<0.116.0,>=0.115.0
|
|
22
|
+
Requires-Dist: httpx<0.29.0,>=0.28.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.7.0
|
|
24
|
+
Requires-Dist: sqlmodel>=0.0.22
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.rst
|
|
3
|
+
pyproject.toml
|
|
4
|
+
fastapi_extra/__init__.py
|
|
5
|
+
fastapi_extra/databases.py
|
|
6
|
+
fastapi_extra/form.py
|
|
7
|
+
fastapi_extra/model.py
|
|
8
|
+
fastapi_extra/response.py
|
|
9
|
+
fastapi_extra/settings.py
|
|
10
|
+
fastapi_extra/types.py
|
|
11
|
+
fastapi_extra/utils.py
|
|
12
|
+
fastapi_extra.egg-info/PKG-INFO
|
|
13
|
+
fastapi_extra.egg-info/SOURCES.txt
|
|
14
|
+
fastapi_extra.egg-info/dependency_links.txt
|
|
15
|
+
fastapi_extra.egg-info/requires.txt
|
|
16
|
+
fastapi_extra.egg-info/top_level.txt
|
|
17
|
+
fastapi_extra/native/cursor.pyx
|
|
18
|
+
fastapi_extra/native/routing.pyx
|
|
19
|
+
fastapi_extra/native/cursor.pyx
|
|
20
|
+
fastapi_extra/native/routing.pyx
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_extra
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=74.1.0",
|
|
4
|
+
"wheel",
|
|
5
|
+
"cython>=3.0"
|
|
6
|
+
]
|
|
7
|
+
build-backend = "setuptools.build_meta"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "fastapi-extra"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
description = "extra package for fastapi."
|
|
13
|
+
readme = "README.rst"
|
|
14
|
+
license = { text = "BSD-3-Clause" }
|
|
15
|
+
requires-python = ">=3.12"
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "Ziyan Yin", email = "408856732@qq.com" }
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Web Environment",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: BSD License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
28
|
+
"Topic :: Internet",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"fastapi>=0.115.0,<0.116.0",
|
|
34
|
+
"httpx>=0.28.0,<0.29.0",
|
|
35
|
+
"pydantic-settings>=2.7.0",
|
|
36
|
+
"sqlmodel>=0.0.22",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
packages = ["fastapi_extra"]
|
|
41
|
+
ext-modules = [
|
|
42
|
+
{ name = "fastapi_extra.cursor", sources = ["fastapi_extra/native/cursor.pyx"] },
|
|
43
|
+
{ name = "fastapi_extra.routing", sources = ["fastapi_extra/native/routing.pyx"] }
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.dynamic]
|
|
47
|
+
version = {attr = "fastapi_extra.__version__"} # any module attribute compatible with ast.literal_eval
|