Skip to main content

Flexible ASGI application lifecycle management for Python web frameworks.

Project description

asgi-lifecycle

Flexible ASGI application lifecycle management for Python web frameworks (Django, FastAPI, Starlette, Celery, and more).

Features

  • Hook-based startup/shutdown: Register async or sync functions to run on app startup/shutdown.
  • Context-aware: Hooks receive a rich context object (service type, environment, config, metadata).
  • Service filtering: Only run hooks for relevant service types (main app, worker, API, scheduler, etc).
  • Singleton manager: Application-wide lifecycle state and hook registry.
  • Decorator API: Clean, Pythonic @lifespan.on_start and @lifespan.on_shutdown decorators.
  • Async and sync support: Works with both async and sync hooks.
  • Timeouts and error handling: Robust shutdown with timeouts and logging.
  • Framework-agnostic: Integrates with any ASGI app, including Django, FastAPI, Starlette, Celery, etc.

Why asgi-lifespan?

vs Django's built-in ASGI

  • Startup/shutdown hooks vs no lifecycle management
  • Service type filtering vs no service differentiation
  • Priority ordering vs no execution control
  • Rich context object vs no context passing
  • Error handling & timeouts vs no error management
  • Multiple hooks per phase vs no hook system at all

vs FastAPI's lifespan

  • ✅ Framework-agnostic
  • ✅ Multiple hooks per phase
  • ✅ Service type filtering
  • ✅ Singleton management

Performance

  • Minimal overhead: Hooks only run during startup/shutdown
  • Async-first: Built for modern async Python
  • Timeout protection: Prevents hanging shutdowns
  • Error isolation: Failed hooks don't stop others

Installation

pip install asgi-lifecycle

Quickstart

from asgi_lifecycle import lifespan, LifespanContext, ServiceType

@lifespan.on_start(priority=1, service_types=["app", "api"])
async def setup_database(context: LifespanContext):
    await database.connect()
    context.set_metadata("database_connected", True)

@lifespan.on_shutdown(priority=1, service_types=["app", "api"])
async def close_database(context: LifespanContext):
    if context.get_metadata("database_connected", False):
        await database.disconnect()

API Reference

Lifespan

  • on_start(priority=0, name=None, service_types=None): Decorator to register a startup hook.
  • on_shutdown(priority=0, name=None, service_types=None): Decorator to register a shutdown hook.
  • startup(context: LifespanContext): Run all startup hooks for the given context.
  • shutdown(context: LifespanContext): Run all shutdown hooks for the given context.
  • is_initialized(): Check if the manager is initialized.
  • reset_instance(): Reset the singleton (for testing).

LifespanContext

  • service_type: The type of service ("app", "worker", "api", "scheduler", "test").
  • environment: Environment name ("development", "production", etc).
  • config: Arbitrary config dict.
  • metadata: Arbitrary metadata dict.
  • get_config(key, default=None): Get config value.
  • set_metadata(key, value): Set metadata value.
  • get_metadata(key, default=None): Get metadata value.

Example Integrations

Django ASGI

1. Register Lifecycle Hooks

Add startup and shutdown hooks to your Django settings module. These will run when the ASGI app starts and stops.

# Basic startup hook
@lifespan.on_start()
async def setup_logging(context: LifespanContext):
    """Configure logging when the ASGI app starts."""
    logging.basicConfig(level=logging.INFO)
    logger.info(f"Starting {context.service_type} in {context.environment}")

# Database setup (only for main app)
@lifespan.on_start(priority=1, service_types=["app"])
async def setup_database(context: LifespanContext):
    """Setup database connections."""
    if context.environment == "production":
        await database.connect_with_ssl()
    else:
        await database.connect()
    
    context.set_metadata("database_connected", True)
    logger.info("Database connected")

# Shutdown hook
@lifespan.on_shutdown(priority=1, service_types=["app"])
async def close_database(context: LifespanContext):
    """Clean up database connections."""
    if context.get_metadata("database_connected", False):
        await database.disconnect()
        logger.info("Database disconnected")

2. Integrate LifespanMiddleware

Wrap your Django ASGI application with LifespanMiddleware to enable lifecycle hooks.

# my_django_project/my_django_app/asgi.py

import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

import django
django.setup()  # Initialize Django before any other imports

from django.core.asgi import get_asgi_application
from asgi_lifecycle import LifespanMiddleware, LifespanContext

# Create the base ASGI application
django_app = get_asgi_application()

# Wrap with lifespan middleware
application = LifespanMiddleware(django_app)

4. Import Your Lifecycle Modules

Make sure to import any modules containing lifecycle hooks:

# In your settings.py
import myapp.lifecycle  # This registers the hooks
import myapp.database.lifecycle  # This too

FastAPI

from contextlib import asynccontextmanager
from fastapi import FastAPI
from asgi_lifecycle import lifespan, LifespanContext

@asynccontextmanager
async def lifespan_context(app: FastAPI):
    # Startup
    context = LifespanContext(service_type="api")
    await lifespan.startup(context)
    
    yield
    
    # Shutdown
    await lifespan.shutdown(context)

app = FastAPI(lifespan=lifespan_context)

Celery Worker

from celery.signals import worker_init, worker_shutdown
from asgiref.sync import async_to_sync
from asgi_lifecycle import lifespan, LifespanContext

@worker_init.connect
def init_worker_process(sender=None, conf=None, **kwargs):
    context = LifespanContext(service_type="worker", environment="production", config={"celery_conf": conf})
    async_to_sync(lifespan.startup)(context)

@worker_shutdown.connect
def shutdown_worker_process(sender=None, **kwargs):
    context = LifespanContext(service_type="worker", environment="production")
    async_to_sync(lifespan.shutdown)(context)

Service Types

# Only runs in main app
@lifespan.on_start(service_types=["app"])
async def setup_web_server():
    pass

# Only runs in worker
@lifespan.on_start(service_types=["worker"])
async def setup_worker_pool():
    pass

# Runs in both
@lifespan.on_start(service_types=["app", "worker"])
async def setup_shared_resources():
    pass

Error Handling

@lifespan.on_start(priority=1)
async def setup_database():
    try:
        await database.connect()
        logger.info("Database connected")
    except Exception as e:
        logger.error(f"Database connection failed: {e}")
        # Don't re-raise - let other hooks continue

License

MIT

Author

Tarek Sanger

Contributing

Pull requests and issues welcome! See CONTRIBUTING.md.

Roadmap

  • Auto-discovery of lifecycle modules
  • Built-in health checks
  • Metrics integration
  • Configuration file support

Project details


Download files

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

Source Distribution

asgi_lifecycle-0.0.1.tar.gz (9.1 kB view details)

Uploaded Source

Built Distribution

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

asgi_lifecycle-0.0.1-py3-none-any.whl (3.4 kB view details)

Uploaded Python 3

File details

Details for the file asgi_lifecycle-0.0.1.tar.gz.

File metadata

  • Download URL: asgi_lifecycle-0.0.1.tar.gz
  • Upload date:
  • Size: 9.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.20

File hashes

Hashes for asgi_lifecycle-0.0.1.tar.gz
Algorithm Hash digest
SHA256 da61cfcc56f579247e205f1c77fe67364a016f7ac12f196dcacf589bc2b387c9
MD5 3fa7110d91e2035951bbece352e2f2c2
BLAKE2b-256 24256ca91765e60b3539fe60aeae693221693c9dfa0bc12a1fbe69137489bc69

See more details on using hashes here.

File details

Details for the file asgi_lifecycle-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for asgi_lifecycle-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c7bc74737a96b87e38a694da166a971673b5dad9798dba686f41676ffd348b58
MD5 0eb25716da9caf23901336a014eb5312
BLAKE2b-256 4c729ad59341937721a38ca1eab7d07ec4026feec69b8905b1ac1e877e91c713

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