Configuration loader with support for multiple formats (env, yaml, json, toml, ini)
Project description
dature
Type-safe configuration loader for Python dataclasses. Load config from YAML, JSON, TOML, INI, ENV files and environment variables with automatic type conversion, validation, and human-readable error messages.
Installation
pip install dature
Optional format support:
pip install dature[yaml] # YAML support (ruamel.yaml)
pip install dature[json5] # JSON5 support
Quick Start
from dataclasses import dataclass
from dature import LoadMetadata, load
@dataclass
class Config:
host: str
port: int
debug: bool = False
# From a file
config = load(LoadMetadata(file_="config.yaml"), Config)
# From environment variables
config = load(LoadMetadata(prefix="APP_"), Config)
# As a decorator (auto-loads on instantiation)
@load(LoadMetadata(file_="config.yaml"))
@dataclass
class Config:
host: str
port: int
debug: bool = False
config = Config() # loads from config.yaml
config = Config(port=9090) # override specific fields
Supported Formats
| Format | Extension | Loader | Extra dependency |
|---|---|---|---|
| YAML 1.1 | .yaml, .yml |
yaml |
ruamel.yaml |
| YAML 1.2 | .yaml, .yml |
yaml1.2 |
ruamel.yaml |
| JSON | .json |
json |
- |
| JSON5 | .json5 |
json5 |
json5 |
| TOML | .toml |
toml |
- |
| INI | .ini, .cfg |
ini |
- |
| ENV file | .env |
envfile |
- |
| Environment variables | - | env |
- |
The format is auto-detected from the file extension. When file_ is not specified, environment variables are used. You can also set the loader explicitly:
LoadMetadata(file_="config.txt", loader="json")
LoadMetadata
@dataclass(frozen=True, slots=True, kw_only=True)
class LoadMetadata:
file_: str | None = None
loader: LoaderType | None = None
prefix: str | None = None
split_symbols: str = "__"
name_style: NameStyle | None = None
field_mapping: dict[str, str] | None = None
root_validators: tuple[ValidatorProtocol, ...] | None = None
prefix
Filters keys for ENV, or extracts a nested object from files:
# ENV: APP_HOST=localhost, APP_PORT=8080
config = load(LoadMetadata(prefix="APP_"), Config)
# config.yaml: { app: { database: { host: localhost, port: 5432 } } }
db = load(LoadMetadata(file_="config.yaml", prefix="app.database"), Database)
split_symbols
Delimiter for building nested structures from flat ENV variables. Default: "__".
APP_DB__HOST=localhost
APP_DB__PORT=5432
@dataclass
class Database:
host: str
port: int
@dataclass
class Config:
db: Database
config = load(LoadMetadata(prefix="APP_", split_symbols="__"), Config)
name_style
Maps dataclass field names to config keys using a naming convention:
| Value | Example |
|---|---|
lower_snake |
my_field |
upper_snake |
MY_FIELD |
lower_camel |
myField |
upper_camel |
MyField |
lower_kebab |
my-field |
upper_kebab |
MY-FIELD |
# config.json: { "databaseHost": "localhost", "databasePort": 5432 }
config = load(
LoadMetadata(file_="config.json", name_style="lower_camel"),
Config,
)
field_mapping
Explicit field renaming. Takes priority over name_style:
config = load(
LoadMetadata(
file_="config.json",
field_mapping={"database_url": "db_url", "api_key": "apiKey"},
),
Config,
)
Decorator Mode vs Function Mode
Function mode -- load once and get a result:
config = load(LoadMetadata(file_="config.yaml"), Config)
Decorator mode -- auto-loads on every instantiation with caching:
@load(LoadMetadata(file_="config.yaml"))
@dataclass
class Config:
host: str
port: int
config = Config() # loaded from config.yaml
config = Config(port=9090) # host from config, port overridden
Explicit arguments to __init__ take priority over loaded values.
Caching is enabled by default. Disable it with cache=False:
@load(LoadMetadata(file_="config.yaml"), cache=False)
@dataclass
class Config:
host: str
port: int
Merging Multiple Sources
Load configuration from several sources and merge them into one dataclass:
from dature import LoadMetadata, MergeMetadata, MergeStrategy, load
config = load(
MergeMetadata(
sources=(
LoadMetadata(file_="defaults.yaml"),
LoadMetadata(file_=".env", prefix="APP_"),
LoadMetadata(prefix="APP_"), # env vars, highest priority
),
strategy=MergeStrategy.LAST_WINS,
),
Config,
)
Shorthand with a tuple (uses LAST_WINS by default):
config = load(
(
LoadMetadata(file_="defaults.yaml"),
LoadMetadata(prefix="APP_"),
),
Config,
)
Works as a decorator too:
@load(MergeMetadata(
sources=(
LoadMetadata(file_="defaults.yaml"),
LoadMetadata(prefix="APP_"),
),
strategy=MergeStrategy.FIRST_WINS,
))
@dataclass
class Config:
host: str
port: int
Merge Strategies
| Strategy | Behavior |
|---|---|
LAST_WINS |
Last source overrides (default) |
FIRST_WINS |
First source wins |
RAISE_ON_CONFLICT |
Raises MergeConflictError if the same key appears in multiple sources |
Nested dicts are merged recursively. Lists and scalars are replaced entirely according to the strategy.
Validators
Validators are declared using typing.Annotated:
from dataclasses import dataclass
from typing import Annotated
from dature.validators.number import Ge, Le
from dature.validators.string import MinLength, MaxLength, RegexPattern
from dature.validators.sequence import MinItems, MaxItems, UniqueItems
@dataclass
class Config:
port: Annotated[int, Ge(value=1), Le(value=65535)]
password: Annotated[str, MinLength(value=8), MaxLength(value=128)]
email: Annotated[str, RegexPattern(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")]
tags: Annotated[list[str], MinItems(value=1), MaxItems(value=10), UniqueItems()]
Available Validators
Numbers: Gt, Ge, Lt, Le
Strings: MinLength, MaxLength, RegexPattern
Sequences: MinItems, MaxItems, UniqueItems
Root Validators
Validate the entire object after loading:
from dature.validators.root import RootValidator
def check_privileged_port(obj: Config) -> bool:
if obj.port < 1024:
return obj.user == "root"
return True
config = load(
LoadMetadata(
file_="config.yaml",
root_validators=(
RootValidator(
func=check_privileged_port,
error_message="Ports below 1024 require root user",
),
),
),
Config,
)
Special Types
from dature.fields import SecretStr, PaymentCardNumber, ByteSize
from dature.types import URL, Base64UrlBytes, Base64UrlStr
SecretStr
Masks the value in str() and repr():
@dataclass
class Config:
api_key: SecretStr
config = load(meta, Config)
print(config.api_key) # **********
print(config.api_key.get_secret_value()) # actual_secret
ByteSize
Parses human-readable sizes:
@dataclass
class Config:
max_upload: ByteSize
# config.yaml: { max_upload: "1.5 GB" }
config = load(meta, Config)
print(int(config.max_upload)) # 1500000000
print(config.max_upload.human_readable(decimal=True)) # 1.5GB
Supported units: B, KB, MB, GB, TB, PB, KiB, MiB, GiB, TiB, PiB.
PaymentCardNumber
Validates using the Luhn algorithm and detects the brand:
@dataclass
class Config:
card: PaymentCardNumber
config = load(meta, Config)
print(config.card.brand) # Visa
print(config.card.masked) # ************1111
URL
Parsed into urllib.parse.ParseResult:
@dataclass
class Config:
api_url: URL
config = load(meta, Config)
print(config.api_url.scheme) # https
print(config.api_url.netloc) # api.example.com
Base64UrlBytes / Base64UrlStr
Decoded from Base64 string in the config:
@dataclass
class Config:
token: Base64UrlStr # decoded to str
data: Base64UrlBytes # decoded to bytes
ENV Variable Substitution
All file formats support $VAR and ${VAR} substitution:
# config.yaml
api_url: $BASE_URL/api/v1
secret: ${SECRET_KEY}
Error Messages
dature provides human-readable error messages with source location:
Config loading errors (2)
[database.host] Missing required field
└── FILE 'config.json', line 3
"database": {
[port] Expected int, got str
└── ENV 'APP_PORT'
Merge conflicts:
Config merge conflicts (1)
[host] Conflicting values in multiple sources
└── FILE 'defaults.yaml', line 2
host: localhost
└── FILE 'overrides.yaml', line 2
host: production
Type Coercion
String values from ENV and file formats are automatically converted:
| Source | Target | Example |
|---|---|---|
"42" |
int |
42 |
"3.14" |
float |
3.14 |
"true" |
bool |
True |
"2024-01-15" |
date |
date(2024, 1, 15) |
"2024-01-15T10:30:00" |
datetime |
datetime(...) |
"10:30:00" |
time |
time(10, 30) |
"1 day, 2:30:00" |
timedelta |
timedelta(...) |
"1+2j" |
complex |
(1+2j) |
"192.168.1.1" |
IPv4Address |
IPv4Address(...) |
"[1, 2, 3]" |
list[int] |
[1, 2, 3] |
Nested dataclasses, Optional, and Union types are also supported.
Requirements
- Python >= 3.12
- adaptix >= 3.0.0b11
License
Apache License 2.0
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 dature-0.4.0.tar.gz.
File metadata
- Download URL: dature-0.4.0.tar.gz
- Upload date:
- Size: 38.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48040625800421b21f148be23e9e91629d123f0a94fa1768ae0c55b52e5e4d90
|
|
| MD5 |
23766497e1d09bed418312edd1d5a598
|
|
| BLAKE2b-256 |
969a0dac2a62d9620c0f4ed5ca7665deb8faf1ac94c3d72253443a9d53e2868c
|
Provenance
The following attestation bundles were made for dature-0.4.0.tar.gz:
Publisher:
ci.yml on Niccolum/dature
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dature-0.4.0.tar.gz -
Subject digest:
48040625800421b21f148be23e9e91629d123f0a94fa1768ae0c55b52e5e4d90 - Sigstore transparency entry: 947213718
- Sigstore integration time:
-
Permalink:
Niccolum/dature@fc70640bb838a5b125bc6a852de04af640de1d3e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Niccolum
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fc70640bb838a5b125bc6a852de04af640de1d3e -
Trigger Event:
push
-
Statement type:
File details
Details for the file dature-0.4.0-py3-none-any.whl.
File metadata
- Download URL: dature-0.4.0-py3-none-any.whl
- Upload date:
- Size: 39.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c8c2a3c0787a23d3e8a3ab4ab5656cbde701470eb1343257f3de2e35125b7af
|
|
| MD5 |
c70bb65dd3e6290f58d63d01307ad894
|
|
| BLAKE2b-256 |
2c42e843e3721a0eabebf6813dc9775f00403c1847dd96885f6accd6432321a6
|
Provenance
The following attestation bundles were made for dature-0.4.0-py3-none-any.whl:
Publisher:
ci.yml on Niccolum/dature
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dature-0.4.0-py3-none-any.whl -
Subject digest:
6c8c2a3c0787a23d3e8a3ab4ab5656cbde701470eb1343257f3de2e35125b7af - Sigstore transparency entry: 947213719
- Sigstore integration time:
-
Permalink:
Niccolum/dature@fc70640bb838a5b125bc6a852de04af640de1d3e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Niccolum
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fc70640bb838a5b125bc6a852de04af640de1d3e -
Trigger Event:
push
-
Statement type: