Skip to main content

config-cascade-merge

A small, schema-driven YAML object merger and validator written in Python. It loads a base schema, validates and applies ordered overlay operations, and emits the complete configuration.

Installation

config-cascade-merge requires Python 3.10 or newer.

python -m pip install config-cascade-merge

You can also install the latest development version directly from GitHub:

python -m pip install git+https://github.com/149segolte/config-cascade-merge.git

Quick start

config-cascade-merge --base_config base.yaml --overlays_dir overlays/

The complete configuration object is written to standard output as YAML. To select specific overlays and control their order explicitly, pass their paths with --overlays:

config-cascade-merge --base_config base.yaml \
  --overlays overlays/common.yaml overlays/workstation.yaml

--overlays and --overlays_dir are mutually exclusive. Directory contents are processed in lexical filename order; an explicit list is processed in the order given.

The executable also supports short options:

config-cascade-merge -b base.yaml \
  -o overlays/common.yaml overlays/workstation.yaml

The module form is equivalent:

python -m config_cascade_merge -b base.yaml --overlays_dir overlays/

Library usage

Create a reusable Schema, validate each Overlay against it, and compose an immutable MergePlan:

from config_cascade_merge import ConfigError, MergePlan, Overlay, Schema

try:
    schema = Schema.from_file("base.yaml")
    common = Overlay.from_file("overlays/common.yaml", schema)
    workstation = Overlay.from_file("overlays/workstation.yaml", schema)
    plan = MergePlan(schema).with_overlays([common, workstation])
except ConfigError as error:
    print(f"Invalid configuration: {error}")
else:
    config = plan.create_object()
    print(config)

Schema, Overlay, and MergePlan are immutable public objects. Their factories do not configure logging or exit the calling process. Schema, overlay, and execution failures derive from ConfigError, with the more specific SchemaError, OverlayError, and MergeError types available when callers need to distinguish them.

Calling plan.create_object() constructs the default schema-shaped object, applies each overlay in order, and validates that the final result is complete. Required fields must contain values of their declared types. Use plan.create_object(validate=False) when intentionally constructing a partial result: fixed object fields without values are None, while maps and lists start empty.

Incremental composition and branching

Composition returns a new plan and leaves prior plans unchanged:

base_plan = MergePlan(schema)
common_plan = base_plan.with_overlay(common)

workstation_plan = common_plan.with_overlay(workstation)
server_plan = common_plan.with_overlay(server)

with_overlays(iterable) appends several overlays in iterable order. The read-only plan.schema and plan.overlays properties make plans easy to inspect and reuse. Schemas expose their optional source; overlays expose their name, optional source, and defensively copied operations.

In-memory inputs

All input objects support file, YAML-text, and already-decoded forms:

overlay = Overlay.from_file(path_to_overlay)
overlay = Overlay.from_yaml(yaml_text)
overlay = Overlay.from_data(
    {
        "name": "runtime",
        "operations": [
            {"action": "set", "path": ".profile.name", "data": "Ada"},
        ],
    },
    schema,
    source="runtime settings",
)

Use Schema.from_file(...), Schema.from_yaml(...), and Schema.from_data(...) for similar behavior. Optional source labels are included in validation errors.

Starting from a partial value

Pass an existing partial configuration with initial:

updated = plan.create_object(initial=existing_config)

The starting value is defensively copied, validated, and recursively completed before any overlay operation executes. Missing required object fields receive the same structural defaults used when no initial value is supplied, including inside list items and map values. Omitted optional fields remain absent. Unknown fields, invalid types, invalid tagged unions, and ambiguous untagged unions fail immediately. Neither successful nor failed execution mutates the supplied object.

Final validation runs after every overlay by default, so any required fields that still contain structural None placeholders cause MergeError with an Invalid final configuration message. Pass validate=False to skip only this final completeness check; initial values and overlay data are still validated.

Base schema

The base file describes the allowed configuration shape rather than containing configuration values.

# base.yaml
type: object
keys:
    profile:
        type: object
        keys:
            name:
                type: string
            active:
                type: boolean
                optional: true
    packages:
        type: list
        id: name
        value:
            type: object
            keys:
                name:
                    type: string
                version:
                    type: integer
    labels:
        type: map
        value:
            type: string

Schema types

Type Purpose Main fields
string, integer, float, boolean, any Primitive value optional optional
object Fixed-key mapping keys, optional merge, id, and optional
map Arbitrary-key mapping with uniform values value, optional merge, id, and optional
list Ordered values with a uniform item schema value, optional merge, id, and optional
union Value matching one of at least two schemas value (list of schemas), optional optional
tagged_union Object selected by a discriminator field keys, tag.name, tag.options, optional optional

The merge policy is either append (default) or override. An id identifies values for identity-based merging.

Every schema node accepts a boolean optional field, which defaults to false. Complete objects and tagged unions may omit named fields marked optional. Empty plans do not materialize optional object fields, and remove deletes them instead of replacing them with null. optional does not allow an explicit null value. On roots, list items, map values, and union branches the flag is retained for structural equality but has no effect unless the node is later used as a named fixed field. A field used by an object or list id cannot be optional.

Example tagged union:

type: tagged_union
keys:
    label:
        type: string
tag:
    name: kind
    options:
        file:
            path:
                type: string
        service:
            port:
                type: integer
        disabled: null

Overlays

Each overlay has a non-empty name and an ordered list of operations. Paths start with .; . addresses the schema root.

# overlays/10-workstation.yaml
name: workstation
operations:
    - action: set
      path: .profile
      data:
          name: Ada
          active: true

    - action: merge
      path: .packages
      data:
          - name: ruff
            version: 1

    - action: test
      path: .profile.name
      data: Ada
      on_fail: warn
      message: unexpected profile

    - action: remove
      path: .labels.legacy

    - action: clear
      path: .packages

Operations

Action Behavior Required fields
set Creates or replaces a value; data must fully match the target schema path, data
merge Validates a recursive merge into an object, map, or list path, data
remove Deletes an optional fixed field or map entry; nulls a required field path
test Checks equality before later execution path, data; optional on_fail, message
clear Removes all entries from a map or list path

test.on_fail accepts:

  • error — stop execution (default)
  • warn — report the optional message and continue
  • skip — keep prior operations from this overlay and skip the remainder
  • drop — discard all operations from this overlay

These failure behaviors are applied to each explicit Overlay group while creating the object. drop rolls back earlier changes from that overlay, while skip preserves earlier changes and skips its remaining operations. Separate overlay documents remain separate groups even when they use the same name.

Validation and errors

The validator rejects malformed YAML, invalid schemas, unknown paths or fields, incompatible values, and unsupported operations. Errors include source filenames and line numbers when available:

overlays/10-workstation.yaml:8: Data at '.packages[0].version' must be integer, got str

The CLI exits with status 1 for invalid paths, schemas, overlays, or empty schema documents.

Development

Clone the repository, install development dependencies, and run the test suite:

git clone https://github.com/149segolte/config-cascade-merge.git
cd config-cascade-merge
uv sync --dev
uv run pytest

Build both source and wheel distributions:

uv build

Project layout:

src/config_cascade_merge/api.py         high-level library API
src/config_cascade_merge/cli.py         CLI entry point
src/config_cascade_merge/engine.py      merge-plan execution
src/config_cascade_merge/schema.py      schema parsing and normalization
src/config_cascade_merge/overlay.py     overlay loading and validation
src/config_cascade_merge/yaml_loader.py YAML loading with source locations
tests/                                  pytest test suite

See CONTRIBUTING.md for the contribution and release process.

License

Licensed under the Mozilla Public License 2.0.

Release files for config-cascade-merge 0.12.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for config-cascade-merge 0.12.1
File Size Uploaded
config_cascade_merge-0.12.1.tar.gz 33.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for config-cascade-merge 0.12.1
File Interpreter ABI Platform
config_cascade_merge-0.12.1-py3-none-any.whl Python 3 none any Details

Total release size: 62.9 kB

Release files / config_cascade_merge-0.12.1.tar.gz

Download URL config_cascade_merge-0.12.1.tar.gz
Size 33.0 kB
Tags Source
SHA-256 checksum
How to use checksums
bcf8922d2c6860c465c9e654f916f159e70f70635ad2a115620c89f2b9b1fe0a
BLAKE2b-256 checksum
How to use checksums
549032654eaf724ec0c7b82c3d3848afb520f9b1e9108e9dc09614dbdffac644
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / config_cascade_merge-0.12.1-py3-none-any.whl

Download URL config_cascade_merge-0.12.1-py3-none-any.whl
Size 29.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
530565252aad1a7d2d85fe8c51082cd97d1de12b9539b10da4f16ba4b3ef3137
BLAKE2b-256 checksum
How to use checksums
d1238a464eaf90a3b40c3debbcb6d98e16f34e5be1b4bd54db1e3fd5fc90b00d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.12.1 This release

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page