Integration with Responder¶
Responder is an ASGI web framework built
on Starlette with a Flask-like API: every view receives req and resp, and views
may be asynchronous. Pony works with it in asynchronous mode - a view
opens the session with async with db_session: and the queries are awaited.
The session can be turned into a dependency so that the views do not repeat the same
async with db_session: in every handler. The provider is registered once with
@api.dependency(), and it is a generator: it opens the session, yields control to the
view, and closes the session (committing the transaction) after the view has returned:
import responder
from pony.orm import *
api = responder.API()
db = Database('postgres_async', dsn='dbname=mydb user=postgres host=localhost')
class Task(db.Entity):
title = Required(str)
done = Optional(bool, default=False)
db.generate_mapping(create_tables=True) # schema operations are synchronous
@api.dependency(name='db_session')
async def db_session_dep():
"""A dependency: opens a database session around the view.
Registered under the name ``db_session``, so a view declares ``db_session``
among its parameters. Control is yielded to the view, so the teardown - commit
on session exit, or rollback if the view raised - runs after the response is
ready. The value itself is not used: Pony keeps the session in contextvars.
"""
async with db_session:
yield
@api.get('/tasks')
async def list_tasks(req, resp, *, db_session):
tasks = await select(t for t in Task).order_by(Task.title)
resp.media = [{'id': t.id, 'title': t.title, 'done': t.done} for t in tasks]
@api.get('/tasks/{task_id:int}')
async def get_task(req, resp, *, task_id, db_session):
try:
task = await Task[task_id]
except ObjectNotFound:
resp.status_code = 404
resp.media = {'error': 'task not found'}
return
resp.media = {'id': task.id, 'title': task.title, 'done': task.done}
@api.delete('/tasks/{task_id:int}')
async def delete_task(req, resp, *, task_id, db_session):
try:
task = await Task[task_id]
except ObjectNotFound:
resp.status_code = 404
return
task.delete()
resp.status_code = 204
@api.post('/tasks') # explicit session, see below
async def create_task(req, resp):
data = await req.media() # the body is read before the session
async with db_session: # a pool connection is taken only here
task = Task(title=data['title'])
await flush() # the primary key is available here
resp.status_code = 201
resp.media = {'id': task.id, 'title': task.title, 'done': task.done}
if __name__ == '__main__':
api.run()
The provider is registered under the name db_session, and a view gets it by declaring
a parameter with that name - Responder injects a registered dependency into any such
parameter automatically, so no Depends(...) is needed in the routes. The parameter
itself is a marker: Pony keeps the session in contextvars, so the value is not used, but
the declaration is what makes Responder run the dependency around the view:
@api.get('/tasks')
async def list_tasks(req, resp, *, db_session):
tasks = await select(t for t in Task).order_by(Task.title) # session is open
resp.media = [{'id': t.id, 'title': t.title, 'done': t.done} for t in tasks]
The same mechanism is used when a provider returns something the view needs - then the value is used as well:
@api.dependency()
async def current_user(req):
return await req.api.state.auth.load_user(req.headers.get('Authorization'))
@api.get('/me')
async def me(req, resp, *, current_user):
resp.media = {'user': current_user.name}
Two things about the last handler:
create_taskkeeps the explicitasync with db_session:. The request body is read first, so a session (and a connection from the pool) is taken only for the part of the handler that works with the database.await flush()inside an explicit session sends the pending changes to the database, which is why the primary key of the new object can be used right away.
Error handling and transactions¶
The two forms differ in how failures are handled, and it is worth knowing before choosing one of them.
Explicit session. An exception raised inside the view reaches async with
db_session:, so the transaction is rolled back:
@api.post('/tasks')
async def create_task(req, resp):
data = await req.media()
async with db_session:
Task(title=data['title'])
raise ValueError('something went wrong') # 500, changes are rolled back
Session dependency. Responder turns an exception raised by a view into an error
response, so the dependency’s generator finishes normally and the session is committed -
even when the handler returned 400 or the framework produced 500. Such a
dependency is therefore best for read-only handlers (list_tasks and get_task
above), while a handler that writes should either open the session explicitly, or roll
back before returning an error:
@api.post('/tasks/import')
async def import_tasks(req, resp, *, db_session):
data = await req.media()
if not data.get('items'):
await rollback() # nothing should be written
resp.status_code = 400
resp.media = {'error': 'no items'}
return
for item in data['items']:
Task(title=item['title'])
resp.status_code = 201
The same is true for a “soft” error inside an explicit session: setting an error status
does not roll anything back, await rollback() does.
Other things to keep in mind in this example:
resp.media,resp.status_codeand other response attributes may be assigned inside a session - they are not database operations.The transaction is committed on exit from
async with db_session:, so no explicitcommit()is needed;await commit(),await rollback()andawait flush()are available when a handler needs them.await Task[task_id]looks in the identity map of the session first and sends a query only if the object is not there yet; if the row does not exist,ObjectNotFoundis raised.Collections require explicit loading, and collection elements come as seeds - see Async mode for the two rules of asynchronous sessions.
delete_taskwrites as well, but its only failure path is a404which has not changed anything, so the dependency is safe there; a handler whose failure can leave a half-applied change should follow one of the two recipes above.
Synchronous views¶
Synchronous providers work with Responder as well. A synchronous view of Responder is
executed in a thread pool (there is no event loop in that thread), so the usual
with db_session: pattern applies and the queries are written without await. Keep
the session inside the view here: a session opened elsewhere - in an asynchronous
dependency or in a middleware - belongs to the event-loop task and is not visible in the
thread pool, so such a view fails with TransactionError: db_session is required when
working with the database:
db = Database('postgres', dsn='dbname=mydb user=postgres host=localhost')
class Task(db.Entity):
title = Required(str)
db.generate_mapping(create_tables=True)
@api.get('/tasks-sync')
def list_tasks_sync(req, resp):
with db_session:
resp.media = [{'id': t.id, 'title': t.title} for t in select(t for t in Task)]
Do not mix the two styles inside one transaction: a synchronous with db_session:
cannot be used inside an asynchronous view, and an asynchronous session requires an
asynchronous provider (postgres_async or mariadb_async).
The same approach works with other ASGI frameworks (Starlette, aiohttp, and so on): open
the asynchronous session with async with db_session: and await the queries.