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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19a871421d32f83b72d339154de5f20c80507a8347b5c6afa9c70bb704bd21c9
|
|
| MD5 |
58c7a2b06698164d164fcfe59cfe63d8
|
|
| BLAKE2b-256 |
4a9314f2b5e1fc9bbab73f13a319ec74832e0f137821ca2f1f81bfbbd9109bcf
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e975a14829e71c157e0aa56e0e959ba95bd2a53eccb83f93fd42fd998bbfa7e4
|
|
| MD5 |
767e805a4d849eb0623df7d8f55b7940
|
|
| BLAKE2b-256 |
de99bd37310b2c2037c01f8e490b273a7ef05fa1afa9e44a9f59445c7c7cf62f
|