Repository navigation
API Reference
Import from the submodules: interpose.pipeline, interpose.settings, interpose.secret and interpose.metrics. The package itself exports only __version__ and __description__. Anything starting with _ is internal.
The middleware loop and the guard around every hook. interpose defines no hooks and no unit of work: the project keeps its own Middleware class and unit (a flow, a call), and passes invoke callables that call its hooks.
Policy(on_error, after_order, after_local, observe_failed, *, timeout=None, answers=(), skip_inherited=None): the project's answers, a frozen dataclass. The first four have no default.
-
on_error:"skip"(a failing middleware is out for the rest of the unit, which carries on) or"fail"(HookFailedis raised), unless the middleware says. -
after_order:"chain"(as registered) or"reverse"(the onion). -
after_local: whether after hooks run when a before hook stopped the unit (answered locally). -
observe_failed: whether observer hooks still reach a middleware that has failed. -
timeout: seconds per hook call, unless the middleware says;Noneis no limit. -
answers: exception types a hook raises as its answer (a deny), re-raised unchanged whateveron_errorsays. -
skip_inherited: the project's base class, whose hooks must do nothing; a hook a middleware doesn't override, in its class or on the instance (mw.pre = gatecounts as an override), isn't called (no timing). Ineachandbeforeit counts as returningNone, sountilis still asked (a stop that reads the unit's state still happens there); elsewhere it givesSKIPPED. Overrides are resolved once, when thePipelineis built (0008): a hook assigned after that, on the instance or its class, isn't seen, so set instance hooks before building it. - Raises
ValueErrorfor a value outside these.
Entry(middleware, name, on_error=None, timeout=None): one middleware in a chain with its own policy (None: the project's; a timeout of NO_LIMIT opts it out of the project's limit). Compared by identity. Entry.of(middleware, name=None) reads the middleware's name, on_error and timeout attributes; name= wins. name_of(middleware): its name attribute if a non-empty str, else its class name.
Pipeline(middleware, policy): a frozen chain (middleware or Entrys) and its policy. entries, names, policy.
-
start(*, select=None, select_hook="select", on_failure=None, timings=None, state=None) -> Run: one unit's run.select(middleware) -> boolpicks what applies, once, now; one that raises fails like a hook namedselect_hook.on_failure(Failure)is called for every failure (log and count it there).timingsandstate, if given, are filled in place.
Run: one unit's pass. entries (selected), active (minus failed), timings (seconds per middleware name), state, ran ((entry, context) pairs), stopped, policy, has_failed(entry).
| Method | Calls | Over (default) | Returns |
|---|---|---|---|
call(entry, hook, invoke, *, valid=None) |
invoke() |
one entry | the result, or SKIPPED
|
each(hook, invoke, *, entries, until, valid) |
invoke(mw) until until(result)
|
active |
Stopped(entry, result) or None
|
fold(hook, value, invoke, *, entries, valid, keep_none=False) |
invoke(mw, value); the result replaces value
|
active |
the value |
observe(hook, invoke, *, entries) |
invoke(mw); always fails open |
entries |
None |
before(hook, context, invoke, *, entries, until, valid) |
invoke(mw, context), recording (entry, context) in ran first; a stop is kept in stopped
|
active |
Stopped or None
|
after_pairs() |
ran in after_order, minus failed; [] after a stop unless after_local
|
the pairs | |
after(hook, value, invoke, *, valid, keep_none=False) |
invoke(mw, context, value) |
after_pairs() |
the value |
observe_ran(hook, invoke, *, pairs) |
invoke(mw, context); always fails open |
ran in after_order
|
None |
scoped(entry) |
a ScopedState
|
-
The guard, around every call: skips a failed entry (observers: unless
observe_failed) or an inherited hook; times the call intotimings[name]; bounds it with the entry's or the policy's timeout; re-raisesanswers(not in observers); anything else it raises, or a resultvalidrefuses (or avalidthat raises), is a failure: the entry leavesactive,on_failureis called, and under"fail"HookFailedis raised (under"skip",SKIPPEDis returned).CancelledErroris never caught. A hook that returns after its limit (it ran past the deadline in synchronous code,late="overran", or caught its cancellation,late="swallowed") has its result discarded: it is a timeout failure (timed_out=True, aTimeoutError), handled byon_error. -
Hooks and timeouts: a timeout cancels the hook at its current
await. Hooks must be cancel-safe (cleanup intry/finallyorasync with) and must never swallowCancelledErrororBaseException. Blocking or CPU-bound work goes off the event loop: a timeout can't interrupt synchronous code. A thread started withrun_in_executororasyncio.to_threadkeeps running after itsawaitis cancelled. -
keep_none=Truemakes aNoneresult keep the value (a chunk hook that passes a chunk on). -
untilis the project's predicate and must not raise. Under"fail",start()raisesHookFailedwhen aselectfails.
Failure(middleware, hook, error, timed_out, action, timeout, late=None): what failed, with what, and whether the guard's timeout did it; action is "skip" or "fail"; timeout is the limit that applied (seconds, or None); late (Late) is "overran" or "swallowed" for a result that came back after the limit, else None. Holds no unit data.
HookFailed: raised under "fail". failure is the Failure; __cause__ is what the hook raised (a TimeoutError for a timeout). The message names the middleware and the hook, never a value.
InvalidResult: the Failure.error for a result valid refused; names the result's type only.
Stopped(entry, result): where a phase stopped. SKIPPED: a call that gave no result (falsy). ScopedState(state, name): a MutableMapping view storing keys as "<name>.<key>".
require_async(middleware, hooks, *, name=None): TypeError if any of hooks the middleware has isn't async def, for refusing sync hooks at registration.
from interpose.pipeline import HookFailed, Pipeline, Policy
POLICY = Policy(on_error="fail", after_order="reverse", after_local=True, observe_failed=True)
PIPELINE = Pipeline(chain, POLICY) # built once
async def handle(ctx):
run = PIPELINE.start(select=lambda mw: mw.applies(ctx), state=ctx.state)
try:
await run.before("pre", ctx, lambda mw, c: mw.pre(c), until=lambda _: ctx.responded)
result = ctx.response if run.stopped else await operation(ctx)
return await run.after("post", result, lambda mw, c, r: mw.post(c, r))
except HookFailed as e:
await run.observe_ran("failed", lambda mw, c: mw.failed(c, e))
raise MyDeny(e.failure.middleware) from eSetting(key, default, help, *, secret=False, is_list=False, check=None, aliases=(), is_section=False, fields=(), required=False): one setting, a frozen dataclass.
-
key: dotted lower case,[a-z0-9_]+joined by dots (listen.port). -
default: astr,intorbool. Its type is the setting's type. A list setting's default is"". -
help: Markdown, as users read it in the generated settings table. -
secret: the value resolves to aSecret. Only for str settings. -
is_list: the value is atuple[str, ...], from a YAML list or a comma-separated string. -
check: a callable that gets the coerced value and raisesValueError(reason)if it's invalid. It may return a replacement of the setting's kind (astr, anintthat isn't abool, abool, atupleofstr, aSection, atupleofSection);Nonekeeps the value. Returning the value unchanged keeps it. A replacement of another kind is aConfigErrornaming the key and the type, never the value. A check on a secret setting must returnNone(or the value unchanged). -
aliases: a tuple of old dotted keys for a renamed setting (aliases=("port",)onlisten.port). Each is read wherever the key is: in every layer, as<PREFIX><ALIAS>and as<PREFIX><ALIAS>_FILE. At each level of the lookup order the key and its aliases are looked up together: if one is set it is used; if several are, their coerced values must be equal (the key's is the one read), or it is aConfigErrornaming both, never the values. A higher level beats a lower one whatever the name. Each alias that is set gives analiaswarning, used or not. Public, so aconfigcommand or wiki table can show "formerlyport" (0009). -
is_section: the value is aSection, the YAML subtree at the key, or withis_list=Trueatuple[Section, ...], from a YAML list of mappings. The default must be""(empty), and a section can't besecret(it holds secrets as secret leaves). It is read from<PREFIX><KEY>_FILE, which names a YAML file whose top level is the section (a mapping, or a list for a list section), then from the layers, else it is empty; setting a plain<PREFIX><KEY>is aConfigErrorsaying to use_FILE. interpose checks only the shape: a mapping (or list of mappings) with string keys at every level. Inside a section, a mapping that is exactly{secret_file: <path>}is a secret leaf: it resolves to aSecretholding that file, trailing whitespace stripped as for_FILE, read afresh on every resolve, and only from a layer withsecret_files=True(the config file and a section's_FILE); from any other layer it is aConfigErrorand the file is never opened. A relative path (after${VAR}expansion, where the layer expands) resolves against the directory of the file that holds the leaf: the config file's (Layer.base_dir), or a section's_FILE's, whatever layer the rest came from; a list's items resolve against their list's file. An absolute path is unchanged, and a path whose first part starts with~is aConfigError(0010). A section'scheckgets theSection(or tuple), may return a replacement of the same kind, and its reason has every secret leaf masked. Nothing below a section's key is checked for unknown keys (0009). -
fields: a list section's item fields, a tuple ofSettings, each with a one-part key (cron), its default setting its type as for any setting. Only a list section (is_section=True, is_list=True) has fields. Each item then resolves to aSectionholding every field, in declaration order: a field that is set is coerced and checked as a setting of its kind (str, int, bool, list,secret=True, or a free-form section), and one that is missing (ornull) takes its default. A secret field may be written as a{secret_file: <path>}leaf, read as for a section (from local configuration only); a leaf in any other field that isn't a section is aConfigError. A key in an item that is no field gives anunknownwarning with a suggestion and is ignored. Errors name the item:schedule[2].cron: expected int, got 'x' (from /etc/myapp/config.yaml). With fields, the list may also come from indexed variables,<PREFIX><KEY>_<N>_<FIELD>and its_FILE, forNfrom 0 (an alias's too): itemNexists when any of its field variables is set, and the firstNwith none ends the list. They come after the list's_FILEand before the layers, and replace the layers' list whole, never merged;sourcessays<PREFIX><KEY>_<N>_*. A variable past the end gives anignoredwarning. Each field takes its_FILE(a YAML file for a section field), then its variable; a section field's plain variable is aConfigError. Indexed variables never expand. Errors from them name the exact variable:schedule[1].port: expected int, got 'x' (from MYAPP_SCHEDULE_1_PORT). -
required: only on a field. A required field missing from an item is aConfigErrornaming the item and where:schedule[1].cron: a required field, not set (from /etc/myapp/config.yaml), or from indexed variables,schedule[1].cron: MYAPP_SCHEDULE_1_CRON is not set, but MYAPP_SCHEDULE_1_CONTEXT is. -
env(property): the environment variable, the prefix plus the upper-cased key with dots as underscores (MYAPP_LISTEN_PORT).prefixis set bySettings, never by hand.
Settings(prefix, settings, *, default_path, extra_env=(), expand=False, expand_unset="error"): a Sequence[Setting], each one bound to prefix.
-
config_env:prefix + "CONFIG", the variable that names the YAML file. Alsoprefix,default_path,extra_env,expandandexpand_unset. -
extra_env: names of prefixed variables the project reads itself (("MYAPP_DEBUG_DUMP",)), so they give no unknown-variable warning. Each must start with the prefix, appear once, and not be a variable a setting already reads (or<PREFIX>CONFIG). -
expand:Trueturns on${VAR}expansion in the config file:read_file()gives its layerexpand=True. Off by default, so an existing${in a file keeps its meaning. In a value a setting reads from that layer,${NAME}is the variable's value and${NAME:-default}the default when the variable is unset or empty, as in the shell.NAMEis[A-Za-z_][A-Za-z0-9_]*; the default is literal text up to the first}(nothing in it expands).$${is a literal${, and a$not followed by{is literal. Any other${(unclosed,${},${vars.x}) is aConfigErrornaming the key: write$${for a literal. The variables areresolve()'senviron, read on every resolve. Every string in the value expands, inside longer strings and in list items, never mapping keys; non-strings are untouched, and typing comes after, soport: "${PORT:-5000}"is the int 5000. Only what a setting reads expands: an unknown key's${never fails a load. Variables,_FILEcontents, defaults and aLayerthe project builds withoutexpand=Truenever expand.sourcesstill says"file"0009. -
expand_unset: what a${NAME}with no default gives whenNAMEis unset."error"(the default) is aConfigError:upstream.url: ${HOST} is not set and has no default (from /etc/myapp/config.yaml)."empty"gives the empty string, for a project whose files relied on that. Anything else is aValueError. - Raises
ValueErrorfor a bad or duplicate key, a key part YAML doesn't read as a string (on,no,null,1), two settings that would read the same variable (a.banda_b;x_fileandx's_FILE; a keyconfig), an alias that is malformed, isn't a tuple, repeats (in its setting or another), equals a key, is a parent of a key or alias or below one, or reads a variable another setting or alias reads, a list setting whose default isn't"", a secret setting that isn't a str setting, a section whose default isn't"", a secret section, a section's key that is a parent of another key or alias or below one,fieldson anything but a list section,requiredoutside a field, a field whose key has more than one part, repeats or reads another field's variable (x_fileandx's_FILE), a field with fields or aliases of its own, any other setting's, alias's orextra_envvariable that has the form of a list section's indexed variables (MYAPP_SCHEDULE_0_CRON,MYAPP_SCHEDULE_1_FILE), anextra_envname that breaks its rules, or anexpand_unsetother than"error"or"empty". -
resolve(path=None, environ=None, *, layers=None) -> Resolved: readspath(else$<PREFIX>CONFIG, elsedefault_path; missing means defaults) andenviron(elseos.environ), and resolves every setting. RaisesConfigError.- A section takes its
_FILE(a YAML file), then the first layer that has it, then empty; its plain variable is aConfigError. A list section with fields takes its indexed variables between its_FILEand the layers. -
layers, a sequence ofLayer, replaces the file: no file is read, and each setting takes the first of its_FILEvariable, its variable,layers[0],layers[1], ..., its default.layers=[]means the environment and defaults only. Passingpathwithlayersis aValueError. Withoutlayers,resolve(path, environ)isresolve(environ=environ, layers=[read_file(path, environ)])(0009).
- A section takes its
-
read_file(path=None, environ=None) -> Layer: the config file as aLayerwith source"file",whereits path,expandthe project'sexpand,secret_files=Trueandbase_dirthe file's directory, found asresolve()finds it. A relative path (path,<PREFIX>CONFIGordefault_path) is made absolute against the working directory here, once (Path.absolute(), no symlink resolution), sowhereis absolute and a laterchdirnever moves the base. Nothing expands here:resolve()expands what each setting reads. A missing file is an empty layer; an unreadable or invalid one is the sameConfigErrorresolve()gives.
r = SETTINGS.resolve(Path("config.yaml"), {"MYAPP_LISTEN_PORT": "9090"})
r.values["listen.port"], r.sources["listen.port"] # (9090, "MYAPP_LISTEN_PORT")
# Managed values first, then the local file (layers replace the file, so it's passed too).
payload = Layer(
{"listen": {"port": 9443}}, source="control plane", where="control plane (config v12)"
)
r = SETTINGS.resolve(layers=[payload, SETTINGS.read_file()])
r.sources["listen.port"] # "control plane"Layer(data, source, where, origins={}, expand=False, secret_files=False, base_dir=None): values from one place, a frozen dataclass.
-
data: a YAML-shaped tree, whatyaml.safe_loadgives for a config file.resolve()reads it, and never changes or keeps it. -
source: whatResolved.sourcessays for a value from this layer ("file","control plane"). -
where: what an error names for a value from this layer ("/etc/myapp/config.yaml","control plane (config v12)"). -
origins: dotted key to a finerwherefor that key's value ({"listen.port": "payload.listen.port"}); an error uses it beforewhere. -
expand: whether${VAR}in the values settings read from this layer expands, as described underSettings(expand=...).read_file()sets it to the project'sexpand(a section's_FILEexpands with it too), and is the only place interpose sets it. Never set it on a layer built from a remote payload, such as a control plane's: it would let the sender read the process's environment (Security). -
secret_files: whether a section in this layer may hold{secret_file: <path>}leaves. Without it, such a leaf is aConfigErrornaming the section path and saying that secret files are read only from local configuration, and the file is never opened.read_file()sets it, as does a section's_FILE. Never set it on a layer built from a remote payload: it would let the sender make the process read any file it can (Security, 0009, decision E). -
base_dir: aPath, the directory a relativesecret_filepath in this layer resolves against;None, the default, means none, and a relative path is then aConfigError(x.token: secret_file must be absolute here: this configuration has no file to resolve secrets/token against (from <where>)), the file never opened.read_file()sets it to the config file's directory. A project that builds a layer from a file it read itself sets it to that file's directory. It is used as given, never resolved through symlinks, never checked to exist, and reads nothing. A layer withoutsecret_filesrefuses every leaf first, whateverbase_diris (0010).
Resolved(values, sources, warnings=()): frozen, read-only mappings keyed by setting key, in declaration order, and the warnings.
-
values[key]: the typed value, aValue(str | int | bool | tuple[str, ...] | Secret | Section | tuple[Section, ...]). -
sources[key]:"<PREFIX><KEY>_FILE","<PREFIX><KEY>","file"(or, withlayers, the layer'ssource) or"default". For a value read under an alias, the alias's variable ("MYAPP_PORT","MYAPP_PORT_FILE"), or the layer'ssource. -
warnings: a tuple ofConfigWarning, in the order found: each layer's unknown keys and set aliases in document order, then unknown prefixed variables and set alias variables by name. Empty for a clean configuration. interpose never logs, prints or raises them: log each at WARNING on startup and on every reload, or raise if you want strictness (0009). -
Typed accessors
str(key),int(key)(never abool),bool(key),list(key)(atuple[str, ...]),secret(key)(aSecret),section(key)(aSection) andsections(key)(a list section'stuple[Section, ...]) give a value with its type instead of acast(). An empty list section and an empty list setting are told apart by their declaration. Each raisesTypeErrornaming the key when the setting is another kind; on a secret setting, every one butsecret()does, so a masked"********"can't reach a credential. A key that isn't declared is aKeyError.
r.str("log_level") # "INFO"
r.int("listen.port") # 9090
r.secret("upstream.token").reveal()
# TypeError: 'upstream.token' is a secret setting, not str: use secret() and reveal()
r.str("upstream.token")Section(mapping=None): a section setting's value, a read-only Mapping[str, ...], deep-frozen: nested mappings are Sections, lists are tuples, scalars are as YAML loaded them (str, int, float, bool, date, datetime, None), and secret leaves are Secrets. Nothing in it can be changed at any depth.
- Hashable, and equal only to another
Sectionwith the same keys and values of the same types:1,Trueand1.0differ, and aSectionis never equal to adict(comparedict(section)for that). Key order doesn't matter. A float NaN (.nan) equals any other NaN in aSection, of any sign or payload, though Python'snan != nan, so a section holding one equals itself on reload. A reload compares old and new by equality. -
repr()masks everySecretin it. -
Section(mapping)freezes a mapping of that shape, for a check's replacement; a non-str key, or a value of another type, is aTypeError, and a mapping or list that holds itself is aValueError. It reads no files:{secret_file: ...}means a secret leaf only in configuration.
SETTINGS = Settings(
"MYAPP_",
(
Setting("middleware.config", "", "Per-middleware options.", is_section=True),
Setting("reverse", "", "Reverse-proxy listeners.", is_section=True, is_list=True),
),
default_path=Path("/etc/myapp/config.yaml"),
)
r = SETTINGS.resolve()
auth = r.section("middleware.config").get("auth", Section()) # a Section
for listener in r.sections("reverse"): # each a Section
start_listener(listener["listen"], listener["upstream"])ConfigWarning(kind, name, message): something resolution ignored, a frozen dataclass; str() gives message.
-
kind:"unknown"for a key in a layer that is neither a setting's key nor a parent of one, or a variable that starts with the prefix but that no setting reads (its variable, its_FILE,<PREFIX>CONFIG,extra_env, an alias's variables, a list section's indexed variables), or a key in an item of a list section with fields that is no field (unknown field 'schedule[1].cronn' in /etc/myapp/config.yaml: did you mean 'schedule[1].cron'?)."alias"for an alias that is set, in a layer or as a variable (an alias's indexed variables too), whether its value was used or shadowed by a higher level."ignored"for a list section's indexed variable past the end of its list:ignored variable 'MYAPP_SCHEDULE_3_CRON' in the environment: the list 'schedule' ends before it, as no MYAPP_SCHEDULE_2_* is set. -
name: the key (dotted, at its first unknown level:listne, not each key below it; a non-string YAML key by its YAML text,trueforon:) or the variable; for an alias, the alias or its variable. -
message: one line naming the key or variable and where, with the closest current name when one is close enough (difflib, cutoff 0.75), never a value:unknown key 'listen.prot' in /etc/myapp/config.yaml: did you mean 'listen.port'?,unknown variable 'MYAPP_LISTEN_PROT' in the environment: did you mean 'MYAPP_LISTEN_PORT'?. For a layer, where isorigins[key]when present, else itswhere. An alias warning names the old name and the current one:'port' (in /etc/myapp/config.yaml) is the old name of 'listen.port': rename it,'MYAPP_PORT' (in the environment) is the old name of 'MYAPP_LISTEN_PORT': rename it. Suggestions never offer an alias.
ConfigError: the configuration can't be read, or a value in it is invalid. The message names the key, the expected type and where the value came from (the variable, default, or for a value from the file or a layer its path or where: listen.port: expected int, got 'x' (from /etc/myapp/config.yaml)), and never holds a secret setting's value. For a section, it names the path inside it (reverse[1].auth, middleware.config.true for a non-string key, schedule[2].cron for a field) and types, never values, and never a secret leaf's contents.
Checks, for check=. Each raises ValueError, which resolve() turns into ConfigError("<key>: <reason>"), followed by (from <where>), after a space, when the value came from the file or a layer; one_of also replaces the value:
| Check | Passes |
|---|---|
port |
an int from 1 to 65535 |
positive |
an int above 0 |
non_negative |
an int, 0 or more |
one_of(*choices) |
a str equal to one of choices, in any case; the value becomes the declared spelling (info → INFO) |
Secret(value: str): a secret value.
-
str(),repr(),format()and f-strings give********(MASK);reveal()gives the value. - Equal to another
Secretwith the same value (compared in constant time), never to a plainstr. Hashable. Falsy when the value is empty. - Raises
TypeErrorfor anything but astr.
read_secret_dir(directory: Path) -> Mapping[str, Secret]: each regular file in directory (symlinks followed) as a Secret, keyed by file name, minus one trailing newline. Dot-entries are skipped, which covers a Kubernetes secret volume's ..data. A missing directory gives an empty mapping. Raises ConfigError if the directory or a file can't be read.
MASK: "********".
Prometheus text exposition (format 0.0.4). Every label takes its values from a declared set, so the series count is fixed by code and configuration, never by traffic.
Label(name, values=()): a label name and the only values it may take.
-
name: matches[a-zA-Z_][a-zA-Z0-9_]*, not starting with__. -
values: an iterable ofstr(a list, not a bare string:Label("kind", "ca")is aTypeError); read back as afrozenset[str].value in labeltests one. -
declare(*values): allow more values, from configuration (the middleware names a reload brings), never from measured data. Values are never removed. OneLabelcan be shared by several metrics, so onedeclarecovers all of them.
Counter(name, help, labels=()): inc(*values, amount=1.0). amount must be finite and zero or more.
Gauge(name, help, labels=(), read=None): set(value, *values), add(amount, *values). With read (a callable returning {label values: number}), the samples come from it when the gauge renders.
Histogram(name, help, buckets, labels=()): observe(value, *values). Classic cumulative buckets (inclusive), plus _sum and _count; +Inf is added for you. Buckets are finite, distinct and sorted for you. NaN is refused. samples() gives each label set's count.
-
labelsis a sequence ofLabel. Each call passes one value per label, in order. A value outside its label's set, or the wrong number of values, raisesValueError, which names the metric and the label but never the value. - Declaration errors are
ValueError(orTypeErrorfor a label that isn't aLabel): a bad metric or label name, a repeated label name, a histogram label calledle, bad buckets.
Registry(metrics=()): the metrics one endpoint exports.
-
register(metric) -> metric: adds it and returns it. A second metric with the same name, or one whose samples clash with another's (a counterfoo_countnext to a histogramfoo), raisesValueError. - Iterates in registration order;
len()counts the metrics. -
render() -> str: every metric's# HELP,# TYPEand samples (sorted by label values), joined by newlines and ending in one;""when empty.
CONTENT_TYPE: "text/plain; version=0.0.4; charset=utf-8", for an HTTP response's Content-Type.
from interpose.metrics import CONTENT_TYPE, Counter, Label, Registry
OUTCOME = Label("outcome", ["ok", "denied", "error"])
REGISTRY = Registry()
CALLS = REGISTRY.register(Counter("myapp_calls_total", "Calls, by outcome.", (OUTCOME,)))
CALLS.inc("ok")
CALLS.inc("example.org") # ValueError: label 'outcome' got a value outside its declared set
body = REGISTRY.render() # serve with CONTENT_TYPE, or as a fileinterpose doesn't serve the text: an HTTP endpoint or a virtual file is the project's.
The public API follows semantic versioning: breaking changes only in a major release (a minor one before 1.0), and they're announced in the CHANGELOG.
Everything above is stable within 0.1.x. The message text of ConfigError and ValueError is for people: match on the exception type, not the words.
interpose | Shared mechanism for metalayerlab interceptors — a policy-configurable async middleware pipeline, the layered settings loader, Secret values, and Prometheus text exposition.
GitHub • Releases • Issues • Changelog • Contributing
This wiki is generated from docs/wiki/ on every push to main: edits made here are overwritten. Change it with a pull request.
- 🧅 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
0for a YAML0.5 - 🧭 Where each value came from, for every setting
- 🔒
Secret: masked in logs, reprs and errors - 📈 Prometheus text with labels from declared value sets