fastapi-sqla 3.3.0__py3-none-any.whl → 3.4.1__py3-none-any.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.

Potentially problematic release.


This version of fastapi-sqla might be problematic. Click here for more details.

@@ -3,12 +3,13 @@ from contextlib import asynccontextmanager
3
3
  from typing import Annotated
4
4
 
5
5
  import structlog
6
- from fastapi import Depends, Request
6
+ from fastapi import Depends, Request, Response
7
7
  from fastapi.responses import PlainTextResponse
8
8
  from sqlalchemy import text
9
9
  from sqlalchemy.ext.asyncio import AsyncEngine
10
10
  from sqlalchemy.ext.asyncio import AsyncSession as SqlaAsyncSession
11
11
  from sqlalchemy.orm.session import sessionmaker
12
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
12
13
 
13
14
  from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
14
15
  from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, Base, new_engine
@@ -88,9 +89,7 @@ async def open_session(
88
89
  await session.close()
89
90
 
90
91
 
91
- async def add_session_to_request(
92
- request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
93
- ):
92
+ class AsyncSessionMiddleware:
94
93
  """Middleware which injects a new sqla async session into every request.
95
94
 
96
95
  Handles creation of session, as well as commit, rollback, and closing of session.
@@ -108,36 +107,58 @@ async def add_session_to_request(
108
107
  async def get_users(session: fastapi_sqla.AsyncSession):
109
108
  return await session.execute(...) # use your session here
110
109
  """
111
- async with open_session(key) as session:
112
- setattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{key}", session)
113
- response = await call_next(request)
114
-
115
- is_dirty = bool(session.dirty or session.deleted or session.new)
116
-
117
- # try to commit after response, so that we can return a proper 500 response
118
- # and not raise a true internal server error
119
- if response.status_code < 400:
120
- try:
121
- await session.commit()
122
- except Exception:
123
- logger.exception("commit failed, returning http error")
124
- response = PlainTextResponse(
125
- content="Internal Server Error", status_code=500
126
- )
127
-
128
- if response.status_code >= 400:
129
- # If ever a route handler returns an http exception, we do not want the
130
- # session opened by current context manager to commit anything in db.
131
- if is_dirty:
132
- # optimistically only log if there were uncommitted changes
133
- logger.warning(
134
- "http error, rolling back possibly uncommitted changes",
135
- status_code=response.status_code,
136
- )
137
- # since this is no-op if session is not dirty, we can always call it
138
- await session.rollback()
139
110
 
140
- return response
111
+ def __init__(self, app: ASGIApp, key: str = _DEFAULT_SESSION_KEY) -> None:
112
+ self.app = app
113
+ self.key = key
114
+
115
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
116
+ if scope["type"] != "http":
117
+ return await self.app(scope, receive, send)
118
+
119
+ async with open_session(self.key) as session:
120
+ request = Request(scope=scope, receive=receive, send=send)
121
+ setattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{self.key}", session)
122
+
123
+ async def send_wrapper(message: Message) -> None:
124
+ if message["type"] != "http.response.start":
125
+ return await send(message)
126
+
127
+ response: Response | None = None
128
+ status_code = message["status"]
129
+ is_dirty = bool(session.dirty or session.deleted or session.new)
130
+
131
+ # try to commit after response, so that we can return a proper 500
132
+ # and not raise a true internal server error
133
+ if status_code < 400:
134
+ try:
135
+ await session.commit()
136
+ except Exception:
137
+ logger.exception("commit failed, returning http error")
138
+ status_code = 500
139
+ response = PlainTextResponse(
140
+ content="Internal Server Error", status_code=500
141
+ )
142
+
143
+ if status_code >= 400:
144
+ # If ever a route handler returns an http exception,
145
+ # we do not want the current session to commit anything in db.
146
+ if is_dirty:
147
+ # optimistically only log if there were uncommitted changes
148
+ logger.warning(
149
+ "http error, rolling back possibly uncommitted changes",
150
+ status_code=status_code,
151
+ )
152
+ # since this is no-op if the session is not dirty,
153
+ # we can always call it
154
+ await session.rollback()
155
+
156
+ if response:
157
+ return await response(scope, receive, send)
158
+
159
+ return await send(message)
160
+
161
+ await self.app(scope, receive, send_wrapper)
141
162
 
142
163
 
143
164
  class AsyncSessionDependency:
fastapi_sqla/base.py CHANGED
@@ -36,13 +36,9 @@ def setup_middlewares(app: FastAPI):
36
36
  engines = {key: sqla.new_engine(key) for key in engine_keys}
37
37
  for key, engine in engines.items():
38
38
  if not _is_async_dialect(engine):
39
- app.middleware("http")(
40
- functools.partial(sqla.add_session_to_request, key=key)
41
- )
39
+ app.add_middleware(sqla.SessionMiddleware, key=key)
42
40
  else:
43
- app.middleware("http")(
44
- functools.partial(async_sqla.add_session_to_request, key=key)
45
- )
41
+ app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
46
42
 
47
43
 
48
44
  @deprecated(
@@ -54,16 +50,12 @@ def setup(app: FastAPI):
54
50
  for key, engine in engines.items():
55
51
  if not _is_async_dialect(engine):
56
52
  app.add_event_handler("startup", functools.partial(sqla.startup, key=key))
57
- app.middleware("http")(
58
- functools.partial(sqla.add_session_to_request, key=key)
59
- )
53
+ app.add_middleware(sqla.SessionMiddleware, key=key)
60
54
  else:
61
55
  app.add_event_handler(
62
56
  "startup", functools.partial(async_sqla.startup, key=key)
63
57
  )
64
- app.middleware("http")(
65
- functools.partial(async_sqla.add_session_to_request, key=key)
66
- )
58
+ app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
67
59
 
68
60
 
69
61
  def _get_engine_keys() -> set[str]:
fastapi_sqla/sqla.py CHANGED
@@ -5,7 +5,7 @@ from contextlib import contextmanager
5
5
  from typing import Annotated
6
6
 
7
7
  import structlog
8
- from fastapi import Depends, Request
8
+ from fastapi import Depends, Request, Response
9
9
  from fastapi.concurrency import contextmanager_in_threadpool
10
10
  from fastapi.responses import PlainTextResponse
11
11
  from sqlalchemy import engine_from_config, text
@@ -13,6 +13,7 @@ from sqlalchemy.engine import Engine
13
13
  from sqlalchemy.ext.declarative import DeferredReflection
14
14
  from sqlalchemy.orm.session import Session as SqlaSession
15
15
  from sqlalchemy.orm.session import sessionmaker
16
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
16
17
 
17
18
  from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
18
19
 
@@ -110,9 +111,7 @@ def open_session(key: str = _DEFAULT_SESSION_KEY) -> Generator[SqlaSession, None
110
111
  session.close()
111
112
 
112
113
 
113
- async def add_session_to_request(
114
- request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
115
- ):
114
+ class SessionMiddleware:
116
115
  """Middleware which injects a new sqla session into every request.
117
116
 
118
117
  Handles creation of session, as well as commit, rollback, and closing of session.
@@ -130,39 +129,60 @@ async def add_session_to_request(
130
129
  def get_users(session: fastapi_sqla.Session):
131
130
  return session.execute(...) # use your session here
132
131
  """
133
- async with contextmanager_in_threadpool(open_session(key)) as session:
134
- setattr(request.state, f"{_REQUEST_SESSION_KEY}_{key}", session)
135
-
136
- response = await call_next(request)
137
-
138
- is_dirty = bool(session.dirty or session.deleted or session.new)
139
-
140
- loop = asyncio.get_running_loop()
141
-
142
- # try to commit after response, so that we can return a proper 500 response
143
- # and not raise a true internal server error
144
- if response.status_code < 400:
145
- try:
146
- await loop.run_in_executor(None, session.commit)
147
- except Exception:
148
- logger.exception("commit failed, returning http error")
149
- response = PlainTextResponse(
150
- content="Internal Server Error", status_code=500
151
- )
152
-
153
- if response.status_code >= 400:
154
- # If ever a route handler returns an http exception, we do not want the
155
- # session opened by current context manager to commit anything in db.
156
- if is_dirty:
157
- # optimistically only log if there were uncommitted changes
158
- logger.warning(
159
- "http error, rolling back possibly uncommitted changes",
160
- status_code=response.status_code,
161
- )
162
- # since this is no-op if session is not dirty, we can always call it
163
- await loop.run_in_executor(None, session.rollback)
164
-
165
- return response
132
+
133
+ def __init__(self, app: ASGIApp, key: str = _DEFAULT_SESSION_KEY) -> None:
134
+ self.app = app
135
+ self.key = key
136
+
137
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
138
+ if scope["type"] != "http":
139
+ return await self.app(scope, receive, send)
140
+
141
+ async with contextmanager_in_threadpool(open_session(self.key)) as session:
142
+ request = Request(scope=scope, receive=receive, send=send)
143
+ setattr(request.state, f"{_REQUEST_SESSION_KEY}_{self.key}", session)
144
+
145
+ async def send_wrapper(message: Message) -> None:
146
+ if message["type"] != "http.response.start":
147
+ return await send(message)
148
+
149
+ response: Response | None = None
150
+ status_code = message["status"]
151
+ is_dirty = bool(session.dirty or session.deleted or session.new)
152
+
153
+ loop = asyncio.get_running_loop()
154
+
155
+ # try to commit after response, so that we can return a proper 500
156
+ # and not raise a true internal server error
157
+ if status_code < 400:
158
+ try:
159
+ await loop.run_in_executor(None, session.commit)
160
+ except Exception:
161
+ logger.exception("commit failed, returning http error")
162
+ status_code = 500
163
+ response = PlainTextResponse(
164
+ content="Internal Server Error", status_code=status_code
165
+ )
166
+
167
+ if status_code >= 400:
168
+ # If ever a route handler returns an http exception,
169
+ # we do not want the current session to commit anything in db.
170
+ if is_dirty:
171
+ # optimistically only log if there were uncommitted changes
172
+ logger.warning(
173
+ "http error, rolling back possibly uncommitted changes",
174
+ status_code=status_code,
175
+ )
176
+ # since this is no-op if the session is not dirty,
177
+ # we can always call it
178
+ await loop.run_in_executor(None, session.rollback)
179
+
180
+ if response:
181
+ return await response(scope, receive, send)
182
+
183
+ return await send(message)
184
+
185
+ await self.app(scope, receive, send_wrapper)
166
186
 
167
187
 
168
188
  class SessionDependency:
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-sqla
3
- Version: 3.3.0
3
+ Version: 3.4.1
4
4
  Summary: SQLAlchemy extension for FastAPI with support for pagination, asyncio, SQLModel, and pytest, ready for production.
5
5
  Home-page: https://github.com/dialoguemd/fastapi-sqla
6
6
  License: MIT
7
7
  Keywords: FastAPI,SQLAlchemy,asyncio,pytest,alembic
8
8
  Author: Hadrien David
9
9
  Author-email: hadrien.david@dialogue.co
10
- Requires-Python: >=3.9,<4.0
10
+ Requires-Python: >=3.9,<3.13
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Web Environment
13
13
  Classifier: Framework :: AsyncIO
@@ -35,29 +35,19 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
35
  Classifier: Typing :: Typed
36
36
  Provides-Extra: asyncpg
37
37
  Provides-Extra: aws-rds-iam
38
+ Provides-Extra: psycopg2
39
+ Provides-Extra: pytest-plugin
38
40
  Provides-Extra: sqlmodel
39
- Provides-Extra: tests
40
- Requires-Dist: Faker (>=14.2.0,<15.0.0) ; extra == "tests"
41
- Requires-Dist: alembic (>=1.4.3,<2.0.0) ; extra == "tests"
42
- Requires-Dist: asgi_lifespan (>=1.0.1,<2.0.0) ; extra == "tests"
43
- Requires-Dist: asyncpg (>=0.28.0,<0.29.0) ; extra == "asyncpg"
44
- Requires-Dist: boto3 (>=1.24.74,<2.0.0) ; extra == "aws-rds-iam"
45
- Requires-Dist: deprecated (>=1.2)
46
- Requires-Dist: fastapi (>=0.95.1)
47
- Requires-Dist: greenlet (>=3.0.3,<4.0.0) ; extra == "tests"
48
- Requires-Dist: httpx (>=0.23.0,<0.24.0) ; extra == "tests"
49
- Requires-Dist: mypy[tests] (>=1.0.0,<2.0.0) ; extra == "tests"
50
- Requires-Dist: pdbpp (>=0.10.2,<0.11.0) ; extra == "tests"
51
- Requires-Dist: psycopg2 (>=2.8.6,<3.0.0) ; extra == "tests"
52
- Requires-Dist: pydantic (>=1)
53
- Requires-Dist: pytest (>=7.2.1,<8.0.0) ; extra == "tests"
54
- Requires-Dist: pytest-asyncio (>=0.19.0,<0.20.0) ; extra == "tests"
55
- Requires-Dist: pytest-cov (>=2.10.1,<3.0.0) ; extra == "tests"
56
- Requires-Dist: ruff (>=0.4.5,<0.5.0) ; extra == "tests"
57
- Requires-Dist: sqlalchemy (>=1.3)
58
- Requires-Dist: sqlmodel (>=0.0.14,<0.0.15) ; extra == "sqlmodel"
59
- Requires-Dist: structlog (>=20)
60
- Requires-Dist: tox (>=3.26.0,<4.0.0) ; extra == "tests"
41
+ Requires-Dist: alembic (>=1.4.3,<2) ; extra == "pytest-plugin"
42
+ Requires-Dist: asyncpg (>=0.28.0,<0.30.0) ; extra == "asyncpg"
43
+ Requires-Dist: boto3 (>=1.24.74,<2) ; extra == "aws-rds-iam"
44
+ Requires-Dist: deprecated (>=1.2,<2)
45
+ Requires-Dist: fastapi (>=0.95.1,<0.112)
46
+ Requires-Dist: psycopg2 (>=2.8.6,<3) ; extra == "psycopg2"
47
+ Requires-Dist: pydantic (>=1,<3)
48
+ Requires-Dist: sqlalchemy (>=1.3,<3)
49
+ Requires-Dist: sqlmodel (>=0.0.14,<0.0.20) ; extra == "sqlmodel"
50
+ Requires-Dist: structlog (>=20,<25)
61
51
  Project-URL: Repository, https://github.com/dialoguemd/fastapi-sqla
62
52
  Description-Content-Type: text/markdown
63
53
 
@@ -83,6 +73,8 @@ Using [pip](https://pip.pypa.io/):
83
73
  pip install fastapi-sqla
84
74
  ```
85
75
 
76
+ Note that you need a [SQLAlchemy compatible engine](https://docs.sqlalchemy.org/en/20/core/engines.html) adapter. We test with `psycopg2` which you can install using the `psycopg2` extra.
77
+
86
78
  # Quick Example
87
79
 
88
80
  Assuming it runs against a DB with a table `user` with 3 columns, `id`, `name` and
@@ -663,7 +655,7 @@ If your project uses [SQLModel], then `Session` dependency is an SQLModel sessio
663
655
  # Pytest fixtures
664
656
 
665
657
  This library provides a set of utility fixtures, through its PyTest plugin, which is
666
- automatically installed with the library.
658
+ automatically installed with the library. Using the plugin requires the `pytest_plugin` extra.
667
659
 
668
660
  By default, no records are actually written to the database when running tests.
669
661
  There currently is no way to change this behaviour.
@@ -807,7 +799,7 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
807
799
  ## Setup
808
800
 
809
801
  ```bash
810
- $ poetry install --extras tests --extras asyncpg --extras aws_rds_iam
802
+ $ poetry install --all-extras
811
803
  ```
812
804
 
813
805
  ## Running tests
@@ -1,16 +1,16 @@
1
1
  fastapi_sqla/__init__.py,sha256=RRkwo9xZzidQ-k3BRXfqT0jtWm8d08w94XuOLteFCdU,1221
2
2
  fastapi_sqla/_pytest_plugin.py,sha256=IQUlj-O874Sfuth254LBrEBGCrwH3rNHST9OMZl2pIk,5411
3
3
  fastapi_sqla/async_pagination.py,sha256=3DHGUjvrpkbWMIc_BEX4GvM-_PTcn62K9z48ucTJlH0,3164
4
- fastapi_sqla/async_sqla.py,sha256=7SXRH3DMsQvpjCQdvCBEjQhDZ4-cPAg175MlgNVBnCg,5888
4
+ fastapi_sqla/async_sqla.py,sha256=L5tHuYgHMpmSdGAsgxs30K7joVmMX-06NYJ7E5WaoUo,6865
5
5
  fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
6
6
  fastapi_sqla/aws_rds_iam_support.py,sha256=Uw-XaiwShMMWYKCvlSqXoxvtKMblCAvbCZ1m6BYVpJk,1257
7
- fastapi_sqla/base.py,sha256=8XHIKO8sBOmnRvCsOYRWhg5Y-XYqobM5DypeIMvJTFs,2501
7
+ fastapi_sqla/base.py,sha256=40lov7wREct4q4rfXiKjlwsjFC9iFE0eDq-ghiJwGgY,2279
8
8
  fastapi_sqla/models.py,sha256=-B1xwINpTc9rEQd3KYHEC1s5s7jdVQkJ6Gy6xpmT13c,1108
9
9
  fastapi_sqla/pagination.py,sha256=1gfIGcmt1OFspbRgtJ8AZOZdFd14DGRc4FkDgyh5bJ8,4517
10
10
  fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- fastapi_sqla/sqla.py,sha256=irHuZaun027o9WJrdcZPavCN2vXNHupCWYhU5Dx-AqE,6348
12
- fastapi_sqla-3.3.0.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
- fastapi_sqla-3.3.0.dist-info/METADATA,sha256=sNlfIYlzk2w8fmVxJh8jsOinZLJHrmOgWMLCZi4DV4o,21391
14
- fastapi_sqla-3.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
- fastapi_sqla-3.3.0.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
- fastapi_sqla-3.3.0.dist-info/RECORD,,
11
+ fastapi_sqla/sqla.py,sha256=3mIGdYmyOUZaClJiHCoaZpUlzbJilH2lrMzoc2gjZN0,7335
12
+ fastapi_sqla-3.4.1.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
+ fastapi_sqla-3.4.1.dist-info/METADATA,sha256=kzyKdVOQiyO78Xts1ZhGUw8p2r6tRfLJxsJ0Exu1FyA,20980
14
+ fastapi_sqla-3.4.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ fastapi_sqla-3.4.1.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
+ fastapi_sqla-3.4.1.dist-info/RECORD,,