Skip to content

Configuration Files

apywire supports loading specs from JSON, TOML, and INI configuration files. The apywire.formats module provides utilities for parsing and serializing specs in these formats.

Why Use Configuration Files?

  • Separation of Concerns: Keep configuration separate from code
  • Environment-Specific: Different configs for dev, staging, production
  • Version Control: Track configuration changes
  • Non-Developers: Allow non-programmers to modify configuration
  • CLI Workflow: Generate specs with CLI, edit, then compile

Quick Start with CLI

The fastest way to create a configuration file is using the CLI:

# Generate a spec from class introspection
python -m apywire generate --format toml "datetime.datetime now" > config.toml

# Edit config.toml to customize values, then compile
python -m apywire compile --format toml config.toml > wiring.py

See Command Line Interface for full CLI documentation.

Supported Formats

JSON

JSON provides direct mapping with no conversion needed.

Example: config.json

{
  "database_url": "postgresql://localhost/mydb",
  "pool_size": 20,

  "myapp.Database db": {
    "url": "{database_url}",
    "pool_size": "{pool_size}"
  }
}

Loading:

from apywire import Wiring
from apywire.formats import json_to_spec

with open("config.json") as f:
    spec = json_to_spec(f.read())

wired = Wiring(spec)
db = wired.db()

Benefits:

  • ✅ Built into Python standard library
  • ✅ Universal format
  • ✅ No conversion needed
  • ❌ No comments

TOML

TOML is Python-friendly with top-level keys as constants and tables as wiring entries.

Example: config.toml

# Constants as top-level keys
database_url = "postgresql://localhost/mydb"
pool_size = 20

# Wiring entries as tables (quote names with spaces)
["myapp.Database db"]
url = "{database_url}"
pool_size = "{pool_size}"

Quoted Section Names

TOML section names with spaces must be quoted: ["module.Class name"]

Loading:

from apywire import Wiring
from apywire.formats import toml_to_spec

with open("config.toml") as f:
    spec = toml_to_spec(f.read())

wired = Wiring(spec)

Benefits:

  • ✅ Built into Python 3.11+ (tomllib)
  • ✅ Supports comments
  • ✅ Type-safe (integers, booleans, etc.)
  • ✅ Clean syntax for nested structures

INI

INI uses a [constants] section for constants (required by the format).

Example: config.ini

[constants]
database_url = postgresql://localhost/mydb
pool_size = 20

[myapp.Database db]
url = {database_url}
pool_size = {pool_size}

Loading:

from apywire import Wiring
from apywire.formats import ini_to_spec

with open("config.ini") as f:
    spec = ini_to_spec(f.read())

wired = Wiring(spec)

Benefits:

  • ✅ Built into Python standard library
  • ✅ Simple syntax
  • ✅ Supports comments
  • ❌ All values are strings (automatic type conversion provided)

YAML

YAML is not directly supported by the formats module, but you can use PyYAML:

import yaml
from apywire import Wiring

with open("config.yaml") as f:
    spec = yaml.safe_load(f)

wired = Wiring(spec)

Format Conversion

The formats module provides functions to convert between formats:

from apywire.formats import (
    json_to_spec, spec_to_json,
    toml_to_spec, spec_to_toml,
    ini_to_spec, spec_to_ini,
)

# Load from one format
with open("config.json") as f:
    spec = json_to_spec(f.read())

# Save to another format
toml_output = spec_to_toml(spec)
with open("config.toml", "w") as f:
    f.write(toml_output)

Placeholder Expansion

Constants can reference other constants and wired objects using {name} syntax.

Constant → Constant

When a constant references only other constants, it's expanded immediately:

host = "localhost"
port = 5432
database_url = "postgresql://{host}:{port}/mydb"

Constant → Wired Object

When a constant references a wired object, it becomes a lazy accessor:

["datetime.datetime server_start"]
year = 2025
month = 1
day = 1

# This is auto-promoted to an accessor
status = "Server started at {server_start}"
wired = Wiring(spec)
msg = wired.status()  # "Server started at 2025-01-01 00:00:00"

Environment-Based Configuration

Load different configs based on environment:

import os
from apywire import Wiring
from apywire.formats import toml_to_spec

env = os.getenv("APP_ENV", "dev")
config_file = f"config.{env}.toml"

with open(config_file) as f:
    spec = toml_to_spec(f.read())

wired = Wiring(spec, thread_safe=(env == "production"))

Environment Variables

You can substitute environment variables before parsing:

import os
import re
from apywire import Wiring
from apywire.formats import toml_to_spec

def substitute_env(content: str) -> str:
    """Replace ${VAR} with environment variable values."""
    return re.sub(
        r'\$\{([^}]+)\}',
        lambda m: os.getenv(m.group(1), ''),
        content
    )

with open("config.toml") as f:
    content = substitute_env(f.read())
    spec = toml_to_spec(content)

wired = Wiring(spec)

Complete Example

Project Structure

myapp/
├── config/
│   ├── dev.toml
│   ├── production.toml
│   └── config.example.toml
├── app/
│   ├── __init__.py
│   ├── config.py
│   └── main.py
└── wiring.py  # Generated via CLI

config/dev.toml

debug = true
database_url = "postgresql://localhost/myapp_dev"
pool_size = 5

["myapp.Database db"]
url = "{database_url}"
pool_size = "{pool_size}"

["myapp.Cache cache"]
backend = "memory"

app/config.py

import os
from pathlib import Path
from apywire import Wiring
from apywire.formats import toml_to_spec

def load_wiring() -> Wiring:
    env = os.getenv("APP_ENV", "dev")
    config_dir = Path(__file__).parent.parent / "config"
    config_file = config_dir / f"{env}.toml"

    with open(config_file) as f:
        spec = toml_to_spec(f.read())

    return Wiring(spec, thread_safe=(env == "production"))

app/main.py

from app.config import load_wiring

def main():
    wired = load_wiring()
    db = wired.db()
    cache = wired.cache()
    # Use services...

if __name__ == "__main__":
    main()

Best Practices

  1. Keep Secrets Out: Use environment variables for passwords and API keys
  2. Separate Configs: Use different files for dev/staging/production
  3. Document Your Config: Include a config.example.toml in version control
  4. Validate Early: Check required keys before creating Wiring instance

Next Steps