Skip to content

Custom Source Classes#

For formats that dature doesn't support out of the box, you can create your own source by subclassing one of the base classes from dature.sources.base:

Choosing a base class#

Base class Use when You implement You get for free
Source Non-file data (API, database, custom protocol) format_name, _load() -> JSONValue Prefix filtering, env var expansion, type coercion, validation, merge support
FileSource File-based format (XML, CSV, HCL, …) format_name, _load_file(path: FileOrStream) -> JSONValue Everything from Source + file parameter, stream support, file_display(), file_path_for_errors(), __repr__
FlatKeySource Flat key=value data (custom env store, Consul KV, …) format_name, _load() -> JSONValue (flat dict[str, str]) Everything from Source + nested_sep nesting, nested_resolve, automatic string→type parsing (int, bool, date, …)
RemoteSource Network/remote data (AWS Secrets Manager, Consul, Kubernetes, …) format_name, remote_address() -> str, _fetch() -> JSONValue Everything from Source + display_name(), file_display(), location_label = "REMOTE"

All base classes are in dature.sources.base:

```python
from dature.sources.base import FileSource, FlatKeySource, Source

__all__ = ["FileSource", "FlatKeySource", "Source"]

```

Minimal interface#

Every custom source needs:

  1. format_name — class-level string shown in __repr__ and error messages (e.g. "xml", "consul")
  2. A load method_load() for Source/FlatKeySource, or _load_file(path) for FileSource. Must return JSONValue (a nested dict).

Optional overrides#

Method Default Override when
additional_loaders() [] (FileSource) or string-value loaders (FlatKeySource) Your format stores all values as strings and needs extra type parsers (e.g. bool, float).
_build_line_index(content) None (no diagnostics) You want errors to show exact line numbers from your source. Return a dict[tuple[str, ...], LineRange] mapping dotted key paths to line ranges. See sources/yaml_.py as reference.
file_display() None Your source has a meaningful display path (shown in logs and errors).
file_path_for_errors() None Your source points to a file on disk (used in error messages).
resolve_location(...) Uses _build_line_index + caret computation Low-level escape hatch — override only when _build_line_index is not enough (e.g. env var name in error messages).
location_label inherited Change the label in error messages (e.g. "FILE", "ENV", "API").

Example: FileSource subclass#

The most common case — reading a file format:

```python

import xml.etree.ElementTree as ET
from dataclasses import dataclass

import dature
from adaptix import Provider, loader

from dature.coercion import bool_loader, float_from_string
from dature.sources.base import FileSource
from dature.type_aliases import FileOrStream, JSONValue


@dataclass(kw_only=True, repr=False)
class XmlSource(FileSource):
    format_name = "xml"

    def _load_file(self, path: FileOrStream) -> JSONValue:
        if not isinstance(path, Path):
            msg = "XmlSource only supports file paths"
            raise TypeError(msg)
        tree = ET.parse(path)  # noqa: S314
        root = tree.getroot()
        return {child.tag: child.text or "" for child in root}

    def additional_loaders(self) -> list[Provider]:
        return [
            loader(bool, bool_loader),
            loader(float, float_from_string),
        ]

    # Override _build_line_index(content) to add line-number diagnostics.
    # Return dict[tuple[str, ...], LineRange] mapping paths to line ranges,
    # or None to disable. See sources/yaml_.py for a reference.


@dataclass
class Config:
    host: str
    port: int
    debug: bool


config = dature.load(
    XmlSource(
        file=SOURCES_DIR / "custom_loader.xml",
    ),
    schema=Config,
)

assert config == Config(host="localhost", port=9090, debug=True)

```

```xml title="custom_loader.xml"
<config>
    <host>localhost</host>
    <port>9090</port>
    <debug>true</debug>
</config>

```

FileSource handles the file parameter, path expansion, and stream detection. Your _load_file() receives a Path or file-like object and returns a dict.

Example: Source subclass (non-file)#

For sources that don't read files — e.g. an API, a database, or an in-memory dict:

```python
from dataclasses import dataclass
from typing import Any, cast

import dature
from dature.sources.base import Source
from dature.type_aliases import JSONValue


@dataclass(kw_only=True, repr=False)
class DictSource(Source):
    format_name = "dict"
    location_label = "DICT"
    data: dict[str, Any]

    def _load(self) -> JSONValue:
        return cast("JSONValue", self.data)


@dataclass
class Config:
    host: str
    port: int


config = dature.load(
    DictSource(data={"host": "localhost", "port": 8080}),
    schema=Config,
)

assert config == Config(host="localhost", port=8080)

```

Protocol-based sources#

Subclassing Source is the recommended path — you get all the shared behaviour for free. But dature's loader uses structural typing: it checks isinstance(source, SourceProtocol), not issubclass(MySource, Source). Any @dataclass that implements the full SourceProtocol interface is accepted, without inheriting from Source.

from dature.sources.protocol import SourceProtocol, FileSourceProtocol

SourceProtocol (in dature.sources.protocol) is the complete interface the loader requires — class-level attributes (format_name, location_label, config_group), instance fields (prefix, tag, when, …), and methods (load_raw(), display_name(), resolve_location(), …).

FileSourceProtocol is the narrower interface for sources that point to a file on disk. If your source implements it (exposes skip_if_broken, skip_if_missing, file_path_for_errors(), encoding_for_errors()), the loading machinery will use these for per-source skip behaviour and error enrichment automatically.

Tip

Implementing SourceProtocol from scratch is verbose — you'd replicate most of what Source already provides. Consider this path only when you have an existing class hierarchy that can't inherit from Source. For every other case, subclass Source, FileSource, or FlatKeySource as shown above.

Tips#

  • All built-in features (type coercion, validation, prefix extraction, ENV expansion, merge support) work automatically with any custom source.
  • Override additional_loaders() to return string_value_loaders() from dature.sources.base if your format stores everything as strings (like INI or ENV).
  • Pass your custom source to dature.load() the same way as any built-in source.