User Guide¶
Overview¶
chumicro-config is how ChuMicro libraries read their settings on a device. Apps read the deployed runtime_config.msgpack once with load_runtime_config(), then hand the whole config to each consuming library. Each library pulls its own prefix's keys (wifi.*, mqtt.broker.*, …) off the shared dict.
Most consumer libraries (mqtt, ntp, requests, websockets, http_server) read their slice with plain config.get("<prefix>.<key>", <default>) calls inside their <Name>Config.from_config classmethod. When a library's constructor signature maps 1:1 onto a flat-prefix subkey set, load_section packages the boilerplate; chumicro-wifi uses it today.
Every library reads its keys using the same <prefix>.<subkey> dotted-key layout, so the on-wire shape stays uniform regardless of which API a library uses to parse it.
Getting started¶
In an app, the read is one line:
from chumicro_config import load_runtime_config
from chumicro_wifi import WifiConfig, WifiService
config = load_runtime_config() # /runtime_config.msgpack
wifi = WifiService(WifiConfig.from_config(config)) # reads + types wifi.* keys
load_runtime_config() opens /runtime_config.msgpack (the default path, chumicro_config.runtime.DEFAULT_RUNTIME_CONFIG_PATH), msgpack-decodes it, and returns a RuntimeConfig, a thin lookup wrapper over the flat-key payload. Every chumicro library reads its own prefix's keys (wifi.ssid, mqtt.broker.host, …) from that one object.
Writing a from_config for your own library¶
Two shapes, picked by what's being constructed. The picker rule:
- Pattern A, value-object. When every
__init__kwarg maps 1:1 from a flat-prefix subkey and nothing else is injected at construction time, useload_section. This is the WifiConfig shape. - Pattern B, client-with-injection. When
__init__mixes config-derived kwargs with non-config injectables (sockets, radios, TLS contexts, listeners, factories, event handlers) or has call-site logic (mode-conditional sub-key reads, broker-required guards, half-TLS guards, computed defaults), skipload_sectionand read keys directly withconfig.get/config.require. This is the shape used by mqtt, ntp, requests, websockets, http_server.
Reading both __init__ and the planned from_config side-by-side usually settles which shape applies. When in doubt, start with Pattern A; if you find yourself adding non-config kwargs or call-site ifs, that's the signal to flip to Pattern B.
Pattern A, value-object (load_section)¶
When the constructor's keyword arguments line up exactly with a flat-prefix subkey set, load_section cuts the boilerplate down to ~10 lines:
from chumicro_config import load_section
class WifiConfig:
def __init__(self, ssid, password, hostname=None, connect_timeout_ms=15_000):
self.ssid = ssid
self.password = password
self.hostname = hostname
self.connect_timeout_ms = connect_timeout_ms
@classmethod
def from_config(cls, config):
return load_section(
cls,
config,
prefix="wifi",
required=("ssid", "password"),
optional={"hostname": None, "connect_timeout_ms": 15_000},
)
load_section does four things:
- Asserts
configis aRuntimeConfigor plain dict, raisingInvalidConfigTypeotherwise. - For each
requiredsubkey, pullsconfig[f"{prefix}.{subkey}"], raisingMissingConfigKeyif any is absent. - For each
optionalsubkey, pullsconfig[f"{prefix}.{subkey}"]if present, else uses the default. - Calls
cls(**kwargs)and returns the instance. Each kwarg name is the bare subkey (no prefix), soWifiConfiggetsssid=…,password=…, etc.
Unknown keys are ignored, which is deliberate forward-compat. An older library version reads a config file that has newer libraries' keys in it without exploding on the unfamiliar prefixes.
There is no type coercion. "1883" stays a string; the __init__ does any conversion the library wants.
Pattern B, client-with-injection (direct config.get)¶
When the class being constructed is a runner / client / service that takes both config-derived fields and non-config injectables (or has per-class guards), don't wrap load_section; it has no slot for the injectables and no hook for the guards. Read keys directly:
class NTPClient:
@classmethod
def from_config(cls, config, *, socket=None, ticks=None):
server = config.get("ntp.server", "pool.ntp.org")
port = config.get("ntp.port", 123)
timeout_ms = config.get("ntp.timeout_ms", 5_000)
return cls(
server=server, port=port, timeout_ms=timeout_ms,
socket=socket, ticks=ticks,
)
No upfront isinstance guard is required. Some Pattern B factories add a one-line hasattr(config, "get") check that raises the library's own error (MQTTClient.from_config raises ValueError); the rest let a None / str / int fail on the first .get(...) with AttributeError, clear enough at the call site to diagnose.
Pattern B's freedom to mix is exactly its point: MQTTClient.from_config reads mqtt.broker.host and takes an injectable socket factory and enforces a broker-required guard; trying to express any of that through load_section's required / optional mapping bends it into a less-readable shape than the inline reads.
Soft loading with try_load_section¶
load_section raises when required keys are missing, the strict path most libraries want. When a section is genuinely optional (e.g. an MQTT section the user may not configure if the app doesn't publish), use try_load_section:
from chumicro_config import try_load_section
@classmethod
def try_from_config(cls, config):
return try_load_section(
cls,
config,
prefix="mqtt",
required=("broker",),
optional={"port": 1883, "client_id": None},
)
try_load_section returns None whenever load_section would raise: config is None, config is the wrong type, or any required key is absent. Treat a None return as "this section wasn't configured; skip the feature." Callers that want to distinguish "no config at all" from "partial config with a missing key" call load_section directly and catch MissingConfigKey.
Exception handling¶
Three classes, one base:
| Exception | Raised when |
|---|---|
ConfigError |
Base; catch this to handle every config failure uniformly. |
MissingConfigKey |
A required key wasn't in the config. |
InvalidConfigType |
config itself wasn't a RuntimeConfig / dict (caller passed the wrong shape). |
from chumicro_config import ConfigError, MissingConfigKey
try:
wifi = WifiConfig.from_config(config)
except MissingConfigKey as error:
print(f"Add wifi.* keys to your config: {error}")
raise
except ConfigError:
raise # let it propagate; logs higher up
MissingConfigKey and InvalidConfigType are single-inheritance only. They do not also subclass KeyError or TypeError. MicroPython rejects multiple inheritance from Exception subclasses with differing memory layouts, so the natural CPython idiom (class MissingConfigKey(ConfigError, KeyError)) doesn't load on device. Catch via ConfigError if you want broad handling.
On-device config shape¶
The runtime config is one msgpack file at /runtime_config.msgpack. Its on-device shape is flat with dotted keys, one entry per <prefix>.<subkey>:
{
"wifi.ssid": "HomeNet",
"wifi.password": "secret",
"wifi.hostname": "back-porch",
"mqtt.broker.host": "mqtt.local",
"mqtt.broker.port": 1883,
"ntp.server": "pool.ntp.org",
"app.sample_period_ms": 5000,
}
On device, load_runtime_config() reads this file back and returns a RuntimeConfig that behaves like a flat dict: config["wifi.ssid"], "mqtt.broker.host" in config, etc.
How the workspace tool produces it¶
You can ignore this section if you're building the msgpack file yourself. If you're using chumicro-workspace, it composes the file from per-library TOML templates whose source shape is nested for human readability:
# project_config.toml, what the user edits.
[wifi]
ssid = "HomeNet"
password = "secret"
hostname = "back-porch"
[mqtt.broker]
host = "mqtt.local"
port = 1883
[ntp]
server = "pool.ntp.org"
[app]
sample_period_ms = 5000
At deploy time the workspace tool flattens nested sections to dotted keys, merges per-library defaults, and msgpack-encodes the result using msgpack(use_single_float=True) for wire-compatibility with chumicro-msgpack on the device side.
Platform notes¶
Works identically on CPython, MicroPython, and CircuitPython. Only dependency: chumicro-msgpack (for the runtime-config decode path).
Examples¶
| Example | What it shows |
|---|---|
examples/end_to_end.py |
Both patterns (<Name>Config.from_config() for library authors, multi-section app wiring for users) plus MissingConfigKey / InvalidConfigType error handling. Runs on every runtime; no device or runtime_config.msgpack needed. |