Skip to main content

Lazily created ASGI apps

Project description

lazgi

Lazily initialized ASGI apps.

Why?

Web servers like Gunicorn and Uvicorn prefer¹ to have applications as an attribute on a module, something like:

from fastapi import FastAPI

app = FastAPI()
  1. Uvicorn can be run programmatically as long as you are not running Gunicorn on top of it. To the best of my knowledge Gunicorn can only be run from the command line.

There are entire patterns built around this, like the @app.<method> pattern that FastAPI and Flask use.

The problem with this pattern, especially for ASGI apps, is that resources like database connections, http clients and even TaskGroups require an async context to be initialized.

The solution to this was ASGI lifespans, which are part of the ASGI spec. This solution works great for simple cases but doesn't completely solve the issue for larger applications, primarily because it doesn't provide a good place to store state. There's several workarounds frameworks use for this, including:

  • Storing data on the app instance. Starlette and Quart do this. This generally works of course, but is not without it's downsides.
  • Dependency injection. FastAPI and Xpresso (I am the author of the latter) propose using dependency injection. In the case of FastAPI you still need to store things on the app instance but at least you can move the type casting outside of your endpoint function. Xpresso provides storage for lifepsan-scoped dependencies, but it can be boilerplatey and error prone. There are also many valid objections to using a dependency injection container in and of itself.

The ideal solution to all of this would be if the servers supported app being a Callable[[], AsyncContextManager[ASGIApp]] but sadly no server supports this.

This repo tries to provide a solution to this problem that is the next best/closest thing: lazily initialized apps. LazyApp is simply a valid ASGI app that can live as a module global. When it's lifespan is triggered, it calls a user defined async context manager that initializes the ASGI app and any resources it depends on and also calls that ASGI app's lifespan.

Example (Starlette)

This example employs so called "pure" dependency injection to initialize a Starlette application with a database dependency.

from contextlib import asynccontextmanager
from functools import partial
from typing import AsyncIterator
from lazgi import LazyApp
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route

# provided by the database driver
class DBConnection:
    async def execute(self, query: str) -> None:
        print(query)

@asynccontextmanager
async def connect() -> AsyncIterator[DBConnection]:
    yield DBConnection()

# user code, maybe in some endpoints.py file
async def endpoint(request: Request, db: DBConnection) -> Response:
    await db.execute("SELECT 1!")
    return Response()

# user code, in main.py or app.py
# note how create_app _explicitly_ lists all of the dependencies
# with their appropriate types
def create_app(db: DBConnection) -> Starlette:
    return Starlette(
        routes=[Route("/", partial(endpoint, db=db))]
    )

# user code, probably in main.py
# the composition root (dependency injection term)
# where we create all dependencies and "bind them"
# (in this case that just means passing them into create_app)
@asynccontextmanager
async def main() -> AsyncIterator[Starlette]:
    async with connect() as db:
        yield create_app(db)

# a global object that can be accessed
# by Guncicorn or Uvicorn as main:app
app = LazyApp(main)

# an example test
# you'd probably use a pytest fixture to create
# and tear down the db and such
async def test_app() -> None:
    async with connect() as db:
        app = create_app(db)
        # run some tests
        # maybe using Starlette's TestClient
        # client = TestClient(app)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lazgi-0.1.0.tar.gz (46.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lazgi-0.1.0-py3-none-any.whl (4.8 kB view details)

Uploaded Python 3

File details

Details for the file lazgi-0.1.0.tar.gz.

File metadata

  • Download URL: lazgi-0.1.0.tar.gz
  • Upload date:
  • Size: 46.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.1

File hashes

Hashes for lazgi-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9b877ef0769a48df231c247af42d35c2bf80f8035fe691103e8c5045ec728824
MD5 60d1eca3602549e26ffa47a98d6e34a3
BLAKE2b-256 48376a543770a204725650989790a056634a18baa2a365fb31ce453345a36a93

See more details on using hashes here.

File details

Details for the file lazgi-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: lazgi-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 4.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.1

File hashes

Hashes for lazgi-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ab722cad4b70c515fb3268fe31e28d354974d8b6cf1936abe511a6d0fba0732b
MD5 7cf65f2dbb09e766026ebbf8e4c70618
BLAKE2b-256 fce49deedeb9b7c0beaac6fe6134daf849b77aea40c6df84f3fe195875b6921a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page