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