Package: fastapi-asyncpg
FastAPI AsyncPGFastAPI integration for AsyncPGNarrativeFirst of all, so sorry for my poor english. I will be so happy,
if someone pushes a PR correcting all my english mistakes. Anyway
I will try to do my best.Looking at fastapi ecosystem seems like everyone is trying to integrate
fastapi with orms, but from my experience working with raw
sql I'm so productive.If you think a bit around, your real model layer, is the schema on your
db (you can add abastractions on top of it), but what ends
is your data, and these are tables, columns and rows.Also, sql, it's one of the best things I learned
because it's something that always is there.On another side, postgresql it's robust and rock solid,
thousands of projects depend on it, and use it as their storage layer.
AsyncPG it's a crazy fast postgresql driver
written from scratch.FastAPI seems like a clean, and developer productive approach to web
frameworks. It's crazy how well it integrates with OpenAPI,
and how easy makes things to a developer to move on.Integrationfastapi_asyncpg trys to integrate fastapi and asyncpg in an idiomatic way.
fastapi_asyncpg when configured exposes two injectable providers to
fastapi path functions, can use:db.connection: it's just a raw connection picked from the pool,
that it's auto released when pathfunction ends, this is mostly
merit of the DI system around fastapi.db.transaction: the same, but wraps the pathfuncion on a transaction
this is more or less the same than theatomicdecorator from Django.
alsodb.atomicit's aliasedfromfastapiimportFastAPIfromfastapiimportDependsfromfastapi_asyncpgimportconfigure_asyncpgapp=FastAPI()# we need to pass the fastapi app to make use of lifespan asgi eventsdb=configure_asyncpg(app,"postgresql://postgres:postgres@localhost/db")@db.on_initasyncdefinitialization(conn):# you can run your db initialization code hereawaitconn.execute("SELECT 1")@app.get("/")asyncdefget_content(db=Depends(db.connection)):rows=awaitdb.fetch("SELECT wathever FROM tablexxx")return[dict(r)forrinrows]@app.post("/")asyncdefmutate_something_compled(db=Depends(db.atomic))awaitdb.execute()awaitdb.execute()# if something fails, everyting is rolleback, you know all or nothingAnd there's also aninitializationcallable on the main factory function.
That can be used like in flask to initialize whatever you need on the db.
Theinitializationis called right after asyncpg stablishes a connection,
and before the app fully boots. (Some projects use this as a poor migration
runner, not the best practice if you are deploying multiple
instances of the app).TestingFor testing we usepytest-docker-fixtures, it requires docker on the host machine or on whatever CI you use
(seems like works as expected with github actions)It works, creating a container for the session and exposing it as pytest fixture.
It's a good practice to run tests with a real database, and
pytest-docker-fixtures make it's so easy. As a bonus, all fixtures run on a CI.
We use Jenkins witht docker and docker, but also seems like travis and github actions
also work.The fixture needs to be added to the pytest pluginsconftest.pyfile.on conftest.pypytest_plugins=["pytest_docker_fixtures",]With this in place, we can just yield a pg fixturefrompytest_docker_fixturesimportimages# image params can be configured from hereimages.configure("postgresql","postgres","11.1",env={"POSTGRES_DB":"test_db"})# and then on our test we have a pg container running# ready to recreate our dbasyncdeftest_pg(pg):host,port=pgdsn=f"postgresql://postgres@{host}:{port}/test_db"awaitasyncpg.Connect(dsn=dsn)# let's goWith this in place, we can just create our own pytest.fixture thatpatchesthe app dsn to make it work with our custom created
container.from.appimportapp,dbfromasync_asgi_testclientimportTestClientimportpytestpytestmark=pytest.mark.asyncio@pytest.fixtureasyncdefasgi_app(pg)host,port=pgdsn=f"postgresql://postgres@{host}:{port}/test_db"# here we patch the dsn for the db# con_opts: are also accessibledb.dsn=dsnyieldapp,dbasyncdeftest_something(asgi_app):app,db=asgi_appasyncwithdb.pool.acquire()asdb:# setup your test state# this context manager handlers lifespan eventsasyncwithTestClient(app)asclient:res=awaitclient.request("/")```Anywayiftheapplicationwillgrow,tomultiplessubpackages,andapps,wetrendtobuildthemainappasafactory,thatcreatesit,somethinglike:```pythonfromfastapi_asyncpgimportconfigure_asyncpgfromapppackageimportsettingsimportvenusiandefmake_asgi_app(settings):app=FastAPI()db=configure_asyncpg(settings.DSN)scanner=venusian.Scanner(app=app)venusian.scan(theapp)returnappThen on the fixture, we just need, to factorze and app from our functionfrom.factoryimportmake_asgi_appfromasync_asgi_testclientimportTestClientimportpytestpytestmark=pytest.mark.asyncio@pytest.fixtureasyncdefasgi_app(pg)host,port=pgdsn=f"postgresql://postgres@{host}:{port}/test_db"app=make_asgi_app({"dsn":dsn})# ther's a pointer on the pool into app.stateyieldappasyncdeftest_something(asgi_app):app=asgi_apppool=app.state.poolasyncwithdb.pool.acquire()asdb:# setup your test state# this context manager handlers lifespan eventsasyncwithTestClient(app)asclient:res=awaitclient.request("/")There's also another approach exposed and used ontests,
that exposes a single connection to the test and rolls back changes on end.
We use this approach on a large project (500 tables per schema and
multiples schemas), and seems like it speeds up a bit test creation.
This approach is whatDatabasesit's using.
Feel free to follow the tests to see if it feets better.ExtrasThere are some utility functions I daily use with asyncpg that helps me
speed up some sql operations like, they are all on sql.py, and mostly are
self documented. They are in use on tests.Authorsfastapi_asyncpgwas written byJordi collell <jordic@gmail.com>_.