Skip to main content

Dependency injection and routing extensions for Starlette.

Project description

from starlette.routing import Routefrom examples.dependencies import Variable

Starlette Dispatch

Routing extensions and dependency injection library for Starlette.

PyPI GitHub Libraries.io dependency status for latest release PyPI - Downloads GitHub Release Date

Installation

Install starlette_dispatch using PIP:

pip install starlette_dispatch

Features

  • Route groups. Group routes by common path prefix and common middleware.
  • Route method decorators. Convenient decorators for common HTTP methods.
  • Dependency injection. Route handlers can request dependencies by adding a parameter with the dependency type hint.
  • Backward compatible with Starlette. You can use it with your existing Starlette application.
  • No performance overhead. Dependency injection takes exact the same time as if you would write the handler manually.
  • Fully typed. Starlette Dispatch is fully typed and supports type hints.
  • Async support. Starlette Dispatch supports async handlers and async dependencies.

And the most important -- it does not erase route handler signature. You can compose it with any other decorators.

Quick start

Starlette Dispatch does not require any changes to your existing Starlette application. You can use it with your existing Starlette application.

Here is a simple snippet that demonstrates dependency injection and route group usage:

import typing

from starlette.applications import Starlette
from starlette.authentication import SimpleUser
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from starlette_dispatch import RouteGroup, RequestResolver

admin_middleware = [
    Middleware(AuthenticationMiddleware, backend=...)
]
admin_routes = RouteGroup('/admin', middleware=admin_middleware)

CurrentUser = typing.Annotated[SimpleUser, RequestResolver(lambda r: r.user)]


async def index_view(request: Request, user: CurrentUser) -> JSONResponse:
    """This is your landing page."""
    return JSONResponse({'message': f'Hello, {user}!'})


@admin_routes.get('/')
async def admin_index_view(request: Request) -> JSONResponse:
    """This is your admin landing page."""
    return JSONResponse({'message': 'Hello, admin!'})


app = Starlette(
    routes=[
        Route('/', index_view),  # regular Starlette route
        *admin_routes,
    ]
)

Route groups

A route group is a way to group routes by common path prefix and common middleware. Instead of writing the same prefix for each route, you can define a group and add routes to it using convenient decorators.

Route groups support all common HTTP methods and add some extra helpers like get_or_post.

from starlette.requests import Request
from starlette.responses import JSONResponse

from starlette_dispatch import RouteGroup

group = RouteGroup('/group')


@group.get('/')
def my_view(request: Request) -> JSONResponse:
    return JSONResponse({'message': 'Hello, world!'})


@group.get_or_post('/')
def form_view(request: Request) -> JSONResponse:
    return JSONResponse({'message': 'Hello, world!'})

Multiple routes on a single handler

You can call route decorators multiple times on a single handler. This way you can share the same handler for multiple routes without creating a new handler.

from starlette.requests import Request
from starlette.responses import JSONResponse

from starlette_dispatch import RouteGroup, FromPath

group = RouteGroup('/group')


@group.get('/new')
@group.get('/edit/{id}')
def create_view(request: Request, id: FromPath[int | None]) -> JSONResponse:
    ...

Route injections

Each route handler can request a dependency by adding a parameter with the dependency type hint. The dependency will be properly resolved and injected into the handler on handler call. See more about dependency injection below.

For each injection Starlette Dispatch creates a resolver function. This means, it does not add a noticeable overhead to your application and takes exact the same time as if you would write the handler manually.

import typing
from starlette.requests import Request
from starlette.responses import JSONResponse

from starlette_dispatch import RouteGroup, VariableResolver


class User: ...


user = User()
CurrentUser = typing.Annotated[str, VariableResolver(user)]

group = RouteGroup('/')


@group.get('/')
def index_view(request: Request, user: CurrentUser) -> JSONResponse:
    return JSONResponse({'message': f'Hello, {user}!'})

Route middleware

Each route can have its own middleware.

If route group has middleware, it will be merged with route middleware. Route middleware has a higher priority.

import typing

from starlette.requests import Request
from starlette.responses import JSONResponse

from starlette_dispatch import RouteGroup, VariableResolver


class User: ...


user = User()
CurrentUser = typing.Annotated[str, VariableResolver(user)]

group = RouteGroup('/')


@group.get('/')
def my_view(request: Request, user: CurrentUser) -> JSONResponse:
    return JSONResponse({'message': f'Hello, {user}!'})

Dependency injection

In a nutshell, the dependency is a type, annotated with a value or a factory function that resolves to the value. The factory function is called dependency resolver.

Variable dependency

Variable dependency is a resolver that returns a simple value.

import typing

from starlette_dispatch import RouteGroup, VariableResolver

Value = typing.Annotated[str, VariableResolver('hello')]

group = RouteGroup('/')


@group.get('/')
def my_view(value: Value) -> None:
    assert value == 'hello'

Factory dependency

Factory dependency is a resolver that creates a value on each call. The result can be cached globally or per request. The factory can have dependencies and can be async.

Request cached dependencies are resolved once per request and cached for the duration of the request. In order to use request cached dependencies, you need to use DependencyScope.REQUEST scope. If you want to cache the dependency globally, you need to use DependencyScope.SINGLETON scope.

import typing

from starlette_dispatch import FactoryResolver, RouteGroup, DependencyScope


def make_dependency():
    return 'hello'


async def async_dependency():
    return 'hello'


Value = typing.Annotated[str, FactoryResolver(make_dependency)]
AsyncValue = typing.Annotated[str, FactoryResolver(async_dependency)]
CachedValue = typing.Annotated[str, FactoryResolver(make_dependency, scope=DependencyScope.SINGLETON)]
RequestCachedValue = typing.Annotated[str, FactoryResolver(make_dependency, scope=DependencyScope.REQUEST)]

group = RouteGroup('/')


@group.get('/')
def my_view(value: Value, async_value: AsyncValue, cached_value: CachedValue) -> None:
    assert value == 'hello'
    assert async_value == 'hello'
    assert cached_value == 'hello'

Factory function dependencies

The factory function itself can have dependencies. They are defined in the same way as regular dependencies.

import typing

from starlette_dispatch import FactoryResolver, RouteGroup


def parent_dependency():
    return 'hello'


ParentValue = typing.Annotated[str, FactoryResolver(parent_dependency)]


def make_dependency(parent: ParentValue):
    return parent + ' world'


Value = typing.Annotated[str, FactoryResolver(make_dependency)]

group = RouteGroup('/')


@group.get('/')
def my_view(value: Value) -> None:
    assert value == 'hello world'

Predefined dependencies

There are several predefined dependencies: starlette.requests.Request, starlette_dispatch.injections.DependencySpec.

Request is a Starlette request object and DependencySpec is a special object that contains meta information about the dependency. DependencySpec object is very useful in complex cases.

from starlette.requests import Request

from starlette_dispatch import DependencySpec


def make_dependency(request: Request, spec: DependencySpec):
    assert request  # Starlette request object
    assert spec.param_name  # name of the parameter
    assert spec.param_type  # type of the parameter
    assert spec.optional  # is the parameter optional
    assert spec.default  # default value of the parameter
    assert spec.annotation  # type annotation of the parameter

Request resolver

If your dependency available in the request object, instead of creating a factory function, you can use a RequestDependency resolver. It takes a function that accepts Request and DependencySpec (optionally) objects.

import typing
from starlette_dispatch import RequestResolver

# example dependency that resolves to a value from query parameter
Value = typing.Annotated[str, RequestResolver(lambda request, spec,: request.query_params['value'])]
NoSpecValue = typing.Annotated[str, RequestResolver(lambda request: request.query_params['value'])]

Custom resolver

You are not limited to predefined resolvers. You can create your own resolver by subclassing DependencyResolver and implementing the resolve method.

from starlette.requests import Request

from starlette_dispatch import DependencyResolver, DependencySpec, ResolveContext


class MyResolver(DependencyResolver):
    async def resolve(self, context: ResolveContext, spec: DependencySpec):
        """Use request and spec objects to create a value."""
        return 'my dependency value'

Dependencies with decorators

Almost any view decorator can work with Starlette Dispatch if it accepts this signature: async def view(request) -> Response. However, this has some requirements:

  1. it should return an async function of async def view(request, **kwargs)
  2. it should call functools.wraps on the inner view, otherwise the view will lose its dependencies
  3. it should pass **kwargs to the inner view, as Starlette Dispatch passes dependencies via kwargs.

Full listing:

import functools
from starlette.requests import Request
from starlette_dispatch import RouteGroup
from starlette.responses import Response, RedirectResponse


def login_required(fn):
    @functools.wraps(fn)
    async def view(request, **kwargs):
        if not request.user.is_authenticated:
            return RedirectResponse('/')
        return await fn(request, **kwargs)

    return view


group = RouteGroup()


@group.get('/')
@login_required
async def view(request: Request) -> Response: ...

Contrib and support

Simple dependency definition

Instead of using resolver classes, you can use these shortcuts to define dependencies.

import typing

SimpleValueDependency = typing.Annotated[str, 'simple_value']
LambdaDependency = typing.Annotated[str, lambda: 'some value']
RequestOnlyLambdaDependency = typing.Annotated[str, lambda request: request.query_params['value']]
RequestAndSpecLambdaDependency = typing.Annotated[str, lambda request, spec: ...]

FromPath - inject path parameter as a dependency

from starlette_dispatch import FromPath, RouteGroup

group = RouteGroup('/')


@group.get('/{value}')
def my_view(value: FromPath[str]) -> None:
    assert value is not None

If path value does not exist in Request.path_parameters then it will fail with error. However, you can mark dependency as optional and then it will be None if path value does not exist.

from starlette_dispatch import FromPath, RouteGroup

group = RouteGroup('/')


def my_view(value: FromPath[str] | None) -> None:
    assert value is None

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

starlette_dispatch-0.27.2.tar.gz (34.5 kB view details)

Uploaded Source

Built Distribution

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

starlette_dispatch-0.27.2-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

File details

Details for the file starlette_dispatch-0.27.2.tar.gz.

File metadata

  • Download URL: starlette_dispatch-0.27.2.tar.gz
  • Upload date:
  • Size: 34.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.29

File hashes

Hashes for starlette_dispatch-0.27.2.tar.gz
Algorithm Hash digest
SHA256 69f716e06cea1d1f214dd25f3e3b9510d5f84ac5d50c4771f663a5cfe64b1ff2
MD5 531250bce83d0a70575be9b65128d99c
BLAKE2b-256 5be6a784fd3d0174e6c7b25521a9edcb5bf2ce9ef88bf104ec9bfe5663c829df

See more details on using hashes here.

File details

Details for the file starlette_dispatch-0.27.2-py3-none-any.whl.

File metadata

File hashes

Hashes for starlette_dispatch-0.27.2-py3-none-any.whl
Algorithm Hash digest
SHA256 7efc239ee50edd1a88eea178e9e0a03124caa2dc5a614a8848e2625070ee2b28
MD5 df60beb420e63f85e058b79b69cf331c
BLAKE2b-256 de1a551fa0f715f07dee30f87c67e745ce9003bb14f21872ca3456ad0564af99

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