fastapi-async-sqlalchemy 0.7.0.dev5__tar.gz → 0.7.1.post1__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 (31) hide show
  1. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/PKG-INFO +2 -4
  2. fastapi_async_sqlalchemy-0.7.1.post1/fastapi_async_sqlalchemy/__init__.py +22 -0
  3. fastapi_async_sqlalchemy-0.7.1.post1/fastapi_async_sqlalchemy/middleware.py +211 -0
  4. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/PKG-INFO +2 -4
  5. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/SOURCES.txt +10 -1
  6. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/pyproject.toml +7 -1
  7. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/setup.py +1 -3
  8. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/tests/test_additional_coverage.py +34 -29
  9. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_coverage_improvements.py +285 -0
  10. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_custom_engine_branch.py +107 -0
  11. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_edge_cases_coverage.py +305 -0
  12. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_import_fallback_simulation.py +192 -0
  13. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_import_fallbacks.py +82 -0
  14. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_maximum_coverage.py +440 -0
  15. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_multi_sessions_cleanup.py +89 -0
  16. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_multisession_pool.py +82 -0
  17. fastapi_async_sqlalchemy-0.7.1.post1/tests/test_type_hints_compatibility.py +213 -0
  18. fastapi_async_sqlalchemy-0.7.0.dev5/fastapi_async_sqlalchemy/__init__.py +0 -9
  19. fastapi_async_sqlalchemy-0.7.0.dev5/fastapi_async_sqlalchemy/middleware.py +0 -172
  20. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/LICENSE +0 -0
  21. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/README.md +0 -0
  22. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy/exceptions.py +0 -0
  23. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy/py.typed +0 -0
  24. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/dependency_links.txt +0 -0
  25. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/not-zip-safe +0 -0
  26. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/requires.txt +0 -0
  27. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/fastapi_async_sqlalchemy.egg-info/top_level.txt +0 -0
  28. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/setup.cfg +0 -0
  29. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/tests/test_coverage_boost.py +0 -0
  30. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/tests/test_session.py +0 -0
  31. {fastapi_async_sqlalchemy-0.7.0.dev5 → fastapi_async_sqlalchemy-0.7.1.post1}/tests/test_sqlmodel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.7.0.dev5
3
+ Version: 0.7.1.post1
4
4
  Summary: SQLAlchemy middleware for FastAPI
5
5
  Home-page: https://github.com/h0rn3t/fastapi-async-sqlalchemy.git
6
6
  Author: Eugene Shershen
@@ -14,8 +14,6 @@ Classifier: Framework :: AsyncIO
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
19
17
  Classifier: Programming Language :: Python :: 3.9
20
18
  Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
@@ -25,7 +23,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
25
23
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
26
24
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
27
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.7
26
+ Requires-Python: >=3.9
29
27
  Description-Content-Type: text/markdown
30
28
  License-File: LICENSE
31
29
  Requires-Dist: starlette>=0.13.6
@@ -0,0 +1,22 @@
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.7.1.post1"
@@ -0,0 +1,211 @@
1
+ import asyncio
2
+ from contextvars import ContextVar
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, Union
5
+
6
+ from sqlalchemy.engine.url import URL
7
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
8
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
9
+ from starlette.requests import Request
10
+ from starlette.types import ASGIApp
11
+
12
+ from fastapi_async_sqlalchemy.exceptions import (
13
+ MissingSessionError,
14
+ SessionNotInitialisedError,
15
+ )
16
+
17
+ try:
18
+ from sqlalchemy.ext.asyncio import async_sessionmaker
19
+ except ImportError:
20
+ from sqlalchemy.orm import sessionmaker as async_sessionmaker # type: ignore
21
+
22
+ try:
23
+ from sqlmodel.ext.asyncio.session import AsyncSession as SQLModelAsyncSession
24
+
25
+ DefaultAsyncSession: type[AsyncSession] = SQLModelAsyncSession # type: ignore
26
+ except ImportError:
27
+ DefaultAsyncSession: type[AsyncSession] = AsyncSession # type: ignore
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class MultiSessionState:
32
+ """State for multi_sessions mode."""
33
+
34
+ tracked: set[AsyncSession] = field(default_factory=set)
35
+ task_sessions: dict[int, AsyncSession] = field(default_factory=dict)
36
+ cleanup_tasks: list[asyncio.Task] = field(default_factory=list)
37
+ parent_task_id: int = 0
38
+ commit_on_exit: bool = False
39
+
40
+
41
+ def create_middleware_and_session_proxy() -> tuple:
42
+ _Session: Optional[async_sessionmaker] = None
43
+ _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
44
+ _multi_state: ContextVar[Optional[MultiSessionState]] = ContextVar("_multi_state", default=None)
45
+
46
+ class _SQLAlchemyMiddleware(BaseHTTPMiddleware):
47
+ __test__ = False # Prevent pytest from collecting this as a test class
48
+
49
+ def __init__(
50
+ self,
51
+ app: ASGIApp,
52
+ db_url: Optional[Union[str, URL]] = None,
53
+ custom_engine: Optional[AsyncEngine] = None,
54
+ engine_args: Optional[dict] = None,
55
+ session_args: Optional[dict] = None,
56
+ commit_on_exit: bool = False,
57
+ ):
58
+ super().__init__(app)
59
+ self.commit_on_exit = commit_on_exit
60
+ engine_args = engine_args or {}
61
+ session_args = session_args or {}
62
+
63
+ if not custom_engine and not db_url:
64
+ raise ValueError("You need to pass a db_url or a custom_engine parameter.")
65
+ if custom_engine:
66
+ engine = custom_engine
67
+ else:
68
+ engine = create_async_engine(db_url, **engine_args)
69
+
70
+ nonlocal _Session
71
+ _Session = async_sessionmaker(
72
+ engine,
73
+ class_=DefaultAsyncSession,
74
+ expire_on_commit=False,
75
+ **session_args,
76
+ )
77
+
78
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
79
+ async with DBSession(commit_on_exit=self.commit_on_exit):
80
+ return await call_next(request)
81
+
82
+ class DBSessionMeta(type):
83
+ @property
84
+ def session(self) -> AsyncSession:
85
+ """Return an instance of Session local to the current async context."""
86
+ if _Session is None:
87
+ raise SessionNotInitialisedError
88
+
89
+ state = _multi_state.get()
90
+ if state is not None:
91
+ task = asyncio.current_task()
92
+ if task is None:
93
+ raise RuntimeError("Cannot get current task")
94
+ task_id = id(task)
95
+
96
+ if task_id in state.task_sessions:
97
+ return state.task_sessions[task_id]
98
+
99
+ session = _Session()
100
+ state.task_sessions[task_id] = session
101
+ state.tracked.add(session)
102
+
103
+ # Add cleanup callback only for child tasks
104
+ if task_id != state.parent_task_id:
105
+
106
+ def cleanup_callback(_task):
107
+ try:
108
+ loop = asyncio.get_running_loop()
109
+ if loop.is_closed():
110
+ return
111
+ except RuntimeError:
112
+ return
113
+
114
+ async def cleanup():
115
+ try:
116
+ if state.commit_on_exit:
117
+ try:
118
+ await session.commit()
119
+ except Exception:
120
+ await session.rollback()
121
+ finally:
122
+ await session.close()
123
+ state.tracked.discard(session)
124
+ state.task_sessions.pop(task_id, None)
125
+
126
+ t = loop.create_task(cleanup())
127
+ state.cleanup_tasks.append(t)
128
+
129
+ task.add_done_callback(cleanup_callback)
130
+
131
+ return session
132
+ else:
133
+ session = _session.get()
134
+ if session is None:
135
+ raise MissingSessionError
136
+ return session
137
+
138
+ class DBSession(metaclass=DBSessionMeta):
139
+ def __init__(
140
+ self,
141
+ session_args: Optional[dict] = None,
142
+ commit_on_exit: bool = False,
143
+ multi_sessions: bool = False,
144
+ ):
145
+ self.token = None
146
+ self.multi_state_token = None
147
+ self.session_args = session_args or {}
148
+ self.commit_on_exit = commit_on_exit
149
+ self.multi_sessions = multi_sessions
150
+
151
+ async def __aenter__(self):
152
+ if not isinstance(_Session, async_sessionmaker):
153
+ raise SessionNotInitialisedError
154
+
155
+ if self.multi_sessions:
156
+ state = MultiSessionState(
157
+ parent_task_id=id(asyncio.current_task()),
158
+ commit_on_exit=self.commit_on_exit,
159
+ )
160
+ self.multi_state_token = _multi_state.set(state)
161
+ else:
162
+ self.token = _session.set(_Session(**self.session_args))
163
+ return type(self)
164
+
165
+ async def __aexit__(self, exc_type, exc_value, traceback):
166
+ if self.multi_sessions:
167
+ state = _multi_state.get()
168
+
169
+ # Wait for cleanup tasks
170
+ if state.cleanup_tasks:
171
+ await asyncio.sleep(0)
172
+ await asyncio.gather(*state.cleanup_tasks, return_exceptions=True)
173
+
174
+ # Clean up remaining sessions
175
+ for session in list(state.tracked):
176
+ try:
177
+ if exc_type is not None:
178
+ await session.rollback()
179
+ elif self.commit_on_exit:
180
+ try:
181
+ await session.commit()
182
+ except Exception:
183
+ await session.rollback()
184
+ except Exception:
185
+ pass
186
+ finally:
187
+ try:
188
+ await session.close()
189
+ except Exception:
190
+ pass
191
+
192
+ _multi_state.reset(self.multi_state_token)
193
+ else:
194
+ session = _session.get()
195
+ try:
196
+ if exc_type is not None:
197
+ await session.rollback()
198
+ elif self.commit_on_exit:
199
+ try:
200
+ await session.commit()
201
+ except Exception:
202
+ await session.rollback()
203
+ raise
204
+ finally:
205
+ await session.close()
206
+ _session.reset(self.token)
207
+
208
+ return _SQLAlchemyMiddleware, DBSession
209
+
210
+
211
+ 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.7.0.dev5
3
+ Version: 0.7.1.post1
4
4
  Summary: SQLAlchemy middleware for FastAPI
5
5
  Home-page: https://github.com/h0rn3t/fastapi-async-sqlalchemy.git
6
6
  Author: Eugene Shershen
@@ -14,8 +14,6 @@ Classifier: Framework :: AsyncIO
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
19
17
  Classifier: Programming Language :: Python :: 3.9
20
18
  Classifier: Programming Language :: Python :: 3.10
21
19
  Classifier: Programming Language :: Python :: 3.11
@@ -25,7 +23,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
25
23
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
26
24
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
27
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.7
26
+ Requires-Python: >=3.9
29
27
  Description-Content-Type: text/markdown
30
28
  License-File: LICENSE
31
29
  Requires-Dist: starlette>=0.13.6
@@ -14,5 +14,14 @@ fastapi_async_sqlalchemy.egg-info/requires.txt
14
14
  fastapi_async_sqlalchemy.egg-info/top_level.txt
15
15
  tests/test_additional_coverage.py
16
16
  tests/test_coverage_boost.py
17
+ tests/test_coverage_improvements.py
18
+ tests/test_custom_engine_branch.py
19
+ tests/test_edge_cases_coverage.py
20
+ tests/test_import_fallback_simulation.py
21
+ tests/test_import_fallbacks.py
22
+ tests/test_maximum_coverage.py
23
+ tests/test_multi_sessions_cleanup.py
24
+ tests/test_multisession_pool.py
17
25
  tests/test_session.py
18
- tests/test_sqlmodel.py
26
+ tests/test_sqlmodel.py
27
+ tests/test_type_hints_compatibility.py
@@ -1,6 +1,6 @@
1
1
  [tool.ruff]
2
2
  line-length = 100
3
- target-version = "py37"
3
+ target-version = "py39"
4
4
  exclude = [
5
5
  ".git",
6
6
  ".venv",
@@ -31,3 +31,9 @@ line-ending = "auto"
31
31
  [tool.ruff.lint.isort]
32
32
  combine-as-imports = true
33
33
  split-on-trailing-comma = true
34
+
35
+ [tool.pytest.ini_options]
36
+ filterwarnings = [
37
+ "ignore::DeprecationWarning",
38
+ "ignore:The garbage collector is trying to clean up:sqlalchemy.exc.SAWarning",
39
+ ]
@@ -26,7 +26,7 @@ setup(
26
26
  packages=["fastapi_async_sqlalchemy"],
27
27
  package_data={"fastapi_async_sqlalchemy": ["py.typed"]},
28
28
  zip_safe=False,
29
- python_requires=">=3.7",
29
+ python_requires=">=3.9",
30
30
  install_requires=["starlette>=0.13.6", "SQLAlchemy>=1.4.19"],
31
31
  classifiers=[
32
32
  "Development Status :: 5 - Production/Stable",
@@ -35,8 +35,6 @@ setup(
35
35
  "Intended Audience :: Developers",
36
36
  "License :: OSI Approved :: MIT License",
37
37
  "Operating System :: OS Independent",
38
- "Programming Language :: Python :: 3.7",
39
- "Programming Language :: Python :: 3.8",
40
38
  "Programming Language :: Python :: 3.9",
41
39
  "Programming Language :: Python :: 3.10",
42
40
  "Programming Language :: Python :: 3.11",
@@ -1,23 +1,24 @@
1
1
  """
2
2
  Additional tests to reach target coverage of 97.22%
3
3
  """
4
- import pytest
4
+
5
5
  from fastapi import FastAPI
6
6
 
7
7
 
8
8
  def test_commit_on_exit_parameter():
9
9
  """Test commit_on_exit parameter in middleware initialization"""
10
10
  from sqlalchemy.ext.asyncio import create_async_engine
11
+
11
12
  from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
12
-
13
+
13
14
  SQLAlchemyMiddleware, db = create_middleware_and_session_proxy()
14
15
  app = FastAPI()
15
-
16
+
16
17
  # Test commit_on_exit=True
17
18
  custom_engine = create_async_engine("sqlite+aiosqlite://")
18
19
  middleware = SQLAlchemyMiddleware(app, custom_engine=custom_engine, commit_on_exit=True)
19
20
  assert middleware.commit_on_exit is True
20
-
21
+
21
22
  # Test commit_on_exit=False (default)
22
23
  middleware2 = SQLAlchemyMiddleware(app, custom_engine=custom_engine, commit_on_exit=False)
23
24
  assert middleware2.commit_on_exit is False
@@ -26,33 +27,30 @@ def test_commit_on_exit_parameter():
26
27
  def test_exception_classes_simple():
27
28
  """Test exception classes are properly defined"""
28
29
  from fastapi_async_sqlalchemy.exceptions import MissingSessionError, SessionNotInitialisedError
29
-
30
+
30
31
  # Test exception instantiation without parameters
31
32
  missing_error = MissingSessionError()
32
33
  assert isinstance(missing_error, Exception)
33
-
34
+
34
35
  init_error = SessionNotInitialisedError()
35
36
  assert isinstance(init_error, Exception)
36
37
 
37
38
 
38
39
  def test_middleware_properties():
39
40
  """Test middleware properties and methods"""
40
- from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
41
- from sqlalchemy.ext.asyncio import create_async_engine
42
41
  from fastapi import FastAPI
43
-
42
+ from sqlalchemy.ext.asyncio import create_async_engine
43
+
44
+ from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
45
+
44
46
  SQLAlchemyMiddleware, db = create_middleware_and_session_proxy()
45
47
  app = FastAPI()
46
-
48
+
47
49
  # Test middleware properties
48
50
  custom_engine = create_async_engine("sqlite+aiosqlite://")
49
- middleware = SQLAlchemyMiddleware(
50
- app,
51
- custom_engine=custom_engine,
52
- commit_on_exit=True
53
- )
54
-
55
- assert hasattr(middleware, 'commit_on_exit')
51
+ middleware = SQLAlchemyMiddleware(app, custom_engine=custom_engine, commit_on_exit=True)
52
+
53
+ assert hasattr(middleware, "commit_on_exit")
56
54
  assert middleware.commit_on_exit is True
57
55
 
58
56
 
@@ -60,41 +58,48 @@ def test_basic_imports():
60
58
  """Test basic imports and module structure"""
61
59
  # Test main module imports
62
60
  from fastapi_async_sqlalchemy import SQLAlchemyMiddleware, db
61
+
63
62
  assert SQLAlchemyMiddleware is not None
64
63
  assert db is not None
65
-
64
+
66
65
  # Test exception imports
67
66
  from fastapi_async_sqlalchemy.exceptions import MissingSessionError, SessionNotInitialisedError
68
- assert MissingSessionError is not None
67
+
68
+ assert MissingSessionError is not None
69
69
  assert SessionNotInitialisedError is not None
70
-
70
+
71
71
  # Test middleware module imports
72
- from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy, DefaultAsyncSession
72
+ from fastapi_async_sqlalchemy.middleware import (
73
+ DefaultAsyncSession,
74
+ create_middleware_and_session_proxy,
75
+ )
76
+
73
77
  assert create_middleware_and_session_proxy is not None
74
78
  assert DefaultAsyncSession is not None
75
79
 
76
80
 
77
81
  def test_middleware_factory_different_instances():
78
82
  """Test creating multiple middleware/db instances"""
79
- from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
80
83
  from fastapi import FastAPI
81
84
  from sqlalchemy.ext.asyncio import create_async_engine
82
-
85
+
86
+ from fastapi_async_sqlalchemy.middleware import create_middleware_and_session_proxy
87
+
83
88
  # Create first instance
84
89
  SQLAlchemyMiddleware1, db1 = create_middleware_and_session_proxy()
85
-
90
+
86
91
  # Create second instance
87
92
  SQLAlchemyMiddleware2, db2 = create_middleware_and_session_proxy()
88
-
93
+
89
94
  # They should be different instances
90
95
  assert SQLAlchemyMiddleware1 is not SQLAlchemyMiddleware2
91
96
  assert db1 is not db2
92
-
97
+
93
98
  # Test both instances work
94
99
  app = FastAPI()
95
100
  engine = create_async_engine("sqlite+aiosqlite://")
96
-
101
+
97
102
  middleware1 = SQLAlchemyMiddleware1(app, custom_engine=engine)
98
103
  middleware2 = SQLAlchemyMiddleware2(app, custom_engine=engine)
99
-
100
- assert middleware1 is not middleware2
104
+
105
+ assert middleware1 is not middleware2