All Projects â†’ bocadilloproject â†’ Aiodine

bocadilloproject / Aiodine

Licence: mit
🧪 Async-first Python dependency injection library

Programming Languages

python
139335 projects - #7 most used programming language

Projects that are alternatives of or similar to Aiodine

Chili
Chili: HTTP Served Hot
Stars: ✭ 7 (-86.27%)
Mutual labels:  async, asyncio
Turbulette
😴 Turbulette - A batteries-included framework to build high performance, fully async GraphQL APIs
Stars: ✭ 29 (-43.14%)
Mutual labels:  async, asyncio
Parallel Ssh
Asynchronous parallel SSH client library.
Stars: ✭ 864 (+1594.12%)
Mutual labels:  async, library
Arq
Fast job queuing and RPC in python with asyncio and redis.
Stars: ✭ 695 (+1262.75%)
Mutual labels:  async, asyncio
Vim Strand
A barebones Vim plugin manger written in Rust
Stars: ✭ 38 (-25.49%)
Mutual labels:  async, asyncio
Fastapi Users
Ready-to-use and customizable users management for FastAPI
Stars: ✭ 713 (+1298.04%)
Mutual labels:  async, asyncio
Fastapi
FastAPI framework, high performance, easy to learn, fast to code, ready for production
Stars: ✭ 39,588 (+77523.53%)
Mutual labels:  async, asyncio
Lahja
Lahja is a generic multi process event bus implementation written in Python 3.6+ to enable lightweight inter-process communication, based on non-blocking asyncio
Stars: ✭ 374 (+633.33%)
Mutual labels:  async, asyncio
Google Books Android Viewer
Android library to bridge between RecyclerView and sources like web page or database. Includes demonstrator (Google Books viewer)
Stars: ✭ 37 (-27.45%)
Mutual labels:  async, library
Handle Path Oz
Android Library to handle multiple Uri's(paths) received through Intents.
Stars: ✭ 36 (-29.41%)
Mutual labels:  async, library
Aredis
redis client for Python asyncio (has support for redis server, sentinel and cluster)
Stars: ✭ 576 (+1029.41%)
Mutual labels:  async, asyncio
Uvloop
Ultra fast asyncio event loop.
Stars: ✭ 8,246 (+16068.63%)
Mutual labels:  async, asyncio
Tornado Celery
Non-blocking Celery client for Tornado
Stars: ✭ 561 (+1000%)
Mutual labels:  async, asyncio
Fennel
A task queue library for Python and Redis
Stars: ✭ 24 (-52.94%)
Mutual labels:  async, asyncio
Fasy
FP iterators that are both eager and asynchronous
Stars: ✭ 488 (+856.86%)
Mutual labels:  async, library
Gitter Api
[production-ready] Gitter API implementation for php 7.0+ allowing sync, async and streaming access.
Stars: ✭ 11 (-78.43%)
Mutual labels:  async, library
Github Stats
Better GitHub statistics images for your profile, no external server required
Stars: ✭ 338 (+562.75%)
Mutual labels:  async, asyncio
Requests Threads
🎭 Twisted Deferred Thread backend for Requests.
Stars: ✭ 366 (+617.65%)
Mutual labels:  async, asyncio
Restless
Express.js api, type safe validations and more
Stars: ✭ 32 (-37.25%)
Mutual labels:  async, library
Async
An awesome asynchronous event-driven reactor for Ruby.
Stars: ✭ 1,000 (+1860.78%)
Mutual labels:  async, asyncio

aiodine

python pypi travis black codecov license

aiodine provides async-first dependency injection in the style of Pytest fixtures for Python 3.6+.

Installation

pip install "aiodine==1.*"

Concepts

aiodine revolves around two concepts:

  • Providers are in charge of setting up, returning and optionally cleaning up resources.
  • Consumers can access these resources by declaring the provider as one of their parameters.

This approach is an implementation of Dependency Injection and makes providers and consumers:

  • Explicit: referencing providers by name on the consumer's signature makes dependencies clear and predictable.
  • Modular: a provider can itself consume other providers, allowing to build ecosystems of reusable (and replaceable) dependencies.
  • Flexible: provided values are reused within a given scope, and providers and consumers support a variety of syntaxes (asynchronous/synchronous, function/generator) to make provisioning fun again.

aiodine is async-first in the sense that:

  • It was made to work with coroutine functions and the async/await syntax.
  • Consumers can only be called in an asynchronous setting.
  • But provider and consumer functions can be regular Python functions and generators too, if only for convenience.

Usage

Providers

Providers make a resource available to consumers within a certain scope. They are created by decorating a provider function with @aiodine.provider.

Here's a "hello world" provider:

import aiodine

@aiodine.provider
async def hello():
    return "Hello, aiodine!"

Providers are available in two scopes:

  • function: the provider's value is re-computed everytime it is consumed.
  • session: the provider's value is computed only once (the first time it is consumed) and is reused in subsequent calls.

By default, providers are function-scoped.

Consumers

Once a provider has been declared, it can be used by consumers. A consumer is built by decorating a consumer function with @aiodine.consumer. A consumer can declare a provider as one of its parameters and aiodine will inject it at runtime.

Here's an example consumer:

@aiodine.consumer
async def show_friendly_message(hello):
    print(hello)

All aiodine consumers are asynchronous, so you'll need to run them in an asynchronous context:

from asyncio import run

async def main():
    await show_friendly_message()

run(main())  # "Hello, aiodine!"

Of course, a consumer can declare non-provider parameters too. aiodine is smart enough to figure out which parameters should be injected via providers, and which should be expected from the callee.

@aiodine.consumer
async def show_friendly_message(hello, repeat=1):
    for _ in range(repeat):
        print(hello)

async def main():
    await show_friendly_message(repeat=10)

Providers consuming other providers

Providers are modular in the sense that they can themselves consume other providers.

For this to work however, providers need to be frozen first. This ensures that the dependency graph is correctly resolved regardless of the declaration order.

import aiodine

@aiodine.provider
def email():
    return "[email protected]"

@aiodine.provider
async def send_email(email):
    print(f"Sending email to {email}…")

aiodine.freeze()  # <- Ensures that `send_email` has resolved `email`.

Note: it is safe to call .freeze() multiple times.

A context manager syntax is also available:

import aiodine

with aiodine.exit_freeze():
    @aiodine.provider
    def email():
        return "[email protected]"

    @aiodine.provider
    async def send_email(email):
        print(f"Sending email to {email}…")

Generator providers

Generator providers can be used to perform cleanup (finalization) operations after a provider has gone out of scope.

import os
import aiodine

@aiodine.provider
async def complex_resource():
    print("setting up complex resource…")
    yield "complex"
    print("cleaning up complex resource…")

Tip: cleanup code is executed even if an exception occurred in the consumer, so there's no need to surround the yield statement with a try/finally block.

Important: session-scoped generator providers will only be cleaned up if using them in the context of a session. See Sessions for details.

Lazy async providers

Async providers are eager by default: their return value is awaited before being injected into the consumer.

You can mark a provider as lazy in order to defer awaiting the provided value to the consumer. This is useful when the provider needs to be conditionally evaluated.

from asyncio import sleep
import aiodine

@aiodine.provider(lazy=True)
async def expensive_io_call():
    await sleep(10)
    return 42

@aiodine.consumer
async def compute(expensive_io_call, cache=None):
    if cache:
        return cache
    return await expensive_io_call

Factory providers

Instead of returning a scalar value, factory providers return a function. Factory providers are useful to implement reusable providers that accept a variety of inputs.

This is a design pattern more than anything else. In fact, there's no extra code in aiodine to support this feature.

The following example defines a factory provider for a (simulated) database query:

import aiodine

@aiodine.provider(scope="session")
async def notes():
    # Some hard-coded sticky notes.
    return [
        {"id": 1, "text": "Groceries"},
        {"id": 2, "text": "Make potatoe smash"},
    ]

@aiodine.provider
async def get_note(notes):
    async def _get_note(pk: int) -> list:
        try:
            # TODO: fetch from a database instead?
            return next(note for note in notes if note["id"] == pk)
        except StopIteration:
            raise ValueError(f"Note with ID {pk} does not exist.")

    return _get_note

Example usage in a consumer:

@aiodine.consumer
async def show_note(pk: int, get_note):
    print(await get_note(pk))

Tip: you can combine factory providers with generator providers to cleanup any resources the factory needs to use. Here's an example that provides temporary files and removes them on cleanup:

import os
import aiodine

@aiodine.provider(scope="session")
def tmpfile():
    files = set()

    async def _create_tmpfile(path: str):
        with open(path, "w") as tmp:
            files.add(path)
            return tmp

    yield _create_tmpfile

    for path in files:
        os.remove(path)

Using providers without declaring them as parameters

Sometimes, a consumer needs to use a provider but doesn't care about the value it returns. In these situations, you can use the @useprovider decorator and skip declaring it as a parameter.

Tip: the @useprovider decorator accepts a variable number of providers, which can be given by name or by reference.

import os
import aiodine

@aiodine.provider
def cache():
    os.makedirs("cache", exist_ok=True)

@aiodine.provider
def debug_log_file():
    with open("debug.log", "w"):
        pass
    yield
    os.remove("debug.log")

@aiodine.consumer
@aiodine.useprovider("cache", debug_log_file)
async def build_index():
    ...

Auto-used providers

Auto-used providers are automatically activated (within their configured scope) without having to declare them as a parameter in the consumer.

This can typically spare you from decorating all your consumers with an @useprovider.

For example, the auto-used provider below would result in printing the current date and time to the console every time a consumer is called.

import datetime
import aiodine

@aiodine.provider(autouse=True)
async def logdatetime():
    print(datetime.now())

Sessions

A session is the context in which session providers live.

More specifically, session providers (resp. generator session providers) are instanciated (resp. setup) when entering a session, and destroyed (resp. cleaned up) when exiting the session.

To enter a session, use:

await aiodine.enter_session()

To exit it:

await aiodine.exit_session()

An async context manager syntax is also available:

async with aiodine.session():
    ...

Context providers

WARNING: this is an experimental feature.

Context providers were introduced to solve the problem of injecting context-local resources. These resources are typically undefined at the time of provider declaration, but become well-defined when entering some kind of context.

This may sound abstract, so let's see an example before showing the usage of context providers.

Example

Let's say we're in a restaurant. There, a waiter executes orders submitted by customers. Each customer is given an Order object which they can .write() their desired menu items to.

In aiodine terminilogy, the waiter is the provider of the order, and the customer is a consumer.

During service, the waiter needs to listen to new customers, create a new Order object, provide it to the customer, execute the order as written by the customer, and destroy the executed order.

So, in this example, the context spans from when an order is created to when it is destroyed, and is specific to a given customer.

Here's what code simulating this situation on the waiter's side may look like:

from asyncio import Queue

import aiodine

class Order:
    def write(self, item: str):
        ...

class Waiter:
    def __init__(self):
        self._order = None
        self.queue = Queue()

        # Create an `order` provider for customers to use.
        # NOTE: the actually provided value is not defined yet!
        @aiodine.provider
        def order():
            return self._order

    async def _execute(self, order: Order):
        ...

    async def _serve(self, customer):
        # NOTE: we've now entered the *context* of serving
        # a particular customer.

        # Create a new order that the customer can
        # via the `order` provider.
        self._order = Order()

        await customer()

        # Execute the order and destroy it.
        await self._execute(self._order)
        self._order = None

    async def start(self):
        while True:
            customer = await self.queue.get()
            await self._serve(customer)

It's important to note that customers can do anything with the order. In particular, they may take some time to think about what they are going to order. In the meantime, the server will be listening to other customer calls. In this sense, this situation is an asynchronous one.

An example customer code may look like this:

from asyncio import sleep

@aiodine.consumer
def alice(order: Order):
    # Pondering while looking at the menu…
    await sleep(10)
    order.write("Pizza Margheritta")

Let's reflect on this for a second. Have you noticed that the waiter holds only one reference to an Order? This means that the code works fine as long as only one customer is served at a time.

But what if another customer, say bob, comes along while alice is thinking about what she'll order? With the current implementation, the waiter will simply forget about alice's order, and end up executing bob's order twice. In short: we'll encounter a race condition.

By using a context provider, we transparently turn the waiter's order into a context variable (a.k.a. ContextVar). It is local to the context of each customer, which solves the race condition.

Here's how the code would then look like:

import aiodine

class Waiter:
    def __init__(self):
        self.queue = Queue()
        self.provider = aiodine.create_context_provider("order")

    async def _execute(self, order: Order):
        ...

    async def _serve(self, customer):
        order = Order()
        with self.provider.assign(order=order):
            await customer()
            await self._execute(order)

    async def start(self):
        while True:
            customer = await self.queue.get()
            await self._serve(customer)

Note:

  • Customers can use the order provider just like before. In fact, it was created when calling .create_context_provider().
  • The order is now context-local, i.e. its value won't be forgotten or scrambled if other customers come and make orders concurrently.

This situation may look trivial to some, but it is likely to be found in client/server architectures, including in web frameworks.

Usage

To create a context provider, use aiodine.create_context_provider(). This method accepts a variable number of arguments and returns a ContextProvider. Each argument is used as the name of a new @provider which provides the contents of a ContextVar object.

import aiodine

provider = aiodine.create_context_provider("first_name", "last_name")

Each context variable contains None initially. This means that consumers will receive None — unless they are called within the context of an .assign() block:

with provider.assign(first_name="alice"):
    # Consumers called in this block will receive `"alice"`
    # if they consume the `first_name` provider.
    ...

FAQ

Why "aiodine"?

aiodine contains "aio" as in asyncio, and "di" as in Dependency Injection. The last two letters end up making aiodine pronounce like iodine, the chemical element.

Changelog

See CHANGELOG.md.

License

MIT

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].