fastapi-sqla 2.10.1__py3-none-any.whl → 3.0.0__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,7 +1,7 @@
1
1
  from fastapi_sqla.base import setup
2
2
  from fastapi_sqla.models import Base, Collection, Item, Page
3
3
  from fastapi_sqla.pagination import Paginate, PaginateSignature, Pagination
4
- from fastapi_sqla.sqla import Session, open_session
4
+ from fastapi_sqla.sqla import Session, SessionDependency, SqlaSession, open_session
5
5
 
6
6
  __all__ = [
7
7
  "Base",
@@ -12,20 +12,33 @@ __all__ = [
12
12
  "PaginateSignature",
13
13
  "Pagination",
14
14
  "Session",
15
+ "SessionDependency",
16
+ "SqlaSession",
15
17
  "open_session",
16
18
  "setup",
17
19
  ]
18
20
 
19
21
 
20
22
  try:
21
- from fastapi_sqla.async_pagination import AsyncPaginate, AsyncPagination
22
- from fastapi_sqla.async_sqla import AsyncSession
23
+ from fastapi_sqla.async_pagination import (
24
+ AsyncPaginate,
25
+ AsyncPaginateSignature,
26
+ AsyncPagination,
27
+ )
28
+ from fastapi_sqla.async_sqla import (
29
+ AsyncSession,
30
+ AsyncSessionDependency,
31
+ SqlaAsyncSession,
32
+ )
23
33
  from fastapi_sqla.async_sqla import open_session as open_async_session
24
34
 
25
35
  __all__ += [
26
36
  "AsyncPaginate",
37
+ "AsyncPaginateSignature",
27
38
  "AsyncPagination",
28
39
  "AsyncSession",
40
+ "AsyncSessionDependency",
41
+ "SqlaAsyncSession",
29
42
  "open_async_session",
30
43
  ]
31
44
  has_asyncio_support = True
@@ -6,6 +6,7 @@ from alembic import command
6
6
  from alembic.config import Config
7
7
  from pytest import fixture
8
8
  from sqlalchemy import create_engine, text
9
+ from sqlalchemy.orm.session import sessionmaker
9
10
 
10
11
  try:
11
12
  import asyncpg # noqa
@@ -106,7 +107,6 @@ def sqla_reflection(sqla_modules, sqla_connection):
106
107
  @fixture
107
108
  def patch_engine_from_config(request, sqla_connection, sqla_transaction):
108
109
  """So that all DB operations are never written to db for real."""
109
- from fastapi_sqla.sqla import _Session
110
110
 
111
111
  if "dont_patch_engines" in request.keywords: # pragma: no cover
112
112
  yield
@@ -114,7 +114,6 @@ def patch_engine_from_config(request, sqla_connection, sqla_transaction):
114
114
  else:
115
115
  with patch("fastapi_sqla.sqla.engine_from_config") as engine_from_config:
116
116
  engine_from_config.return_value = sqla_connection
117
- _Session.configure(bind=sqla_connection)
118
117
  yield engine_from_config
119
118
 
120
119
 
@@ -125,18 +124,27 @@ def sqla_transaction(sqla_connection):
125
124
  transaction.rollback()
126
125
 
127
126
 
127
+ @fixture
128
+ def session_factory():
129
+ return sessionmaker()
130
+
131
+
128
132
  @fixture
129
133
  def session(
130
- sqla_transaction, sqla_connection, sqla_reflection, patch_engine_from_config
134
+ session_factory,
135
+ sqla_connection,
136
+ sqla_transaction,
137
+ sqla_reflection,
138
+ patch_engine_from_config,
131
139
  ):
132
140
  """Sqla session to use when creating db fixtures.
133
141
 
134
142
  While it does not write any record in DB, the application will still be able to
135
143
  access any record committed with that session.
136
144
  """
137
- import fastapi_sqla.sqla
138
-
139
- yield fastapi_sqla.sqla._Session(bind=sqla_connection)
145
+ session = session_factory(bind=sqla_connection)
146
+ yield session
147
+ session.close()
140
148
 
141
149
 
142
150
  def format_async_async_sqlalchemy_url(url):
@@ -171,9 +179,8 @@ if asyncio_support: # noqa: C901
171
179
  await transaction.rollback()
172
180
 
173
181
  @fixture
174
- async def patch_new_engine(async_sqlalchemy_url, async_sqla_connection, request):
182
+ async def patch_new_engine(request, async_sqla_connection, async_sqla_transaction):
175
183
  """So that all async DB operations are never written to db for real."""
176
- from fastapi_sqla.async_sqla import _AsyncSession
177
184
 
178
185
  if "dont_patch_engines" in request.keywords: # pragma: no cover
179
186
  yield
@@ -181,9 +188,6 @@ if asyncio_support: # noqa: C901
181
188
  else:
182
189
  with patch("fastapi_sqla.async_sqla.new_engine") as new_engine:
183
190
  new_engine.return_value = async_sqla_connection
184
- _AsyncSession.configure(
185
- bind=async_sqla_connection, expire_on_commit=False
186
- )
187
191
  yield new_engine
188
192
 
189
193
  @fixture
@@ -192,15 +196,20 @@ if asyncio_support: # noqa: C901
192
196
 
193
197
  await async_sqla_connection.run_sync(lambda conn: Base.prepare(conn.engine))
194
198
 
199
+ @fixture
200
+ def async_session_factory():
201
+ from fastapi_sqla.async_sqla import SqlaAsyncSession
202
+
203
+ return sessionmaker(class_=SqlaAsyncSession)
204
+
195
205
  @fixture
196
206
  async def async_session(
207
+ async_session_factory,
197
208
  async_sqla_connection,
198
209
  async_sqla_transaction,
199
210
  async_sqla_reflection,
200
211
  patch_new_engine,
201
212
  ):
202
- from fastapi_sqla.async_sqla import _AsyncSession
203
-
204
- session = _AsyncSession(bind=async_sqla_connection)
213
+ session = async_session_factory(bind=async_sqla_connection)
205
214
  yield session
206
215
  await session.close()
@@ -1,28 +1,31 @@
1
1
  import math
2
2
  from collections.abc import Awaitable, Callable
3
- from typing import Iterator, Optional, Union, cast
3
+ from typing import Annotated, Iterator, Optional, Union, cast
4
4
 
5
5
  from fastapi import Depends, Query
6
6
  from sqlalchemy.sql import Select, func, select
7
7
 
8
- from fastapi_sqla.async_sqla import AsyncSession
8
+ from fastapi_sqla.async_sqla import AsyncSessionDependency, SqlaAsyncSession
9
9
  from fastapi_sqla.models import Page
10
+ from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY
10
11
 
11
12
  QueryCountDependency = Callable[..., Awaitable[int]]
12
- PaginateSignature = Callable[[Select, Optional[bool]], Awaitable[Page]]
13
- DefaultDependency = Callable[[AsyncSession, int, int], PaginateSignature]
14
- WithQueryCountDependency = Callable[[AsyncSession, int, int, int], PaginateSignature]
13
+ AsyncPaginateSignature = Callable[[Select, Optional[bool]], Awaitable[Page]]
14
+ DefaultDependency = Callable[[SqlaAsyncSession, int, int], AsyncPaginateSignature]
15
+ WithQueryCountDependency = Callable[
16
+ [SqlaAsyncSession, int, int, int], AsyncPaginateSignature
17
+ ]
15
18
  PaginateDependency = Union[DefaultDependency, WithQueryCountDependency]
16
19
 
17
20
 
18
- async def default_query_count(session: AsyncSession, query: Select) -> int:
21
+ async def default_query_count(session: SqlaAsyncSession, query: Select) -> int:
19
22
  result = await session.execute(select(func.count()).select_from(query.subquery()))
20
23
  return cast(int, result.scalar())
21
24
 
22
25
 
23
26
  async def paginate_query(
24
27
  query: Select,
25
- session: AsyncSession,
28
+ session: SqlaAsyncSession,
26
29
  total_items: int,
27
30
  offset: int,
28
31
  limit: int,
@@ -48,15 +51,16 @@ async def paginate_query(
48
51
 
49
52
 
50
53
  def AsyncPagination(
54
+ session_key: str = _DEFAULT_SESSION_KEY,
51
55
  min_page_size: int = 10,
52
56
  max_page_size: int = 100,
53
57
  query_count: Union[QueryCountDependency, None] = None,
54
58
  ) -> PaginateDependency:
55
59
  def default_dependency(
56
- session: AsyncSession = Depends(),
60
+ session: SqlaAsyncSession = Depends(AsyncSessionDependency(key=session_key)),
57
61
  offset: int = Query(0, ge=0),
58
62
  limit: int = Query(min_page_size, ge=1, le=max_page_size),
59
- ) -> PaginateSignature:
63
+ ) -> AsyncPaginateSignature:
60
64
  async def paginate(query: Select, scalars=True) -> Page:
61
65
  total_items = await default_query_count(session, query)
62
66
  return await paginate_query(
@@ -66,11 +70,11 @@ def AsyncPagination(
66
70
  return paginate
67
71
 
68
72
  def with_query_count_dependency(
69
- session: AsyncSession = Depends(),
73
+ session: SqlaAsyncSession = Depends(AsyncSessionDependency(key=session_key)),
70
74
  offset: int = Query(0, ge=0),
71
75
  limit: int = Query(min_page_size, ge=1, le=max_page_size),
72
76
  total_items: int = Depends(query_count),
73
- ):
77
+ ) -> AsyncPaginateSignature:
74
78
  async def paginate(query: Select, scalars=True) -> Page:
75
79
  return await paginate_query(
76
80
  query, session, total_items, offset, limit, scalars=scalars
@@ -84,4 +88,4 @@ def AsyncPagination(
84
88
  return default_dependency
85
89
 
86
90
 
87
- AsyncPaginate: PaginateDependency = AsyncPagination()
91
+ AsyncPaginate = Annotated[AsyncPaginateSignature, Depends(AsyncPagination())]
@@ -1,10 +1,9 @@
1
- import os
2
1
  from collections.abc import AsyncGenerator
3
2
  from contextlib import asynccontextmanager
4
- from typing import cast
3
+ from typing import Annotated
5
4
 
6
5
  import structlog
7
- from fastapi import Request
6
+ from fastapi import Depends, Request
8
7
  from fastapi.responses import PlainTextResponse
9
8
  from sqlalchemy import text
10
9
  from sqlalchemy.ext.asyncio import AsyncEngine
@@ -13,82 +12,66 @@ from sqlalchemy.orm.session import sessionmaker
13
12
 
14
13
  from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
15
14
  from fastapi_sqla.models import Base
16
- from fastapi_sqla.sqla import new_engine
15
+ from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, new_engine
17
16
 
18
17
  logger = structlog.get_logger(__name__)
19
- _ASYNC_SESSION_KEY = "fastapi_sqla_async_session"
20
- _AsyncSession = sessionmaker(class_=SqlaAsyncSession)
21
18
 
19
+ _ASYNC_REQUEST_SESSION_KEY = "fastapi_sqla_async_session"
20
+ _async_session_factories: dict[str, sessionmaker] = {}
22
21
 
23
- def new_async_engine():
24
- envvar_prefix = None
25
- if "async_sqlalchemy_url" in os.environ:
26
- envvar_prefix = "async_sqlalchemy_"
27
22
 
28
- engine = new_engine(envvar_prefix=envvar_prefix)
23
+ def new_async_engine(key: str = _DEFAULT_SESSION_KEY):
24
+ engine = new_engine(key)
29
25
  return AsyncEngine(engine)
30
26
 
31
27
 
32
- async def startup():
33
- engine = new_async_engine()
28
+ async def startup(key: str = _DEFAULT_SESSION_KEY):
29
+ engine = new_async_engine(key)
34
30
  aws_rds_iam_support.setup(engine.sync_engine)
35
31
  aws_aurora_support.setup(engine.sync_engine)
36
32
 
37
- # Fail early:
33
+ # Fail early
38
34
  try:
39
35
  async with engine.connect() as connection:
40
36
  await connection.execute(text("select 'ok'"))
41
37
  except Exception:
42
38
  logger.critical(
43
- "Failed querying db: is sqlalchemy_url or async_sqlalchemy_url envvar "
44
- "correctly configured?"
39
+ f"Failed querying db for key '{key}': "
40
+ "are the the environment variables correctly configured for this key?"
45
41
  )
46
42
  raise
47
43
 
48
44
  async with engine.connect() as connection:
49
45
  await connection.run_sync(lambda conn: Base.prepare(conn.engine))
50
46
 
51
- _AsyncSession.configure(bind=engine, expire_on_commit=False)
52
- logger.info("startup", async_engine=engine)
47
+ _async_session_factories[key] = sessionmaker(
48
+ class_=SqlaAsyncSession, bind=engine, expire_on_commit=False
49
+ )
53
50
 
54
-
55
- class AsyncSession(SqlaAsyncSession):
56
- def __new__(cls, request: Request):
57
- """Yield the sqlalchmey async session for that request.
58
-
59
- It is meant to be used as a FastAPI dependency::
60
-
61
- from fastapi import APIRouter, Depends
62
- from fastapi_sqla import AsyncSession
63
-
64
- router = APIRouter()
65
-
66
- @router.get("/users")
67
- async def get_users(session: AsyncSession = Depends()):
68
- pass
69
- """
70
- try:
71
- return request.scope[_ASYNC_SESSION_KEY]
72
- except KeyError: # pragma: no cover
73
- raise Exception(
74
- "No async session found in request, please ensure you've setup "
75
- "fastapi_sqla."
76
- )
51
+ logger.info("engine startup", engine_key=key, async_engine=engine)
77
52
 
78
53
 
79
54
  @asynccontextmanager
80
- async def open_session() -> AsyncGenerator[SqlaAsyncSession, None]:
55
+ async def open_session(
56
+ key: str = _DEFAULT_SESSION_KEY,
57
+ ) -> AsyncGenerator[SqlaAsyncSession, None]:
81
58
  """Context manager to open an async session and properly close it when exiting.
82
59
 
83
60
  If no exception is raised before exiting context, session is committed when exiting
84
61
  context. If an exception is raised, session is rollbacked.
85
62
  """
86
- session = cast(SqlaAsyncSession, _AsyncSession())
63
+ try:
64
+ session: SqlaAsyncSession = _async_session_factories[key]()
65
+ except KeyError as exc:
66
+ raise KeyError(
67
+ f"No async session with key '{key}' found, "
68
+ "please ensure you've configured the environment variables for this key."
69
+ ) from exc
70
+
87
71
  logger.bind(db_async_session=session)
88
72
 
89
73
  try:
90
74
  yield session
91
-
92
75
  except Exception:
93
76
  logger.warning("context failed, rolling back", exc_info=True)
94
77
  await session.rollback()
@@ -106,7 +89,9 @@ async def open_session() -> AsyncGenerator[SqlaAsyncSession, None]:
106
89
  await session.close()
107
90
 
108
91
 
109
- async def add_session_to_request(request: Request, call_next):
92
+ async def add_session_to_request(
93
+ request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
94
+ ):
110
95
  """Middleware which injects a new sqla async session into every request.
111
96
 
112
97
  Handles creation of session, as well as commit, rollback, and closing of session.
@@ -121,11 +106,11 @@ async def add_session_to_request(request: Request, call_next):
121
106
  fastapi_sqla.setup(app) # includes middleware
122
107
 
123
108
  @app.get("/users")
124
- async def get_users(session: fastapi_sqla.AsyncSession = Depends()):
109
+ async def get_users(session: fastapi_sqla.AsyncSession):
125
110
  return await session.execute(...) # use your session here
126
111
  """
127
- async with open_session() as session:
128
- request.scope[_ASYNC_SESSION_KEY] = session
112
+ async with open_session(key) as session:
113
+ setattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{key}", session)
129
114
  response = await call_next(request)
130
115
 
131
116
  is_dirty = bool(session.dirty or session.deleted or session.new)
@@ -154,3 +139,38 @@ async def add_session_to_request(request: Request, call_next):
154
139
  await session.rollback()
155
140
 
156
141
  return response
142
+
143
+
144
+ class AsyncSessionDependency:
145
+ def __init__(self, key: str = _DEFAULT_SESSION_KEY) -> None:
146
+ self.key = key
147
+
148
+ def __call__(self, request: Request) -> SqlaAsyncSession:
149
+ """Yield the sqlalchemy async session for that request.
150
+
151
+ It is meant to be used as a FastAPI dependency::
152
+
153
+ from fastapi import APIRouter, Depends
154
+ from fastapi_sqla import SqlaAsyncSession, AsyncSessionDependency
155
+
156
+ router = APIRouter()
157
+
158
+ @router.get("/users")
159
+ async def get_users(
160
+ session: SqlaAsyncSession = Depends(AsyncSessionDependency())
161
+ ):
162
+ pass
163
+ """
164
+ try:
165
+ return getattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{self.key}")
166
+ except AttributeError:
167
+ logger.exception(
168
+ f"No async session with key '{self.key}' found in request, "
169
+ "please ensure you've setup fastapi_sqla.",
170
+ session_key=self.key,
171
+ )
172
+ raise
173
+
174
+
175
+ default_async_session_dep = AsyncSessionDependency()
176
+ AsyncSession = Annotated[SqlaAsyncSession, Depends(default_async_session_dep)]
@@ -1,4 +1,5 @@
1
1
  from os import environ
2
+ from typing import Any
2
3
 
3
4
  try:
4
5
  import boto3
@@ -11,9 +12,10 @@ except ImportError as err:
11
12
  from functools import lru_cache
12
13
 
13
14
  from sqlalchemy import event
15
+ from sqlalchemy.engine import Engine
14
16
 
15
17
 
16
- def setup(engine):
18
+ def setup(engine: Engine):
17
19
  lc_environ = {k.lower(): v for k, v in environ.items()}
18
20
  aws_rds_iam_enabled = lc_environ.get("fastapi_sqla_aws_rds_iam_enabled") == "true"
19
21
 
@@ -30,13 +32,13 @@ def get_rds_client():
30
32
  return session.client("rds")
31
33
 
32
34
 
33
- def get_authentication_token(host, port, user):
35
+ def get_authentication_token(host: str, port: int, user: str):
34
36
  client = get_rds_client()
35
37
  token = client.generate_db_auth_token(DBHostname=host, Port=port, DBUsername=user)
36
38
  return token
37
39
 
38
40
 
39
- def set_connection_token(dialect, conn_rec, cargs, cparams):
41
+ def set_connection_token(dialect, conn_rec, cargs, cparams: dict[str, Any]):
40
42
  cparams["password"] = get_authentication_token(
41
43
  host=cparams["host"], port=cparams.get("port", 5432), user=cparams["user"]
42
44
  )
fastapi_sqla/base.py CHANGED
@@ -1,4 +1,6 @@
1
+ import functools
1
2
  import os
3
+ import re
2
4
 
3
5
  from fastapi import FastAPI
4
6
  from sqlalchemy.engine import Engine
@@ -15,19 +17,41 @@ except ImportError as err: # pragma: no cover
15
17
  asyncio_support_err = str(err)
16
18
 
17
19
 
18
- def setup(app: FastAPI):
19
- engine = sqla.new_engine()
20
-
21
- if not is_async_dialect(engine):
22
- app.add_event_handler("startup", sqla.startup)
23
- app.middleware("http")(sqla.add_session_to_request)
20
+ _ENGINE_KEYS_REGEX = re.compile(r"fastapi_sqla__(?!_)(.+)(?<!_)__(?!_).+")
24
21
 
25
- has_async_config = "async_sqlalchemy_url" in os.environ or is_async_dialect(engine)
26
- if has_async_config:
27
- assert has_asyncio_support, asyncio_support_err
28
- app.add_event_handler("startup", async_sqla.startup)
29
- app.middleware("http")(async_sqla.add_session_to_request)
30
22
 
31
-
32
- def is_async_dialect(engine: Engine):
23
+ def setup(app: FastAPI):
24
+ engine_keys = _get_engine_keys()
25
+ engines = {key: sqla.new_engine(key) for key in engine_keys}
26
+ for key, engine in engines.items():
27
+ if not _is_async_dialect(engine):
28
+ 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
+ )
32
+ else:
33
+ assert has_asyncio_support, asyncio_support_err
34
+ app.add_event_handler(
35
+ "startup", functools.partial(async_sqla.startup, key=key)
36
+ )
37
+ app.middleware("http")(
38
+ functools.partial(async_sqla.add_session_to_request, key=key)
39
+ )
40
+
41
+
42
+ def _get_engine_keys() -> set[str]:
43
+ keys = {sqla._DEFAULT_SESSION_KEY}
44
+
45
+ lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
46
+ for env_var in lowercase_environ:
47
+ match = _ENGINE_KEYS_REGEX.search(env_var)
48
+ if not match:
49
+ continue
50
+
51
+ keys.add(match.group(1))
52
+
53
+ return keys
54
+
55
+
56
+ def _is_async_dialect(engine: Engine):
33
57
  return engine.dialect.is_async if hasattr(engine.dialect, "is_async") else False
@@ -1,24 +1,24 @@
1
1
  import math
2
2
  from collections.abc import Callable
3
3
  from functools import singledispatch
4
- from typing import Iterator, Optional, Union, cast
4
+ from typing import Annotated, Iterator, 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
10
  from fastapi_sqla.models import Page
11
- from fastapi_sqla.sqla import Session
11
+ from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, SessionDependency, SqlaSession
12
12
 
13
13
  DbQuery = Union[LegacyQuery, Select]
14
14
  QueryCountDependency = Callable[..., int]
15
15
  PaginateSignature = Callable[[DbQuery, Optional[bool]], Page]
16
- DefaultDependency = Callable[[Session, int, int], PaginateSignature]
17
- WithQueryCountDependency = Callable[[Session, int, int, int], PaginateSignature]
16
+ DefaultDependency = Callable[[SqlaSession, int, int], PaginateSignature]
17
+ WithQueryCountDependency = Callable[[SqlaSession, int, int, int], PaginateSignature]
18
18
  PaginateDependency = Union[DefaultDependency, WithQueryCountDependency]
19
19
 
20
20
 
21
- def default_query_count(session: Session, query: DbQuery) -> int:
21
+ def default_query_count(session: SqlaSession, query: DbQuery) -> int:
22
22
  """Default function used to count items returned by a query.
23
23
 
24
24
  It is slower than a manually written query could be: It runs the query in a
@@ -46,7 +46,7 @@ def default_query_count(session: Session, query: DbQuery) -> int:
46
46
  @singledispatch
47
47
  def paginate_query(
48
48
  query: DbQuery,
49
- session: Session,
49
+ session: SqlaSession,
50
50
  total_items: int,
51
51
  offset: int,
52
52
  limit: int,
@@ -59,7 +59,7 @@ def paginate_query(
59
59
  @paginate_query.register
60
60
  def _paginate_legacy(
61
61
  query: LegacyQuery,
62
- session: Session,
62
+ session: SqlaSession,
63
63
  total_items: int,
64
64
  offset: int,
65
65
  limit: int,
@@ -81,7 +81,7 @@ def _paginate_legacy(
81
81
  @paginate_query.register
82
82
  def _paginate(
83
83
  query: Select,
84
- session: Session,
84
+ session: SqlaSession,
85
85
  total_items: int,
86
86
  offset: int,
87
87
  limit: int,
@@ -107,12 +107,13 @@ def _paginate(
107
107
 
108
108
 
109
109
  def Pagination(
110
+ session_key: str = _DEFAULT_SESSION_KEY,
110
111
  min_page_size: int = 10,
111
112
  max_page_size: int = 100,
112
113
  query_count: Union[QueryCountDependency, None] = None,
113
114
  ) -> PaginateDependency:
114
115
  def default_dependency(
115
- session: Session = Depends(),
116
+ session: SqlaSession = Depends(SessionDependency(key=session_key)),
116
117
  offset: int = Query(0, ge=0),
117
118
  limit: int = Query(min_page_size, ge=1, le=max_page_size),
118
119
  ) -> PaginateSignature:
@@ -125,7 +126,7 @@ def Pagination(
125
126
  return paginate
126
127
 
127
128
  def with_query_count_dependency(
128
- session: Session = Depends(),
129
+ session: SqlaSession = Depends(SessionDependency(key=session_key)),
129
130
  offset: int = Query(0, ge=0),
130
131
  limit: int = Query(min_page_size, ge=1, le=max_page_size),
131
132
  total_items: int = Depends(query_count),
@@ -143,4 +144,4 @@ def Pagination(
143
144
  return default_dependency
144
145
 
145
146
 
146
- Paginate: PaginateDependency = Pagination()
147
+ Paginate = Annotated[PaginateSignature, Depends(Pagination())]
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 Union
5
+ from typing import Annotated
6
6
 
7
7
  import structlog
8
- from fastapi import Request
8
+ from fastapi import Depends, Request
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
@@ -18,75 +18,62 @@ from fastapi_sqla.models import Base
18
18
 
19
19
  logger = structlog.get_logger(__name__)
20
20
 
21
- _SESSION_KEY = "fastapi_sqla_session"
21
+ _DEFAULT_SESSION_KEY = "default"
22
+ _REQUEST_SESSION_KEY = "fastapi_sqla_session"
23
+ _session_factories: dict[str, sessionmaker] = {}
22
24
 
23
- _Session = sessionmaker()
24
25
 
26
+ def new_engine(key: str = _DEFAULT_SESSION_KEY) -> Engine:
27
+ envvar_prefix = "sqlalchemy_"
28
+ if key != _DEFAULT_SESSION_KEY:
29
+ envvar_prefix = f"fastapi_sqla__{key}__{envvar_prefix}"
25
30
 
26
- def new_engine(*, envvar_prefix: Union[str, None] = None) -> Engine:
27
- envvar_prefix = envvar_prefix if envvar_prefix else "sqlalchemy_"
28
- lowercase_environ = {
29
- k.lower(): v for k, v in os.environ.items() if k.lower() != "sqlalchemy_warn_20"
30
- }
31
+ lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
32
+ lowercase_environ.pop(f"{envvar_prefix}warn_20", None)
31
33
  return engine_from_config(lowercase_environ, prefix=envvar_prefix)
32
34
 
33
35
 
34
- def startup():
35
- engine = new_engine()
36
+ def startup(key: str = _DEFAULT_SESSION_KEY):
37
+ engine = new_engine(key)
36
38
  aws_rds_iam_support.setup(engine.engine)
37
39
  aws_aurora_support.setup(engine.engine)
38
40
 
39
- # Fail early:
41
+ # Fail early
40
42
  try:
41
43
  with engine.connect() as connection:
42
44
  connection.execute(text("select 'OK'"))
43
45
  except Exception:
44
46
  logger.critical(
45
- "Fail querying db: is sqlalchemy_url envvar correctly configured?"
47
+ f"Failed querying db for key '{key}': "
48
+ "are the the environment variables correctly configured for this key?"
46
49
  )
47
50
  raise
48
51
 
49
52
  Base.prepare(engine)
50
- _Session.configure(bind=engine)
51
- logger.info("startup", engine=engine)
53
+ _session_factories[key] = sessionmaker(bind=engine)
52
54
 
53
-
54
- class Session(SqlaSession):
55
- def __new__(cls, request: Request):
56
- """Yield the sqlalchmey session for that request.
57
-
58
- It is meant to be used as a FastAPI dependency::
59
-
60
- from fastapi import APIRouter, Depends
61
- from fastapi_sqla import Session
62
-
63
- router = APIRouter()
64
-
65
- @router.get("/users")
66
- def get_users(session: Session = Depends()):
67
- pass
68
- """
69
- try:
70
- return request.scope[_SESSION_KEY]
71
- except KeyError: # pragma: no cover
72
- raise Exception(
73
- "No session found in request, please ensure you've setup fastapi_sqla."
74
- )
55
+ logger.info("engine startup", engine_key=key, engine=engine)
75
56
 
76
57
 
77
58
  @contextmanager
78
- def open_session() -> Generator[SqlaSession, None, None]:
59
+ def open_session(key: str = _DEFAULT_SESSION_KEY) -> Generator[SqlaSession, None, None]:
79
60
  """Context manager that opens a session and properly closes session when exiting.
80
61
 
81
62
  If no exception is raised before exiting context, session is committed when exiting
82
63
  context. If an exception is raised, session is rollbacked.
83
64
  """
84
- session = _Session()
65
+ try:
66
+ session: SqlaSession = _session_factories[key]()
67
+ except KeyError as exc:
68
+ raise KeyError(
69
+ f"No session with key '{key}' found, "
70
+ "please ensure you've configured the environment variables for this key."
71
+ ) from exc
72
+
85
73
  logger.bind(db_session=session)
86
74
 
87
75
  try:
88
76
  yield session
89
-
90
77
  except Exception:
91
78
  logger.warning("context failed, rolling back", exc_info=True)
92
79
  session.rollback()
@@ -104,7 +91,9 @@ def open_session() -> Generator[SqlaSession, None, None]:
104
91
  session.close()
105
92
 
106
93
 
107
- async def add_session_to_request(request: Request, call_next):
94
+ async def add_session_to_request(
95
+ request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
96
+ ):
108
97
  """Middleware which injects a new sqla session into every request.
109
98
 
110
99
  Handles creation of session, as well as commit, rollback, and closing of session.
@@ -119,11 +108,11 @@ async def add_session_to_request(request: Request, call_next):
119
108
  fastapi_sqla.setup(app) # includes middleware
120
109
 
121
110
  @app.get("/users")
122
- def get_users(session: fastapi_sqla.Session = Depends()):
111
+ def get_users(session: fastapi_sqla.Session):
123
112
  return session.execute(...) # use your session here
124
113
  """
125
- async with contextmanager_in_threadpool(open_session()) as session:
126
- request.scope[_SESSION_KEY] = session
114
+ async with contextmanager_in_threadpool(open_session(key)) as session:
115
+ setattr(request.state, f"{_REQUEST_SESSION_KEY}_{key}", session)
127
116
 
128
117
  response = await call_next(request)
129
118
 
@@ -155,3 +144,36 @@ async def add_session_to_request(request: Request, call_next):
155
144
  await loop.run_in_executor(None, session.rollback)
156
145
 
157
146
  return response
147
+
148
+
149
+ class SessionDependency:
150
+ def __init__(self, key: str = _DEFAULT_SESSION_KEY) -> None:
151
+ self.key = key
152
+
153
+ def __call__(self, request: Request) -> SqlaSession:
154
+ """Yield the sqlalchemy session for that request.
155
+
156
+ It is meant to be used as a FastAPI dependency::
157
+
158
+ from fastapi import APIRouter, Depends
159
+ from fastapi_sqla import SqlaSession, SessionDependency
160
+
161
+ router = APIRouter()
162
+
163
+ @router.get("/users")
164
+ def get_users(session: SqlaSession = Depends(SessionDependency())):
165
+ pass
166
+ """
167
+ try:
168
+ return getattr(request.state, f"{_REQUEST_SESSION_KEY}_{self.key}")
169
+ except AttributeError:
170
+ logger.exception(
171
+ f"No session with key '{self.key}' found in request, "
172
+ "please ensure you've setup fastapi_sqla.",
173
+ session_key=self.key,
174
+ )
175
+ raise
176
+
177
+
178
+ default_session_dep = SessionDependency()
179
+ Session = Annotated[SqlaSession, Depends(default_session_dep)]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-sqla
3
- Version: 2.10.1
3
+ Version: 3.0.0
4
4
  Summary: SQLAlchemy extension for FastAPI with support for pagination, asyncio, 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.7,<4.0
10
+ Requires-Python: >=3.9,<4.0
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Web Environment
13
13
  Classifier: Framework :: AsyncIO
@@ -19,8 +19,6 @@ Classifier: License :: OSI Approved :: MIT License
19
19
  Classifier: Operating System :: OS Independent
20
20
  Classifier: Programming Language :: Python
21
21
  Classifier: Programming Language :: Python :: 3
22
- Classifier: Programming Language :: Python :: 3.7
23
- Classifier: Programming Language :: Python :: 3.8
24
22
  Classifier: Programming Language :: Python :: 3.9
25
23
  Classifier: Programming Language :: Python :: 3.10
26
24
  Classifier: Programming Language :: Python :: 3.11
@@ -43,7 +41,7 @@ Requires-Dist: asgi_lifespan (>=1.0.1,<2.0.0) ; extra == "tests"
43
41
  Requires-Dist: asyncpg (>=0.28.0,<0.29.0) ; extra == "asyncpg"
44
42
  Requires-Dist: black (>=22.8.0,<23.0.0) ; extra == "tests"
45
43
  Requires-Dist: boto3 (>=1.24.74,<2.0.0) ; extra == "aws-rds-iam"
46
- Requires-Dist: fastapi (>=0.61)
44
+ Requires-Dist: fastapi (>=0.95.1)
47
45
  Requires-Dist: greenlet (>=1.1.3,<2.0.0) ; extra == "tests"
48
46
  Requires-Dist: httpx (>=0.23.0,<0.24.0) ; extra == "tests"
49
47
  Requires-Dist: isort (>=5.5.3,<6.0.0) ; extra == "tests"
@@ -90,7 +88,7 @@ unique `email`:
90
88
 
91
89
  ```python
92
90
  # main.py
93
- from fastapi import Depends, FastAPI, HTTPException
91
+ from fastapi import FastAPI, HTTPException
94
92
  from fastapi_sqla import Base, Item, Page, Paginate, Session, setup
95
93
  from pydantic import BaseModel, EmailStr
96
94
  from sqlalchemy import select
@@ -118,12 +116,12 @@ class UserModel(UserIn):
118
116
 
119
117
 
120
118
  @app.get("/users", response_model=Page[UserModel])
121
- def list_users(paginate: Paginate = Depends()):
119
+ def list_users(paginate: Paginate):
122
120
  return paginate(select(User))
123
121
 
124
122
 
125
123
  @app.get("/users/{user_id}", response_model=Item[UserModel])
126
- def get_user(user_id: int, session: Session = Depends()):
124
+ def get_user(user_id: int, session: Session):
127
125
  user = session.get(User, user_id)
128
126
  if user is None:
129
127
  raise HTTPException(404)
@@ -131,7 +129,7 @@ def get_user(user_id: int, session: Session = Depends()):
131
129
 
132
130
 
133
131
  @app.post("/users", response_model=Item[UserModel])
134
- def create_user(new_user: UserIn, session: Session = Depends()):
132
+ def create_user(new_user: UserIn, session: Session):
135
133
  user = User(**new_user.model_dump())
136
134
  session.add(user)
137
135
  try:
@@ -173,6 +171,21 @@ The only required key is `sqlalchemy_url`, which provides the database URL, exam
173
171
  export sqlalchemy_url=postgresql://postgres@localhost
174
172
  ```
175
173
 
174
+ ### Multi-session support
175
+
176
+ In order to configure multiple sessions for the application,
177
+ set the environment variables with this prefix format: `fastapi_sqla__MY_KEY__`.
178
+
179
+ As with the default session, each matching key (after the prefix is stripped)
180
+ is treated as though it were the corresponding keyword argument to [`sqlalchemy.create_engine`]
181
+ call.
182
+
183
+ For example, to configure a session with the `read_only` key:
184
+
185
+ ```bash
186
+ export fastapi_sqla__read_only__sqlalchemy_url=postgresql://postgres@localhost
187
+ ```
188
+
176
189
  ### `asyncio` support using [`asyncpg`]
177
190
 
178
191
  SQLAlchemy `>= 1.4` supports `asyncio`.
@@ -182,7 +195,7 @@ To enable `asyncio` support against a Postgres DB, install `asyncpg`:
182
195
  pip install asyncpg
183
196
  ```
184
197
 
185
- And define environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
198
+ And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
186
199
 
187
200
  ```bash
188
201
  export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
@@ -216,23 +229,71 @@ class Entity(Base):
216
229
 
217
230
  Use [FastAPI dependency injection] to get a session as a parameter of a path operation
218
231
  function.
219
- SQLAlchemy session is committed before response is returned or rollbacked if any
232
+
233
+ The SQLAlchemy session is committed before the response is returned or rollbacked if any
220
234
  exception occurred:
221
235
 
222
236
  ```python
237
+ from fastapi import APIRouter
238
+ from fastapi_sqla import AsyncSession, Session
239
+
240
+ router = APIRouter()
241
+
242
+
243
+ @router.get("/example")
244
+ def example(session: Session):
245
+ return session.execute("SELECT now()").scalar()
246
+
247
+
248
+ @router.get("/async_example")
249
+ async def async_example(session: AsyncSession):
250
+ return await session.scalar("SELECT now()")
251
+ ```
252
+
253
+ In order to get a session configured with a custom key:
254
+
255
+ ```python
256
+ from typing import Annotated
257
+
223
258
  from fastapi import APIRouter, Depends
224
- from fastapi_sqla import Session, AsyncSession
259
+ from fastapi_sqla import (
260
+ AsyncSessionDependency,
261
+ SessionDependency,
262
+ SqlaAsyncSession,
263
+ SqlaSession,
264
+ )
225
265
 
226
266
  router = APIRouter()
227
267
 
228
268
 
269
+ # Preferred
270
+
271
+ ReadOnlySession = Annotated[SqlaSession, Depends(SessionDependency(key="read_only"))]
272
+ AsyncReadOnlySession = Annotated[
273
+ SqlaAsyncSession, Depends(AsyncSessionDependency(key="read_only"))
274
+ ]
275
+
229
276
  @router.get("/example")
230
- def example(session: Session = Depends()):
277
+ def example(session: ReadOnlySession):
231
278
  return session.execute("SELECT now()").scalar()
232
279
 
233
280
 
234
281
  @router.get("/async_example")
235
- async def async_example(session: AsyncSession = Depends()):
282
+ async def async_example(session: AsyncReadOnlySession):
283
+ return await session.scalar("SELECT now()")
284
+
285
+
286
+ # Alternative
287
+
288
+ @router.get("/example/alt")
289
+ def example_alt(session: SqlaSession = Depends(SessionDependency(key="read_only"))):
290
+ return session.execute("SELECT now()").scalar()
291
+
292
+
293
+ @router.get("/async_example/alt")
294
+ async def async_example_alt(
295
+ session: SqlaAsyncSession = Depends(AsyncSessionDependency(key="read_only")),
296
+ ):
236
297
  return await session.scalar("SELECT now()")
237
298
  ```
238
299
 
@@ -240,12 +301,12 @@ async def async_example(session: AsyncSession = Depends()):
240
301
 
241
302
  When needing a session outside of a path operation, like when using
242
303
  [FastAPI background tasks], use `fastapi_sqla.open_session` context manager.
243
- SQLAlchemy session is committed when exiting context or rollbacked if any exception
304
+ The SQLAlchemy session is committed when exiting context or rollbacked if any exception
244
305
  occurred:
245
306
 
246
307
  ```python
247
308
  from fastapi import APIRouter, BackgroundTasks
248
- from fastapi_sqla import open_session, open_async_session
309
+ from fastapi_sqla import open_async_session, open_session
249
310
 
250
311
  router = APIRouter()
251
312
 
@@ -260,16 +321,23 @@ def run_bg():
260
321
  with open_session() as session:
261
322
  session.execute("SELECT now()").scalar()
262
323
 
324
+ def run_bg_with_key():
325
+ with open_session(key="read_only") as session:
326
+ session.execute("SELECT now()").scalar()
263
327
 
264
328
  async def run_async_bg():
265
329
  async with open_async_session() as session:
266
330
  await session.scalar("SELECT now()")
331
+
332
+ async def run_async_bg_with_key():
333
+ async with open_async_session(key="read_only") as session:
334
+ await session.scalar("SELECT now()")
267
335
  ```
268
336
 
269
337
  ## Pagination
270
338
 
271
339
  ```python
272
- from fastapi import APIRouter, Depends
340
+ from fastapi import APIRouter
273
341
  from fastapi_sqla import Base, Page, Paginate
274
342
  from pydantic import BaseModel
275
343
  from sqlalchemy import select
@@ -290,7 +358,7 @@ class UserModel(BaseModel):
290
358
 
291
359
 
292
360
  @router.get("/users", response_model=Page[UserModel])
293
- def all_users(paginate: Paginate = Depends()):
361
+ def all_users(paginate: Paginate):
294
362
  return paginate(select(User))
295
363
  ```
296
364
 
@@ -327,7 +395,7 @@ To paginate a query which doesn't return [scalars], specify `scalars=False` when
327
395
  `paginate`:
328
396
 
329
397
  ```python
330
- from fastapi import APIRouter, Depends
398
+ from fastapi import APIRouter
331
399
  from fastapi_sqla import Base, Page, Paginate
332
400
  from pydantic import BaseModel
333
401
  from sqlalchemy import func, select
@@ -352,7 +420,7 @@ class UserModel(BaseModel):
352
420
 
353
421
 
354
422
  @router.get("/users", response_model=Page[UserModel])
355
- def all_users(paginate: Paginate = Depends()):
423
+ def all_users(paginate: Paginate):
356
424
  query = (
357
425
  select(User.id, User.name, func.count(Note.id).label("notes_count"))
358
426
  .join(Note)
@@ -388,15 +456,15 @@ class UserModel(BaseModel):
388
456
  name: str
389
457
 
390
458
 
391
- def query_count(session: Session = Depends()) -> int:
459
+ def query_count(session: Session) -> int:
392
460
  return session.execute(select(func.count()).select_from(User)).scalar()
393
461
 
394
462
 
395
- Paginate = Pagination(min_page_size=5, max_page_size=500, query_count=query_count)
463
+ CustomPaginate = Pagination(min_page_size=5, max_page_size=500, query_count=query_count)
396
464
 
397
465
 
398
466
  @router.get("/users", response_model=Page[UserModel])
399
- def all_users(paginate: Paginate = Depends()):
467
+ def all_users(paginate: CustomPaginate = Depends()):
400
468
  return paginate(select(User))
401
469
  ```
402
470
 
@@ -405,7 +473,7 @@ def all_users(paginate: Paginate = Depends()):
405
473
  When using the asyncio support, use the `AsyncPaginate` dependency:
406
474
 
407
475
  ```python
408
- from fastapi import APIRouter, Depends
476
+ from fastapi import APIRouter
409
477
  from fastapi_sqla import Base, Page, AsyncPaginate
410
478
  from pydantic import BaseModel
411
479
  from sqlalchemy import select
@@ -426,7 +494,7 @@ class UserModel(BaseModel):
426
494
 
427
495
 
428
496
  @router.get("/users", response_model=Page[UserModel])
429
- async def all_users(paginate: AsyncPaginate = Depends()):
497
+ async def all_users(paginate: AsyncPaginate):
430
498
  return await paginate(select(User))
431
499
  ```
432
500
 
@@ -450,16 +518,85 @@ class UserModel(BaseModel):
450
518
  name: str
451
519
 
452
520
 
453
- async def query_count(session: AsyncSession = Depends()) -> int:
521
+ async def query_count(session: AsyncSession) -> int:
454
522
  result = await session.execute(select(func.count()).select_from(User))
455
523
  return result.scalar()
456
524
 
457
525
 
458
- Paginate = AsyncPagination(min_page_size=5, max_page_size=500, query_count=query_count)
526
+ CustomPaginate = AsyncPagination(min_page_size=5, max_page_size=500, query_count=query_count)
459
527
 
460
528
 
461
529
  @router.get("/users", response_model=Page[UserModel])
462
- def all_users(paginate: CustomPaginate = Depends()):
530
+ async def all_users(paginate: CustomPaginate = Depends()):
531
+ return await paginate(select(User))
532
+ ```
533
+
534
+ ### Multi-session support
535
+
536
+ Pagination supports multiple sessions as well. To paginate using a session
537
+ configured with a custom key:
538
+
539
+ ```python
540
+ from typing import Annotated
541
+
542
+ from fastapi import APIRouter, Depends
543
+ from fastapi_sqla import (
544
+ AsyncPaginateSignature,
545
+ AsyncPagination,
546
+ Base,
547
+ Page,
548
+ PaginateSignature,
549
+ Pagination,
550
+ )
551
+ from pydantic import BaseModel
552
+ from sqlalchemy import func, select
553
+
554
+ router = APIRouter()
555
+
556
+
557
+ class User(Base):
558
+ __tablename__ = "user"
559
+
560
+
561
+ class UserModel(BaseModel):
562
+ id: int
563
+ name: str
564
+
565
+
566
+ # Preferred
567
+
568
+ ReadOnlyPaginate = Annotated[
569
+ PaginateSignature, Depends(Pagination(session_key="read_only"))
570
+ ]
571
+ AsyncReadOnlyPaginate = Annotated[
572
+ AsyncPaginateSignature, Depends(AsyncPagination(session_key="read_only"))
573
+ ]
574
+
575
+ @router.get("/users", response_model=Page[UserModel])
576
+ def all_users(paginate: ReadOnlyPaginate):
577
+ return paginate(select(User))
578
+
579
+ @router.get("/async_users", response_model=Page[UserModel])
580
+ async def async_all_users(paginate: AsyncReadOnlyPaginate):
581
+ return await paginate(select(User))
582
+
583
+
584
+ # Alternative
585
+
586
+ @router.get("/users/alt", response_model=Page[UserModel])
587
+ def all_users_alt(
588
+ paginate: PaginateSignature = Depends(
589
+ Pagination(session_key="read_only")
590
+ ),
591
+ ):
592
+ return paginate(select(User))
593
+
594
+ @router.get("/async_users/alt", response_model=Page[UserModel])
595
+ async def async_all_users_alt(
596
+ paginate: AsyncPaginateSignature = Depends(
597
+ AsyncPagination(session_key="read_only")
598
+ ),
599
+ ):
463
600
  return await paginate(select(User))
464
601
  ```
465
602
 
@@ -0,0 +1,16 @@
1
+ fastapi_sqla/__init__.py,sha256=4-MHiYF6TclnQpgs2Z1KDwYe2OPHLHGEu5oJzV3494s,1128
2
+ fastapi_sqla/_pytest_plugin.py,sha256=I7wmRmJHaws-wOHDBo2N50RUkmKd-zutMMpymI9Tg1w,5769
3
+ fastapi_sqla/async_pagination.py,sha256=UA3KxkTa48utBFDHeAkOFxTGUIvtNEWKm4uH-bqzQH4,3114
4
+ fastapi_sqla/async_sqla.py,sha256=BERggjeIjBW9hPqVxE7ry3iQyjBFc9KLvDGwV_LOfK8,5919
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=QhnPCX-Gz5exAZfWyCRyYSaZ7SM_8QY0Eir6b4_4oI8,1432
9
+ fastapi_sqla/pagination.py,sha256=NsI4ZeOkgbiNNDBjtqZL1rF6j1ya--jjXiyf0GlLaXU,4459
10
+ fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ fastapi_sqla/sqla.py,sha256=ih4JBAQzn-lT86jK5ZOX3tewT1UOrw2NUJauOso0mNg,5939
12
+ fastapi_sqla-3.0.0.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
+ fastapi_sqla-3.0.0.dist-info/METADATA,sha256=8l-HvdC7q5crdm7NJrSMg_bhf43jg73gx_gO0RM1eEc,19809
14
+ fastapi_sqla-3.0.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
15
+ fastapi_sqla-3.0.0.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
+ fastapi_sqla-3.0.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- fastapi_sqla/__init__.py,sha256=QKvBNGd3irKqWp9PQMZ_QpqoqpTzl8jI7sa-zbmOdn8,824
2
- fastapi_sqla/_pytest_plugin.py,sha256=eRUXB-me7CmUFTl362Rd2lBfgs-njvGi_YqiOjZT5UM,5766
3
- fastapi_sqla/async_pagination.py,sha256=YUNF6ORaFTiUisLNCKz996ptisr355UlEB6ZPJ06W48,2801
4
- fastapi_sqla/async_sqla.py,sha256=BwwHi52Hy5NpWnCcEinh3iG-dF7qRrquLhwaPac9qag,5104
5
- fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
6
- fastapi_sqla/aws_rds_iam_support.py,sha256=hGs8erX4e7zYCqwandiokw9LHx_jlW83_fJBIYAb7c4,1090
7
- fastapi_sqla/base.py,sha256=jiL_4n0r_HhyAxkPhu9rwMfh110y2GLrqWDNgDmJk7E,933
8
- fastapi_sqla/models.py,sha256=QhnPCX-Gz5exAZfWyCRyYSaZ7SM_8QY0Eir6b4_4oI8,1432
9
- fastapi_sqla/pagination.py,sha256=wIDVTUYogE7LD1Gzt7AmR2uQU8FM2t2iMm7FP4A7Fe0,4239
10
- fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- fastapi_sqla/sqla.py,sha256=RgR-4VPKxfJw6QFC8oISL4htBrCyQHYc2EhjL8m0H_c,4955
12
- fastapi_sqla-2.10.1.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
- fastapi_sqla-2.10.1.dist-info/METADATA,sha256=Hy-FqTO2rykCVvoO1AF5dL48qN66nziHIEi0xXnC1fE,16597
14
- fastapi_sqla-2.10.1.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
15
- fastapi_sqla-2.10.1.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
- fastapi_sqla-2.10.1.dist-info/RECORD,,