fastapi-sqla 3.4.7__py3-none-any.whl → 3.5.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.
@@ -1,16 +1,23 @@
1
1
  import os
2
+ from collections.abc import AsyncGenerator, Generator
2
3
  from unittest.mock import patch
3
4
  from urllib.parse import urlsplit, urlunsplit
4
5
 
5
6
  from alembic import command
6
7
  from alembic.config import Config
7
- from pytest import fixture
8
+ from pytest import FixtureRequest, fixture
8
9
  from sqlalchemy import create_engine, text
9
- from sqlalchemy.orm.session import sessionmaker
10
+ from sqlalchemy.engine import Connection, Engine
11
+ from sqlalchemy.orm.session import Session, sessionmaker
10
12
 
11
13
  try:
12
14
  import asyncpg # noqa
13
- from sqlalchemy.ext.asyncio import create_async_engine
15
+ from sqlalchemy.ext.asyncio import (
16
+ create_async_engine,
17
+ AsyncEngine,
18
+ AsyncConnection,
19
+ AsyncSession,
20
+ )
14
21
 
15
22
  asyncio_support = True
16
23
  except ImportError:
@@ -22,7 +29,7 @@ def pytest_configure(config):
22
29
 
23
30
 
24
31
  @fixture(scope="session")
25
- def db_host():
32
+ def db_host() -> str:
26
33
  """Default db host used by depending fixtures.
27
34
 
28
35
  When CI key is set in environment variables, it uses `postgres` as host name else,
@@ -32,7 +39,7 @@ def db_host():
32
39
 
33
40
 
34
41
  @fixture(scope="session")
35
- def db_user():
42
+ def db_user() -> str:
36
43
  """Default db user used by depending fixtures.
37
44
 
38
45
  postgres
@@ -41,7 +48,7 @@ def db_user():
41
48
 
42
49
 
43
50
  @fixture(scope="session")
44
- def db_url(db_host, db_user):
51
+ def db_url(db_host: str, db_user: str) -> str:
45
52
  """Default db url used by depending fixtures.
46
53
 
47
54
  db url example postgresql://{db_user}@{db_host}/postgres
@@ -50,24 +57,24 @@ def db_url(db_host, db_user):
50
57
 
51
58
 
52
59
  @fixture(scope="session")
53
- def engine(db_url):
60
+ def engine(db_url: str) -> Engine:
54
61
  return create_engine(db_url)
55
62
 
56
63
 
57
64
  @fixture(scope="session")
58
- def sqla_connection(engine):
65
+ def sqla_connection(engine: Engine) -> Generator[Connection]:
59
66
  with engine.connect() as connection:
60
67
  yield connection
61
68
 
62
69
 
63
70
  @fixture(scope="session")
64
- def alembic_ini_path(): # pragma: no cover
71
+ def alembic_ini_path() -> str: # pragma: no cover
65
72
  """Path for alembic.ini file, defaults to `./alembic.ini`."""
66
73
  return "./alembic.ini"
67
74
 
68
75
 
69
76
  @fixture(scope="session")
70
- def db_migration(db_url, sqla_connection, alembic_ini_path):
77
+ def db_migration(db_url: str, sqla_connection: Connection, alembic_ini_path: str):
71
78
  """Run alembic upgrade at test session setup and downgrade at tear down.
72
79
 
73
80
  Override fixture `alembic_ini_path` to change path of `alembic.ini` file.
@@ -94,54 +101,52 @@ def sqla_modules():
94
101
 
95
102
 
96
103
  @fixture
97
- def sqla_reflection(sqla_modules, sqla_connection):
104
+ def sqla_reflection(sqla_modules, sqla_connection: Connection):
98
105
  import fastapi_sqla
99
106
 
100
- fastapi_sqla.Base.metadata.bind = sqla_connection
107
+ fastapi_sqla.Base.metadata.bind = sqla_connection # type: ignore
101
108
  fastapi_sqla.Base.prepare(sqla_connection.engine)
102
109
 
103
110
 
104
111
  @fixture
105
- def patch_engine_from_config(request, sqla_connection):
112
+ def patch_new_engine(request: FixtureRequest, sqla_connection: Connection):
106
113
  """So that all DB operations are never written to db for real."""
107
114
  if "dont_patch_engines" in request.keywords:
108
115
  yield
109
116
  else:
110
- transaction = sqla_connection.begin()
111
-
112
- with patch("fastapi_sqla.sqla.engine_from_config") as engine_from_config:
113
- engine_from_config.return_value = sqla_connection
114
- yield
117
+ with sqla_connection.begin() as transaction:
118
+ with patch("fastapi_sqla.sqla.new_engine", return_value=sqla_connection):
119
+ yield
115
120
 
116
- transaction.rollback()
121
+ transaction.rollback()
117
122
 
118
123
 
119
124
  @fixture
120
- def session_factory():
121
- return sessionmaker()
125
+ def session_factory(
126
+ sqla_connection: Connection, sqla_reflection, patch_new_engine
127
+ ) -> sessionmaker:
128
+ return sessionmaker(bind=sqla_connection)
122
129
 
123
130
 
124
131
  @fixture
125
- def session(
126
- session_factory, sqla_connection, sqla_reflection, patch_engine_from_config
127
- ):
132
+ def session(session_factory: sessionmaker) -> Generator[Session]:
128
133
  """Sqla session to use when creating db fixtures.
129
134
 
130
135
  While it does not write any record in DB, the application will still be able to
131
136
  access any record committed with that session.
132
137
  """
133
- session = session_factory(bind=sqla_connection)
138
+ session: Session = session_factory()
134
139
  yield session
135
140
  session.close()
136
141
 
137
142
 
138
- def format_async_async_sqlalchemy_url(url):
143
+ def format_async_async_sqlalchemy_url(url: str) -> str:
139
144
  scheme, location, path, query, fragment = urlsplit(url)
140
145
  return urlunsplit([f"{scheme}+asyncpg", location, path, query, fragment])
141
146
 
142
147
 
143
148
  @fixture(scope="session")
144
- def async_sqlalchemy_url(db_url):
149
+ def async_sqlalchemy_url(db_url: str) -> str:
145
150
  """Default async db url.
146
151
 
147
152
  It is the same as `db_url` with `postgresql+asyncpg://` as scheme.
@@ -152,46 +157,56 @@ def async_sqlalchemy_url(db_url):
152
157
  if asyncio_support:
153
158
 
154
159
  @fixture
155
- def async_engine(async_sqlalchemy_url):
160
+ def async_engine(async_sqlalchemy_url: str) -> AsyncEngine:
156
161
  return create_async_engine(async_sqlalchemy_url)
157
162
 
158
163
  @fixture
159
- async def async_sqla_connection(async_engine):
164
+ async def async_sqla_connection(
165
+ async_engine: AsyncEngine,
166
+ ) -> AsyncGenerator[AsyncConnection]:
160
167
  async with async_engine.connect() as connection:
161
168
  yield connection
162
169
 
163
170
  @fixture
164
- async def patch_new_engine(request, async_sqla_connection):
171
+ async def patch_new_async_engine(
172
+ request: FixtureRequest, async_sqla_connection: AsyncConnection
173
+ ):
165
174
  """So that all async DB operations are never written to db for real."""
166
175
  if "dont_patch_engines" in request.keywords:
167
176
  yield
168
177
  else:
169
178
  async with async_sqla_connection.begin() as transaction:
170
- with patch("fastapi_sqla.async_sqla.new_engine") as new_engine:
171
- new_engine.return_value = async_sqla_connection
179
+ with patch(
180
+ "fastapi_sqla.async_sqla.new_async_engine",
181
+ return_value=async_sqla_connection,
182
+ ):
172
183
  yield
173
184
 
174
185
  await transaction.rollback()
175
186
 
176
187
  @fixture
177
- async def async_sqla_reflection(sqla_modules, async_sqla_connection):
188
+ async def async_sqla_reflection(
189
+ sqla_modules, async_sqla_connection: AsyncConnection
190
+ ):
178
191
  from fastapi_sqla import Base
179
192
 
180
193
  await async_sqla_connection.run_sync(lambda conn: Base.prepare(conn.engine))
181
194
 
182
195
  @fixture
183
- def async_session_factory():
184
- from fastapi_sqla.async_sqla import SqlaAsyncSession
185
-
186
- return sessionmaker(class_=SqlaAsyncSession)
196
+ def async_session_factory(
197
+ async_sqla_connection: AsyncConnection,
198
+ async_sqla_reflection,
199
+ patch_new_async_engine,
200
+ ) -> sessionmaker:
201
+ # TODO: Use async_sessionmaker once only supporting 2.x+
202
+ return sessionmaker(
203
+ bind=async_sqla_connection, expire_on_commit=False, class_=AsyncSession
204
+ ) # type: ignore
187
205
 
188
206
  @fixture
189
207
  async def async_session(
190
- async_session_factory,
191
- async_sqla_connection,
192
- async_sqla_reflection,
193
- patch_new_engine,
194
- ):
195
- session = async_session_factory(bind=async_sqla_connection)
208
+ async_session_factory: sessionmaker,
209
+ ) -> AsyncGenerator[AsyncSession]:
210
+ session: AsyncSession = async_session_factory()
196
211
  yield session
197
212
  await session.close()
@@ -1,18 +1,23 @@
1
+ import os
1
2
  from collections.abc import AsyncGenerator
2
3
  from contextlib import asynccontextmanager
3
- from typing import Annotated
4
+ from typing import Annotated, Union
4
5
 
5
6
  import structlog
6
7
  from fastapi import Depends, Request, Response
7
8
  from fastapi.responses import PlainTextResponse
8
9
  from sqlalchemy import text
9
- from sqlalchemy.ext.asyncio import AsyncEngine
10
+ from sqlalchemy.ext.asyncio import (
11
+ AsyncConnection,
12
+ AsyncEngine,
13
+ async_engine_from_config,
14
+ )
10
15
  from sqlalchemy.ext.asyncio import AsyncSession as SqlaAsyncSession
11
16
  from sqlalchemy.orm.session import sessionmaker
12
17
  from starlette.types import ASGIApp, Message, Receive, Scope, Send
13
18
 
14
19
  from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
15
- from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, Base, new_engine
20
+ from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, Base, get_envvar_prefix
16
21
 
17
22
  logger = structlog.get_logger(__name__)
18
23
 
@@ -20,19 +25,29 @@ _ASYNC_REQUEST_SESSION_KEY = "fastapi_sqla_async_session"
20
25
  _async_session_factories: dict[str, sessionmaker] = {}
21
26
 
22
27
 
23
- def new_async_engine(key: str = _DEFAULT_SESSION_KEY):
24
- engine = new_engine(key)
25
- return AsyncEngine(engine)
28
+ def new_async_engine(
29
+ key: str = _DEFAULT_SESSION_KEY,
30
+ ) -> Union[AsyncEngine, AsyncConnection]:
31
+ envvar_prefix = get_envvar_prefix(key)
32
+ lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
33
+ lowercase_environ.pop(f"{envvar_prefix}warn_20", None)
34
+ return async_engine_from_config(lowercase_environ, prefix=envvar_prefix)
26
35
 
27
36
 
28
37
  async def startup(key: str = _DEFAULT_SESSION_KEY):
29
- engine = new_async_engine(key)
30
- aws_rds_iam_support.setup(engine.sync_engine)
31
- aws_aurora_support.setup(engine.sync_engine)
38
+ engine_or_connection = new_async_engine(key)
39
+ aws_rds_iam_support.setup(engine_or_connection.sync_engine)
40
+ aws_aurora_support.setup(engine_or_connection.sync_engine)
41
+
42
+ async_engine = (
43
+ engine_or_connection
44
+ if isinstance(engine_or_connection, AsyncEngine)
45
+ else engine_or_connection.engine
46
+ )
32
47
 
33
48
  # Fail early
34
49
  try:
35
- async with engine.connect() as connection:
50
+ async with async_engine.connect() as connection:
36
51
  await connection.execute(text("select 'ok'"))
37
52
  except Exception:
38
53
  logger.critical(
@@ -41,14 +56,15 @@ async def startup(key: str = _DEFAULT_SESSION_KEY):
41
56
  )
42
57
  raise
43
58
 
44
- async with engine.connect() as connection:
59
+ async with async_engine.connect() as connection:
45
60
  await connection.run_sync(lambda conn: Base.prepare(conn.engine))
46
61
 
62
+ # TODO: Use async_sessionmaker once only supporting 2.x+
47
63
  _async_session_factories[key] = sessionmaker(
48
- class_=SqlaAsyncSession, bind=engine, expire_on_commit=False
49
- )
64
+ class_=SqlaAsyncSession, bind=engine_or_connection, expire_on_commit=False
65
+ ) # type: ignore
50
66
 
51
- logger.info("engine startup", engine_key=key, async_engine=engine)
67
+ logger.info("engine startup", engine_key=key, async_engine=engine_or_connection)
52
68
 
53
69
 
54
70
  @asynccontextmanager
fastapi_sqla/base.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import functools
2
2
  import os
3
3
  import re
4
+ from typing import Union
4
5
 
5
6
  from deprecated import deprecated
6
7
  from fastapi import FastAPI
7
- from sqlalchemy.engine import Engine
8
+ from sqlalchemy.engine import Connection, Engine
8
9
 
9
10
  from fastapi_sqla import sqla
10
11
 
@@ -72,5 +73,5 @@ def _get_engine_keys() -> set[str]:
72
73
  return keys
73
74
 
74
75
 
75
- def _is_async_dialect(engine: Engine):
76
+ def _is_async_dialect(engine: Union[Engine, Connection]):
76
77
  return engine.dialect.is_async if hasattr(engine.dialect, "is_async") else False
fastapi_sqla/sqla.py CHANGED
@@ -2,14 +2,14 @@ import asyncio
2
2
  import os
3
3
  from collections.abc import Generator
4
4
  from contextlib import contextmanager
5
- from typing import Annotated
5
+ from typing import Annotated, Union
6
6
 
7
7
  import structlog
8
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
12
- from sqlalchemy.engine import Engine
12
+ from sqlalchemy.engine import Connection, 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
@@ -42,24 +42,29 @@ class Base(DeclarativeBase, DeferredReflection):
42
42
  __abstract__ = True
43
43
 
44
44
 
45
- def new_engine(key: str = _DEFAULT_SESSION_KEY) -> Engine:
45
+ def get_envvar_prefix(key: str) -> str:
46
46
  envvar_prefix = "sqlalchemy_"
47
47
  if key != _DEFAULT_SESSION_KEY:
48
48
  envvar_prefix = f"fastapi_sqla__{key}__{envvar_prefix}"
49
49
 
50
+ return envvar_prefix
51
+
52
+
53
+ def new_engine(key: str = _DEFAULT_SESSION_KEY) -> Union[Engine, Connection]:
54
+ envvar_prefix = get_envvar_prefix(key)
50
55
  lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
51
56
  lowercase_environ.pop(f"{envvar_prefix}warn_20", None)
52
57
  return engine_from_config(lowercase_environ, prefix=envvar_prefix)
53
58
 
54
59
 
55
60
  def startup(key: str = _DEFAULT_SESSION_KEY):
56
- engine = new_engine(key)
57
- aws_rds_iam_support.setup(engine.engine)
58
- aws_aurora_support.setup(engine.engine)
61
+ engine_or_connection = new_engine(key)
62
+ aws_rds_iam_support.setup(engine_or_connection.engine)
63
+ aws_aurora_support.setup(engine_or_connection.engine)
59
64
 
60
65
  # Fail early
61
66
  try:
62
- with engine.connect() as connection:
67
+ with engine_or_connection.engine.connect() as connection:
63
68
  connection.execute(text("select 'OK'"))
64
69
  except Exception:
65
70
  logger.critical(
@@ -68,11 +73,13 @@ def startup(key: str = _DEFAULT_SESSION_KEY):
68
73
  )
69
74
  raise
70
75
 
71
- Base.prepare(engine)
76
+ Base.prepare(engine_or_connection.engine)
72
77
 
73
- _session_factories[key] = sessionmaker(bind=engine, class_=SqlaSession)
78
+ _session_factories[key] = sessionmaker(
79
+ bind=engine_or_connection, class_=SqlaSession
80
+ )
74
81
 
75
- logger.info("engine startup", engine_key=key, engine=engine)
82
+ logger.info("engine startup", engine_key=key, engine=engine_or_connection)
76
83
 
77
84
 
78
85
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-sqla
3
- Version: 3.4.7
3
+ Version: 3.5.0
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
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.9
23
23
  Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
26
27
  Classifier: Programming Language :: Python :: 3 :: Only
27
28
  Classifier: Programming Language :: SQL
28
29
  Classifier: Topic :: Internet
@@ -46,8 +47,8 @@ Requires-Dist: fastapi (>=0.95.1,<0.116)
46
47
  Requires-Dist: psycopg2 (>=2.8.6,<3) ; extra == "psycopg2"
47
48
  Requires-Dist: pydantic (>=1,<3)
48
49
  Requires-Dist: sqlalchemy (>=1.3,<3)
49
- Requires-Dist: sqlmodel (>=0.0.14,<0.0.23) ; extra == "sqlmodel"
50
- Requires-Dist: structlog (>=20,<25)
50
+ Requires-Dist: sqlmodel (>=0.0.14,<0.0.25) ; extra == "sqlmodel"
51
+ Requires-Dist: structlog (>=20,<26)
51
52
  Project-URL: Repository, https://github.com/dialoguemd/fastapi-sqla
52
53
  Description-Content-Type: text/markdown
53
54
 
@@ -0,0 +1,16 @@
1
+ fastapi_sqla/__init__.py,sha256=RRkwo9xZzidQ-k3BRXfqT0jtWm8d08w94XuOLteFCdU,1221
2
+ fastapi_sqla/_pytest_plugin.py,sha256=ADISQajpoqSh9Rb3HoNDTaSH3A06I8qMlfmlnUzKsy0,6176
3
+ fastapi_sqla/async_pagination.py,sha256=3DHGUjvrpkbWMIc_BEX4GvM-_PTcn62K9z48ucTJlH0,3164
4
+ fastapi_sqla/async_sqla.py,sha256=ogctvsj3TFL6xTqhB57DLlcANrocjy1LWUEb799w4AI,7498
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=bmwRfG1MQfqc6lg9SIsAdul3n5CXGgaVvoEHTnv2NNs,2335
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=RLBDuBjs2QvkZuVdgoeEBPnDokRQlmdPeHtdBji8WhE,7609
12
+ fastapi_sqla-3.5.0.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
+ fastapi_sqla-3.5.0.dist-info/METADATA,sha256=MQrAqU5dGgPMtfqApzaeYpJsyfoR8DvjbzRjfQKa2xU,21031
14
+ fastapi_sqla-3.5.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
15
+ fastapi_sqla-3.5.0.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
+ fastapi_sqla-3.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,16 +0,0 @@
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.7.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
13
- fastapi_sqla-3.4.7.dist-info/METADATA,sha256=M5It4By2n-EJKy6HtorAlfpVAmlXHUP6CAhuaMUGd0M,20980
14
- fastapi_sqla-3.4.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
- fastapi_sqla-3.4.7.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
16
- fastapi_sqla-3.4.7.dist-info/RECORD,,