Catalog PyPI
PACKAGE EXPLORERView in registry

af-di-core

PyPI

Reusable framework-agnostic DI container & @component auto-discovery

Versions collected1
Direct dependencies2From release 0.0.3
Last observed20 Oct, 21:02 UTC

README

Version 0.0.3 documentation

af-di-core

A small, framework-agnostic dependency-injection framework for Python. Decorate your classes with @component, point the auto-discovery scanner at your package, and get a wired global container — no manual registration boilerplate. Works in a FastAPI app, a plain script, an AWS Lambda, or anywhere else.

Built on dependency-injector for the underlying provider machinery.

Installation

pip install af-di-core
# or with Poetry:
poetry add af-di-core

Quickstart

Mark the classes you want managed with @component (pairs naturally with @attrs.define):

import attrs
from allfly.di.core import component


@component
@attrs.define
class GreetingRepository:
    def greeting(self) -> str:
        return "hello"


@component
@attrs.define
class GreetingService:
    _repo: GreetingRepository

    def greet(self) -> str:
        return self._repo.greeting()

At startup, scan your package once and then resolve anything:

from allfly.di.core import auto_discover_components, di_provide

auto_discover_components(base_package="myapp")

service = di_provide(GreetingService)   # GreetingRepository injected automatically
service.greet()

Constructor dependencies are resolved from their type hints. Registration is multi-pass, so the order in which components are discovered does not matter. Parameters with default values are treated as optional and skipped, and an Optional[T] / T | None dependency is skipped when no provider for T exists.

How resolution works

auto_discover_components(base_package, registrars=None) runs in three steps:

  1. @settings functions are registered first as singletons (see below).
  2. registrars — optional callbacks for manual singletons that need special construction (e.g. third-party clients) — are invoked.
  3. @component classes are registered with multi-pass dependency resolution.
from allfly.di.core import auto_discover_components, di_register_singleton


def register_external_clients() -> None:
    di_register_singleton(SomeClient, api_key="...")


auto_discover_components(base_package="myapp", registrars=[register_external_clients])

Settings providers

Use @settings on a function whose return type is the type to register. Combine with functools.lru_cache for single instantiation:

from functools import lru_cache
from allfly.di.core import settings


@settings
@lru_cache
def get_db_settings() -> DatabaseSettings:
    return DatabaseSettings()

The returned instance is registered as a singleton keyed by the return annotation, so any @component depending on DatabaseSettings receives it.

Core providers (app-supplied)

af-di-core ships no opinionated providers — it is deliberately decoupled from databases, sessions, and web frameworks. Your application supplies its own "core" providers (things that must exist before anything is resolved, e.g. a DB session factory) by registering one or more callables on the container. They run once, lazily, the first time anything is provided:

from allfly.di.core import register_core_provider, di_register, di_provide


def register_db_providers() -> None:
    di_register(DatabaseSettings, providers.Object(get_db_settings()))
    di_register_singleton(SessionFactory, settings=get_provider(DatabaseSettings))


register_core_provider(register_db_providers)

# First di_provide(...) anywhere triggers ensure_core_providers() internally.
di_provide(SessionFactory)

You can also drive this explicitly via ensure_core_providers(). Initialization is guarded by a lock and a one-time flag, so it is safe to call repeatedly. Register core providers at startup, before the first di_provide.

Public API

from allfly.di.core import (
    component,                       # class decorator → register for auto-discovery
    settings,                        # function decorator → register a singleton by return type
    auto_discover_components,        # scan a package and wire the container

    di_provide,                      # resolve an instance by type
    di_register,                     # register a raw dependency-injector provider
    di_register_factory,             # register a Factory provider
    di_register_singleton,           # register a Singleton provider
    get_provider,                    # get the provider (not the instance) for chaining
    provider_exists,                 # check whether a type is registered

    register_core_provider,          # add an app-supplied core provider callable
    ensure_core_providers,           # run core providers once (called lazily by di_provide)

    get_global_container,            # the GlobalDependencyContainer singleton
    dependency_container,            # alias for the same singleton
    GlobalDependencyContainer,       # the container type
    get_component_registry,          # introspection: everything @component/@settings collected
    analyze_component_dependencies,  # introspection: a class's required constructor deps

    providers,                       # re-export of dependency_injector.providers (Object/Factory/Singleton/Callable)
    containers,                      # re-export of dependency_injector.containers
)

providers and containers are re-exported so consumers can build custom providers (e.g. providers.Object(instance)) without importing dependency-injector directly — this library is the single DI surface.

FastAPI

The framework intentionally does not import FastAPI. To expose a component to routes, write the small glue in your app:

from typing import Annotated
from fastapi import Depends, Request
from allfly.di.core import get_global_container

# Make the container available on app.state at startup:
#   application.state.provide = get_global_container().provide

def build(request: Request) -> GreetingService:
    return request.app.state.provide(GreetingService)

GreetingServiceDI = Annotated[GreetingService, Depends(build)]

Logging

The library logs through loguru (mostly at trace/debug). If your app does not configure loguru, these messages are simply silent by default at higher levels.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.0.330 Jun 20262

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 21:02 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:70b50ea69276244164b2b3af453b8ad95aa64bae8c1c78503025838751e553b8