Configuration

Flexible configuration management with multi-format support, cascading search, and type-checked access.

TL;DR

from kstlib.config import ConfigLoader

# Load with auto-discovery (recommended)
config = ConfigLoader().config

# Access with dot notation
print(config.app.name)
print(config.database.host)
# Export default config to customize
kstlib config export --out kstlib.conf.yml

Key Features

  • Multi-format support: YAML, TOML, JSON, and INI

  • Cascading search: Automatic discovery across multiple locations

  • Include system: Compose configs from multiple files

  • Deep merge: Intelligent merging of nested configurations

  • Dot notation: Easy access to nested values via Box

  • Type safety: Full type hints for IDE support

Quick Start

# kstlib.conf.yml
app:
  name: "My Application"
  debug: true

database:
  host: "localhost"
  port: 5432
from kstlib.config import load_from_file

# 1. Load from specific file
config = load_from_file("kstlib.conf.yml")

# 2. Or use auto-discovery
from kstlib.config import ConfigLoader
config = ConfigLoader().config

# 3. Access values with dot notation
print(config.app.name)       # "My Application"
print(config.database.port)  # 5432

How It Works

Loading Strategies

Cascading mode (recommended) searches multiple locations in order:

config = ConfigLoader().config

Search order (priority from highest to lowest):

  1. Current working directory (./kstlib.conf.yml)

  2. User’s home directory (~/kstlib.conf.yml)

  3. User’s config directory (~/.config/kstlib.conf.yml)

  4. System-wide config dirs via platformdirs.site_config_dir:

    • Linux: /etc/xdg/kstlib/ plus every entry in $XDG_CONFIG_DIRS

    • macOS: /Library/Application Support/kstlib/

    • Windows: %PROGRAMDATA%/kstlib/

  5. Package defaults (lowest priority)

System-wide entries are merged silently. If a file does not exist at a given location, it is skipped without warning or error. This lets operators drop a shared kstlib.conf.yml in /etc/xdg/kstlib/ (or the platform equivalent) while users override individual keys in their home directory.

See System-Wide Configuration below for the full per-OS defaults and the rules that apply when XDG_CONFIG_DIRS lists several directories.

Direct mode loads from a specific file:

config = load_from_file("path/to/config.yml")

Environment variable mode loads from a path in an env var:

# Uses CONFIG_PATH env var by default
config = ConfigLoader(auto_source="env").config

# Or specify a different env var name
config = ConfigLoader(auto_source="env", auto_env_var="MYAPP_CONFIG_FILE").config

System-Wide Configuration

System-wide configuration lets operators ship shared defaults (corporate endpoints, logging targets, TLS settings, audit rules, …) from a location that every user of a machine inherits automatically. Users keep their own ~/.config/kstlib.conf.yml and the cascade merges the two without any manual wiring.

Under the hood, kstlib delegates path discovery to platformdirs.site_config_dir, so the behavior matches every other well-behaved XDG-aware tool.

Default paths per OS

Platform

Default system config path

Linux / BSD

/etc/xdg/kstlib/kstlib.conf.yml

macOS

/Library/Application Support/kstlib/kstlib.conf.yml

Windows

%PROGRAMDATA%\kstlib\kstlib.conf.yml (typically C:\ProgramData\kstlib\kstlib.conf.yml)

Note

On Windows and macOS, writing to these paths usually requires administrator or root privileges. That is intentional: system-wide config is meant to be provisioned by an operator or a configuration management tool (Ansible, Puppet, Chef, Intune, …), not hand-edited by end users.

Linux: XDG_CONFIG_DIRS semantics

On Linux and other XDG-compliant Unices, kstlib honors the standard XDG Base Directory Specification. The XDG_CONFIG_DIRS environment variable takes a colon-separated list of directories, ordered from highest to lowest priority:

# /etc/corp/kstlib wins over /etc/xdg/kstlib for common keys
export XDG_CONFIG_DIRS=/etc/corp:/etc/xdg

If XDG_CONFIG_DIRS is unset, kstlib falls back to the single default /etc/xdg/kstlib/.

Each directory in the list is probed for kstlib.conf.yml. kstlib then deep-merges every file it finds, so keys defined in the higher-priority directory override identical keys in the lower-priority ones, while non-conflicting keys are unioned.

Tip

You can inspect what kstlib will probe on your machine:

python -c "import platformdirs; print(platformdirs.site_config_dir('kstlib', appauthor=False, multipath=True))"

Full cascade at a glance

From lowest to highest priority (later entries override earlier ones):

1. Package defaults (shipped inside kstlib)
2. System config dirs:
     - $XDG_CONFIG_DIRS entries (Linux, if set)      <- lowest system priority
     - /etc/xdg/kstlib (Linux default, if XDG unset)
     - /Library/Application Support/kstlib (macOS)
     - %PROGRAMDATA%\kstlib (Windows)                <- highest system priority
3. ~/.config/kstlib.conf.yml
4. ~/kstlib.conf.yml
5. ./kstlib.conf.yml (cwd)                           <- highest overall
6. Runtime kwargs (supersede every file source)

Missing files at any level are skipped silently. This is the whole point: deployments can pre-provision a system file, and machines that do not have one simply fall through to the next layer.

Example: corporate baseline + user overrides

# /etc/xdg/kstlib/kstlib.conf.yml (shipped by IT)
logger:
  defaults:
    output: file
    rotation: daily
alerts:
  channels:
    slack:
      webhook_url: https://hooks.slack.com/services/OPS/CORP/XXXXX
# ~/.config/kstlib.conf.yml (written by the developer)
logger:
  defaults:
    level: DEBUG  # Adds to the corporate baseline

The effective configuration for this user is:

logger:
  defaults:
    output: file       # from system
    rotation: daily    # from system
    level: DEBUG       # from user (added)
alerts:
  channels:
    slack:
      webhook_url: https://hooks.slack.com/services/OPS/CORP/XXXXX  # from system

The developer cannot accidentally lose the corporate Slack webhook or the file-rotation policy, but they can still opt into DEBUG locally.

Opting out

System-wide config is always active in cascading mode, but you can bypass it entirely by using direct mode:

from kstlib.config import load_from_file

# Only this file is loaded - no cascade, no system dirs
config = load_from_file("/opt/myapp/isolated.yml")

Or by scoping the loader to an explicit file:

from kstlib.config import ConfigLoader

loader = ConfigLoader(auto_source="file", auto_path="./test.yml")

Include System

Compose configurations from multiple files:

# main.yml
include:
  - database.toml
  - features.json

app:
  name: "My App"

Deep merge behavior:

  • Nested dictionaries are recursively merged

  • Lists are replaced (not merged)

  • Later values override earlier ones

Warning

Override priority matters! Values are merged left-to-right with later sources overwriting earlier ones:

package defaultsuser config fileincludeskwargs

This means a value in your config file will override package defaults, and kwargs passed at runtime will override everything else.

Example: If package defaults set app.debug: false and your config file has app.debug: true, the final value is true. If you then pass debug=False as a kwarg, it becomes False again.

Supported Formats

Format

Extensions

Notes

YAML

.yml, .yaml

Recommended, supports comments

TOML

.toml

Good for hierarchical data

JSON

.json

Strict, no comments

INI

.ini

Legacy support

Caching

Config is cached after first load:

from kstlib.config import get_config, clear_config

config = get_config()        # Cached config (fast)
config = get_config(max_age=0)  # Force reload
clear_config()               # Clear cache entirely

Interactive usage (Jupyter / REPL)

The config singleton is intentionally cached: services that run for hours should not re-read the YAML files on every access. In interactive sessions though, you often edit the config and want the change to take effect immediately, without restarting the kernel.

Warning

If you edit a kstlib.conf.yml file (for example /etc/xdg/kstlib/kstlib.conf.yml, ~/.config/kstlib.conf.yml, or the one in your current working directory) while a Python session is running, call reload_config() to force a refresh. Without this, the singleton cache keeps the old values and get_config() will continue to return the stale Box.

reload_config() is the explicit, discoverable alias for “flush the cache and re-read from disk”. It is equivalent to clear_config() followed by get_config(), but expresses the intent in a single call.

from kstlib.config import reload_config

# ... you just edited ~/.config/kstlib.conf.yml in another window ...
cfg = reload_config()
print(cfg.mail.default)  # reflects the edit

It is also available at the top level, consistent with get_config and clear_config:

import kstlib

cfg = kstlib.reload_config()

When to use which:

Call

Purpose

reload_config()

One-shot refresh in interactive work. Clearest intent.

get_config(force_reload=True)

Same behaviour, but the intent is hidden in a kwarg.

clear_config()

Only flushes the cache. The next get_config() call reloads. Useful in tests that want to isolate the cache boundary explicitly.

Note

Known issue fixed in 2.3.1: on kstlib 2.3.0, importing kstlib.mail as the very first kstlib symbol in a fresh Python process (for example right after Restart Kernel in Jupyter) could raise ImportError due to a circular import between kstlib.limits and kstlib.config.loader. Affected versions: 2.3.0 only. Workaround for users still on 2.3.0: import kstlib.config before the first from kstlib.mail import .... Upgrading to 2.3.1 or later removes the need for the workaround.

Configuration

CLI Export

Bootstrap configuration files from package defaults:

# Export full default config
kstlib config export --out kstlib.conf.yml

# Export specific section
kstlib config export --section secrets --out secrets.yml

# Preview to stdout
kstlib config export --stdout

Environment-Based Structure

Recommended project layout:

myapp/
├── config/
│   ├── base.yml          # Defaults (committed)
│   ├── development.yml   # Dev overrides
│   ├── production.yml    # Prod overrides
│   └── secrets.yml       # Local secrets (gitignored)
└── src/
# config/base.yml
app:
  name: "My Application"
  debug: false
  log_level: INFO

database:
  pool_size: 10
  timeout: 30
# config/development.yml
include: base.yml

app:
  debug: true
  log_level: DEBUG

database:
  host: localhost

Strict Format Mode

Enforce format consistency (all includes must match parent format):

config = load_from_file("config.yml", strict_format=True)

Default Configuration

The package ships with sensible defaults. Export to customize:

kstlib config export --out kstlib.conf.yml

Note

Partial override only: You do not need to copy the entire default configuration. The system deep-merges your config with package defaults, so you only specify what you want to change:

# Minimal user config - only override what you need
logger:
  defaults:
    output: file  # Everything else uses package defaults

cache:
  default_strategy: lru

This keeps your config clean and maintainable. For larger projects, you can also split your config into multiple files using the include: directive.

View default configuration
   1# Default configuration for kstlib
   2# This file is used to set default values for the kstlib library.
   3
   4###########################################################################################
   5## Internal kstlib behavior (opt-in switches controlling library-side features)
   6###########################################################################################
   7kstlib:
   8  # Internal logging activation
   9  # ---------------------------
  10  # Controls whether kstlib's own log records (from kstlib.auth, kstlib.mail,
  11  # kstlib.rapi, ...) are emitted when a consumer never calls init_logging()
  12  # explicitly. Disabled by default so applications embedding kstlib stay
  13  # silent unless they opt in.
  14  #
  15  # When enabled, the first call to get_logger() reads this section and
  16  # triggers init_logging(preset=...) transparently. Any error (missing
  17  # config file, parse failure, etc.) is swallowed silently so kstlib can
  18  # never break the host application through this cascade.
  19  logging:
  20    enabled: false   # Opt-in: set to true to activate internal kstlib logs
  21    preset: prod     # Preset used by auto-init: dev, prod, debug, trace, ...
  22                     # Unknown presets fall back to "prod" with a one-line
  23                     # stderr notice listing available names.
  24
  25    # Per-logger level override (optional, default = no filtering).
  26    #
  27    # When set, each entry calls logging.getLogger(name).setLevel(level)
  28    # right after the kstlib root logger is registered. Useful to silence
  29    # the noise of a specific sub-package (e.g. RAPI config loader emitting
  30    # 1300+ DEBUG lines per startup) without dropping the level globally.
  31    #
  32    # Cascade (lowest to highest priority):
  33    #   1. kstlib.logging.modules               (this section, global default)
  34    #   2. logger.presets.<active>.modules      (preset-specific override)
  35    #   3. CLI flag --log-module                (repeatable, REPLACES YAML;
  36    #                                            three syntaxes, see below)
  37    # Steps 1 and 2 are merged key-by-key; preset wins on shared keys.
  38    #
  39    # Convention: every kstlib logger follows kstlib.<sub-package>[.<module>],
  40    # mirroring the layout of src/kstlib/. Examples:
  41    #   kstlib.rapi, kstlib.rapi.client, kstlib.rapi.config
  42    #   kstlib.transform, kstlib.transform.chain
  43    #
  44    # Available sub-packages:
  45    #   kstlib.alerts, kstlib.auth, kstlib.cache, kstlib.cli, kstlib.config,
  46    #   kstlib.db, kstlib.helpers, kstlib.logging, kstlib.mail, kstlib.metrics,
  47    #   kstlib.monitoring, kstlib.ops, kstlib.pipeline, kstlib.rapi,
  48    #   kstlib.resilience, kstlib.secrets, kstlib.secure, kstlib.ssl,
  49    #   kstlib.transform, kstlib.ui, kstlib.utils, kstlib.websocket
  50    #
  51    # For the full module list, browse src/kstlib/ or the Sphinx
  52    # "Logging introspection guide".
  53    #
  54    # Supported levels (see Sphinx "Logging introspection guide"):
  55    #   TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL
  56    # Names are case-insensitive. Invalid entries are skipped with a
  57    # WARNING (and WARNING [SECURITY] for names not starting with kstlib.).
  58    #
  59    # Defaults: kstlib.rapi.config and kstlib.config.loader are the
  60    # most verbose loggers at startup (1300+ DEBUG/TRACE lines for
  61    # rapi.config on a typical Viya install, large cascade trace for
  62    # config.loader when many includes are present). Both muted to
  63    # WARNING by default; raise them locally when you need to debug.
  64    #
  65    # Module mutes are PERSISTENT by design: -v/-vv/-vvv and --log-level
  66    # only adjust the root handler level. They do NOT reset these
  67    # mutes, so noisy modules stay quiet even under -vvv.
  68    #
  69    # Override surfaces (priority increasing):
  70    #   - user kstlib.conf.yml: modules: {kstlib.rapi.config: DEBUG}
  71    #     (deep merge with these defaults, override per-module)
  72    #   - preset-specific: presets.<name>.modules: {...}
  73    #   - CLI --log-module: per-invocation override, three syntaxes
  74    #     (1) classic, fully-qualified
  75    #         --log-module kstlib.rapi.config=TRACE
  76    #     (2) classic, prefix omitted (kstlib. auto-prepended)
  77    #         --log-module rapi.config=TRACE
  78    #     (3) inverse, level groups module list
  79    #         --log-module DEBUG=foo.bar,baz.qux
  80    modules:
  81      kstlib.rapi.config: WARNING
  82      kstlib.config.loader: WARNING
  83
  84###########################################################################################
  85## Datetime formatting (global settings for timestamp display)
  86###########################################################################################
  87datetime:
  88  # Format string for timestamps (pendulum format tokens)
  89  # See: https://pendulum.eustace.io/docs/#tokens
  90  # Common formats:
  91  #   - "YYYY-MM-DD HH:mm:ss" (ISO-like, default)
  92  #   - "DD/MM/YYYY HH:mm:ss" (European)
  93  #   - "MM/DD/YYYY hh:mm:ss A" (US with AM/PM)
  94  #   - "ddd D MMM YYYY HH:mm" (Human: "Mon 29 Jan 2026 15:30")
  95  # Hard limit: max 64 chars, alphanumeric + common punctuation only
  96  format: "YYYY-MM-DD HH:mm:ss"
  97
  98  # Timezone for display: "local" (system timezone) or IANA timezone name
  99  # Examples: "local", "UTC", "Europe/Paris", "America/New_York"
 100  # Hard limit: max 64 chars, validated against pendulum timezones
 101  timezone: "local"
 102
 103###########################################################################################
 104## Cache configuration
 105###########################################################################################
 106cache:
 107  # Default caching strategy (ttl | lru | memoize | file)
 108  default_strategy: ttl
 109
 110  # TTL (Time-To-Live) cache settings
 111  ttl:
 112    default_seconds: 300 # 5 minutes
 113    max_entries: 1000 # Maximum number of cached entries
 114    cleanup_interval: 60 # Cleanup expired entries every 60s
 115
 116  # LRU (Least Recently Used) cache settings
 117  lru:
 118    maxsize: 128 # Maximum cache size
 119    typed: false # Separate cache for different argument types
 120
 121  # File-based cache settings
 122  file:
 123    enabled: true
 124    cache_dir: ".cache" # Directory for cache files
 125    check_mtime: true # Invalidate cache on file modification
 126    serializer: json # json (default) | pickle | auto
 127    # Maximum cache file size (prevents OOM on corrupted files)
 128    # Accepts: bytes (int) or human-readable string ("100M", "50 MiB")
 129    # Hard limit enforced in code: 100 MiB
 130    max_file_size: "50M"
 131
 132  # Async cache support
 133  async_support:
 134    enabled: true
 135    executor_workers: 4 # ThreadPoolExecutor workers for sync functions
 136
 137  # Metrics and monitoring
 138  metrics:
 139    enabled: false # Track cache hits/misses (opt-in)
 140    log_stats: false # Log statistics periodically
 141    stats_interval: 300 # Log stats every 5 minutes
 142
 143###########################################################################################
 144## Logging configuration
 145###########################################################################################
 146logger:
 147  defaults:
 148    output: console # console | file | both
 149
 150    # Color theme for Rich console output
 151    theme:
 152      trace: "medium_purple4 on dark_olive_green1"
 153      debug: "black on deep_sky_blue1"
 154      info: "sky_blue1"
 155      success: "black on sea_green3"
 156      warning: "bold white on salmon1"
 157      error: "bold white on deep_pink2"
 158      critical: "blink bold white on red3"
 159
 160    # Icons for each log level
 161    icons:
 162      show: true
 163      trace: "🔬"
 164      debug: "🔎"
 165      info: "📄"
 166      success: "✅"
 167      warning: "🚨"
 168      error: "❌"
 169      critical: "💀"
 170
 171    # Console handler settings
 172    console:
 173      level: WARNING  # Log level: TRACE | DEBUG | INFO | SUCCESS | WARNING | ERROR | CRITICAL
 174      datefmt: "%Y-%m-%d %H:%M:%S"
 175      format: "::: PID %(process)d / TID %(thread)d ::: %(message)s"
 176      show_path: true
 177      tracebacks_show_locals: true
 178
 179    # File handler settings
 180    # Two configuration styles are supported:
 181    #   - New style (recommended): file_path: ./logs/kstlib.log
 182    #   - Legacy style: log_path + log_dir + log_name (for backward compatibility)
 183    # The new style takes priority if file_path is defined.
 184    file:
 185      level: WARNING  # Log level: TRACE | DEBUG | INFO | SUCCESS | WARNING | ERROR | CRITICAL
 186      datefmt: "%Y-%m-%d %H:%M:%S"
 187      format: "[%(asctime)s | %(levelname)-8s] ::: PID %(process)d / TID %(thread)d ::: %(message)s"
 188      # file_path: ./logs/kstlib.log  # New style (recommended)
 189      # auto_create_dir: true          # New style auto-create
 190      log_path: "./"                   # Legacy style (kept for backward compatibility)
 191      log_dir: "logs"
 192      log_name: "kstlib.log"
 193      log_dir_auto_create: true
 194
 195    # File rotation settings
 196    rotation:
 197      when: midnight # midnight | S | M | H | D | W0-W6
 198      interval: 1
 199      backup_count: 7
 200
 201  presets:
 202    dev:
 203      output: console
 204      console:
 205        level: DEBUG
 206        show_path: true
 207        tracebacks_show_locals: true
 208      icons:
 209        show: true
 210
 211    prod:
 212      output: file
 213      file:
 214        level: INFO
 215      icons:
 216        show: false
 217
 218    # debug: deeper-than-dev preset for investigation sessions.
 219    # Persists to file (output: both) so logs survive the terminal and can be
 220    # grepped or shared after the fact, and emits at TRACE so HTTP traces and
 221    # detailed diagnostics are captured. Use 'dev' for everyday iteration
 222    # (console-only, DEBUG); use 'debug' when actively investigating an issue.
 223    debug:
 224      output: both
 225      console:
 226        level: TRACE
 227        show_path: true
 228        tracebacks_show_locals: true
 229      file:
 230        level: TRACE
 231        format: "[%(asctime)s | %(levelname)-8s] ::: PID %(process)d / TID %(thread)d ::: [%(filename)s:%(lineno)d %(funcName)s] %(message)s"
 232      icons:
 233        show: true
 234
 235    trace:
 236      output: both
 237      console:
 238        level: TRACE
 239        show_path: true
 240        tracebacks_show_locals: true
 241      file:
 242        level: TRACE
 243        format: "[%(asctime)s | %(levelname)-8s] ::: PID %(process)d / TID %(thread)d ::: [%(filename)s:%(lineno)d %(funcName)s] %(message)s"
 244      icons:
 245        show: true
 246
 247    # Mail trace preset - verbose SMTP/SSL debugging to dedicated file
 248    # Usage: LogManager(preset="trace_mail") or logger.preset: trace_mail
 249    trace_mail:
 250      output: both
 251      console:
 252        level: WARNING  # Keep console quiet
 253      file:
 254        level: TRACE
 255        file_path: ./logs/mail-trace.log
 256        auto_create_dir: true
 257      icons:
 258        show: true
 259
 260###########################################################################################
 261## UI helpers configuration
 262###########################################################################################
 263ui:
 264  panels:
 265    defaults:
 266      panel:
 267        # border_style supports any Rich color/style (e.g. "blue", "bold green")
 268        border_style: "bright_blue"
 269        title_align: "left"
 270        subtitle_align: "left"
 271        padding: [1, 2]
 272        expand: true
 273        highlight: false
 274        # https://rich.readthedocs.io/en/stable/appendix/box.html#appendix-box
 275        box: "ROUNDED"
 276      content:
 277        box: "SIMPLE"
 278        expand: true
 279        show_header: false
 280        key_label: "Key"
 281        value_label: "Value"
 282        key_style: "bold white"
 283        value_style: null
 284        header_style: "bold"
 285        pad_edge: false
 286        sort_keys: false
 287        use_markup: true
 288        use_pretty: true
 289        pretty_indent: 2
 290    presets:
 291      info:
 292        panel:
 293          border_style: "cyan"
 294          title: "Information"
 295          icon: "📘"
 296      success:
 297        panel:
 298          border_style: "sea_green3"
 299          title: "Success"
 300          icon: "✅"
 301      warning:
 302        panel:
 303          border_style: "orange3"
 304          title: "Warning"
 305          icon: "🔔"
 306      error:
 307        panel:
 308          border_style: "red3"
 309          title: "Error"
 310          icon: "❌"
 311      summary:
 312        panel:
 313          border_style: "light_steel_blue1"
 314          title: "Execution Summary"
 315          icon: "📝"
 316        content:
 317          sort_keys: true
 318          key_style: "bold orchid2"
 319          value_style: "dim white"
 320  tables:
 321    defaults:
 322      table:
 323        title: null
 324        caption: null
 325        box: "SIMPLE"
 326        show_header: true
 327        header_style: "bold cyan"
 328        show_lines: false
 329        row_styles: null
 330        expand: true
 331        pad_edge: false
 332        highlight: false
 333      columns:
 334        - header: "Key"
 335          key: "key"
 336          justify: "left"
 337          style: "bold white"
 338          overflow: "fold"
 339          no_wrap: false
 340        - header: "Value"
 341          key: "value"
 342          justify: "left"
 343          style: null
 344          overflow: "fold"
 345          no_wrap: false
 346    presets:
 347      inventory:
 348        table:
 349          title: "Inventory"
 350          box: "SIMPLE_HEAVY"
 351          show_lines: true
 352          header_style: "bold yellow"
 353        columns:
 354          - header: "Component"
 355            key: "component"
 356            style: "bold"
 357            width: 18
 358          - header: "Version"
 359            key: "version"
 360            style: "cyan"
 361            width: 12
 362          - header: "Status"
 363            key: "status"
 364            justify: "center"
 365            style: "bold"
 366            width: 10
 367      metrics:
 368        table:
 369          title: "Metrics"
 370          box: "SIMPLE_HEAD"
 371          header_style: "bold green"
 372        columns:
 373          - header: "Metric"
 374            key: "metric"
 375            style: "bold"
 376          - header: "Value"
 377            key: "value"
 378            justify: "right"
 379  spinners:
 380    defaults:
 381      # Spinner character style: BRAILLE | DOTS | LINE | ARROW | BLOCKS | CIRCLE | SQUARE | MOON | CLOCK
 382      style: "BRAILLE"
 383      # Position relative to message: before | after
 384      position: "before"
 385      # Animation type: spin | bounce | color_wave
 386      animation_type: "spin"
 387      # Seconds between animation frames
 388      interval: 0.08
 389      # Rich style for spinner character
 390      spinner_style: "cyan"
 391      # Rich style for message text (null = default)
 392      text_style: null
 393      # Character shown on success
 394      done_character: "✓"
 395      done_style: "green"
 396      # Character shown on failure
 397      fail_character: "✗"
 398      fail_style: "red"
 399    presets:
 400      minimal:
 401        style: "LINE"
 402        spinner_style: "dim white"
 403        interval: 0.1
 404      fancy:
 405        style: "BRAILLE"
 406        spinner_style: "bold cyan"
 407        interval: 0.06
 408      blocks:
 409        style: "BLOCKS"
 410        spinner_style: "blue"
 411        interval: 0.05
 412      bounce:
 413        animation_type: "bounce"
 414        spinner_style: "yellow"
 415        interval: 0.08
 416      color_wave:
 417        animation_type: "color_wave"
 418        interval: 0.1
 419
 420###########################################################################################
 421## Mail configuration
 422###########################################################################################
 423mail:
 424  # Default named preset used when MailBuilder() is instantiated without
 425  # transport= or preset=. Must match a key under mail.presets below.
 426  # Leave null to force callers to pass transport= or preset= explicitly.
 427  default: null
 428
 429  # Anti-spam throttle (kill switch). Enforced before the transport on
 430  # every MailBuilder.send(), including indirect calls via @mail.notify.
 431  #
 432  # Cascade (highest priority first):
 433  #   1. mail.presets.<name>.throttle.<key>  (preset-level override)
 434  #   2. mail.throttle.<key>                 (this section, mail-wide default)
 435  #   3. Code defaults                       (rate=20, per=60.0, on_exceed=raise)
 436  #
 437  # Each key cascades independently: a preset can override only ``rate``
 438  # while keeping the mail-wide ``per`` and ``on_exceed``.
 439  #
 440  # Modes:
 441  #   - raise (default): emits WARNING [SECURITY] then raises
 442  #     MailThrottledError. The caller decides how to back off.
 443  #   - warn: emits WARNING [SECURITY] then drops the mail silently
 444  #     (returns the built message without sending).
 445  #   - drop (silent) is INTENTIONALLY REJECTED at init: a security
 446  #     event must never be silent (kstlib logging convention).
 447  #
 448  # Singleton: a single MailThrottle is shared across all builders that
 449  # use the same preset, including snapshots taken by the @notify
 450  # decorator. This prevents bypass via creating many builder instances.
 451  #
 452  # Hard limits enforced in code (see kstlib.limits):
 453  #   - rate: 1 to 1000 mails per period
 454  #   - per: 1.0 to 86400.0 seconds (1 day)
 455  #   - on_exceed: "raise" or "warn"
 456  throttle:
 457    enabled: true
 458    rate: 20
 459    per: 60.0
 460    on_exceed: raise
 461
 462  # SSL/TLS configuration for mail transports.
 463  #
 464  # Values here override the root ``ssl:`` section (bottom of this file)
 465  # for mail only, and are themselves overridden by ``ssl_verify`` and
 466  # ``ssl_ca_bundle`` keys set inside an individual preset. Each key
 467  # cascades independently: you can set ``verify: false`` here and still
 468  # provide ``ssl_ca_bundle`` at the preset level.
 469  #
 470  # Security: setting ``verify: false`` at any level emits a WARNING log
 471  # at transport build time. Prefer ``ca_bundle: /path/to/private-ca.pem``
 472  # for internal PKI rather than disabling verification outright.
 473  ssl:
 474    verify: true
 475    ca_bundle: null
 476
 477  # Named transport presets. Each preset declares a "transport" field
 478  # (smtp or resend) and backend-specific parameters. Define your own
 479  # presets here and reference them via MailBuilder(preset="name").
 480  presets: {}
 481  # Example presets:
 482  #
 483  # corporate:
 484  #   transport: smtp
 485  #   host: smtp-secure.corp.local
 486  #   port: 25
 487  #   login: svc_mail
 488  #   password: "secret"
 489  #   starttls: false
 490  #   ssl: false
 491  #   timeout: 30
 492  #   # Optional SSL overrides for this preset (highest priority in the cascade):
 493  #   ssl_verify: false
 494  #   ssl_ca_bundle: /etc/ssl/certs/corp-ca.pem
 495  #   # Optional envelope defaults (sender / reply_to only, never to/cc/bcc):
 496  #   defaults:
 497  #     sender: "Service Notifications <notify@corp.local>"
 498  #     reply_to: "Service Notifications <notify@corp.local>"
 499  #   # Optional throttle override (highest priority, see mail.throttle above):
 500  #   throttle:
 501  #     rate: 5            # corporate is more restrictive than mail-wide default
 502  #     per: 60.0
 503  #     on_exceed: warn    # operational critical, prefer drop+log over raise
 504  #
 505  # transactional:
 506  #   transport: resend
 507  #   api_key: re_xxxxxxxxxxxxx
 508  #   timeout: 30
 509  #
 510  # local:
 511  #   transport: smtp
 512  #   host: localhost
 513  #   port: 1025
 514  #   starttls: false
 515
 516  # Attachment and message limits
 517  limits:
 518    # Maximum size for a single attachment
 519    # Accepts: bytes (int) or human-readable string ("25M", "10 MiB")
 520    # Hard limit enforced in code: 25 MiB
 521    max_attachment_size: "25M"
 522    # Maximum number of attachments per message
 523    # Hard limit enforced in code: 50
 524    max_attachments: 20
 525
 526  filesystem:
 527    attachments_root: "~/.cache/kstlib/mail/attachments"
 528    inline_root: "~/.cache/kstlib/mail/inline"
 529    templates_root: "~/.cache/kstlib/mail/templates"
 530    allow_external_attachments: false
 531    allow_external_templates: false
 532    auto_create_roots: true
 533    enforce_permissions: true
 534    max_permission_octal: 448 # 0o700
 535
 536###########################################################################################
 537## Secrets configuration
 538###########################################################################################
 539secrets:
 540  name: "default"
 541  providers:
 542    - name: environment
 543      settings:
 544        prefix: "KSTLIB"
 545        delimiter: "__"
 546    - name: keyring
 547      settings:
 548        service: "kstlib"
 549  sops:
 550    # Path to the encrypted secrets file (set to null to disable by default)
 551    path: null
 552    # Override the sops executable if it is not on PATH
 553    binary: "sops"
 554    # autodetect | json | yaml | text
 555    format: "auto"
 556    # Maximum cached decrypted files (LRU eviction)
 557    # Hard limit enforced in code: 256
 558    max_cache_entries: 64
 559
 560###########################################################################################
 561## Authentication configuration (OAuth2/OIDC)
 562###########################################################################################
 563auth:
 564  # Default provider to use when none specified
 565  default_provider: null
 566
 567  # Token storage backend: "memory" (dev/testing), "file" (persistent), or "sops" (encrypted)
 568  token_storage: "memory"
 569
 570  # OIDC discovery document cache TTL (seconds)
 571  discovery_ttl: 3600
 572
 573  # TRACE level HTTP logging settings
 574  trace:
 575    # Pretty-print JSON bodies in TRACE logs (indent with 2 spaces)
 576    pretty: true
 577    # Maximum body length before truncation (chars)
 578    # TRACE = debug mode, show full body by default
 579    # Hard limit enforced in code: 10000 (10KB)
 580    max_body_length: 10000
 581
 582  # Local callback server for authorization code flow
 583  callback_server:
 584    host: "127.0.0.1"
 585    port: 8400
 586    # Port range to try if primary port is busy (optional)
 587    port_range: null # e.g., [8400, 8410]
 588    # Timeout waiting for callback (seconds)
 589    # Hard limit enforced in code: 600 (10 minutes)
 590    timeout: 120
 591
 592  # Status display settings (kstlib auth status)
 593  status:
 594    # Access token considered "expiring soon" when remaining time < threshold
 595    # Hard limits enforced in code: min 60s, max 3600s (1 hour)
 596    expiring_soon_threshold: 120  # seconds (2 minutes)
 597    # Refresh token considered "expiring soon" when remaining time < threshold
 598    # Hard limits enforced in code: min 60s, max 172800s (48 hours)
 599    # Typically higher since refresh tokens can live days/weeks/months
 600    refresh_expiring_soon_threshold: 600  # seconds (10 minutes)
 601    # Timezone for displaying timestamps: "local" or "utc"
 602    display_timezone: "local"
 603
 604  # Token storage configuration per backend
 605  storage:
 606    file:
 607      directory: "~/.config/kstlib/auth/tokens"
 608    sops:
 609      directory: "~/.config/kstlib/auth/tokens"
 610
 611  # Named providers (empty by default, users define their own)
 612  providers: {}
 613  # Example provider configuration:
 614  # providers:
 615  #   corporate:
 616  #     type: "oidc"              # oauth2 | oidc
 617  #
 618  #     # OIDC Discovery modes:
 619  #     # - Auto: only issuer provided, endpoints auto-discovered
 620  #     # - Hybrid: issuer + some explicit endpoints (explicit wins)
 621  #     # - Manual: no issuer, all endpoints explicit (no discovery)
 622  #     issuer: "https://idp.corp.local/realms/main"
 623  #
 624  #     client_id: "my-app"
 625  #     # Secret can be inline or SOPS reference
 626  #     client_secret: null       # or "sops://secrets/auth.yaml#corporate.client_secret"
 627  #     scopes:
 628  #       - openid
 629  #       - profile
 630  #       - email
 631  #
 632  #     # PKCE enabled by default (recommended for all clients)
 633  #     pkce: true
 634  #
 635  #     # Optional endpoint overrides (auto-discovered if issuer provided)
 636  #     authorization_endpoint: null
 637  #     token_endpoint: null
 638  #     userinfo_endpoint: null
 639  #     jwks_uri: null
 640  #
 641  #     # Custom HTTP headers sent with all IDP requests
 642  #     # Useful for load balancer validation, tenant routing, etc.
 643  #     headers: {}
 644  #     # Example:
 645  #     # headers:
 646  #     #   Host: "idp.corp.local"
 647  #     #   X-Tenant-Id: "corp"
 648  #
 649  #     # Provider-specific token storage (overrides global)
 650  #     token_storage: null       # "memory" | "file" | "sops"
 651
 652###########################################################################################
 653## Utilities configuration
 654###########################################################################################
 655utilities:
 656  secure_delete:
 657    method: "auto"
 658    passes: 3
 659    zero_last_pass: true
 660    chunk_size: 1048576 # 1 MiB
 661
 662###########################################################################################
 663## Resilience configuration
 664###########################################################################################
 665resilience:
 666  # --- Core components (used by WebSocket trading bots) ---
 667
 668  heartbeat:
 669    # Seconds between heartbeats
 670    # Hard limits enforced in code: min 1s, max 300s (5 minutes)
 671    interval: 10
 672
 673  watchdog:
 674    # Seconds of inactivity before triggering timeout callback
 675    # Hard limits enforced in code: min 1s, max 3600s (1 hour)
 676    timeout: 30
 677
 678  # --- Advanced components (for REST API calls, order placement) ---
 679  # Note: WebSocket connections have built-in reconnection logic.
 680  # These are useful for REST API resilience (e.g., placing orders, account queries).
 681
 682  shutdown:
 683    # GracefulShutdown: Orderly cleanup on SIGTERM/SIGINT with prioritized callbacks.
 684    # Use case: Ensure open orders are cancelled, positions closed before exit.
 685    # Total timeout for all cleanup callbacks (seconds)
 686    # Hard limits enforced in code: min 5s, max 300s (5 minutes)
 687    timeout: 30
 688    # Exit code when timeout exceeded
 689    force_exit_code: 1
 690
 691  circuit_breaker:
 692    # CircuitBreaker: Fail-fast pattern for external service calls.
 693    # Use case: REST API calls (order placement, account info) - after N failures,
 694    # stop calling the failing endpoint and fail immediately until recovery.
 695    # Failures before opening circuit
 696    # Hard limits enforced in code: min 1, max 100
 697    max_failures: 5
 698    # Cooldown before attempting recovery (seconds)
 699    # Hard limits enforced in code: min 1s, max 3600s (1 hour)
 700    reset_timeout: 60
 701    # Calls allowed in half-open state for testing
 702    # Hard limits enforced in code: min 1, max 10
 703    half_open_max_calls: 1
 704
 705###########################################################################################
 706## Database configuration
 707###########################################################################################
 708db:
 709  pool:
 710    # Minimum connections to maintain in pool (0 = lazy pool, on-demand)
 711    # Hard limits enforced in code: min 0, max 10
 712    min_size: 1
 713    # Maximum connections allowed in pool
 714    # Hard limits enforced in code: min 1, max 100
 715    max_size: 10
 716    # Timeout for acquiring a connection (seconds)
 717    # Hard limits enforced in code: min 1.0, max 300.0 (5 minutes)
 718    acquire_timeout: 30.0
 719  retry:
 720    # Retry attempts on connection failure
 721    # Hard limits enforced in code: min 1, max 10
 722    max_attempts: 3
 723    # Delay between retries (seconds)
 724    # Hard limits enforced in code: min 0.1, max 60.0
 725    delay: 0.5
 726
 727  # SQLCipher encryption (opt-in, requires: pip install kstlib[db-crypto])
 728  # System deps: libsqlcipher-dev (Debian/Ubuntu), sqlcipher (macOS/brew)
 729  cipher:
 730    # Enable SQLCipher encryption (default: false)
 731    enabled: false
 732    # Key source: env | sops | passphrase
 733    # - env: Read from environment variable (key_env)
 734    # - sops: Read from SOPS-encrypted file (sops_path + sops_key)
 735    # - passphrase: Direct passphrase (ONLY for development/testing)
 736    key_source: env
 737    # Environment variable containing the encryption key
 738    key_env: "KSTLIB_DB_KEY"
 739    # SOPS configuration (when key_source: sops)
 740    sops_path: null
 741    sops_key: "db_key"
 742    # Direct passphrase (NEVER use in production)
 743    passphrase: null
 744
 745###########################################################################################
 746## Credentials configuration (multi-source credential resolution)
 747###########################################################################################
 748credentials:
 749  # Credentials are named entries that can be referenced by rapi services.
 750  # Supported types: env, file, sops, provider
 751  #
 752  # Examples:
 753  #
 754  # # Type: env - from environment variable
 755  # github:
 756  #   type: env
 757  #   var: "GITHUB_TOKEN"
 758  #
 759  # # Type: env - key+secret pair from environment
 760  # kraken_env:
 761  #   type: env
 762  #   var_key: "KRAKEN_API_KEY"
 763  #   var_secret: "KRAKEN_API_SECRET"
 764  #
 765  # # Type: file - from JSON/YAML file with jq-like path extraction
 766  # azure_cli:
 767  #   type: file
 768  #   path: "~/.azure/msal_token_cache.json"
 769  #   token_path: ".AccessToken.secret"
 770  #
 771  # # Type: file - key+secret from file fields
 772  # api_file:
 773  #   type: file
 774  #   path: "~/.config/api_keys.json"
 775  #   key_field: "api_key"
 776  #   secret_field: "api_secret"
 777  #
 778  # # Type: sops - from SOPS-encrypted file
 779  # kraken_prod:
 780  #   type: sops
 781  #   path: "secrets/kraken.sops.json"
 782  #   key_field: "api_key"
 783  #   secret_field: "api_secret"
 784  #
 785  # # Type: provider - from kstlib.auth provider (OAuth2/OIDC)
 786  # corporate:
 787  #   type: provider
 788  #   provider: "corporate"
 789
 790###########################################################################################
 791## REST API configuration (config-driven HTTP client)
 792###########################################################################################
 793rapi:
 794  # Hard limits enforced in code for deep defense:
 795  # - timeout: min 1s, max 300s (5 minutes)
 796  # - max_response_size: max 100M
 797  # - max_retries: min 0, max 10
 798  # - retry_delay: min 0.1s, max 60s
 799  # - retry_backoff: min 1.0, max 5.0
 800  limits:
 801    timeout: 30                # Request timeout in seconds
 802    max_response_size: "10M"   # Maximum response body size
 803    max_retries: 3             # Retry attempts on failure
 804    retry_delay: 1.0           # Initial delay between retries (seconds)
 805    retry_backoff: 2.0         # Exponential backoff multiplier
 806
 807  # Safeguard configuration for dangerous HTTP methods
 808  # Endpoints using these methods MUST define a safeguard string
 809  # to prevent accidental destructive operations
 810  safeguard:
 811    # HTTP methods that require a safeguard to be defined on endpoints
 812    # Default: DELETE and PUT (most destructive operations)
 813    # Set to empty list [] to disable safeguard requirements
 814    required_methods:
 815      - DELETE
 816      - PUT
 817
 818  # Pretty-print settings for CLI output
 819  # Controls formatting of JSON and XML responses in terminal
 820  pretty_render:
 821    # JSON indentation (spaces). Set to null or 0 to disable pretty-printing.
 822    json: 2
 823    # XML pretty-print. Set to true to enable formatted XML output.
 824    xml: true
 825
 826  # API services and their endpoints
 827  # Define your own APIs here or use external *.rapi.yml files
 828  api: {}
 829
 830  # Example: Azure Resource Manager API
 831    # azure:
 832    #   base_url: "https://management.azure.com"
 833    #   credentials: azure_cli      # Reference to credentials section
 834    #   auth_type: bearer
 835    #   headers:
 836    #     X-Custom-Header: "service-value"
 837    #   endpoints:
 838    #     list_subscriptions:
 839    #       path: "/subscriptions"
 840    #       query:
 841    #         api-version: "2020-01-01"
 842    #       headers:
 843    #         X-Request-ID: "{request_id}"
 844
 845###########################################################################################
 846## Alerts configuration (multi-channel alerting)
 847###########################################################################################
 848alerts:
 849  # Hard limits enforced in code for deep defense:
 850  # - throttle.rate: min 1, max 1000 alerts per period
 851  # - throttle.per: min 1.0, max 86400.0 seconds (1 day)
 852  # - throttle.burst: min 1, max rate value
 853
 854  # Default throttle settings (anti-spam protection)
 855  throttle:
 856    rate: 10        # Maximum alerts per period
 857    per: 60.0       # Period duration in seconds (1 minute)
 858    burst: 5        # Initial burst capacity
 859
 860  # Default channel settings
 861  channels:
 862    # Timeout for sending alerts (seconds)
 863    # Hard limits enforced in code: min 1.0, max 120.0
 864    timeout: 30.0
 865    # Retry attempts on delivery failure
 866    # Hard limits enforced in code: min 0, max 5
 867    max_retries: 2
 868
 869  presets:
 870    dev:
 871      throttle:
 872        rate: 100     # More lenient for development
 873        per: 60.0
 874        burst: 20
 875      channels:
 876        timeout: 10.0
 877        max_retries: 0
 878
 879    prod:
 880      throttle:
 881        rate: 10      # Strict rate limiting
 882        per: 60.0
 883        burst: 3
 884      channels:
 885        timeout: 30.0
 886        max_retries: 3
 887
 888    critical_only:
 889      throttle:
 890        rate: 5       # Very strict for critical-only channels
 891        per: 300.0    # 5 minutes
 892        burst: 2
 893
 894###########################################################################################
 895## Metrics configuration
 896###########################################################################################
 897metrics:
 898  # Enable colored output
 899  colors: true
 900
 901  # Output destination: stderr | stdout
 902  output: stderr
 903
 904  # Default behavior for @metrics decorator (can be overridden per-call)
 905  defaults:
 906    time: true      # Track execution time
 907    memory: true    # Track peak memory (tracemalloc)
 908    step: false     # Enable step numbering
 909
 910  # Step format string
 911  # Variables: {n} (step number), {title}, {function}, {module}, {file}, {line}
 912  step_format: "[STEP {n}] {title}"
 913
 914  # Lap format string (for Stopwatch)
 915  # Variables: {n} (lap number), {name}
 916  lap_format: "[LAP {n}] {name}"
 917
 918  # Title format (auto-generated when no custom title provided)
 919  # Variables: {function}, {module}, {file}, {line}
 920  title_format: "{function} [dim green]({file}:{line})[/dim green]"
 921
 922  # Time display precision (decimal places for seconds)
 923  time_precision: 3
 924
 925  # Thresholds for color warnings
 926  thresholds:
 927    time_warn: 5           # Warn color if >= 5 seconds
 928    time_crit: 30          # Critical color if >= 30 seconds
 929    memory_warn: 100000000 # Warn color if >= 100 MB
 930    memory_crit: 500000000 # Critical color if >= 500 MB
 931
 932  # Icons (set to "" to disable)
 933  icons:
 934    time: "⏱"
 935    memory: "🧠"
 936    peak: "Peak:"    # Text after memory icon
 937
 938  # Color theme (Rich style names)
 939  # See: https://rich.readthedocs.io/en/stable/appendix/colors.html
 940  theme:
 941    label: "bold green"
 942    title: "bold white"
 943    text: "white"
 944    muted: "dim"
 945    table_header: "bold cyan"
 946    time_ok: "cyan"
 947    time_warn: "orange3"
 948    time_crit: "bold red"
 949    memory_ok: "rosy_brown"
 950    memory_warn: "orange3"
 951    memory_crit: "bold red"
 952    step_number: "dim"
 953    separator: "dim white"
 954
 955  # Summary display style: table | simple
 956  summary_style: table
 957
 958  # Show percentage of total time in summaries
 959  show_percentages: true
 960
 961  # Print metrics to stderr by default
 962  print_results: true
 963
 964###########################################################################################
 965## WebSocket configuration (proactive connection control)
 966###########################################################################################
 967websocket:
 968  # Ping/Pong heartbeat settings
 969  ping:
 970    # Seconds between ping frames
 971    # Hard limits: [5, 60] - values outside bounds will be clamped
 972    interval: 20
 973    # Seconds to wait for pong response
 974    # Hard limits: [5, 30]
 975    timeout: 10
 976
 977  # Connection settings
 978  connection:
 979    # Timeout for initial connection (seconds)
 980    # Hard limits: [5, 120]
 981    timeout: 30
 982
 983  # Reconnection behavior
 984  reconnect:
 985    # Initial delay between reconnect attempts (seconds)
 986    # Hard limits: [0, 300] - 0 = immediate reconnect allowed
 987    delay: 1.0
 988    # Maximum delay for exponential backoff (seconds)
 989    # Hard limits: [1, 600]
 990    max_delay: 60.0
 991    # Maximum consecutive reconnection attempts
 992    # Hard limits: [0, 100] - 0 = no retry
 993    max_attempts: 10
 994
 995  # Message queue settings
 996  queue:
 997    # Maximum messages in queue (0 = unlimited)
 998    # Hard limits: [0, 10000]
 999    size: 1000
1000
1001  # Proactive control settings (KEY FEATURE)
1002  proactive:
1003    # Seconds between should_disconnect callback checks
1004    # Hard limits: [1, 60]
1005    disconnect_check_interval: 10.0
1006    # Seconds between should_reconnect callback checks
1007    # Hard limits: [0.5, 60]
1008    reconnect_check_interval: 5.0
1009    # Disconnect X seconds before 24h limit (Binance, etc.)
1010    # Hard limits: [60, 3600] - at least 1min, max 1h
1011    disconnect_margin: 300.0
1012
1013  # Presets for common use cases
1014  presets:
1015    trading:
1016      ping: { interval: 15, timeout: 10 }
1017      reconnect: { delay: 0.5, max_delay: 30.0, max_attempts: 20 }
1018      proactive: { disconnect_check_interval: 5.0, reconnect_check_interval: 2.0 }
1019    monitoring:
1020      ping: { interval: 30, timeout: 15 }
1021      reconnect: { delay: 5.0, max_delay: 120.0, max_attempts: 50 }
1022      proactive: { disconnect_check_interval: 30.0, reconnect_check_interval: 10.0 }
1023
1024###########################################################################################
1025## Pipeline configuration (declarative workflow execution)
1026###########################################################################################
1027pipeline:
1028  # Default step timeout in seconds
1029  # Hard limits enforced in code: min 1s, max 3600s (1 hour)
1030  default_timeout: 300
1031
1032  # Error handling policy: fail_fast | continue
1033  # fail_fast: abort pipeline on first step failure (run on_failure steps)
1034  # continue: execute all remaining steps even after failure
1035  on_error: fail_fast
1036
1037  # Named pipelines (user-defined)
1038  pipelines: {}
1039    # Example pipeline configuration:
1040    # morning-monitoring:
1041    #   steps:
1042    #     - name: build_logs
1043    #       type: shell
1044    #       command: |
1045    #         for host in server1 server2; do
1046    #           ssh $host "systemctl status viya"
1047    #         done
1048    #       timeout: 300
1049    #     - name: process_data
1050    #       type: python
1051    #       module: my.analytics.runner
1052    #       args: ["--output", "report.html"]
1053    #     - name: notify
1054    #       type: callable
1055    #       callable: my.alerts:send_summary
1056    #       when: always
1057    #     - name: cleanup
1058    #       type: shell
1059    #       command: "rm -f /tmp/pipeline_*.tmp"
1060    #       when: on_failure
1061
1062# ============================================================================
1063# OPS - Session Management Configuration
1064# ============================================================================
1065# Config-driven session management for persistent processes (bots, services).
1066# Supports tmux (local dev) and container (Podman/Docker) backends.
1067#
1068# Hard limits enforced:
1069#   - Session name: max 64 chars, alphanumeric + underscore + hyphen
1070#   - Image name: max 256 chars, valid OCI format
1071#   - Volumes: max 20, no path traversal
1072#   - Ports: max 50, range 1-65535
1073#   - Env vars: max 100, key max 128 chars, value max 32KB
1074#   - Command: max 4096 chars, dangerous patterns blocked
1075# ============================================================================
1076ops:
1077  # Default backend when not specified (tmux | container)
1078  default_backend: tmux
1079
1080  # Tmux binary path (default: tmux)
1081  tmux_binary: tmux
1082
1083  # Container runtime (podman | docker | null for auto-detect)
1084  container_runtime: null
1085
1086  # Pre-defined sessions (config-driven)
1087  # Sessions can be started with: kstlib ops start <name>
1088  sessions: {}
1089    # Example session configuration:
1090    # mybot:
1091    #   backend: tmux                    # Override default_backend
1092    #   command: "python -m mybot.main"  # Command to run
1093    #   working_dir: "/opt/mybot"        # Working directory
1094    #   env:                             # Environment variables
1095    #     BOT_ENV: production
1096    #     LOG_LEVEL: INFO
1097    #
1098    # mybot-prod:
1099    #   backend: container
1100    #   image: "mybot:latest"            # Container image (required)
1101    #   volumes:                         # Volume mounts (host:container[:ro|:rw])
1102    #     - "./data:/app/data"
1103    #     - "./logs:/app/logs:rw"
1104    #   ports:                           # Port mappings (host:container[/tcp|/udp])
1105    #     - "8080:80"
1106    #   log_volume: "./logs:/app/logs"   # Persistent logs for post-mortem
1107
1108###########################################################################################
1109## SSL/TLS configuration (global settings for all HTTP clients)
1110###########################################################################################
1111ssl:
1112  # Enable SSL certificate verification (default: true)
1113  # Set to false ONLY for development with self-signed certificates
1114  # WARNING: Disabling verification exposes you to MITM attacks
1115  verify: true
1116
1117  # Custom CA bundle path for corporate PKI or self-signed certificates
1118  # If provided, ssl_verify is implicitly true
1119  # Accepts: null (use system CAs), or path to PEM file
1120  ca_bundle: null

Common Patterns

Development vs Production

import os
from kstlib.config import load_from_file

env = os.getenv("APP_ENV", "development")
config = load_from_file(f"config/{env}.yml")

Override from environment

# Load base config, then override specific values
config = ConfigLoader().config

# Override at runtime (config is a Box, so this works)
if os.getenv("DEBUG"):
    config.app.debug = True

Testing with isolated config

from pathlib import Path
from kstlib.config import clear_config, load_from_file

def test_custom_config(tmp_path: Path):
    config_file = tmp_path / "test.yml"
    config_file.write_text("""
    app:
      debug: true
    """)

    clear_config()  # Isolate from other tests
    config = load_from_file(config_file)

    assert config.app.debug is True

Advanced: AutoDiscoveryConfig

Bundle discovery settings into a reusable object:

from pathlib import Path
from kstlib.config import ConfigLoader
from kstlib.config.loader import AutoDiscoveryConfig

auto = AutoDiscoveryConfig(
    enabled=True,
    source="file",
    filename="kstlib.conf.yml",
    env_var="APP_CONFIG",
    path=Path("/srv/kstlib/prod.yml"),
)

loader = ConfigLoader(auto=auto)
config = loader.config

Troubleshooting

ConfigFileNotFoundError

File doesn’t exist at the specified path:

from kstlib.config import load_from_file
from kstlib.exceptions import ConfigFileNotFoundError

try:
    config = load_from_file("config.yml")
except ConfigFileNotFoundError:
    # Fall back to defaults or create config
    config = bootstrap_defaults()

ConfigFormatError

Invalid syntax or parse error in config file:

from kstlib.exceptions import ConfigFormatError

try:
    config = load_from_file("config.yml")
except ConfigFormatError as exc:
    raise SystemExit(f"Invalid configuration: {exc}")

ConfigCircularIncludeError

Include loop detected (A includes B, B includes A):

# This will fail
# a.yml includes b.yml, b.yml includes a.yml

Fix: Review your include chain and remove the circular dependency.

Config not updating after file change

Config is cached by default. In interactive sessions, force a reload with reload_config():

from kstlib.config import reload_config

config = reload_config()  # Flush cache + reload from disk

See Interactive usage (Jupyter / REPL) for the full discussion and alternatives.

Environment variable not found

When using auto_source="env", ensure the variable is set:

export CONFIG_PATH=/path/to/config.yml
# This fails if CONFIG_PATH is not set
config = ConfigLoader(auto_source="env").config

API Reference

Full autodoc: Configuration Loader

Function

Description

ConfigLoader()

Main loader class with auto-discovery

load_from_file(path)

Load from specific file

get_config()

Get cached config (singleton)

clear_config()

Clear the config cache

reload_config()

Flush cache + reload from disk (Jupyter/REPL)

Path resolution in configuration

When a configuration value is a filesystem path, for example ssl_ca_bundle, attachments_root, credentials.path, or logging.handlers.file.path, kstlib resolves it with Python’s standard path logic:

  1. Absolute paths are used as-is: /etc/ssl/certs/corp-ca.pem

  2. Tilde-prefixed paths are expanded to the user’s home: ~/ca-bundles/corp.pem becomes /home/alice/ca-bundles/corp.pem

  3. Relative paths are resolved against the current working directory of the Python process, NOT the directory of the YAML file that declared the path.

Why this matters

Point 3 is a common source of surprise, especially in interactive environments such as Jupyter:

  • You have ssl_ca_bundle: ./corp-ca.pem in your config, with corp-ca.pem sitting next to your notebook file.

  • Your JupyterHub launches the kernel with cwd=/home/alice, which does not contain the file.

  • kstlib raises MailConfigurationError: ssl_ca_bundle path does not exist: ./corp-ca.pem.

The same trap applies to scripts launched by cron, systemd units, container entry points, or any process whose cwd does not match the directory holding the YAML file.

Debugging a relative path

If you must use a relative path, verify the Python process cwd:

import os

print(f"Process cwd: {os.getcwd()}")

The relative path is resolved against this value. If the printed cwd differs from where your YAML and its referenced files sit, the path will not resolve. Either os.chdir(...) before the import, or switch to an absolute path.

Future direction

Resolving relative paths against the YAML file that declared them is a candidate enhancement on the backlog (tracked as feat-config-paths-relative-to-yaml). Implementing it requires kstlib to track, for every configuration value, the file it originated from, which is a substantial refactor of the loader. For now, use absolute paths to avoid ambiguity across different execution contexts.