Metadata-Version: 2.4
Name: af-di-core
Version: 0.0.3
Summary: Reusable framework-agnostic DI container & @component auto-discovery
License: MIT
Author: Allfly
Author-email: engineering@allfly.io
Requires-Python: >=3.13,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: dependency-injector (>=4.48.1,<5.0.0)
Requires-Dist: loguru (>=0.7.2,<0.8.0)
Description-Content-Type: text/markdown

# 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`](https://python-dependency-injector.ets-labs.org/) for the underlying provider machinery.

## Installation

```bash
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`):

```python
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:

```python
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.

```python
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:

```python
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:

```python
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

```python
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:

```python
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](https://github.com/Delgan/loguru) (mostly at `trace`/`debug`). If your app does not configure loguru, these messages are simply silent by default at higher levels.

