Observed since 20 Oct, 17:17 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
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.
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.
pip install ab-dependency
Or with uv:
uv add ab-dependency
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)
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
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.
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
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"
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://"))
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 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")
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
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.
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 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")
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 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.
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"])
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.
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()
from ab_core.dependency import (
Depends,
Load,
inject,
sentinel,
pydanticize_data,
pydanticize_type,
pydanticize_object,
cached_type_adapter,
is_supported_by_pydantic,
)
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)
Run tests:
pytest
Run formatting and linting:
ruff check .
ruff format .
The package aims to keep existing behaviour stable:
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.1 | 27 Apr 2026 | 6 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 17:17 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:7c16e05e9871b8f3154b885f849c65aa4f5abb3d581d588bc184b29a3138b5f0