ProvenPin Install

PyYAML config validated with jsonschema Draft 2020-12: working versions (tested 2026-10-16)

Load a YAML config with PyYAML safe loading and validate it offline against a JSON Schema Draft 2020-12 with jsonschema, including format checks and duplicate-key detection.

passed6 passed · 0 failed2026-10-16 00:56 UTC
Status
passed
Last passed
2026-10-16 00:56 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions3
pyyaml@6.0.3jsonschema@4.26.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38832319459

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pyyaml-jsonschema-config") (install), or see them on GitHub.

requirements.txt
pyyaml==6.0.3
jsonschema==4.26.0
pytest==9.1.1
config.py
from __future__ import annotations

from typing import Any

import yaml
from jsonschema import Draft202012Validator, FormatChecker

SCHEMA: dict[str, Any] = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": ["name", "port", "released"],
    "additionalProperties": False,
    "properties": {
        "name": {"type": "string", "minLength": 1},
        "port": {"type": "integer", "minimum": 1, "maximum": 65535},
        "released": {"type": "string", "format": "date"},
        "debug": {"type": "boolean"},
        "hosts": {"type": "array", "items": {"type": "string", "format": "ipv4"}},
    },
}


class ConfigError(ValueError):
    pass


class ConfigLoader(yaml.SafeLoader):
    # YAML 1.1 parses unquoted 2024-01-01 into a datetime.date, which would
    # then fail the schema's "type: string". Drop the timestamp resolver so
    # dates stay plain strings for format: date to check.
    yaml_implicit_resolvers = {
        ch: [(tag, rx) for tag, rx in resolvers if tag != "tag:yaml.org,2002:timestamp"]
        for ch, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items()
    }


def _unique_mapping(loader: ConfigLoader, node: yaml.MappingNode) -> dict[Any, Any]:
    # PyYAML silently keeps the last value for duplicate keys; make it loud.
    loader.flatten_mapping(node)
    pairs = loader.construct_pairs(node)
    keys = [key for key, _ in pairs]
    dupes = sorted({key for key in keys if keys.count(key) > 1})
    if dupes:
        raise ConfigError(f"duplicate config keys: {dupes}")
    return dict(pairs)


ConfigLoader.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _unique_mapping
)

VALIDATOR = Draft202012Validator(SCHEMA, format_checker=FormatChecker())


def load_config(text: str) -> dict[str, Any]:
    try:
        data = yaml.load(text, Loader=ConfigLoader)
    except yaml.YAMLError as exc:
        raise ConfigError(f"invalid YAML: {exc}") from exc
    if data is None:
        raise ConfigError("config is empty")
    if not isinstance(data, dict):
        raise ConfigError("config root must be a mapping")
    errors = sorted(VALIDATOR.iter_errors(data), key=lambda e: list(e.path))
    if errors:
        path = "/".join(str(p) for p in errors[0].path) or "<root>"
        raise ConfigError(f"{path}: {errors[0].message}")
    return data
tests/test_config.py
import pytest
from config import ConfigError, load_config

VALID = """
name: api
port: 8080
released: 2024-01-01
hosts: [10.0.0.1, 10.0.0.2]
"""


def test_valid_config_loads():
    cfg = load_config(VALID)
    assert cfg["port"] == 8080
    assert cfg["hosts"] == ["10.0.0.1", "10.0.0.2"]


def test_unquoted_date_stays_a_string():
    assert load_config(VALID)["released"] == "2024-01-01"


def test_bad_date_fails_format_check():
    with pytest.raises(ConfigError, match="is not a 'date'"):
        load_config("name: api\nport: 80\nreleased: 2024-13-40\n")


def test_yaml11_boolean_breaks_string_field():
    with pytest.raises(ConfigError, match="not of type 'string'"):
        load_config("name: no\nport: 80\nreleased: 2024-01-01\n")


def test_unknown_key_is_rejected():
    with pytest.raises(ConfigError, match="Additional properties are not allowed"):
        load_config("name: api\nport: 80\nreleased: 2024-01-01\nlevel: 3\n")


def test_duplicate_keys_are_rejected():
    with pytest.raises(ConfigError, match="duplicate config keys"):
        load_config("name: api\nname: web\nport: 80\nreleased: 2024-01-01\n")
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.