App Config (your own settings in drakkar.yaml)¶
Business logic needs configuration too — thresholds, service URLs, feature
switches, credentials for the systems your hooks call. Without framework
support that config ends up in a second file with its own loader, its own
env-var story, and its own validation bugs. This feature removes the
second file: you declare your settings as an ordinary Pydantic model, and
the framework delivers a validated instance from the same
drakkar.yaml the worker already reads — the reserved top-level app:
section — with your own env-var prefix and the framework’s exact
precedence:
model defaults → YAML app: section → env vars (with __ nesting)
Validation is fail-fast at startup: a typo’d value stops the worker with a
clear error instead of surfacing mid-pipeline. The loaded instance is
available as self.app_config in every hook — including on_startup —
and appears in the Debug UI’s config reference as one more group, with
secrets masked.
Declaring the model¶
Two class attributes opt a handler in:
from pydantic import BaseModel, Field, SecretStr
from drakkar import BaseDrakkarHandler
class AppConfig(BaseModel):
priority_threshold: int = Field(
default=10,
description='Tasks scoring above this are prioritized.',
)
scoring_url: str = 'http://localhost:9000/score'
api_key: SecretStr = SecretStr('')
class MyHandler(BaseDrakkarHandler[MyInput, MyOutput]):
app_config_model = AppConfig # opt-in: the framework loads it at startup
app_env_prefix = 'MYAPP_' # your env namespace (default: 'APP_')
async def arrange(self, messages, pending):
if self.app_config.priority_threshold > 0:
...
self.app_config is the validated AppConfig instance — typed, with
defaults applied. It is None only when the handler declares no
app_config_model.
Field(description=...) is worth writing: descriptions, types, and
defaults all surface in the Debug UI’s config reference exactly like the
framework’s own fields.
The app: section¶
# drakkar.yaml — framework sections and your section, one file
kafka:
source_topic: input-events
app:
priority_threshold: 20
scoring_url: "http://scoring-service:9000/score"
The framework passes the section through unvalidated — its shape is entirely yours; only your model judges it. Nested models nest as YAML mappings, exactly like framework sections.
If the YAML carries a non-empty app: section but the handler declares no
app_config_model, the worker logs an app_config_ignored warning at
startup — a typo’d setup is loud, not silent.
Env-var overrides¶
Your prefix, the framework’s syntax — __ between nesting levels:
MYAPP_PRIORITY_THRESHOLD=20 # app.priority_threshold
MYAPP_SCORING_URL=http://scoring:9000 # app.scoring_url
MYAPP_API_KEY=s3cret # app.api_key
MYAPP_SCORING__TIMEOUT_SECONDS=15 # nested: app.scoring.timeout_seconds
Values are strings in the environment; your model’s types coerce them
("20" → int, "true" → bool), like the framework config.
DK_APP__* is rejected. The DK_ namespace belongs to the framework,
and the app: section does not: setting any DK_APP__* variable fails
startup with a message naming the offending variables and pointing at your
handler’s own prefix. Likewise, app_env_prefix itself must not start
with DK_ — the loader refuses it.
Secrets¶
Two conventions mark a field as secret; the Debug UI masks either:
SecretStr— the Pydantic type. Also protects against accidentalrepr()/log leaks in your own code, so prefer it for new fields.json_schema_extra={'drakkar_secret': True}on a plain field — the marker convention the framework’s own config uses (for fields like DSNs where the code needs the raw string).
class AppConfig(BaseModel):
api_key: SecretStr = SecretStr('')
webhook_url: str = Field(default='', json_schema_extra={'drakkar_secret': True})
In the config reference, a non-empty secret value renders as ••••••
(same mask as framework secrets); an empty one stays visibly empty so
“never configured” is distinguishable from “configured and hidden”.
The config reference group¶
GET /api/v1/config-reference — the Configs tab in the Debug UI — gains
one more group, Application, built at runtime from your model: one entry
per field (nested models walked recursively) with path (app.scoring.url),
env name (MYAPP_SCORING__URL), type, description, default, live value,
and an is_default flag, secrets masked. The group is simply absent when
no model is declared.
Standalone loading¶
The same loader is public, for scripts and tools that need the app config without a running worker:
from drakkar import load_app_config
app_config = load_app_config(AppConfig, '/etc/drakkar/drakkar.yaml', env_prefix='MYAPP_')
Path resolution mirrors load_config: explicit argument → the
DK_CONFIG env var → env-only (defaults + your env vars, no file). A
section= argument selects a different top-level key than app, and
yaml_data= accepts an already-loaded dict instead of a file.
Notes¶
- Precedence and merge semantics are identical to the framework config — see Configuration Loading.
- The
app:section never appears in the one-line config summary the worker logs at startup. - The reserved
app:section is part of the config contract, with its own model binding, so a mixed fleet shares one YAML layout.