fastapi-async-sqlalchemy 0.8.0a1__tar.gz → 0.8.0b1__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 (38) hide show
  1. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/PKG-INFO +19 -1
  2. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/README.md +18 -0
  3. fastapi_async_sqlalchemy-0.8.0b1/fastapi_async_sqlalchemy/__init__.py +32 -0
  4. fastapi_async_sqlalchemy-0.8.0b1/fastapi_async_sqlalchemy/_types.py +63 -0
  5. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy/middleware.py +12 -2
  6. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/PKG-INFO +19 -1
  7. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/SOURCES.txt +1 -0
  8. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_type_hints_compatibility.py +31 -0
  9. fastapi_async_sqlalchemy-0.8.0a1/fastapi_async_sqlalchemy/__init__.py +0 -22
  10. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/LICENSE +0 -0
  11. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy/exceptions.py +0 -0
  12. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy/py.typed +0 -0
  13. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/dependency_links.txt +0 -0
  14. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/not-zip-safe +0 -0
  15. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/requires.txt +0 -0
  16. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/fastapi_async_sqlalchemy.egg-info/top_level.txt +0 -0
  17. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/pyproject.toml +0 -0
  18. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/setup.cfg +0 -0
  19. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/setup.py +0 -0
  20. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_additional_coverage.py +0 -0
  21. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_backward_compat_gather.py +0 -0
  22. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_concurrent_queries.py +0 -0
  23. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_coverage_boost.py +0 -0
  24. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_coverage_improvements.py +0 -0
  25. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_custom_engine_branch.py +0 -0
  26. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_edge_cases_coverage.py +0 -0
  27. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_full_coverage.py +0 -0
  28. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_import_fallback_simulation.py +0 -0
  29. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_import_fallbacks.py +0 -0
  30. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_import_without_sqlmodel.py +0 -0
  31. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_maximum_coverage.py +0 -0
  32. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_multi_session_fixes.py +0 -0
  33. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_pool_throttling.py +0 -0
  34. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_resource_lifecycle.py +0 -0
  35. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_session.py +0 -0
  36. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_single_session_no_gather.py +0 -0
  37. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_sqlmodel.py +0 -0
  38. {fastapi_async_sqlalchemy-0.8.0a1 → fastapi_async_sqlalchemy-0.8.0b1}/tests/test_streaming_and_waiter_shutdown.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.8.0a1
3
+ Version: 0.8.0b1
4
4
  Summary: SQLAlchemy middleware for FastAPI
5
5
  Home-page: https://github.com/h0rn3t/fastapi-async-sqlalchemy.git
6
6
  Author: Eugene Shershen
@@ -219,6 +219,24 @@ above. This keeps database access available for the whole body while making it
219
219
  clear that the session lifetime belongs to the stream, not the original request
220
220
  transaction.
221
221
 
222
+ #### Type hints for `db`
223
+
224
+ Use `DBSessionMeta` (or its alias `DBSessionType`) when you need to annotate
225
+ a function or attribute that holds the `db` proxy:
226
+
227
+ ```python
228
+ from fastapi_async_sqlalchemy import DBSessionMeta, db
229
+
230
+ def get_db() -> DBSessionMeta:
231
+ return db
232
+ ```
233
+
234
+ At runtime `DBSessionMeta` is the metaclass of `db`, so `isinstance(db,
235
+ DBSessionMeta)` and `type(db) is DBSessionMeta` both work. For static type
236
+ checkers (mypy, pyright) and IDE autocompletion `DBSessionMeta` resolves to
237
+ a structural `Protocol` describing the public API (`session`, `connection`,
238
+ `gather`, and the `db(...)` call).
239
+
222
240
  #### SQLAlchemy events (`before_insert`, `after_insert`, ...)
223
241
 
224
242
  SQLAlchemy's event system is independent of the session/engine — register
@@ -176,6 +176,24 @@ above. This keeps database access available for the whole body while making it
176
176
  clear that the session lifetime belongs to the stream, not the original request
177
177
  transaction.
178
178
 
179
+ #### Type hints for `db`
180
+
181
+ Use `DBSessionMeta` (or its alias `DBSessionType`) when you need to annotate
182
+ a function or attribute that holds the `db` proxy:
183
+
184
+ ```python
185
+ from fastapi_async_sqlalchemy import DBSessionMeta, db
186
+
187
+ def get_db() -> DBSessionMeta:
188
+ return db
189
+ ```
190
+
191
+ At runtime `DBSessionMeta` is the metaclass of `db`, so `isinstance(db,
192
+ DBSessionMeta)` and `type(db) is DBSessionMeta` both work. For static type
193
+ checkers (mypy, pyright) and IDE autocompletion `DBSessionMeta` resolves to
194
+ a structural `Protocol` describing the public API (`session`, `connection`,
195
+ `gather`, and the `db(...)` call).
196
+
179
197
  #### SQLAlchemy events (`before_insert`, `after_insert`, ...)
180
198
 
181
199
  SQLAlchemy's event system is independent of the session/engine — register
@@ -0,0 +1,32 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from fastapi_async_sqlalchemy.middleware import (
4
+ SQLAlchemyMiddleware,
5
+ create_middleware_and_session_proxy,
6
+ db,
7
+ )
8
+
9
+ # DBSessionMeta exposure (Issue #18).
10
+ #
11
+ # At type-check time, expose a structural Protocol so users get full mypy /
12
+ # IDE autocomplete on ``db.session``, ``db.connection()`` and ``db.gather()``
13
+ # when they annotate code with ``DBSessionMeta``.
14
+ #
15
+ # At runtime, expose the actual metaclass of ``db`` (a closure-local class
16
+ # created by ``create_middleware_and_session_proxy``) so ``isinstance(db,
17
+ # DBSessionMeta)`` and ``type(db) is DBSessionMeta`` keep working as in v0.5.
18
+ if TYPE_CHECKING:
19
+ from fastapi_async_sqlalchemy._types import DBSessionMeta, DBSessionType
20
+ else:
21
+ DBSessionMeta = type(db)
22
+ DBSessionType = DBSessionMeta
23
+
24
+ __all__ = [
25
+ "db",
26
+ "SQLAlchemyMiddleware",
27
+ "create_middleware_and_session_proxy",
28
+ "DBSessionMeta",
29
+ "DBSessionType",
30
+ ]
31
+
32
+ __version__ = "0.8.0b1"
@@ -0,0 +1,63 @@
1
+ """Type hints for the public ``db`` session proxy (Issue #18).
2
+
3
+ The runtime ``DBSessionMeta`` is the metaclass produced by
4
+ ``create_middleware_and_session_proxy`` and lives inside that closure, so it is
5
+ not visible to static type checkers. This module exposes a structural
6
+ ``Protocol`` with the same public API so users can annotate code as::
7
+
8
+ def get_db() -> DBSessionMeta:
9
+ return db
10
+
11
+ and get full IDE autocomplete / mypy support on ``db.session``,
12
+ ``db.connection()`` and ``db.gather()``.
13
+
14
+ At runtime ``fastapi_async_sqlalchemy.DBSessionMeta`` is rebound to
15
+ ``type(db)`` (the actual metaclass) so ``isinstance(db, DBSessionMeta)``
16
+ keeps working as before.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from contextlib import AbstractAsyncContextManager
22
+ from typing import Any, Protocol, runtime_checkable
23
+
24
+ from sqlalchemy.ext.asyncio import AsyncSession
25
+
26
+
27
+ @runtime_checkable
28
+ class DBSessionMeta(Protocol):
29
+ """Structural type for the ``db`` session proxy.
30
+
31
+ Use as a return / parameter annotation when you want to pass ``db``
32
+ around with proper type hints instead of falling back to ``Any``.
33
+ """
34
+
35
+ @property
36
+ def session(self) -> AsyncSession:
37
+ """Return the ``AsyncSession`` bound to the current async context."""
38
+ ...
39
+
40
+ def connection(self) -> AbstractAsyncContextManager[AsyncSession]:
41
+ """Async context manager that yields a throttled session."""
42
+ ...
43
+
44
+ async def gather(
45
+ self,
46
+ *coros_or_futures: Any,
47
+ return_exceptions: bool = ...,
48
+ ) -> list[Any]:
49
+ """Pool-aware drop-in replacement for ``asyncio.gather``."""
50
+ ...
51
+
52
+ def __call__(
53
+ self,
54
+ session_args: dict[str, Any] | None = ...,
55
+ commit_on_exit: bool = ...,
56
+ multi_sessions: bool = ...,
57
+ max_concurrent: int | None = ...,
58
+ ) -> AbstractAsyncContextManager[Any]:
59
+ """Open an explicit session context: ``async with db(): ...``."""
60
+ ...
61
+
62
+
63
+ DBSessionType = DBSessionMeta
@@ -5,6 +5,7 @@ import logging
5
5
  import warnings
6
6
  from contextvars import ContextVar
7
7
  from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, cast
8
9
 
9
10
  from sqlalchemy.engine.url import URL
10
11
  from sqlalchemy.ext.asyncio import (
@@ -20,6 +21,11 @@ from fastapi_async_sqlalchemy.exceptions import (
20
21
  SessionNotInitialisedError,
21
22
  )
22
23
 
24
+ if TYPE_CHECKING:
25
+ # Imported under an alias to avoid colliding with the closure-local
26
+ # `DBSessionMeta` metaclass defined inside ``create_middleware_and_session_proxy``.
27
+ from fastapi_async_sqlalchemy._types import DBSessionMeta as _DBSessionMetaProtocol
28
+
23
29
  try:
24
30
  from sqlmodel.ext.asyncio.session import AsyncSession as SQLModelAsyncSession
25
31
 
@@ -28,7 +34,7 @@ except ImportError:
28
34
  DefaultAsyncSession: type[AsyncSession] = AsyncSession # type: ignore
29
35
 
30
36
 
31
- def create_middleware_and_session_proxy() -> tuple:
37
+ def create_middleware_and_session_proxy() -> tuple[type, _DBSessionMetaProtocol]:
32
38
  _Session: async_sessionmaker | None = None
33
39
  _Session_engine: AsyncEngine | None = None
34
40
  _session: ContextVar[AsyncSession | None] = ContextVar("_session", default=None)
@@ -771,7 +777,11 @@ def create_middleware_and_session_proxy() -> tuple:
771
777
  _request_session.reset(self.request_session_token)
772
778
  _session.reset(self.token)
773
779
 
774
- return _SQLAlchemyMiddleware, DBSession
780
+ # `db` is the `DBSession` class itself; its public API (`session`,
781
+ # `connection`, `gather`, `__call__`) lives on the metaclass. Cast to the
782
+ # `DBSessionMeta` Protocol so static type checkers see that surface
783
+ # instead of the class object's own attributes.
784
+ return _SQLAlchemyMiddleware, cast("_DBSessionMetaProtocol", DBSession)
775
785
 
776
786
 
777
787
  SQLAlchemyMiddleware, db = create_middleware_and_session_proxy()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.8.0a1
3
+ Version: 0.8.0b1
4
4
  Summary: SQLAlchemy middleware for FastAPI
5
5
  Home-page: https://github.com/h0rn3t/fastapi-async-sqlalchemy.git
6
6
  Author: Eugene Shershen
@@ -219,6 +219,24 @@ above. This keeps database access available for the whole body while making it
219
219
  clear that the session lifetime belongs to the stream, not the original request
220
220
  transaction.
221
221
 
222
+ #### Type hints for `db`
223
+
224
+ Use `DBSessionMeta` (or its alias `DBSessionType`) when you need to annotate
225
+ a function or attribute that holds the `db` proxy:
226
+
227
+ ```python
228
+ from fastapi_async_sqlalchemy import DBSessionMeta, db
229
+
230
+ def get_db() -> DBSessionMeta:
231
+ return db
232
+ ```
233
+
234
+ At runtime `DBSessionMeta` is the metaclass of `db`, so `isinstance(db,
235
+ DBSessionMeta)` and `type(db) is DBSessionMeta` both work. For static type
236
+ checkers (mypy, pyright) and IDE autocompletion `DBSessionMeta` resolves to
237
+ a structural `Protocol` describing the public API (`session`, `connection`,
238
+ `gather`, and the `db(...)` call).
239
+
222
240
  #### SQLAlchemy events (`before_insert`, `after_insert`, ...)
223
241
 
224
242
  SQLAlchemy's event system is independent of the session/engine — register
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  setup.py
5
5
  fastapi_async_sqlalchemy/__init__.py
6
+ fastapi_async_sqlalchemy/_types.py
6
7
  fastapi_async_sqlalchemy/exceptions.py
7
8
  fastapi_async_sqlalchemy/middleware.py
8
9
  fastapi_async_sqlalchemy/py.typed
@@ -210,3 +210,34 @@ def test_multiple_imports():
210
210
  assert db1 is db2
211
211
  assert type(db1) is Meta1
212
212
  assert type(db2) is Meta2
213
+
214
+
215
+ def test_protocol_describes_public_api():
216
+ """The static-typing Protocol must list every public attribute of `db`.
217
+
218
+ Issue #18 — guarantee that mypy/IDE see the full public surface of the
219
+ proxy via the `DBSessionMeta` annotation, not just an opaque `type`.
220
+ """
221
+ from fastapi_async_sqlalchemy._types import DBSessionMeta as ProtocolMeta
222
+
223
+ # Protocol attributes that mypy / IDEs will see.
224
+ expected = {"session", "connection", "gather", "__call__"}
225
+ declared = set(getattr(ProtocolMeta, "__protocol_attrs__", set()))
226
+ assert expected.issubset(declared), f"Protocol is missing expected attrs: {expected - declared}"
227
+
228
+ # Each declared attribute must actually exist on `type(db)` so the
229
+ # annotation is structurally honest.
230
+ for attr in expected:
231
+ assert hasattr(type(db), attr), f"type(db) is missing public attr {attr!r}"
232
+
233
+
234
+ def test_protocol_runtime_checkable_against_db():
235
+ """`isinstance(db, DBSessionMeta)` must succeed via the Protocol path too."""
236
+ from fastapi_async_sqlalchemy._types import DBSessionMeta as ProtocolMeta
237
+
238
+ assert isinstance(db, ProtocolMeta)
239
+
240
+ from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
241
+
242
+ _, custom_db = create_middleware_and_session_proxy()
243
+ assert isinstance(custom_db, ProtocolMeta)
@@ -1,22 +0,0 @@
1
- from fastapi_async_sqlalchemy.middleware import (
2
- SQLAlchemyMiddleware,
3
- create_middleware_and_session_proxy,
4
- db,
5
- )
6
-
7
- # Export DBSessionMeta type for type hints (Issue #18)
8
- # Note: DBSessionMeta is the metaclass of db, created dynamically.
9
- # It can be used in runtime type checks (isinstance, type(db) is DBSessionMeta)
10
- # but mypy may show warnings when used in type annotations due to its dynamic nature.
11
- DBSessionMeta = type(db)
12
- DBSessionType = DBSessionMeta # Alternative name for backwards compatibility
13
-
14
- __all__ = [
15
- "db",
16
- "SQLAlchemyMiddleware",
17
- "create_middleware_and_session_proxy",
18
- "DBSessionMeta",
19
- "DBSessionType",
20
- ]
21
-
22
- __version__ = "0.8.0a1"