Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dature-0.4.0.tar.gz (38.8 kB view details)

Uploaded Source

Built Distribution

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

dature-0.4.0-py3-none-any.whl (39.7 kB view details)

Uploaded Python 3

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

Hashes for dature-0.4.0.tar.gz
Algorithm Hash digest
SHA256 48040625800421b21f148be23e9e91629d123f0a94fa1768ae0c55b52e5e4d90
MD5 23766497e1d09bed418312edd1d5a598
BLAKE2b-256 969a0dac2a62d9620c0f4ed5ca7665deb8faf1ac94c3d72253443a9d53e2868c

See more details on using hashes here.

Provenance

The following attestation bundles were made for dature-0.4.0.tar.gz:

Publisher: ci.yml on Niccolum/dature

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for dature-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c8c2a3c0787a23d3e8a3ab4ab5656cbde701470eb1343257f3de2e35125b7af
MD5 c70bb65dd3e6290f58d63d01307ad894
BLAKE2b-256 2c42e843e3721a0eabebf6813dc9775f00403c1847dd96885f6accd6432321a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for dature-0.4.0-py3-none-any.whl:

Publisher: ci.yml on Niccolum/dature

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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