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.
@@ -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,4 @@
1
+ fastapi<0.116.0,>=0.115.0
2
+ httpx<0.29.0,>=0.28.0
3
+ pydantic-settings>=2.7.0
4
+ sqlmodel>=0.0.22
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+