Skip to content

Quick Start

github-actions[bot] edited this page Nov 4, 2026 · 7 revisions

Quick Start

From nothing to a working interpose in a few minutes. Everything else is in API Reference.

Prerequisites

  • For running from source: Python >= 3.13 and uv
  • Access to GitHub: interpose is installed from a release tag, not from PyPI. It brings pyyaml.

Install

Pin a release tag, never main:

uv add "interpose @ git+https://github.com/metalayerlab/interpose@v0.1.0"

To move to a newer release, change the tag in pyproject.toml and run uv lock. Dependabot can't do this for you yet (see FAQ).

First run

A project's config.py declares its settings and turns the resolved values into its own Config:

import logging
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from interpose.secret import Secret
from interpose.settings import ConfigError, Setting, Settings, one_of, port

__all__ = ["CONFIG_ENV", "SETTINGS", "Config", "ConfigError", "load_config"]

SETTINGS = Settings(
    "MYAPP_",
    (
        Setting(
            "log_level", "INFO", "Log level.", check=one_of("DEBUG", "INFO", "WARNING", "ERROR")
        ),
        Setting("listen.port", 8080, "Port the API listens on.", check=port),
        Setting("upstream.token", "", "Token for the upstream API.", secret=True),
        Setting("allow.hosts", "", "Hosts to allow.", is_list=True),
    ),
    default_path=Path("/etc/myapp/config.yaml"),
)
CONFIG_ENV = SETTINGS.config_env  # "MYAPP_CONFIG"
log = logging.getLogger(__name__)


@dataclass(frozen=True)
class Config:
    log_level: str
    port: int
    token: Secret
    hosts: tuple[str, ...]
    sources: Mapping[str, str] = field(default_factory=dict)


def load_config(path: Path | None = None, environ: Mapping[str, str] | None = None) -> Config:
    r = SETTINGS.resolve(path, environ)
    for warning in r.warnings:  # unknown keys and variables: names only, never values
        log.warning("%s", warning)
    # Rules that span settings go here, raising ConfigError.
    # Each accessor checks the setting's kind: r.str() on a secret setting is a TypeError.
    return Config(
        log_level=r.str("log_level"),  # one_of gives the declared spelling: "debug" -> "DEBUG"
        port=r.int("listen.port"),
        token=r.secret("upstream.token"),  # reveal() at the point of use
        hosts=r.list("allow.hosts"),
        sources=dict(r.sources),
    )

With this /etc/myapp/config.yaml:

listen:
  port: 9090
allow:
  hosts: [example.org, "*.example.org"]

and MYAPP_UPSTREAM_TOKEN_FILE=/run/secrets/token in the environment, load_config() gives:

Config(log_level='INFO', port=9090, token=Secret('********'), hosts=('example.org', '*.example.org'),
       sources={'log_level': 'default', 'listen.port': 'file',
                'upstream.token': 'MYAPP_UPSTREAM_TOKEN_FILE', 'allow.hosts': 'file'})

config.token.reveal() gives the token at the point of use. A misspelt key, such as prot: under listen:, is ignored and logs unknown key 'listen.prot' in /etc/myapp/config.yaml: did you mean 'listen.port'?. A bad value stops it with one line, such as listen.port: expected int, got 0.5 (from /etc/myapp/config.yaml).

A project that passes Settings(..., expand=True) lets its config file use the environment: port: "${PORT:-9090}" takes $PORT, else 9090, and an unset ${HOST} with no default is an error. It is off by default.

Options that don't fit a fixed setting, such as each middleware's own, go in a section: Setting("middleware.config", "", "Per-middleware options.", is_section=True) takes the whole subtree under middleware: config: as a read-only Section, which the project validates in load_config. A secret inside it is written as a file, never inline:

middleware:
  config:
    ratelimit: {per_minute: 60}
    auth: {token: {secret_file: /run/secrets/auth-token}}

r.section("middleware.config")["auth"]["token"] is then a Secret. In Kubernetes, mount the section as a file and point MYAPP_MIDDLEWARE_CONFIG_FILE at it.

A list whose items have a fixed shape declares its fields, and interpose types each one:

Setting(
    "schedule",
    "",
    "Cron schedule.",
    is_section=True,
    is_list=True,
    fields=(
        Setting("cron", "", "A 5-field cron expression.", required=True),
        Setting("context", "", "The context to enqueue.", required=True),
    ),
)

r.sections("schedule") is then a tuple of Sections, each holding cron and context. The list can also be set in the environment, item by item from 0: MYAPP_SCHEDULE_0_CRON="0 * * * *" and MYAPP_SCHEDULE_0_CONTEXT=hourly, then _1_.... Set this way, it replaces the file's list. A missing required field stops it with schedule[1].context: MYAPP_SCHEDULE_1_CONTEXT is not set, but MYAPP_SCHEDULE_1_CRON is.

For secrets that aren't settings (a middleware's API keys, say), read_secret_dir(Path("/run/secrets")) reads each file in a directory as a Secret.

Next steps

interpose

GitHub • Releases


📚 Getting Started

📖 Core Concepts

🔒 Security

🆘 Help & Support

🧑‍💻 For Developers


🔑 Key Features

  • 🧅 One middleware loop; fail open or closed is policy, per project and per middleware
  • 🥞 One lookup order: _FILE > environment > YAML > default
  • 🧱 Strict types: no silent 0 for a YAML 0.5
  • 🧭 Where each value came from, for every setting
  • 🔒 Secret: masked in logs, reprs and errors
  • 📈 Prometheus text with labels from declared value sets

Clone this wiki locally