fastapi-extra 0.1.8__cp312-cp312-win_amd64.whl → 0.2.0__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,4 +1,4 @@
1
- __version__ = "0.1.8"
1
+ __version__ = "0.2.0"
2
2
 
3
3
 
4
4
  from fastapi import FastAPI
@@ -1,7 +1,7 @@
1
1
  __author__ = "ziyan.yin"
2
2
  __date__ = "2025-01-10"
3
3
 
4
- from .redis import DefaultRedis as Redis
4
+ from .redis import RedisCli as Redis
5
5
 
6
6
  __all__ = [
7
7
  "Redis"
@@ -2,7 +2,7 @@ __author__ = "ziyan.yin"
2
2
  __date__ = "2025-01-17"
3
3
 
4
4
 
5
- from typing import Annotated, AsyncGenerator
5
+ from typing import Annotated, AsyncGenerator, Self
6
6
 
7
7
  from fastapi.params import Depends
8
8
  from pydantic import BaseModel, Field, RedisDsn
@@ -23,37 +23,37 @@ class DefaultRedisSettings(Settings):
23
23
 
24
24
 
25
25
  _settings = DefaultRedisSettings() # type: ignore
26
- _loaded_pools: list[ConnectionPool] = []
27
26
 
28
27
 
29
- class RedisCli(AbstractComponent):
28
+ class RedisPool(AbstractComponent):
30
29
  default_config = _settings.redis
31
30
 
32
-
33
31
  def __init__(self):
34
- self._pool = None
32
+ self._pool: ConnectionPool | None = None
35
33
 
36
- @property
37
- def pool(self) -> ConnectionPool:
38
- if not self._pool:
39
- self._pool = ConnectionPool.from_url(
40
- self.default_config.url,
41
- **self.default_config.model_dump(exclude_defaults=True, exclude={"url", "connection_kwargs"}),
42
- **self.default_config.connection_kwargs
43
- )
44
- _loaded_pools.append(self)
45
- return self._pool
46
-
34
+ @classmethod
35
+ def setup(cls, **options) -> Self:
36
+ redis = cls()
37
+ redis._pool = ConnectionPool.from_url(
38
+ cls.default_config.url,
39
+ **cls.default_config.model_dump(exclude_defaults=True, exclude={"url", "connection_kwargs"}),
40
+ **cls.default_config.connection_kwargs
41
+ **options
42
+ )
43
+ return redis
44
+
45
+ def get_client(self) -> Redis:
46
+ return Redis(connection_pool=self._pool)
47
47
 
48
- async def dispose() -> None:
49
- for redis_pool in _loaded_pools:
50
- redis_pool.aclose()
48
+ async def dispose(self) -> None:
49
+ if self._pool:
50
+ await self._pool.aclose()
51
51
 
52
52
 
53
53
 
54
- async def get_redis(redis_cli: RedisCli) -> AsyncGenerator[Redis, None]:
55
- async with Redis(connection_pool=redis_cli.pool) as redis:
56
- yield redis
54
+ async def get_redis(pool: RedisPool) -> AsyncGenerator[Redis, None]:
55
+ async with RedisPool.get_client as client:
56
+ yield client
57
57
 
58
58
 
59
- DefaultRedis = Annotated[Redis, Depends(get_redis)]
59
+ RedisCli = Annotated[Redis, Depends(get_redis)]
Binary file
@@ -5,10 +5,11 @@ __date__ = "2025-01-05"
5
5
  from fastapi_extra.database.model import SQLBase
6
6
  from fastapi_extra.database.service import ModelService
7
7
  from fastapi_extra.database.session import DefaultSession as Session
8
+ from fastapi_extra.database.session import SessionFactory
8
9
 
9
10
  __all__ = [
10
- "AsyncSessionMaker",
11
- "ModelService",
11
+ "SessionFactory",
12
12
  "Session",
13
- "SQLBase"
13
+ "SQLBase",
14
+ "ModelService"
14
15
  ]
@@ -56,7 +56,7 @@ class Versioned(SQLModel):
56
56
 
57
57
  @declared_attr # type: ignore
58
58
  def __mapper_args__(cls) -> dict:
59
- return {"version_id_col": "version_id"}
59
+ return {"version_id_col": cls.version_id}
60
60
 
61
61
 
62
62
  class Optime(SQLModel):
@@ -1,18 +1,20 @@
1
1
  __author__ = "ziyan.yin"
2
2
  __date__ = "2025-01-12"
3
3
 
4
+ from contextvars import ContextVar
4
5
  from typing import Any, Generic, Self, TypeVar
5
6
 
6
7
  from fastapi_extra.database.model import SQLModel
7
8
  from fastapi_extra.database.session import DefaultSession
8
- from fastapi_extra.dependency import AbstractDependency
9
+ from fastapi_extra.dependency import AbstractService
9
10
 
10
11
  Model = TypeVar("Model", bound=SQLModel)
11
12
 
12
13
 
13
- class ModelService(AbstractDependency, Generic[Model], annotated=False):
14
- __slot__ = ("session", )
14
+ class ModelService(AbstractService, Generic[Model], abstract=True):
15
+ __slot__ = ()
15
16
  __model__: Model
17
+ __session_container__ = ContextVar("__session_container__", default=None)
16
18
 
17
19
  @classmethod
18
20
  def __class_getitem__(cls, item: type[SQLModel]) -> Self:
@@ -27,7 +29,13 @@ class ModelService(AbstractDependency, Generic[Model], annotated=False):
27
29
  return SubService
28
30
 
29
31
  def __init__(self, session: DefaultSession):
30
- self.session = session
32
+ self.__session_container__.set(session)
33
+
34
+ @property
35
+ def session(self):
36
+ _session = self.__session_container__.get()
37
+ assert _session is not None, "Session is not initialized"
38
+ return _session
31
39
 
32
40
  async def get(self, ident: int | str, **kwargs: Any) -> Model | None:
33
41
  return await self.session.get(self.__model__, ident, **kwargs)
@@ -1,26 +1,68 @@
1
1
  __author__ = "ziyan.yin"
2
- __date__ = "2025-01-05"
2
+ __date__ = "2024-12-26"
3
3
 
4
-
5
- from typing import Annotated, AsyncGenerator, Generator
4
+ from typing import Annotated, AsyncGenerator, Literal, Self
6
5
 
7
6
  from fastapi.params import Depends
8
- from sqlmodel import Session
7
+ from pydantic import AnyUrl, BaseModel, Field
8
+ from sqlalchemy.ext.asyncio import create_async_engine
9
9
  from sqlmodel.ext.asyncio.session import AsyncSession
10
10
 
11
- from fastapi_extra.database.driver import DB, AsyncDB
11
+ from fastapi_extra.dependency import AbstractComponent
12
+ from fastapi_extra.settings import Settings
12
13
 
13
14
 
14
- async def get_async_session(db: AsyncDB) -> AsyncGenerator[AsyncSession, None]:
15
- async with db.session as session:
16
- yield session
17
- await session.commit()
15
+ class DatabaseConfig(BaseModel):
16
+ url: AnyUrl
17
+ echo: bool = False
18
+ echo_pool: bool = False
19
+ isolation_level: Literal[
20
+ "SERIALIZABLE",
21
+ "REPEATABLE READ",
22
+ "READ COMMITTED",
23
+ "READ UNCOMMITTED",
24
+ "AUTOCOMMIT",
25
+ ] | None = None
26
+ options: dict = Field(default_factory=dict)
27
+
28
+
29
+ class DefaultDatabaseSettings(Settings):
30
+ datasource: DatabaseConfig
31
+
18
32
 
33
+ _settings = DefaultDatabaseSettings() # type: ignore
19
34
 
20
- def get_session(db: DB) -> Generator[Session, None, None]:
21
- with db.session as session:
35
+
36
+ class SessionFactory(AbstractComponent):
37
+ __slot__ = ("_engine",)
38
+ default_config = _settings.datasource
39
+
40
+ def __init__(self):
41
+ self._engine = None
42
+
43
+ @classmethod
44
+ def setup(cls, **options) -> Self:
45
+ db = cls()
46
+ db._engine = create_async_engine(
47
+ url=str(cls.default_config.url),
48
+ **cls.default_config.model_dump(exclude_defaults=True, exclude={"url", "options"}),
49
+ **cls.default_config.options,
50
+ **options
51
+ )
52
+ return db
53
+
54
+ def create_session(self) -> AsyncSession:
55
+ return AsyncSession(self._engine)
56
+
57
+ async def dispose(self) -> None:
58
+ if self._engine:
59
+ await self._engine.dispose()
60
+
61
+
62
+ async def get_session(factory: SessionFactory) -> AsyncGenerator[AsyncSession, None]:
63
+ async with factory.create_session() as session:
22
64
  yield session
23
- session.commit()
65
+ await session.commit()
24
66
 
25
67
 
26
- DefaultSession = Annotated[AsyncSession, Depends(get_async_session)]
68
+ DefaultSession = Annotated[AsyncSession, Depends(get_session)]
@@ -1,36 +1,72 @@
1
1
  __author__ = "ziyan.yin"
2
2
  __date__ = "2025-01-05"
3
3
 
4
-
5
4
  from abc import ABCMeta
6
- from typing import Annotated, Any, Self
5
+ from functools import update_wrapper
6
+ from typing import Annotated, Any, Callable, ClassVar, Self, final
7
+
8
+ from fastapi import Depends, FastAPI, Request
9
+
7
10
 
8
- from fastapi.params import Depends
11
+ def async_wrapper(func: Callable):
12
+
13
+ async def func_wrapper(*args, **kwds):
14
+ return func(*args, **kwds)
15
+
16
+ return update_wrapper(func_wrapper, func)
9
17
 
10
18
 
11
19
  class DependencyMetaClass(ABCMeta):
20
+ __load__ = None
21
+ __token__ = None
12
22
 
13
23
  def __new__(
14
24
  mcs,
15
25
  name: str,
16
26
  bases: tuple[type, ...],
17
27
  attrs: dict,
18
- annotated: bool = True
28
+ abstract: bool = False
19
29
  ):
20
30
  new_cls = super().__new__(mcs, name, bases, attrs)
21
- if annotated:
22
- return Annotated[new_cls, Depends(new_cls)]
31
+ new_cls.__token__ = f"{new_cls.__module__}.{new_cls.__name__}"
32
+ if not abstract:
33
+ if not new_cls.__load__:
34
+ return Annotated[new_cls, Depends(async_wrapper(new_cls))]
35
+ return Annotated[new_cls, Depends(new_cls.__load__)]
23
36
  return new_cls
24
37
 
25
38
 
26
- class AbstractDependency(metaclass=DependencyMetaClass, annotated=False):
39
+ class AbstractComponent(metaclass=DependencyMetaClass, abstract=True):
27
40
  __slot__ = ()
41
+ __token__: ClassVar[str]
42
+
43
+ @classmethod
44
+ def setup(cls, *args: Any, **kwargs: Any) -> Self:
45
+ raise NotImplementedError
28
46
 
47
+ @final
48
+ @classmethod
49
+ def install(cls, app: FastAPI, *args: Any, **kwargs: Any) -> Self:
50
+ component = cls.setup(*args, **kwargs)
51
+ setattr(app.state, cls.__token__, component)
52
+ return component
29
53
 
30
- class AbstractComponent(AbstractDependency, annotated=False):
54
+ @final
55
+ @classmethod
56
+ async def __load__(cls, request: Request) -> Self:
57
+ assert hasattr(request.app.state, cls.__token__), f"{cls.__name__} must be installed in lifespan"
58
+ return getattr(request.app.state, cls.__token__)
59
+
60
+ async def dispose(self) -> None:
61
+ pass
62
+
63
+
64
+ class AbstractService(metaclass=DependencyMetaClass, abstract=True):
31
65
  __slot__ = ()
32
- __instance__: Any = None
33
-
66
+ __load__ = None
67
+ __instance__ = None
68
+ __token__: ClassVar[str]
69
+
34
70
  def __new__(cls, *args, **kwargs) -> Self:
35
71
  if cls.__instance__ is None:
36
72
  cls.__instance__ = super().__new__(cls)
Binary file
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fastapi-extra
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: extra package for fastapi.
5
5
  Author-email: Ziyan Yin <408856732@qq.com>
6
6
  License: BSD-3-Clause
@@ -32,3 +32,4 @@ Provides-Extra: pgsql
32
32
  Requires-Dist: asyncpg; extra == "pgsql"
33
33
  Provides-Extra: aiomysql
34
34
  Requires-Dist: aiomysql; extra == "aiomysql"
35
+ Dynamic: license-file
@@ -0,0 +1,23 @@
1
+ fastapi_extra/__init__.py,sha256=MXf66njTRTUqUMbjSkWGF6HxU-1Dm8UVgfC5qh-uehM,286
2
+ fastapi_extra/cursor.cp312-win_amd64.pyd,sha256=0HTvLo9ZEY-GwvATHIVeaU5x-B1nxhdA8EvCZ6WhOu8,53760
3
+ fastapi_extra/dependency.py,sha256=LtYnOTMyhOQUFSbNEViw7lxJvAFpPWbyuY4e2goSG-8,2130
4
+ fastapi_extra/form.py,sha256=Fs9uEDOQThjFroDVTrjWnIGJ107BgXCppIVTymwQLzg,1247
5
+ fastapi_extra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ fastapi_extra/response.py,sha256=DHvhOSgwot5eBNKuI_jPYxZ5rshZ55Xkg-FNBJlHD1E,9609
7
+ fastapi_extra/routing.cp312-win_amd64.pyd,sha256=XlIyK5YoFeGcNGb_Mv27BYdjEEYJh2qlJagu7dZIJHk,87552
8
+ fastapi_extra/settings.py,sha256=cCcwaper5GiNNoT4gNKqf-iloSOTNnMsiUR0knJx4Mw,1461
9
+ fastapi_extra/types.py,sha256=3z6gUnao6WZL76asZYmex20xfY9mvYA-RbnsxUcui30,819
10
+ fastapi_extra/utils.py,sha256=tsPX3kpF_P5D9Bd3gnlG6rkVsLkv5gbxjml-s6ZL_6I,346
11
+ fastapi_extra/cache/__init__.py,sha256=kq4b_AYKCSJ0fEp4rqpeaoNJilko4XbtfC81xzUaYGI,122
12
+ fastapi_extra/cache/redis.py,sha256=-hr2DRkmruzH4cIArdjsytqRiiWWtDJt9GIDy38wmtQ,1600
13
+ fastapi_extra/database/__init__.py,sha256=pCYUoEylTponWqpR0AXJHcRo_cs4fnEXVVV9B5J9rt0,384
14
+ fastapi_extra/database/model.py,sha256=2lDi9PQ5F0PSM7BGZozZf1wSefpXnTWqAVzEyGPaXRI,2453
15
+ fastapi_extra/database/service.py,sha256=-tBC17jEbGZhnxdtWyftP2BUXfCwcOtxzNZ53qLp518,1798
16
+ fastapi_extra/database/session.py,sha256=PALArHhXNS2gCgeMkiKjyatvINV_VW1LjgEIwXDKyfQ,1903
17
+ fastapi_extra/native/cursor.pyx,sha256=bESprFDgk9gGjyPQ4YCSg51dov2WB6s60XrOs3r5-r0,1146
18
+ fastapi_extra/native/routing.pyx,sha256=GrdGAoBospwCpxMHBon5cuRYcz9ifAFSSYa2Ytf49lg,3841
19
+ fastapi_extra-0.2.0.dist-info/licenses/LICENSE,sha256=0vTjHDa3VDsxTT-R-sH6SpYcA2F1hKtbX9ZFZQm-EcU,1516
20
+ fastapi_extra-0.2.0.dist-info/METADATA,sha256=CRLVgSMLpfE9MQ-pj1aEAdC_c_7wCtGG9pCA2ARn-ts,1371
21
+ fastapi_extra-0.2.0.dist-info/WHEEL,sha256=RYNUKzg4pggqpqERKe4OLbPF4ZPP-Ng-rmq_sekLDXg,101
22
+ fastapi_extra-0.2.0.dist-info/top_level.txt,sha256=B7D80bEftE2E-eSd1be2r9BWkLLMZN21dRTWpb4y4Ig,14
23
+ fastapi_extra-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-win_amd64
5
5
 
@@ -1,83 +0,0 @@
1
- __author__ = "ziyan.yin"
2
- __date__ = "2024-12-26"
3
-
4
-
5
- from typing import Literal
6
-
7
- from pydantic import AnyUrl, BaseModel, Field
8
- from sqlalchemy import Engine, NullPool
9
- from sqlalchemy.ext.asyncio import AsyncEngine
10
- from sqlalchemy.util import _concurrency_py3k
11
- from sqlmodel import Session, create_engine
12
- from sqlmodel.ext.asyncio.session import AsyncSession
13
-
14
- from fastapi_extra.dependency import AbstractComponent
15
- from fastapi_extra.settings import Settings
16
-
17
-
18
- class DatabaseConfig(BaseModel):
19
- url: AnyUrl
20
- echo: bool = False
21
- echo_pool: bool = False
22
- isolation_level: Literal[
23
- "SERIALIZABLE",
24
- "REPEATABLE READ",
25
- "READ COMMITTED",
26
- "READ UNCOMMITTED",
27
- "AUTOCOMMIT",
28
- ] | None = None
29
- options: dict = Field(default_factory=dict)
30
-
31
-
32
- class DefaultDatabaseSettings(Settings):
33
- datasource: DatabaseConfig
34
-
35
-
36
- _settings = DefaultDatabaseSettings() # type: ignore
37
- _loaded_engines: list[Engine] = []
38
-
39
-
40
- class DB(AbstractComponent):
41
- default_config = _settings.datasource
42
- default_options = {}
43
-
44
- def __init__(self):
45
- self._engine = None
46
-
47
- @property
48
- def engine(self) -> Engine:
49
- if not self._engine:
50
- self._engine = create_engine(
51
- url=str(self.default_config.url),
52
- **self.default_config.model_dump(exclude_defaults=True, exclude={"url", "options"}),
53
- **self.default_config.options,
54
- **self.default_options
55
- )
56
- _loaded_engines.append(self._engine)
57
- return self._engine
58
-
59
- @property
60
- def session(self) -> Session:
61
- return Session(self.engine)
62
-
63
-
64
- class AsyncDB(DB):
65
-
66
- @property
67
- def engine(self) -> AsyncEngine:
68
- if not self._engine:
69
- self._engine = AsyncEngine(super().engine)
70
- return self._engine
71
-
72
- @property
73
- def session(self) -> AsyncSession:
74
- return AsyncSession(self.engine)
75
-
76
-
77
- if _settings.mode == "test":
78
- DB.default_options = {"poolclass": NullPool}
79
-
80
-
81
- async def dispose() -> None:
82
- for engine in _loaded_engines:
83
- await _concurrency_py3k.greenlet_spawn(engine.dispose)
@@ -1,24 +0,0 @@
1
- fastapi_extra/__init__.py,sha256=VmSUMSzhrVb6zSyHIl2-mi71gyGRMC9n400N5Gr3aT8,286
2
- fastapi_extra/cursor.cp312-win_amd64.pyd,sha256=cuMCDVcrH1ltvx1s2zm2PXsbkj20hP-plud-YBKI9D8,57344
3
- fastapi_extra/dependency.py,sha256=8SgAky63hMa9pnttqRRZMyPAa3BwBMUvFA3ZLH86h44,910
4
- fastapi_extra/form.py,sha256=Fs9uEDOQThjFroDVTrjWnIGJ107BgXCppIVTymwQLzg,1247
5
- fastapi_extra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- fastapi_extra/response.py,sha256=DHvhOSgwot5eBNKuI_jPYxZ5rshZ55Xkg-FNBJlHD1E,9609
7
- fastapi_extra/routing.cp312-win_amd64.pyd,sha256=6ZHARNFCgL_tlmp6WPSZMR07yrlPe2WAg5jpeXFQTfc,94720
8
- fastapi_extra/settings.py,sha256=cCcwaper5GiNNoT4gNKqf-iloSOTNnMsiUR0knJx4Mw,1461
9
- fastapi_extra/types.py,sha256=3z6gUnao6WZL76asZYmex20xfY9mvYA-RbnsxUcui30,819
10
- fastapi_extra/utils.py,sha256=tsPX3kpF_P5D9Bd3gnlG6rkVsLkv5gbxjml-s6ZL_6I,346
11
- fastapi_extra/cache/__init__.py,sha256=2bwWFRf6giDo0QiFWEvekQwga9kGTK_9BJdxe32Nru8,126
12
- fastapi_extra/cache/redis.py,sha256=fLNJfL8V-HYek38WVNwxvW6cnd7rJpcd62vy4O07C44,1592
13
- fastapi_extra/database/__init__.py,sha256=B59umaoNjDuXyoNh7EYWYEk4xr9tfgVjXsSaOPz3y_Q,328
14
- fastapi_extra/database/driver.py,sha256=ar_vnfqjpoWjiW5eEdrisyi9CrhfdHtbhAGFxGMKC5E,2223
15
- fastapi_extra/database/model.py,sha256=2ISHT0NUz3-uoqlogxrthzM6GxAfPdghzx1j-lvkSRQ,2451
16
- fastapi_extra/database/service.py,sha256=TdKlcTQ_WoMqy-kmlYUcFKF7XpyFL8O4vkQ2glZc7P4,1495
17
- fastapi_extra/database/session.py,sha256=cpKj_NDBSATRCBbmfyYa4v-TKGrMMgRJQCEnkCR153s,691
18
- fastapi_extra/native/cursor.pyx,sha256=bESprFDgk9gGjyPQ4YCSg51dov2WB6s60XrOs3r5-r0,1146
19
- fastapi_extra/native/routing.pyx,sha256=GrdGAoBospwCpxMHBon5cuRYcz9ifAFSSYa2Ytf49lg,3841
20
- fastapi_extra-0.1.8.dist-info/LICENSE,sha256=0vTjHDa3VDsxTT-R-sH6SpYcA2F1hKtbX9ZFZQm-EcU,1516
21
- fastapi_extra-0.1.8.dist-info/METADATA,sha256=Mg9fJV2_mAo_PZXq3JCq_Mv9t69noysoU7HmNnN1wzA,1348
22
- fastapi_extra-0.1.8.dist-info/WHEEL,sha256=A8mRFNvJcDL8dRPZ6k2ICKEOXwE8pzYFXYxEla0rR0g,101
23
- fastapi_extra-0.1.8.dist-info/top_level.txt,sha256=B7D80bEftE2E-eSd1be2r9BWkLLMZN21dRTWpb4y4Ig,14
24
- fastapi_extra-0.1.8.dist-info/RECORD,,