fastapi-extra 0.1.9__tar.gz → 0.2.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.
Files changed (30) hide show
  1. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/PKG-INFO +3 -2
  2. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/__init__.py +1 -1
  3. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/cache/__init__.py +1 -1
  4. fastapi_extra-0.2.0/fastapi_extra/cache/redis.py +59 -0
  5. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/database/__init__.py +4 -3
  6. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/database/service.py +12 -4
  7. fastapi_extra-0.2.0/fastapi_extra/database/session.py +68 -0
  8. fastapi_extra-0.2.0/fastapi_extra/dependency.py +73 -0
  9. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra.egg-info/PKG-INFO +3 -2
  10. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra.egg-info/SOURCES.txt +0 -1
  11. fastapi_extra-0.1.9/fastapi_extra/cache/redis.py +0 -59
  12. fastapi_extra-0.1.9/fastapi_extra/database/driver.py +0 -83
  13. fastapi_extra-0.1.9/fastapi_extra/database/session.py +0 -26
  14. fastapi_extra-0.1.9/fastapi_extra/dependency.py +0 -37
  15. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/LICENSE +0 -0
  16. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/README.rst +0 -0
  17. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/database/model.py +0 -0
  18. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/form.py +0 -0
  19. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/native/cursor.pyx +0 -0
  20. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/native/routing.pyx +0 -0
  21. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/py.typed +0 -0
  22. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/response.py +0 -0
  23. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/settings.py +0 -0
  24. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/types.py +0 -0
  25. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra/utils.py +0 -0
  26. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra.egg-info/dependency_links.txt +0 -0
  27. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra.egg-info/requires.txt +0 -0
  28. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/fastapi_extra.egg-info/top_level.txt +0 -0
  29. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/pyproject.toml +0 -0
  30. {fastapi_extra-0.1.9 → fastapi_extra-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fastapi-extra
3
- Version: 0.1.9
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
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.9"
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"
@@ -0,0 +1,59 @@
1
+ __author__ = "ziyan.yin"
2
+ __date__ = "2025-01-17"
3
+
4
+
5
+ from typing import Annotated, AsyncGenerator, Self
6
+
7
+ from fastapi.params import Depends
8
+ from pydantic import BaseModel, Field, RedisDsn
9
+ from redis.asyncio import ConnectionPool, Redis
10
+
11
+ from fastapi_extra.dependency import AbstractComponent
12
+ from fastapi_extra.settings import Settings
13
+
14
+
15
+ class RedisConfig(BaseModel):
16
+ url: RedisDsn = RedisDsn("redis://localhost:6379/0")
17
+ max_connections: int | None = None
18
+ connection_kwargs: dict = Field(default_factory=dict)
19
+
20
+
21
+ class DefaultRedisSettings(Settings):
22
+ redis: RedisConfig
23
+
24
+
25
+ _settings = DefaultRedisSettings() # type: ignore
26
+
27
+
28
+ class RedisPool(AbstractComponent):
29
+ default_config = _settings.redis
30
+
31
+ def __init__(self):
32
+ self._pool: ConnectionPool | None = None
33
+
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
+
48
+ async def dispose(self) -> None:
49
+ if self._pool:
50
+ await self._pool.aclose()
51
+
52
+
53
+
54
+ async def get_redis(pool: RedisPool) -> AsyncGenerator[Redis, None]:
55
+ async with RedisPool.get_client as client:
56
+ yield client
57
+
58
+
59
+ RedisCli = Annotated[Redis, Depends(get_redis)]
@@ -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
  ]
@@ -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)
@@ -0,0 +1,68 @@
1
+ __author__ = "ziyan.yin"
2
+ __date__ = "2024-12-26"
3
+
4
+ from typing import Annotated, AsyncGenerator, Literal, Self
5
+
6
+ from fastapi.params import Depends
7
+ from pydantic import AnyUrl, BaseModel, Field
8
+ from sqlalchemy.ext.asyncio import create_async_engine
9
+ from sqlmodel.ext.asyncio.session import AsyncSession
10
+
11
+ from fastapi_extra.dependency import AbstractComponent
12
+ from fastapi_extra.settings import Settings
13
+
14
+
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
+
32
+
33
+ _settings = DefaultDatabaseSettings() # type: ignore
34
+
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:
64
+ yield session
65
+ await session.commit()
66
+
67
+
68
+ DefaultSession = Annotated[AsyncSession, Depends(get_session)]
@@ -0,0 +1,73 @@
1
+ __author__ = "ziyan.yin"
2
+ __date__ = "2025-01-05"
3
+
4
+ from abc import ABCMeta
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
+
10
+
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)
17
+
18
+
19
+ class DependencyMetaClass(ABCMeta):
20
+ __load__ = None
21
+ __token__ = None
22
+
23
+ def __new__(
24
+ mcs,
25
+ name: str,
26
+ bases: tuple[type, ...],
27
+ attrs: dict,
28
+ abstract: bool = False
29
+ ):
30
+ new_cls = super().__new__(mcs, name, bases, attrs)
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__)]
36
+ return new_cls
37
+
38
+
39
+ class AbstractComponent(metaclass=DependencyMetaClass, abstract=True):
40
+ __slot__ = ()
41
+ __token__: ClassVar[str]
42
+
43
+ @classmethod
44
+ def setup(cls, *args: Any, **kwargs: Any) -> Self:
45
+ raise NotImplementedError
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
53
+
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):
65
+ __slot__ = ()
66
+ __load__ = None
67
+ __instance__ = None
68
+ __token__: ClassVar[str]
69
+
70
+ def __new__(cls, *args, **kwargs) -> Self:
71
+ if cls.__instance__ is None:
72
+ cls.__instance__ = super().__new__(cls)
73
+ return cls.__instance__
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fastapi-extra
3
- Version: 0.1.9
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
@@ -19,7 +19,6 @@ fastapi_extra/native/routing.pyx
19
19
  fastapi_extra/cache/__init__.py
20
20
  fastapi_extra/cache/redis.py
21
21
  fastapi_extra/database/__init__.py
22
- fastapi_extra/database/driver.py
23
22
  fastapi_extra/database/model.py
24
23
  fastapi_extra/database/service.py
25
24
  fastapi_extra/database/session.py
@@ -1,59 +0,0 @@
1
- __author__ = "ziyan.yin"
2
- __date__ = "2025-01-17"
3
-
4
-
5
- from typing import Annotated, AsyncGenerator
6
-
7
- from fastapi.params import Depends
8
- from pydantic import BaseModel, Field, RedisDsn
9
- from redis.asyncio import ConnectionPool, Redis
10
-
11
- from fastapi_extra.dependency import AbstractComponent
12
- from fastapi_extra.settings import Settings
13
-
14
-
15
- class RedisConfig(BaseModel):
16
- url: RedisDsn = RedisDsn("redis://localhost:6379/0")
17
- max_connections: int | None = None
18
- connection_kwargs: dict = Field(default_factory=dict)
19
-
20
-
21
- class DefaultRedisSettings(Settings):
22
- redis: RedisConfig
23
-
24
-
25
- _settings = DefaultRedisSettings() # type: ignore
26
- _loaded_pools: list[ConnectionPool] = []
27
-
28
-
29
- class RedisCli(AbstractComponent):
30
- default_config = _settings.redis
31
-
32
-
33
- def __init__(self):
34
- self._pool = None
35
-
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
-
47
-
48
- async def dispose() -> None:
49
- for redis_pool in _loaded_pools:
50
- redis_pool.aclose()
51
-
52
-
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
57
-
58
-
59
- DefaultRedis = Annotated[Redis, Depends(get_redis)]
@@ -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,26 +0,0 @@
1
- __author__ = "ziyan.yin"
2
- __date__ = "2025-01-05"
3
-
4
-
5
- from typing import Annotated, AsyncGenerator, Generator
6
-
7
- from fastapi.params import Depends
8
- from sqlmodel import Session
9
- from sqlmodel.ext.asyncio.session import AsyncSession
10
-
11
- from fastapi_extra.database.driver import DB, AsyncDB
12
-
13
-
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()
18
-
19
-
20
- def get_session(db: DB) -> Generator[Session, None, None]:
21
- with db.session as session:
22
- yield session
23
- session.commit()
24
-
25
-
26
- DefaultSession = Annotated[AsyncSession, Depends(get_async_session)]
@@ -1,37 +0,0 @@
1
- __author__ = "ziyan.yin"
2
- __date__ = "2025-01-05"
3
-
4
-
5
- from abc import ABCMeta
6
- from typing import Annotated, Any, Self
7
-
8
- from fastapi.params import Depends
9
-
10
-
11
- class DependencyMetaClass(ABCMeta):
12
-
13
- def __new__(
14
- mcs,
15
- name: str,
16
- bases: tuple[type, ...],
17
- attrs: dict,
18
- annotated: bool = True
19
- ):
20
- new_cls = super().__new__(mcs, name, bases, attrs)
21
- if annotated:
22
- return Annotated[new_cls, Depends(new_cls)]
23
- return new_cls
24
-
25
-
26
- class AbstractDependency(metaclass=DependencyMetaClass, annotated=False):
27
- __slot__ = ()
28
-
29
-
30
- class AbstractComponent(AbstractDependency, annotated=False):
31
- __slot__ = ()
32
- __instance__: Any = None
33
-
34
- def __new__(cls, *args, **kwargs) -> Self:
35
- if cls.__instance__ is None:
36
- cls.__instance__ = super().__new__(cls)
37
- return cls.__instance__
File without changes
File without changes
File without changes