fastapi-extra 0.1.0__cp312-cp312-win_amd64.whl → 0.1.2__cp312-cp312-win_amd64.whl

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/__init__.py CHANGED
@@ -1,13 +1,15 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.2"
2
2
 
3
3
 
4
- def install():
5
- try:
6
- from fastapi import routing
4
+ from fastapi import FastAPI
5
+
7
6
 
7
+ def install(app: FastAPI) -> None:
8
+ try:
8
9
  from fastapi_extra import routing as native_routing # type: ignore
10
+ from fastapi_extra import dependency
9
11
 
10
- routing.APIRouter = native_routing.BaseRouter # type: ignore
11
-
12
+ native_routing.install(app)
13
+ app.dependency_overrides |= dependency.default_dependency_override
12
14
  except ImportError: # pragma: nocover
13
15
  pass
Binary file
@@ -0,0 +1,38 @@
1
+ __author__ = "ziyan.yin"
2
+ __date__ = "2025-01-05"
3
+
4
+
5
+ from abc import ABCMeta
6
+ from typing import Annotated, Any
7
+
8
+ from fastapi.params import Depends
9
+
10
+
11
+ default_dependency_override = {}
12
+
13
+
14
+ class DependencyMetaClass(ABCMeta):
15
+ __root__: Any = None
16
+
17
+ def __new__(
18
+ mcs,
19
+ name: str,
20
+ bases: tuple[type, ...],
21
+ attrs: dict,
22
+ root: bool = False,
23
+ override: bool = False
24
+ ):
25
+ new_cls = super().__new__(mcs, name, bases, attrs)
26
+ if not root:
27
+ if new_cls.__root__ is None:
28
+ new_cls.__root__ = new_cls
29
+ elif override:
30
+ default_dependency_override[new_cls.__root__] = new_cls
31
+ return Annotated[new_cls, Depends(new_cls)]
32
+ return new_cls
33
+
34
+
35
+
36
+ class Service(metaclass=DependencyMetaClass, root=True):
37
+ __slot__ = ()
38
+
fastapi_extra/form.py CHANGED
@@ -28,3 +28,8 @@ class ColumnExpression(BaseModel, Generic[S]):
28
28
  class WhereClause(BaseModel):
29
29
  option: Literal["and", "or"] = Field(default="and", title="关系")
30
30
  column_clauses: list[ColumnExpression | "WhereClause"]
31
+
32
+
33
+ class LoginForm(BaseModel):
34
+ username: str = Field(title="用户名")
35
+ password: str = Field(title="密码")
@@ -5,21 +5,11 @@ cimport cython
5
5
 
6
6
  from typing import MutableMapping
7
7
 
8
- from basex.common import strings
9
- from fastapi import APIRouter
10
8
  from starlette import _utils as starlette_utils
11
9
  from starlette.datastructures import URL
12
10
  from starlette.responses import RedirectResponse
13
11
 
14
12
 
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
13
  @cython.no_gc
24
14
  cdef class RouteNode:
25
15
  cdef readonly:
@@ -78,86 +68,65 @@ cdef list find_routes(unicode path, RouteNode root):
78
68
  routes += current_node.routes
79
69
  continue
80
70
  break
81
-
82
- routes.reverse()
83
71
  return routes
84
72
 
85
73
 
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)
74
+ root_node = RouteNode.__new__(RouteNode, "")
96
75
 
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])
76
+ async def handle(router, scope, receive, send):
77
+ assert scope["type"] in ("http", "websocket", "lifespan")
100
78
 
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])
79
+ if "router" not in scope:
80
+ scope["router"] = router
104
81
 
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])
82
+ if scope["type"] == "lifespan":
83
+ await router.lifespan(scope, receive, send)
84
+ return
108
85
 
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])
86
+ partial = None
112
87
 
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])
88
+ scope["path"] = route_path = starlette_utils.get_route_path(scope)
89
+ scope["root_path"] = ""
90
+ matched_routes = find_routes(route_path, root_node)
91
+ n = len(matched_routes)
116
92
 
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)
93
+ for i in range(n):
94
+ route = matched_routes[n - i - 1]
95
+ match, child_scope = route.matches(scope)
96
+ if match.value == 2:
97
+ scope.update(child_scope)
98
+ await route.handle(scope, receive, send)
125
99
  return
100
+ elif match.value == 1 and partial is None:
101
+ partial = route
102
+ partial_scope = child_scope
126
103
 
127
- partial = None
104
+ if partial is not None:
105
+ scope.update(partial_scope)
106
+ await partial.handle(scope, receive, send)
107
+ return
128
108
 
129
- route_path = get_route_path(scope)
130
- matched_routes = find_routes(route_path, self.root_node)
131
109
 
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)
110
+ if scope["type"] == "http" and router.redirect_slashes and route_path != "/":
111
+ redirect_scope = dict(scope)
112
+ if route_path.endswith("/"):
113
+ redirect_scope["path"] = redirect_scope["path"].rstrip("/")
114
+ else:
115
+ redirect_scope["path"] = redirect_scope["path"] + "/"
116
+
117
+ for i in range(n):
118
+ route = matched_routes[n - i - 1]
119
+ match, child_scope = route.matches(redirect_scope)
120
+ if match.value != 0:
121
+ redirect_url = URL(scope=redirect_scope)
122
+ response = RedirectResponse(url=str(redirect_url))
123
+ await response(scope, receive, send)
137
124
  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
125
 
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"] + "/"
126
+ await router.default(scope, receive, send)
154
127
 
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
128
 
163
- await self.default(scope, receive, send)
129
+ def install(app):
130
+ for route in app.routes:
131
+ add_route(route.path, root_node, route)
132
+ app.router.app = handle
Binary file
fastapi_extra/settings.py CHANGED
@@ -4,6 +4,7 @@ __date__ = "2024-12-26"
4
4
 
5
5
  from typing import Literal, Tuple, Type
6
6
 
7
+ from pydantic import model_validator
7
8
  from pydantic_settings import (BaseSettings, PydanticBaseSettingsSource,
8
9
  SettingsConfigDict, TomlConfigSettingsSource)
9
10
 
@@ -11,12 +12,15 @@ from pydantic_settings import (BaseSettings, PydanticBaseSettingsSource,
11
12
  class Settings(BaseSettings):
12
13
  model_config = SettingsConfigDict(
13
14
  toml_file=["config.default.toml", "config.custom.toml"],
14
- validate_default=False
15
+ validate_default=False,
16
+ extra="ignore"
15
17
  )
16
18
 
17
- title: str = ""
19
+ title: str = "FastAPI"
18
20
  version: str = "0.1.0"
19
21
  debug: bool = False
22
+ root_path: str = ""
23
+ include_in_schema: bool = True
20
24
  mode: Literal["dev", "test", "prod"] = "dev"
21
25
 
22
26
  @classmethod
@@ -34,3 +38,10 @@ class Settings(BaseSettings):
34
38
  init_settings,
35
39
  file_secret_settings
36
40
  )
41
+
42
+ @model_validator(mode="after")
43
+ def validate_mode(self) -> "Settings":
44
+ if self.mode == "prod":
45
+ self.include_in_schema = False
46
+ self.debug = False
47
+ return self
fastapi_extra/types.py CHANGED
@@ -4,7 +4,10 @@ __date__ = "2024-12-25"
4
4
 
5
5
  import datetime
6
6
  import decimal
7
- from typing import Any, TypeVar, Union
7
+ from typing import Annotated, Any, TypeVar, Union
8
+
9
+ from pydantic import PlainSerializer
10
+ from sqlmodel import SQLModel
8
11
 
9
12
  Comparable = Union[int, float, decimal.Decimal, datetime.datetime, datetime.date, datetime.time]
10
13
  Serializable = Union[Comparable, bool, str, None]
@@ -14,3 +17,11 @@ T = TypeVar("T", bound=Any)
14
17
  E = TypeVar("E", bound=Exception)
15
18
  C = TypeVar("C", bound=Comparable)
16
19
  S = TypeVar("S", bound=Serializable)
20
+ Model = TypeVar("Model", bound=SQLModel)
21
+
22
+ Cursor = Annotated[
23
+ int, PlainSerializer(lambda x: str(x), return_type=str)
24
+ ]
25
+ LocalDateTime = Annotated[
26
+ datetime.datetime, PlainSerializer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"), return_type=str)
27
+ ]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fastapi-extra
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: extra package for fastapi.
5
5
  Author-email: Ziyan Yin <408856732@qq.com>
6
6
  License: BSD-3-Clause
@@ -0,0 +1,16 @@
1
+ fastapi_extra/__init__.py,sha256=Q--tEaIQ892MOwYjKzWvrpUD2H3srBTbCBDRDBAFRqE,410
2
+ fastapi_extra/cursor.cp312-win_amd64.pyd,sha256=g21HsiTvEovTioMgGGC9VKnMRhtTH6qOOOxCh7w9ffg,57344
3
+ fastapi_extra/dependency.py,sha256=fs-WUmYImz8UUGC2LkOxn_3w1H_iVA4dVha8oiz6hKA,878
4
+ fastapi_extra/form.py,sha256=h6xaWF8sjnD4uinBRWnFbgWgaxerYjzUAROXQiXyFBk,1079
5
+ fastapi_extra/response.py,sha256=DHvhOSgwot5eBNKuI_jPYxZ5rshZ55Xkg-FNBJlHD1E,9609
6
+ fastapi_extra/routing.cp312-win_amd64.pyd,sha256=14ZPwBtxTMT2rHqp9sGq3vxDsfCO5O9geq8QlTdL1lI,94208
7
+ fastapi_extra/settings.py,sha256=cCcwaper5GiNNoT4gNKqf-iloSOTNnMsiUR0knJx4Mw,1461
8
+ fastapi_extra/types.py,sha256=EUjT9jFryzlazHvWs4m-IfUezmSEvyxwaOGe_vTTBnY,763
9
+ fastapi_extra/utils.py,sha256=tsPX3kpF_P5D9Bd3gnlG6rkVsLkv5gbxjml-s6ZL_6I,346
10
+ fastapi_extra/native/cursor.pyx,sha256=bESprFDgk9gGjyPQ4YCSg51dov2WB6s60XrOs3r5-r0,1146
11
+ fastapi_extra/native/routing.pyx,sha256=sByKvUrILFKkEky2nhlzA4vLJsmvk1B2cZs0s2yH1g0,3762
12
+ fastapi_extra-0.1.2.dist-info/LICENSE,sha256=0vTjHDa3VDsxTT-R-sH6SpYcA2F1hKtbX9ZFZQm-EcU,1516
13
+ fastapi_extra-0.1.2.dist-info/METADATA,sha256=aSjC76xJbexcUAHmFUPFabhn11xEaBcYkW5pK-mNwVI,1006
14
+ fastapi_extra-0.1.2.dist-info/WHEEL,sha256=cRmSBGD-cl98KkuHMNqv9Ac9L9_VqTvcBYwpIvxN0cg,101
15
+ fastapi_extra-0.1.2.dist-info/top_level.txt,sha256=B7D80bEftE2E-eSd1be2r9BWkLLMZN21dRTWpb4y4Ig,14
16
+ fastapi_extra-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-win_amd64
5
5
 
@@ -1,96 +0,0 @@
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()]
fastapi_extra/model.py DELETED
@@ -1,73 +0,0 @@
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"}
@@ -1,17 +0,0 @@
1
- fastapi_extra/__init__.py,sha256=DgMHXQo8iKj_jrge574Zb0iq9_zNmPKUWIVaigBvu5I,310
2
- fastapi_extra/cursor.cp312-win_amd64.pyd,sha256=c0rgFuCuy2msqHfTQxI7Nk64UvgnTrr0qAQpPyd_hzM,57344
3
- fastapi_extra/databases.py,sha256=KNMQUGn7ABsZfk7_XfkqNZ1zP8IkP1JoJf2MQxtmaBo,2639
4
- fastapi_extra/form.py,sha256=LikaJkA16dSRGCqX9K7z2S8-e3SJcMiVXX5nRZU_kVY,957
5
- fastapi_extra/model.py,sha256=_I-cp_E4Vm1D73OKGqNImiUNuKl9DvkhiONrGt5DEZg,2306
6
- fastapi_extra/response.py,sha256=DHvhOSgwot5eBNKuI_jPYxZ5rshZ55Xkg-FNBJlHD1E,9609
7
- fastapi_extra/routing.cp312-win_amd64.pyd,sha256=dCFyilewdtYWCBPulIWER6kWus3fntLBGJRaJzMFqMc,115200
8
- fastapi_extra/settings.py,sha256=gegO1r-lV3rO_qauk8J9L5qikn1ffKAp-Z9luyMv5iQ,1118
9
- fastapi_extra/types.py,sha256=FFX5831Bkjy5DGkgpiScAZuOv5AqrI58ReuP3aRvtp8,421
10
- fastapi_extra/utils.py,sha256=tsPX3kpF_P5D9Bd3gnlG6rkVsLkv5gbxjml-s6ZL_6I,346
11
- fastapi_extra/native/cursor.pyx,sha256=bESprFDgk9gGjyPQ4YCSg51dov2WB6s60XrOs3r5-r0,1146
12
- fastapi_extra/native/routing.pyx,sha256=Umgpiz6FMevIo7428LdRm_v2O1NHKEgXWxDqGiZUS3c,5115
13
- fastapi_extra-0.1.0.dist-info/LICENSE,sha256=0vTjHDa3VDsxTT-R-sH6SpYcA2F1hKtbX9ZFZQm-EcU,1516
14
- fastapi_extra-0.1.0.dist-info/METADATA,sha256=TNS1sgrLbwu85E9qVtgLcNofU16JRbtnGfJJy_HNqAk,1006
15
- fastapi_extra-0.1.0.dist-info/WHEEL,sha256=pWXrJbnZSH-J-PhYmKs2XNn4DHCPNBYq965vsBJBFvA,101
16
- fastapi_extra-0.1.0.dist-info/top_level.txt,sha256=B7D80bEftE2E-eSd1be2r9BWkLLMZN21dRTWpb4y4Ig,14
17
- fastapi_extra-0.1.0.dist-info/RECORD,,