fastapi-sqla 3.1.1__py3-none-any.whl → 3.4.8__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.

fastapi_sqla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi_sqla.base import setup
1
+ from fastapi_sqla.base import setup, setup_middlewares, startup
2
2
  from fastapi_sqla.models import Collection, Item, Page
3
3
  from fastapi_sqla.pagination import Paginate, PaginateSignature, Pagination
4
4
  from fastapi_sqla.sqla import (
@@ -22,6 +22,8 @@ __all__ = [
22
22
  "SqlaSession",
23
23
  "open_session",
24
24
  "setup",
25
+ "setup_middlewares",
26
+ "startup",
25
27
  ]
26
28
 
27
29
 
@@ -28,7 +28,6 @@ def db_host():
28
28
  When CI key is set in environment variables, it uses `postgres` as host name else,
29
29
  host used is `localhost`
30
30
  """
31
-
32
31
  return "postgres" if "CI" in os.environ else "localhost"
33
32
 
34
33
 
@@ -38,7 +37,6 @@ def db_user():
38
37
 
39
38
  postgres
40
39
  """
41
-
42
40
  return "postgres"
43
41
 
44
42
 
@@ -48,7 +46,6 @@ def db_url(db_host, db_user):
48
46
 
49
47
  db url example postgresql://{db_user}@{db_host}/postgres
50
48
  """
51
-
52
49
  return f"postgresql://{db_user}@{db_host}/postgres"
53
50
 
54
51
 
@@ -107,7 +104,6 @@ def sqla_reflection(sqla_modules, sqla_connection):
107
104
  @fixture
108
105
  def patch_engine_from_config(request, sqla_connection):
109
106
  """So that all DB operations are never written to db for real."""
110
-
111
107
  if "dont_patch_engines" in request.keywords:
112
108
  yield
113
109
  else:
@@ -153,7 +149,7 @@ def async_sqlalchemy_url(db_url):
153
149
  return format_async_async_sqlalchemy_url(db_url)
154
150
 
155
151
 
156
- if asyncio_support: # noqa: C901
152
+ if asyncio_support:
157
153
 
158
154
  @fixture
159
155
  def async_engine(async_sqlalchemy_url):
@@ -167,7 +163,6 @@ if asyncio_support: # noqa: C901
167
163
  @fixture
168
164
  async def patch_new_engine(request, async_sqla_connection):
169
165
  """So that all async DB operations are never written to db for real."""
170
-
171
166
  if "dont_patch_engines" in request.keywords:
172
167
  yield
173
168
  else:
@@ -1,12 +1,12 @@
1
1
  import math
2
- from collections.abc import Awaitable, Callable
3
- from typing import Annotated, Iterator, Optional, Union, cast
2
+ from collections.abc import Awaitable, Callable, Iterator
3
+ from typing import Annotated, Optional, Union, cast
4
4
 
5
5
  from fastapi import Depends, Query
6
6
  from sqlalchemy.sql import Select, func, select
7
7
 
8
8
  from fastapi_sqla.async_sqla import AsyncSessionDependency, SqlaAsyncSession
9
- from fastapi_sqla.models import Page
9
+ from fastapi_sqla.models import Meta, Page
10
10
  from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY
11
11
 
12
12
  QueryCountDependency = Callable[..., Awaitable[int]]
@@ -19,7 +19,7 @@ PaginateDependency = Union[DefaultDependency, WithQueryCountDependency]
19
19
 
20
20
 
21
21
  async def default_query_count(session: SqlaAsyncSession, query: Select) -> int:
22
- result = await session.execute(select(func.count()).select_from(query.subquery())) # type: ignore # noqa
22
+ result = await session.execute(select(func.count()).select_from(query.subquery()))
23
23
  return cast(int, result.scalar())
24
24
 
25
25
 
@@ -33,20 +33,20 @@ async def paginate_query(
33
33
  scalars: bool = True,
34
34
  ) -> Page:
35
35
  total_pages = math.ceil(total_items / limit)
36
- page_number = offset / limit + 1
37
- query = query.offset(offset).limit(limit) # type: ignore
36
+ page_number = math.floor(offset / limit + 1)
37
+ query = query.offset(offset).limit(limit)
38
38
  result = await session.execute(query)
39
39
  data = iter(
40
- cast(Iterator, result.unique().scalars() if scalars else result.mappings()) # type: ignore # noqa
40
+ cast(Iterator, result.unique().scalars() if scalars else result.mappings())
41
41
  )
42
42
  return Page(
43
- data=data,
44
- meta={
45
- "offset": offset,
46
- "total_items": total_items,
47
- "total_pages": total_pages,
48
- "page_number": page_number,
49
- },
43
+ data=data, # type: ignore # Expected to be a list
44
+ meta=Meta(
45
+ offset=offset,
46
+ total_items=total_items,
47
+ total_pages=total_pages,
48
+ page_number=page_number,
49
+ ),
50
50
  )
51
51
 
52
52
 
@@ -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:
@@ -20,7 +20,8 @@ def setup(engine: Engine):
20
20
  aws_rds_iam_enabled = lc_environ.get("fastapi_sqla_aws_rds_iam_enabled") == "true"
21
21
 
22
22
  if aws_rds_iam_enabled:
23
- assert boto3_installed, boto3_installed_err
23
+ if not boto3_installed:
24
+ raise ImportError(f"boto3 is required for RDS IAM : {boto3_installed_err}")
24
25
  # Cache the client at startup
25
26
  get_rds_client()
26
27
  event.listen(engine, "do_connect", set_connection_token)
fastapi_sqla/base.py CHANGED
@@ -2,6 +2,7 @@ import functools
2
2
  import os
3
3
  import re
4
4
 
5
+ from deprecated import deprecated
5
6
  from fastapi import FastAPI
6
7
  from sqlalchemy.engine import Engine
7
8
 
@@ -20,23 +21,41 @@ except ImportError as err: # pragma: no cover
20
21
  _ENGINE_KEYS_REGEX = re.compile(r"fastapi_sqla__(?!_)(.+)(?<!_)__(?!_).+")
21
22
 
22
23
 
24
+ async def startup():
25
+ engine_keys = _get_engine_keys()
26
+ engines = {key: sqla.new_engine(key) for key in engine_keys}
27
+ for key, engine in engines.items():
28
+ if not _is_async_dialect(engine):
29
+ sqla.startup(key=key)
30
+ else:
31
+ await async_sqla.startup(key=key)
32
+
33
+
34
+ def setup_middlewares(app: FastAPI):
35
+ engine_keys = _get_engine_keys()
36
+ engines = {key: sqla.new_engine(key) for key in engine_keys}
37
+ for key, engine in engines.items():
38
+ if not _is_async_dialect(engine):
39
+ app.add_middleware(sqla.SessionMiddleware, key=key)
40
+ else:
41
+ app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
42
+
43
+
44
+ @deprecated(
45
+ reason="FastAPI events are deprecated. This function will be remove in the upcoming major release." # noqa: E501
46
+ )
23
47
  def setup(app: FastAPI):
24
48
  engine_keys = _get_engine_keys()
25
49
  engines = {key: sqla.new_engine(key) for key in engine_keys}
26
50
  for key, engine in engines.items():
27
51
  if not _is_async_dialect(engine):
28
52
  app.add_event_handler("startup", functools.partial(sqla.startup, key=key))
29
- app.middleware("http")(
30
- functools.partial(sqla.add_session_to_request, key=key)
31
- )
53
+ app.add_middleware(sqla.SessionMiddleware, key=key)
32
54
  else:
33
- assert has_asyncio_support, asyncio_support_err
34
55
  app.add_event_handler(
35
56
  "startup", functools.partial(async_sqla.startup, key=key)
36
57
  )
37
- app.middleware("http")(
38
- functools.partial(async_sqla.add_session_to_request, key=key)
39
- )
58
+ app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
40
59
 
41
60
 
42
61
  def _get_engine_keys() -> set[str]:
fastapi_sqla/models.py CHANGED
@@ -3,7 +3,7 @@ from typing import Generic, TypeVar
3
3
  from pydantic import BaseModel, Field
4
4
  from pydantic import __version__ as pydantic_version
5
5
 
6
- major, _, _ = [int(v) for v in pydantic_version.split(".")]
6
+ major, _, _ = (int(v) for v in pydantic_version.split("."))
7
7
  is_pydantic2 = major == 2
8
8
  if is_pydantic2:
9
9
  GenericModel = BaseModel
@@ -1,13 +1,13 @@
1
1
  import math
2
- from collections.abc import Callable
2
+ from collections.abc import Callable, Iterator
3
3
  from functools import singledispatch
4
- from typing import Annotated, Iterator, Optional, Union, cast
4
+ from typing import Annotated, Optional, Union, cast
5
5
 
6
6
  from fastapi import Depends, Query
7
7
  from sqlalchemy.orm import Query as LegacyQuery
8
8
  from sqlalchemy.sql import Select, func, select
9
9
 
10
- from fastapi_sqla.models import Page
10
+ from fastapi_sqla.models import Meta, Page
11
11
  from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, SessionDependency, SqlaSession
12
12
 
13
13
  DbQuery = Union[LegacyQuery, Select]
@@ -52,7 +52,7 @@ def paginate_query(
52
52
  limit: int,
53
53
  scalars: bool = True,
54
54
  ) -> Page: # pragma: no cover
55
- "Dispatch on registered functions based on `query` type"
55
+ """Dispatch on registered functions based on `query` type"""
56
56
  raise NotImplementedError(f"no paginate_query registered for type {type(query)!r}")
57
57
 
58
58
 
@@ -66,15 +66,15 @@ def _paginate_legacy(
66
66
  scalars: bool = True,
67
67
  ) -> Page:
68
68
  total_pages = math.ceil(total_items / limit)
69
- page_number = offset / limit + 1
69
+ page_number = math.floor(offset / limit + 1)
70
70
  return Page(
71
- data=query.offset(offset).limit(limit).all(), # type: ignore
72
- meta={
73
- "offset": offset,
74
- "total_items": total_items,
75
- "total_pages": total_pages,
76
- "page_number": page_number,
77
- },
71
+ data=query.offset(offset).limit(limit).all(),
72
+ meta=Meta(
73
+ offset=offset,
74
+ total_items=total_items,
75
+ total_pages=total_pages,
76
+ page_number=page_number,
77
+ ),
78
78
  )
79
79
 
80
80
 
@@ -89,20 +89,20 @@ def _paginate(
89
89
  scalars: bool = True,
90
90
  ) -> Page:
91
91
  total_pages = math.ceil(total_items / limit)
92
- page_number = offset / limit + 1
93
- query = query.offset(offset).limit(limit) # type: ignore
92
+ page_number = math.floor(offset / limit + 1)
93
+ query = query.offset(offset).limit(limit)
94
94
  result = session.execute(query)
95
95
  data = iter(
96
- cast(Iterator, result.unique().scalars() if scalars else result.mappings()) # type: ignore # noqa
96
+ cast(Iterator, result.unique().scalars() if scalars else result.mappings())
97
97
  )
98
98
  return Page(
99
- data=data,
100
- meta={
101
- "offset": offset,
102
- "total_items": total_items,
103
- "total_pages": total_pages,
104
- "page_number": page_number,
105
- },
99
+ data=data, # type: ignore # Expected to be a list
100
+ meta=Meta(
101
+ offset=offset,
102
+ total_items=total_items,
103
+ total_pages=total_pages,
104
+ page_number=page_number,
105
+ ),
106
106
  )
107
107
 
108
108
 
fastapi_sqla/sqla.py CHANGED
@@ -2,10 +2,10 @@ import asyncio
2
2
  import os
3
3
  from collections.abc import Generator
4
4
  from contextlib import contextmanager
5
- from typing import Annotated, Generic, TypeVar
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
 
@@ -24,7 +25,7 @@ except ImportError:
24
25
  DeclarativeBase = declarative_base() # type: ignore
25
26
 
26
27
  try:
27
- from sqlmodel import Session as SqlaSession # type: ignore # noqa
28
+ from sqlmodel import Session as SqlaSession # type: ignore
28
29
 
29
30
  except ImportError:
30
31
  pass
@@ -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,49 +129,67 @@ 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
166
-
167
-
168
- S = TypeVar("S", bound=SqlaSession)
169
132
 
133
+ def __init__(self, app: ASGIApp, key: str = _DEFAULT_SESSION_KEY) -> None:
134
+ self.app = app
135
+ self.key = key
170
136
 
171
- class SessionDependency(Generic[S]):
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)
186
+
187
+
188
+ class SessionDependency:
172
189
  def __init__(self, key: str = _DEFAULT_SESSION_KEY) -> None:
173
190
  self.key = key
174
191
 
175
- def __call__(self, request: Request) -> S:
192
+ def __call__(self, request: Request) -> SqlaSession:
176
193
  """Yield the sqlalchemy session for that request.
177
194
 
178
195
  It is meant to be used as a FastAPI dependency::
@@ -197,5 +214,5 @@ class SessionDependency(Generic[S]):
197
214
  raise
198
215
 
199
216
 
200
- default_session_dep = SessionDependency[SqlaSession]()
217
+ default_session_dep = SessionDependency()
201
218
  Session = Annotated[SqlaSession, Depends(default_session_dep)]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-sqla
3
- Version: 3.1.1
4
- Summary: SQLAlchemy extension for FastAPI with support for pagination, asyncio, and pytest, ready for production.
3
+ Version: 3.4.8
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.14
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Web Environment
13
13
  Classifier: Framework :: AsyncIO
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Classifier: Programming Language :: Python :: 3.9
23
23
  Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
25
26
  Classifier: Programming Language :: Python :: 3 :: Only
26
27
  Classifier: Programming Language :: SQL
27
28
  Classifier: Topic :: Internet
@@ -34,30 +35,19 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
35
  Classifier: Typing :: Typed
35
36
  Provides-Extra: asyncpg
36
37
  Provides-Extra: aws-rds-iam
38
+ Provides-Extra: psycopg2
39
+ Provides-Extra: pytest-plugin
37
40
  Provides-Extra: sqlmodel
38
- Provides-Extra: tests
39
- Requires-Dist: Faker (>=14.2.0,<15.0.0) ; extra == "tests"
40
- Requires-Dist: alembic (>=1.4.3,<2.0.0) ; extra == "tests"
41
- Requires-Dist: asgi_lifespan (>=1.0.1,<2.0.0) ; extra == "tests"
42
- Requires-Dist: asyncpg (>=0.28.0,<0.29.0) ; extra == "asyncpg"
43
- Requires-Dist: black (>=22.8.0,<23.0.0) ; extra == "tests"
44
- Requires-Dist: boto3 (>=1.24.74,<2.0.0) ; extra == "aws-rds-iam"
45
- Requires-Dist: fastapi (>=0.95.1)
46
- Requires-Dist: greenlet (>=1.1.3,<2.0.0) ; extra == "tests"
47
- Requires-Dist: httpx (>=0.23.0,<0.24.0) ; extra == "tests"
48
- Requires-Dist: isort (>=5.5.3,<6.0.0) ; extra == "tests"
49
- Requires-Dist: mypy[tests] (>=0.991,<0.992) ; 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: pylama (>=8.4.1,<9.0.0) ; extra == "tests"
54
- Requires-Dist: pytest (>=7.2.1,<8.0.0) ; extra == "tests"
55
- Requires-Dist: pytest-asyncio (>=0.19.0,<0.20.0) ; extra == "tests"
56
- Requires-Dist: pytest-cov (>=2.10.1,<3.0.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.31.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.116)
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.23) ; extra == "sqlmodel"
50
+ Requires-Dist: structlog (>=20,<26)
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
@@ -90,15 +82,22 @@ unique `email`:
90
82
 
91
83
  ```python
92
84
  # main.py
85
+ from contextlib import asynccontextmanager
93
86
  from fastapi import FastAPI, HTTPException
94
- from fastapi_sqla import Base, Item, Page, Paginate, Session, setup
87
+ from fastapi_sqla import Base, Item, Page, Paginate, Session, setup_middlewares, startup
95
88
  from pydantic import BaseModel, EmailStr
96
89
  from sqlalchemy import select
97
90
  from sqlalchemy.exc import IntegrityError
98
91
 
99
- app = FastAPI()
100
92
 
101
- setup(app)
93
+ @asynccontextmanager
94
+ async def lifespan(app: FastAPI):
95
+ await startup()
96
+ yield
97
+
98
+
99
+ app = FastAPI(lifespan=lifespan)
100
+ setup_middlewares(app)
102
101
 
103
102
 
104
103
  class User(Base):
@@ -203,7 +202,23 @@ And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` sch
203
202
  export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
204
203
  ```
205
204
 
206
- ## Setup the app:
205
+ ## Setup the app AsyncContextManager (recommended):
206
+
207
+ ```python
208
+ import fastapi_sqla
209
+ from fastapi import FastAPI
210
+
211
+ @asynccontextmanager
212
+ async def lifespan(app: FastAPI):
213
+ await fastapi_sqla.startup()
214
+ yield
215
+
216
+
217
+ app = FastAPI(lifespan=lifespan)
218
+ fastapi_sqla.setup_middlewares(app)
219
+ ```
220
+
221
+ ## Setup the app using startup/shutdown events (deprecated):
207
222
 
208
223
  ```python
209
224
  import fastapi_sqla
@@ -640,7 +655,7 @@ If your project uses [SQLModel], then `Session` dependency is an SQLModel sessio
640
655
  # Pytest fixtures
641
656
 
642
657
  This library provides a set of utility fixtures, through its PyTest plugin, which is
643
- automatically installed with the library.
658
+ automatically installed with the library. Using the plugin requires the `pytest_plugin` extra.
644
659
 
645
660
  By default, no records are actually written to the database when running tests.
646
661
  There currently is no way to change this behaviour.
@@ -784,7 +799,7 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
784
799
  ## Setup
785
800
 
786
801
  ```bash
787
- $ poetry install --extras tests --extras asyncpg --extras aws_rds_iam
802
+ $ poetry install --all-extras
788
803
  ```
789
804
 
790
805
  ## Running tests
@@ -0,0 +1,16 @@
1
+ fastapi_sqla/__init__.py,sha256=RRkwo9xZzidQ-k3BRXfqT0jtWm8d08w94XuOLteFCdU,1221
2
+ fastapi_sqla/_pytest_plugin.py,sha256=IQUlj-O874Sfuth254LBrEBGCrwH3rNHST9OMZl2pIk,5411
3
+ fastapi_sqla/async_pagination.py,sha256=3DHGUjvrpkbWMIc_BEX4GvM-_PTcn62K9z48ucTJlH0,3164
4
+ fastapi_sqla/async_sqla.py,sha256=L5tHuYgHMpmSdGAsgxs30K7joVmMX-06NYJ7E5WaoUo,6865
5
+ fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
6
+ fastapi_sqla/aws_rds_iam_support.py,sha256=Uw-XaiwShMMWYKCvlSqXoxvtKMblCAvbCZ1m6BYVpJk,1257
7
+ fastapi_sqla/base.py,sha256=40lov7wREct4q4rfXiKjlwsjFC9iFE0eDq-ghiJwGgY,2279
8
+ fastapi_sqla/models.py,sha256=-B1xwINpTc9rEQd3KYHEC1s5s7jdVQkJ6Gy6xpmT13c,1108
9
+ fastapi_sqla/pagination.py,sha256=1gfIGcmt1OFspbRgtJ8AZOZdFd14DGRc4FkDgyh5bJ8,4517
10
+ fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ fastapi_sqla/sqla.py,sha256=3mIGdYmyOUZaClJiHCoaZpUlzbJilH2lrMzoc2gjZN0,7335
12
+ fastapi_sqla-3.4.8.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
+ fastapi_sqla-3.4.8.dist-info/METADATA,sha256=rARChNcTpioOmQMmMGY33T4KvhC8_kD2fDz-16sch3o,20980
14
+ fastapi_sqla-3.4.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ fastapi_sqla-3.4.8.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
+ fastapi_sqla-3.4.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,16 +0,0 @@
1
- fastapi_sqla/__init__.py,sha256=G9bKR6L9EnLgzelE_BLnHgc1cOm0Yu0wMzwLAdyA8iE,1153
2
- fastapi_sqla/_pytest_plugin.py,sha256=bjPzOuwk32Zo4hoOa_bGjTGhtHGPImh3RyiORqz-J84,5430
3
- fastapi_sqla/async_pagination.py,sha256=r5APFbE-xpqQPF1aTG19ILOwYDhuIlHql3UF3mYZMt4,3176
4
- fastapi_sqla/async_sqla.py,sha256=7SXRH3DMsQvpjCQdvCBEjQhDZ4-cPAg175MlgNVBnCg,5888
5
- fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
6
- fastapi_sqla/aws_rds_iam_support.py,sha256=YSJNhrxmhGN-GVk9PLMTmQSWTKZBvuorKkhc_XaoL44,1189
7
- fastapi_sqla/base.py,sha256=0X7Gbt49rBHPiSFmNy5S2PT0dA4UBNnwrAesYSkaHBc,1606
8
- fastapi_sqla/models.py,sha256=fesW7BqkwOA4iC345dbybzcV8Kz4-kLwREo38oqy_7A,1108
9
- fastapi_sqla/pagination.py,sha256=uhsNRf9w2GzJQkF8eUUQZMAudPDi7tN5OCb00kz3Dn4,4515
10
- fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- fastapi_sqla/sqla.py,sha256=zAkzY07bHEnOrmP7_dwO4Gdar18qm1ZbhB1RD3AZGrA,6427
12
- fastapi_sqla-3.1.1.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
- fastapi_sqla-3.1.1.dist-info/METADATA,sha256=caalhTVa91W2oBq_rXUhu5xahUzINgBpq39GRlsbvxc,20905
14
- fastapi_sqla-3.1.1.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
15
- fastapi_sqla-3.1.1.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
- fastapi_sqla-3.1.1.dist-info/RECORD,,