Skip to content

Commit 0f64f98

Browse files
authored
docs: document asyncio support (#32)
1 parent 607b2de commit 0f64f98

14 files changed

Lines changed: 290 additions & 171 deletions

README.md

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ A highly opinionated [SQLAlchemy] extension for [FastAPI]:
88

99
* Setup using environment variables to connect on DB;
1010
* `fastapi_sqla.Base` a declarative base class to reflect DB tables at startup;
11-
* `fastapi_sqla.with_session` a dependency to get an sqla session;
11+
* `fastapi_sqla.Session` a dependency to get an sqla session;
12+
* `fastapi_sqla.open_session` a context manager to get an sqla session;
13+
* `fastapi_sqla.async_support.AsyncSession` a dependency to get an async sqla session ;
14+
* `fastapi_sqla.async_support.open_session` a context manager to get an async sqla
15+
session;
1216
* Automated commit/rollback of sqla session at the end of request before returning
1317
response;
1418
* Pagination utilities;
15-
* Pytest fixtures to easy writing test;
19+
* Pytest fixtures;
1620

1721
## Configuration
1822

@@ -25,6 +29,21 @@ call.
2529

2630
The only required key is `sqlalchemy_url`, which provides the database URL.
2731

32+
#### `asyncio` support using [`asyncpg`]
33+
34+
SQLAlchemy `>= 1.4` supports `asyncio`.
35+
To enable `asyncio` support against a Postgres DB, install `asyncpg`:
36+
37+
```bash
38+
pip install asyncpg
39+
```
40+
41+
And define environment variable `async_sqlalchemy_url` with `postgres+asyncpg` scheme:
42+
43+
```bash
44+
export async_sqlalchemy_url=postgresql+asyncpg://postgres@localhost
45+
```
46+
2847
### Setup the app:
2948

3049
```python
@@ -59,13 +78,19 @@ exception occurred:
5978
```python
6079
from fastapi import APIRouter, Depends
6180
from fastapi_sqla import Session
81+
from fastapi_sqla.asyncio_support import AsyncSession
6282

6383
router = APIRouter()
6484

6585

6686
@router.get("/example")
6787
def example(session: Session = Depends()):
6888
return session.execute("SELECT now()").scalar()
89+
90+
91+
@router.get("/async_example")
92+
async def async_example(session: AsyncSession = Depends()):
93+
return await session.execute("SELECT now()").scalar()
6994
```
7095

7196
#### Using a context manager
@@ -78,18 +103,25 @@ occurred:
78103
```python
79104
from fastapi import APIRouter, BackgroundTasks
80105
from fastapi_sqla import open_session
106+
from fastapi_sqla import asyncio_support
81107

82108
router = APIRouter()
83109

84110

85111
@router.get("/example")
86112
def example(bg: BackgroundTasks):
87113
bg.add_task(run_bg)
114+
bg.add_task(run_async_bg)
88115

89116

90117
def run_bg():
91118
with open_session() as session:
92119
session.execute("SELECT now()").scalar()
120+
121+
122+
async def run_async_bg():
123+
async with asyncio_support.open_session() as session:
124+
await session.execute("SELECT now()").scalar()
93125
```
94126

95127
### Pagination
@@ -238,13 +270,23 @@ def db_url():
238270
return "postgresql://postgres@localhost/test_database"
239271
```
240272

273+
### `async_sqlalchemy_url`
274+
275+
DB url to use when using `asyncio` support. Defaults to `db_url` fixture with
276+
`postgresql+asyncpg://` scheme.
241277

242-
### `session`
243278

244-
Sqla session to create db fixture:
279+
### `session` & `async_session`
280+
281+
Sqla sessions to create db fixture:
245282
* All changes done at test setup or during the test are rollbacked at test tear down;
246283
* No record will actually be written in the database;
247-
* Changes in one session need to be committed to be available from other sessions;
284+
* Changes in one regular session need to be committed to be available from other regular
285+
sessions;
286+
* Changes in one async session need to be committed to be available from other async
287+
sessions;
288+
* Changes from regular sessions are not available from `async` session and vice-versa
289+
even when committed;
248290

249291
Example:
250292
```python
@@ -258,6 +300,15 @@ def patient(session):
258300
session.add(patient)
259301
session.commit()
260302
return patient
303+
304+
305+
@fixture
306+
async def doctor(async_session):
307+
from er.sqla import Doctor
308+
doctor = Doctor(name="who")
309+
async_session.add(doctor)
310+
await async_session.commit()
311+
return doctor
261312
```
262313

263314
### `db_migration`
@@ -307,3 +358,4 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
307358
[FastAPI dependency injection]: https://fastapi.tiangolo.com/tutorial/dependencies/
308359
[FastAPI background tasks]: https://fastapi.tiangolo.com/tutorial/background-tasks/
309360
[SQLAlchemy]: http://sqlalchemy.org/
361+
[`asyncpg`]: https://magicstack.github.io/asyncpg/current/

fastapi_sqla/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@
1111
from pydantic import BaseModel, Field
1212
from pydantic.generics import GenericModel
1313
from sqlalchemy import engine_from_config
14-
from sqlalchemy.ext.declarative import DeferredReflection, declarative_base
14+
from sqlalchemy.ext.declarative import DeferredReflection
1515
from sqlalchemy.orm import Query as LegacyQuery
1616
from sqlalchemy.orm.session import Session as SqlaSession
1717
from sqlalchemy.orm.session import sessionmaker
1818
from sqlalchemy.sql import Select, func, select
1919

20+
try:
21+
from sqlalchemy.orm import declarative_base
22+
except ImportError:
23+
from sqlalchemy.ext.declarative import declarative_base
24+
2025
try:
2126
from . import asyncio_support
2227
from .asyncio_support import AsyncSession
@@ -47,8 +52,8 @@ def setup(app: FastAPI):
4752
app.add_event_handler("startup", startup)
4853
app.middleware("http")(add_session_to_request)
4954

50-
asyncpg_url = os.getenv("asyncpg_url")
51-
if asyncpg_url:
55+
async_sqlalchemy_url = os.getenv("async_sqlalchemy_url")
56+
if async_sqlalchemy_url:
5257
assert asyncio_support, asyncio_support_err
5358
app.add_event_handler("startup", asyncio_support.startup)
5459
app.middleware("http")(asyncio_support.add_session_to_request)

fastapi_sqla/_pytest_plugin.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from alembic import command
55
from alembic.config import Config
66
from pytest import fixture
7-
from sqlalchemy import create_engine
7+
from sqlalchemy import create_engine, text
88

99
try:
1010
from sqlalchemy.ext.asyncio import create_async_engine
@@ -27,12 +27,6 @@ def db_url():
2727
return f"postgresql://postgres@{host}/postgres"
2828

2929

30-
@fixture(scope="session")
31-
def asyncpg_url(db_url):
32-
scheme, parts = db_url.split(":")
33-
return f"{scheme}+asyncpg:{parts}"
34-
35-
3630
@fixture(scope="session")
3731
def sqla_connection(db_url):
3832
engine = create_engine(db_url)
@@ -56,7 +50,7 @@ def db_migration(db_url, sqla_connection, alembic_ini_path):
5650
alembic_config = Config(file_=alembic_ini_path)
5751
alembic_config.set_main_option("sqlalchemy.url", db_url)
5852

59-
sqla_connection.execute("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
53+
sqla_connection.execute(text("DROP SCHEMA public CASCADE; CREATE SCHEMA public;"))
6054

6155
command.upgrade(alembic_config, "head")
6256
yield
@@ -110,15 +104,27 @@ def session(sqla_transaction, sqla_connection):
110104

111105
if asyncio_support:
112106

107+
@fixture(scope="session")
108+
def async_sqlalchemy_url(db_url):
109+
"""Default async db url.
110+
111+
It is the same as `db_url` with `postgresql+asynpg://` as scheme.
112+
"""
113+
scheme, parts = db_url.split(":")
114+
return f"{scheme}+asyncpg:{parts}"
115+
116+
@fixture
117+
async def async_engine(async_sqlalchemy_url):
118+
return create_async_engine(async_sqlalchemy_url)
119+
113120
@fixture
114-
async def async_sqla_connection(asyncpg_url, event_loop):
115-
engine = create_async_engine(asyncpg_url)
116-
async with engine.begin() as connection:
121+
async def async_sqla_connection(async_engine, event_loop):
122+
async with async_engine.begin() as connection:
117123
yield connection
118124
await connection.rollback()
119125

120126
@fixture(autouse=True)
121-
async def patch_async_sessionmaker(asyncpg_url, async_sqla_connection):
127+
async def patch_async_sessionmaker(async_sqlalchemy_url, async_sqla_connection):
122128
"""So that all async DB operations are never written to db for real."""
123129
with patch(
124130
"fastapi_sqla.asyncio_support.create_async_engine"

fastapi_sqla/asyncio_support.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414

1515
def startup():
16-
asyncpg_url = os.environ["asyncpg_url"]
17-
async_engine = create_async_engine(asyncpg_url)
16+
async_sqlalchemy_url = os.environ["async_sqlalchemy_url"]
17+
async_engine = create_async_engine(async_sqlalchemy_url)
1818
_AsyncSession.configure(bind=async_engine, expire_on_commit=False)
1919

2020

0 commit comments

Comments
 (0)