Skip to main content

No project description provided

Project description

ab-dependency

A lightweight dependency loading and injection package for Python.

ab-dependency provides a small dependency system for loading objects from environment variables, Python callables, Pydantic models, attrs classes, and custom loaders. It is designed to feel familiar if you have used FastAPI dependencies, while also working outside FastAPI.

Disclaimer

Whilst this package was originally built for the Open Source auth-broker package. It is published to PyPI and intended to be used for any python project, due to its high lvel of convenience.

Features

  • Load Pydantic models from environment variables
  • Load primitive values from environment variables
  • Support discriminated unions
  • Support attrs classes by converting them to Pydantic-compatible models
  • Support singleton-style persistent dependencies
  • Support transient dependencies
  • Inject dependencies into:
    • sync functions
    • async functions
    • sync generators
    • async generators
    • classes
    • Pydantic models
  • Support generator dependency cleanup
  • Support FastAPI dependency integration
  • Support flattened environment variable conventions
  • Support JSON serialised complex values, such as lists

Installation

pip install ab-dependency

Or with uv:

uv add ab-dependency

Basic usage

from pydantic import BaseModel

from ab_core.dependency import Load


class AppConfig(BaseModel):
    host: str = "localhost"
    port: int = 8080


config = Load(AppConfig)

print(config.host)
print(config.port)

By default, object models are loaded from environment variables using the model name converted to env-var style.

For AppConfig, the default prefix is:

APP_CONFIG

So these environment variables:

APP_CONFIG_HOST=0.0.0.0
APP_CONFIG_PORT=8000

produce:

AppConfig(host="0.0.0.0", port=8000)

Environment variable naming

Model names are converted from PascalCase or camelCase to uppercase snake case.

OAuth2TokenStore -> O_AUTH2_TOKEN_STORE
HTTPServerConfig -> HTTP_SERVER_CONFIG
AppConfig        -> APP_CONFIG

Field names are appended to the prefix.

APP_CONFIG_HOST=0.0.0.0
APP_CONFIG_PORT=8000

Nested field names are flattened using underscores.

class DatabaseConfig(BaseModel):
    host: str
    port: int


class AppConfig(BaseModel):
    database: DatabaseConfig
APP_CONFIG_DATABASE_HOST=localhost
APP_CONFIG_DATABASE_PORT=5432

Loading primitive values

Use LoaderEnvironment when loading a single primitive value from a specific environment variable.

from ab_core.dependency.loaders import LoaderEnvironment

port = LoaderEnvironment[int](key="PORT").load()
PORT=8080

The value is validated and cast using Pydantic.

Persistent dependencies

Load(..., persist=True) caches the loaded dependency.

from pydantic import BaseModel
from ab_core.dependency import Load


class Client(BaseModel):
    name: str = "client"


one = Load(Client, persist=True)
two = Load(Client, persist=True)

assert one is two

Transient dependencies are created each time.

one = Load(Client, persist=False)
two = Load(Client, persist=False)

assert one is not two
assert one == two

Lazy dependencies

Use Depends to defer loading until call time.

from typing import Annotated

from pydantic import BaseModel

from ab_core.dependency import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
def run(settings: Annotated[Settings, Depends(Settings)]):
    return settings.value


assert run() == "hello"

Function injection

from typing import Annotated

from pydantic import BaseModel

from ab_core.dependency import Depends, inject


class Database(BaseModel):
    url: str = "sqlite://"


@inject
def handler(db: Annotated[Database, Depends(Database)]):
    return db.url

Dependencies are only resolved when the argument was not explicitly provided.

handler(Database(url="postgresql://"))

Async function injection

from typing import Annotated

from ab_core.dependency import Depends, inject


async def make_token() -> str:
    return "abc"


@inject
async def handler(token: Annotated[str, Depends(make_token)]):
    return token

Generator dependency support

Generator dependencies are entered before the function runs and cleaned up afterwards.

from typing import Annotated

from ab_core.dependency import Depends, inject


def resource():
    try:
        yield "resource"
    finally:
        print("closed")


@inject
def handler(value: Annotated[str, Depends(resource)]):
    return value

Exceptions are thrown back into the generator so except and finally blocks can run.

def resource():
    try:
        yield "resource"
    except Exception:
        print("caught")
        raise
    finally:
        print("closed")

Class injection

from typing import Annotated

from pydantic import BaseModel

from ab_core.dependency import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
class Service:
    settings: Annotated[Settings, Depends(Settings)]

    def run(self):
        return self.settings.value

Pydantic model injection

from typing import Annotated

from pydantic import BaseModel

from ab_core.dependency import Depends, inject


class Settings(BaseModel):
    value: str = "hello"


@inject
class AppConfig(BaseModel):
    settings: Annotated[Settings, Depends(Settings)]
    retries: int = 3

If a field is supplied by input data, the dependency is not resolved.

FastAPI integration

Depends subclasses FastAPI's dependency parameter when FastAPI is installed.

from typing import Annotated

from fastapi import Depends as FDepends, FastAPI
from pydantic import BaseModel

from ab_core.dependency import Depends, inject


class SomeDependency(BaseModel):
    value: str = "injected"


def provide_dependency() -> SomeDependency:
    return SomeDependency()


@inject
def context(dep: Annotated[SomeDependency, Depends(provide_dependency)]):
    try:
        yield dep
    finally:
        pass


app = FastAPI()


@app.get("/")
def route(dep: Annotated[SomeDependency, FDepends(context)]):
    return {"value": dep.value}

Discriminated unions

Discriminated unions are supported through Pydantic's Discriminator.

from typing import Annotated, Literal

from pydantic import BaseModel, Discriminator

from ab_core.dependency import Load


class FileStore(BaseModel):
    type: Literal["FILE"] = "FILE"
    path: str


class S3Store(BaseModel):
    type: Literal["S3"] = "S3"
    bucket: str


Store = Annotated[FileStore | S3Store, Discriminator("type")]

store = Load(Store)

Environment variables:

STORE_TYPE=S3
STORE_S3_BUCKET=my-bucket

Result:

S3Store(type="S3", bucket="my-bucket")

Flattened discriminator convention

For discriminated unions, the discriminator selects which nested branch is used.

DUMMY_STORE_TYPE=A
DUMMY_STORE_A_FOO=hello
DUMMY_STORE_A_NUM=42

This becomes:

{
    "type": "A",
    "foo": "hello",
    "num": 42,
}

attrs support

attrs classes can be loaded by converting them into Pydantic-compatible models.

import attrs

from ab_core.dependency import Load
from ab_core.dependency.pydanticize import pydanticize_type


@attrs.define
class Settings:
    host: str = "localhost"
    port: int = 8080


SettingsModel = pydanticize_type(Settings)
settings = Load(SettingsModel)

attrs defaults and factories are preserved.

List support

Simple lists can be supplied as JSON strings.

from pydantic import BaseModel

from ab_core.dependency import Load


class Config(BaseModel):
    values: list[str]
CONFIG_VALUES='["A", "B", "C"]'

Result:

Config(values=["A", "B", "C"])

Planned recursive list environment convention

For recursive object loading, lists may also be represented as indexed environment variables.

Simple values:

CONFIG_VALUES_0=A
CONFIG_VALUES_1=B
CONFIG_VALUES_2=C

Equivalent JSON form:

CONFIG_VALUES='["A", "B", "C"]'

Lists of Pydantic models:

from typing import Annotated, Literal

from pydantic import BaseModel, Discriminator


class BlahItem(BaseModel):
    type: Literal["blah"] = "blah"
    label: str


class OtherItem(BaseModel):
    type: Literal["other"] = "other"
    label: str


Item = Annotated[BlahItem | OtherItem, Discriminator("type")]


class SomeObject(BaseModel):
    list_field: list[Item]

Environment variables:

SOME_OBJECT_LIST_FIELD_0_TYPE=blah
SOME_OBJECT_LIST_FIELD_0_BLAH_LABEL=first
SOME_OBJECT_LIST_FIELD_1_TYPE=other
SOME_OBJECT_LIST_FIELD_1_OTHER_LABEL=second

Expected result:

SomeObject(
    list_field=[
        BlahItem(type="blah", label="first"),
        OtherItem(type="other", label="second"),
    ]
)

This keeps backwards compatibility with the existing JSON form while allowing recursive, schema-aware environment unpacking.

Custom loaders

Create a custom loader by subclassing LoaderBase.

from typing import Any

from ab_core.dependency.loaders.base import LoaderBase


class MyLoader(LoaderBase[str]):
    key: str

    def load_raw(self) -> Any:
        return f"value-for-{self.key}"

Then use it directly:

loader = MyLoader[str](key="example")
value = loader.load()

Public API

from ab_core.dependency import (
    Depends,
    Load,
    inject,
    sentinel,
    pydanticize_data,
    pydanticize_type,
    pydanticize_object,
    cached_type_adapter,
    is_supported_by_pydantic,
)

Design notes

Load resolves immediately.

settings = Load(Settings)

Depends resolves lazily.

settings: Annotated[Settings, Depends(Settings)]

persist=True caches by load target or loaded type.

Depends(Settings, persist=True)

persist=False creates a fresh dependency each time.

Depends(Settings, persist=False)

Development

Run tests:

pytest

Run formatting and linting:

ruff check .
ruff format .

Compatibility goals

The package aims to keep existing behaviour stable:

  • Existing JSON list loading should continue to work.
  • Existing flat object env-var loading should continue to work.
  • Existing discriminator conventions should continue to work.
  • New recursive list loading should be additive.

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

ab_dependency-0.2.1.tar.gz (21.9 kB view details)

Uploaded Source

Built Distribution

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

ab_dependency-0.2.1-py3-none-any.whl (28.0 kB view details)

Uploaded Python 3

File details

Details for the file ab_dependency-0.2.1.tar.gz.

File metadata

  • Download URL: ab_dependency-0.2.1.tar.gz
  • Upload date:
  • Size: 21.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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 ab_dependency-0.2.1.tar.gz
Algorithm Hash digest
SHA256 19a871421d32f83b72d339154de5f20c80507a8347b5c6afa9c70bb704bd21c9
MD5 58c7a2b06698164d164fcfe59cfe63d8
BLAKE2b-256 4a9314f2b5e1fc9bbab73f13a319ec74832e0f137821ca2f1f81bfbbd9109bcf

See more details on using hashes here.

File details

Details for the file ab_dependency-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: ab_dependency-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 28.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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 ab_dependency-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e975a14829e71c157e0aa56e0e959ba95bd2a53eccb83f93fd42fd998bbfa7e4
MD5 767e805a4d849eb0623df7d8f55b7940
BLAKE2b-256 de99bd37310b2c2037c01f8e490b273a7ef05fa1afa9e44a9f59445c7c7cf62f

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