Skip to main content

Dependency injection for AioHTTP

Project description

PyPI - Python Version PyPI PyPI - Downloads

AioHTTP deps

This project was initially created to show the abilities of taskiq-dependencies project, which is used by taskiq to provide you with the best experience of sending distributed tasks.

This project adds FastAPI-like dependency injection to your AioHTTP application and swagger documentation based on types.

To start using dependency injection, just initialize the injector.

from aiohttp import web
from aiohttp_deps import init as deps_init


app = web.Application()


app.on_startup.append(deps_init)

web.run_app(app)

If you use mypy, then we have a custom router with proper types.

from aiohttp import web
from aiohttp_deps import init as deps_init
from aiohttp_deps import Router

router = Router()


@router.get("/")
async def handler():
    return web.json_response({})


app = web.Application()

app.router.add_routes(router)

app.on_startup.append(deps_init)

web.run_app(app)

Also, you can nest routers with prefixes,

api_router = Router()

memes_router = Router()

main_router = Router()

main_router.add_routes(api_router, prefix="/api")
main_router.add_routes(memes_router, prefix="/memes")

Swagger

If you use dependencies in you handlers, we can easily generate swagger for you. We have some limitations:

  1. We don't support resolving type aliases if hint is a string. If you define variable like this: myvar = int | None and then in handler you'd create annotation like this: param: "str | myvar" it will fail. You need to unquote type hint in order to get it work.

We will try to fix these limitations later.

To enable swagger, just add it to your startup.

from aiohttp_deps import init, setup_swagger

app = web.Application()

app.on_startup.extend([init, setup_swagger()])

Responses

You can define schema for responses using dataclasses or pydantic models. This would not affect handlers in any way, it's only for documentation purposes, if you want to actually validate values your handler returns, please write your own wrapper.

from dataclasses import dataclass

from aiohttp import web
from pydantic import BaseModel

from aiohttp_deps import Router, openapi_response

router = Router()


@dataclass
class Success:
    data: str


class Unauthorized(BaseModel):
    why: str


@router.get("/")
@openapi_response(200, Success, content_type="application/xml")
@openapi_response(200, Success)
@openapi_response(401, Unauthorized, description="When token is not correct")
async def handler() -> web.Response:
    ...

This example illustrates how much you can do with this decorator. You can have multiple content-types for a single status, or you can have different possible statuses. This function is pretty simple and if you want to make your own decorator for your responses, it won't be hard.

Default dependencies

By default this library provides only two injectables. web.Request and web.Application.

async def handler(app: web.Application = Depends()): ...

async def handler2(request: web.Request = Depends()): ...

It's super useful, because you can use these dependencies in any other dependency. Here's a more complex example of how you can use this library.

from aiohttp_deps import Router, Depends
from aiohttp import web

router = Router()


async def get_db_session(app: web.Application = Depends()):
    async with app[web.AppKey("db")] as sess:
        yield sess


class MyDAO:
    def __init__(self, session=Depends(get_db_session)):
        self.session = session

    async def get_objects(self) -> list[object]:
        return await self.session.execute("SELECT 1")


@router.get("/")
async def handler(db_session: MyDAO = Depends()):
    objs = await db_session.get_objects()
    return web.json_response({"objects": objs})

If you do something like this, you would never think about initializing your DAO. You can just inject it and that's it.

Built-in dependencies

This library also provides you with some default dependencies that can help you in building the best web-service.

Json

To parse json, create a pydantic model and add a dependency to your handler.

from aiohttp import web
from pydantic import BaseModel
from aiohttp_deps import Router, Json, Depends

router = Router()


class UserInfo(BaseModel):
    name: str


@router.post("/users")
async def new_data(user: UserInfo = Depends(Json())):
    return web.json_response({"user": user.model_dump()})

This dependency automatically validates data and send errors if the data doesn't orrelate with schema or body is not a valid json.

If you want to make this data optional, just mark it as optional.

@router.post("/users")
async def new_data(user: Optional[UserInfo] = Depends(Json())):
    if user is None:
        return web.json_response({"user": None})
    return web.json_response({"user": user.model_dump()})

Headers

You can get and validate headers using Header dependency.

Let's try to build simple example for authorization.

from aiohttp_deps import Router, Header, Depends
from aiohttp import web

router = Router()


def decode_token(authorization: str = Depends(Header())) -> str:
    if authorization == "secret":
        # Let's pretend that here we
        # decode our token.
        return authorization
    raise web.HTTPUnauthorized()


@router.get("/secret_data")
async def new_data(token: str = Depends(decode_token)) -> web.Response:
    return web.json_response({"secret": "not a secret"})

As you can see, header name to parse is equal to the name of a parameter that introduces Header dependency.

If you want to use some name that is not allowed in python, or just want to have different names, you can use alias. Like this:

def decode_token(auth: str = Depends(Header(alias="Authorization"))) -> str:

Headers can also be parsed to types. If you want a header to be parsed as int, just add the typehint.

def decode_token(meme_id: int = Depends(Header())) -> str:

If you want to get list of values of one header, use parameter multiple=True.

def decode_token(meme_id: list[int] = Depends(Header(multiple=True))) -> str:

And, of course, you can provide this dependency with default value if the value from user cannot be parsed for some reason.

def decode_token(meme_id: str = Depends(Header(default="not-a-secret"))) -> str:

Queries

You can depend on Query to get and parse query parameters.

from aiohttp_deps import Router, Query, Depends
from aiohttp import web

router = Router()


@router.get("/shop")
async def shop(item_id: str = Depends(Query())) -> web.Response:
    return web.json_response({"id": item_id})

the name of the parameter is the same as the name of function parameter.

The Query dependency is actually the same as the Header dependency, so everything about the Header dependency also applies to Query.

Views

If you use views as handlers, please use View class from aiohttp_deps, otherwise the magic won't work.

from aiohttp_deps import Router, View, Depends
from aiohttp import web

router = Router()


@router.view("/view")
class MyView(View):
    async def get(self, app: web.Application = Depends()):
        return web.json_response({"app": str(app)})

Forms

Now you can easily get and validate form data from your request. To make the magic happen, please add arbitrary_types_allowed to the config of your model.

import pydantic
from aiohttp_deps import Router, Depends, Form
from aiohttp import web

router = Router()


class MyForm(pydantic.BaseModel):
    id: int
    file: web.FileField

    model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)


@router.post("/")
async def handler(my_form: MyForm = Depends(Form())):
    with open("my_file", "wb") as f:
        f.write(my_form.file.file.read())
    return web.json_response({"id": my_form.id})

Path

If you have path variables, you can also inject them in your handler.

from aiohttp_deps import Router, Path, Depends
from aiohttp import web

router = Router()


@router.get("/view/{var}")
async def my_handler(var: str = Depends(Path())):
    return web.json_response({"var": var})

ExtraOpenAPI

This dependency is used to add additional swagger fields to the endpoint's swagger that is using this dependency. It might be even indirect dependency.

You can check how this thing can be used in our examples/swagger_auth.py.

Overriding dependencies

Sometimes for tests you don't want to calculate actual functions and you want to pass another functions instead.

To do so, you can add "dependency_overrides" or "values_overrides" to the application's state. These values should be dicts. The keys for these values can be found in aiohttp_deps.keys module.

Here's an example.

def original_dep() -> int:
    return 1

class MyView(View):
    async def get(self, num: int = Depends(original_dep)):
        """Nothing."""
        return web.json_response({"request": num})

Imagine you have a handler that depends on some function, but instead of 1 you want to have 2 in your tests.

To do it, just add dependency_overrides somewhere, where you create your application. And make sure that keys of that dict are actual function that are being replaced.

from aiohttp_deps import VALUES_OVERRIDES_KEY

my_app[VALUES_OVERRIDES_KEY] = {original_dep: 2}

But values_overrides only overrides returned values. If you want to override functions, you have to use dependency_overrides. Here's an example:

from aiohttp_deps import DEPENDENCY_OVERRIDES_KEY

def replacing_function() -> int:
    return 2


my_app[DEPENDENCY_OVERRIDES_KEY] = {original_dep: replacing_function}

The cool point about dependency_overrides, is that it recalculates graph and you can use dependencies in function that replaces the original.

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

aiohttp_deps-1.1.5.tar.gz (88.5 kB view details)

Uploaded Source

Built Distribution

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

aiohttp_deps-1.1.5-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

Details for the file aiohttp_deps-1.1.5.tar.gz.

File metadata

  • Download URL: aiohttp_deps-1.1.5.tar.gz
  • Upload date:
  • Size: 88.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aiohttp_deps-1.1.5.tar.gz
Algorithm Hash digest
SHA256 f76e49fea15452b4cc960bbec5fcceae3accf67624a40b693618d086f4ce17df
MD5 f9ea58d2dd9bc0974f4c85df9505d716
BLAKE2b-256 1c255f5013f9be6906d2913d4ae3af1209eba4e0fe73680f6596e5ad440fffd4

See more details on using hashes here.

File details

Details for the file aiohttp_deps-1.1.5-py3-none-any.whl.

File metadata

  • Download URL: aiohttp_deps-1.1.5-py3-none-any.whl
  • Upload date:
  • Size: 15.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aiohttp_deps-1.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 363277fd45e81fc9c8786e8b238340fa2e2202993469756dc318f2c6630a13c6
MD5 e497a320e0a526dee6b723a9147cd169
BLAKE2b-256 b13c4370686e75535ecb19f265e4e48bddfe376e45ec6f715f8bf8dae67e5000

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