Since apywire specs are just Python dictionaries, you can load them from any configuration file format: YAML, TOML, JSON, INI, or any other format that converts to Python data structures.
- 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
- Validation: Use schema validation tools for your config format
YAML is readable and supports complex nested structures.
# Database configuration
database_url: postgresql://localhost/mydb
pool_min: 1
pool_max: 20
# Database connection pool
psycopg2.pool.ThreadedConnectionPool pool:
minconn: "{pool_min}"
maxconn: "{pool_max}"
dsn: "{database_url}"
# Redis cache
redis_url: redis://localhost:6379
redis.Redis cache:
url: "{redis_url}"
# Application service
MyService service:
db: "{pool}"
cache: "{cache}"
debug: trueimport yaml
from apywire import Wiring
# Load spec from YAML file
with open("config.yaml", "r") as f:
spec = yaml.safe_load(f)
wired = Wiring(spec, thread_safe=True)
service = wired.service()- ✅ Very human-readable
- ✅ Supports comments
- ✅ Native support for lists, dicts, booleans
- ✅ Multi-line strings
- ❌ Requires
pyyamlpackage
TOML is Python-friendly and great for structured configuration.
# Database configuration
database_url = "postgresql://localhost/mydb"
pool_min = 1
pool_max = 20
# Redis
redis_url = "redis://localhost:6379"
# Database connection pool
["psycopg2.pool.ThreadedConnectionPool pool"]
minconn = "{pool_min}"
maxconn = "{pool_max}"
dsn = "{database_url}"
# Redis cache
["redis.Redis cache"]
url = "{redis_url}"
# Application service
["MyService service"]
db = "{pool}"
cache = "{cache}"
debug = true!!! note "TOML Section Names with Spaces"
TOML section names with spaces must be quoted: ["module.Class name"]
import tomllib # Python 3.11+, or use 'tomli' package
from apywire import Wiring
# Load TOML file
with open("config.toml", "rb") as f:
spec = tomllib.load(f)
# Use directly - TOML structure matches apywire spec format!
wired = Wiring(spec)- ✅ Built into Python 3.11+ (
tomllib) - ✅ Type-safe (strict types)
- ✅ Supports comments
- ✅ Good for structured data
- ✅ Dots in section names work perfectly
- ✅ No conversion needed!
JSON is universal and works everywhere.
{
"database_url": "postgresql://localhost/mydb",
"pool_min": 1,
"pool_max": 20,
"redis_url": "redis://localhost:6379",
"psycopg2.pool.ThreadedConnectionPool pool": {
"minconn": "{pool_min}",
"maxconn": "{pool_max}",
"dsn": "{database_url}"
},
"redis.Redis cache": {
"url": "{redis_url}"
},
"MyService service": {
"db": "{pool}",
"cache": "{cache}",
"debug": true
}
}import json
from apywire import Wiring
# Load spec from JSON file
with open("config.json", "r") as f:
spec = json.load(f)
wired = Wiring(spec)
service = wired.service()- ✅ Built into Python standard library
- ✅ Universal format
- ✅ Easy to generate programmatically
- ✅ Works with REST APIs
- ❌ No comments
- ❌ Verbose syntax
INI files are simple and widely used for configuration.
[constants]
database_url = postgresql://localhost/mydb
pool_min = 1
pool_max = 20
redis_url = redis://localhost:6379
[psycopg2.pool.ThreadedConnectionPool pool]
minconn = {pool_min}
maxconn = {pool_max}
dsn = {database_url}
[redis.Redis cache]
url = {redis_url}
[MyService service]
db = {pool}
cache = {cache}
debug = trueimport configparser
from apywire import Wiring
# Load INI file
config = configparser.ConfigParser()
config.read("config.ini")
# Convert to apywire spec
spec = {}
for section in config.sections():
if section == "constants":
# Constants go in top level
for key, value in config[section].items():
# Type conversion (INI reads everything as strings)
if value.isdigit():
spec[key] = int(value)
elif value.lower() in ("true", "false"):
spec[key] = value.lower() == "true"
else:
spec[key] = value
else:
# Wired objects
spec[section] = dict(config[section])
wired = Wiring(spec)- ✅ Built into Python standard library
- ✅ Simple syntax
- ✅ Widely understood
- ✅ Supports comments
- ❌ All values are strings (need type conversion)
- ❌ Limited nesting
Constants can now reference other constants and wired objects using placeholder syntax {name}.
When a constant references only other constants, it's expanded immediately at initialization:
# config.yaml
host: localhost
port: 5432
database_name: myapp
# This is expanded immediately
database_url: "postgresql://{host}:{port}/{database_name}"import yaml
from apywire import Wiring
with open("config.yaml") as f:
spec = yaml.safe_load(f)
wired = Wiring(spec)
# The database_url constant is already expanded to the full connection stringNested references work too:
base_url: http://api.example.com
v1_url: "{base_url}/v1"
users_endpoint: "{v1_url}/users" # Becomes "http://api.example.com/v1/users"When a constant references a wired object, it's automatically promoted to an accessor with lazy evaluation:
database_url: "postgresql://localhost/mydb"
psycopg2.connect conn:
dsn: "{database_url}"
# This references a wired object, so it becomes an accessor
status: "Connected to {conn}"wired = Wiring(spec)
# status is now an accessor (not in _values)
# It lazily instantiates conn and converts it to string
status_msg = wired.status() # "Connected to <connection object>"Constants with both constant and wired object references are auto-promoted:
host: localhost
datetime.datetime server_start:
year: 2025
month: 1
day: 1
# This has both constant and wired refs, so it's auto-promoted
status: "Server {host} started at {server_start}"wired = Wiring(spec)
# Use constants via placeholders in other wired objects
msg = wired.status() # "Server localhost started at 2025-01-01 00:00:00"- ✅ DRY Configuration: Define values once, reference everywhere
- ✅ Computed Constants: Build strings from multiple parts
- ✅ Flexible: Mix constants and wired objects
- ✅ Lazy Evaluation: Wired objects only instantiated when needed
- ✅ Type Conversion: Non-string constants automatically converted to strings
Circular references in constants are detected at initialization:
a: "{b}"
b: "{a}" # ❌ CircularWiringErrorCircular references with wired objects follow normal lazy detection:
MyClass obj_a:
dep: "{obj_b}"
MyClass obj_b:
dep: "{obj_a}" # ❌ CircularWiringError when accessedLoad different configs based on environment:
import os
import yaml
from apywire import Wiring
# Determine environment
env = os.getenv("APP_ENV", "dev")
# Load appropriate config file
config_file = f"config.{env}.yaml"
with open(config_file, "r") as f:
spec = yaml.safe_load(f)
wired = Wiring(spec, thread_safe=(env == "production"))File structure:
config.dev.yaml
config.staging.yaml
config.production.yaml
Mix configuration files with environment variables:
# config.yaml
database_url: ${DATABASE_URL}
api_key: ${API_KEY}
MyService service:
db_url: "{database_url}"
api_key: "{api_key}"import os
import yaml
import re
from apywire import Wiring
def substitute_env_vars(config):
"""Recursively substitute ${VAR} with environment variables."""
if isinstance(config, dict):
return {k: substitute_env_vars(v) for k, v in config.items()}
elif isinstance(config, list):
return [substitute_env_vars(item) for item in config]
elif isinstance(config, str):
# Replace ${VAR} with environment variable
pattern = r'\$\{([^}]+)\}'
return re.sub(pattern, lambda m: os.getenv(m.group(1), ''), config)
return config
# Load and substitute
with open("config.yaml", "r") as f:
raw_spec = yaml.safe_load(f)
spec = substitute_env_vars(raw_spec)
wired = Wiring(spec)Validate your configuration files before loading:
from pydantic import BaseModel, Field
import yaml
from apywire import Wiring
class DatabaseConfig(BaseModel):
database_url: str
pool_min: int = Field(ge=1)
pool_max: int = Field(ge=1, le=100)
class AppConfig(BaseModel):
database: DatabaseConfig
redis_url: str
debug: bool = False
# Load and validate
with open("config.yaml", "r") as f:
raw_config = yaml.safe_load(f)
# Validate structure
validated = AppConfig(**raw_config)
# Build apywire spec from validated config
spec = {
"database_url": validated.database.database_url,
"pool_min": validated.database.pool_min,
"pool_max": validated.database.pool_max,
# ... rest of spec
}
wired = Wiring(spec)# ❌ Bad: Secrets in config file
database_url: postgresql://user:password@localhost/db
# ✅ Good: Reference environment variables
database_url: ${DATABASE_URL}config/
├── base.yaml # Shared configuration
├── dev.yaml # Development overrides
├── staging.yaml # Staging overrides
└── production.yaml # Production overrides
Add a config.example.yaml to your repository:
# config.example.yaml
# Copy this to config.yaml and customize
database_url: postgresql://localhost/mydb # Database connection string
pool_min: 1 # Minimum pool connections
pool_max: 20 # Maximum pool connectionsAlways validate configuration before passing to Wiring:
def load_config(filename):
"""Load and validate configuration."""
with open(filename) as f:
spec = yaml.safe_load(f)
# Validate required keys
required = ["database_url", "redis_url"]
for key in required:
if key not in spec:
raise ValueError(f"Missing required config: {key}")
return spec
spec = load_config("config.yaml")
wired = Wiring(spec)Putting it all together with best practices:
myapp/
├── config/
│ ├── base.yaml
│ ├── dev.yaml
│ ├── production.yaml
│ └── config.example.yaml
├── app/
│ ├── __init__.py
│ ├── config.py
│ └── main.py
└── .env.example
import os
import yaml
from pathlib import Path
from apywire import Wiring
def load_spec(env: str = None) -> dict:
"""Load wiring spec from YAML configuration."""
if env is None:
env = os.getenv("APP_ENV", "dev")
config_dir = Path(__file__).parent.parent / "config"
# Load base config
with open(config_dir / "base.yaml") as f:
spec = yaml.safe_load(f)
# Load environment-specific overrides
env_file = config_dir / f"{env}.yaml"
if env_file.exists():
with open(env_file) as f:
env_spec = yaml.safe_load(f)
spec.update(env_spec)
# Substitute environment variables
spec = substitute_env_vars(spec)
return spec
def get_wired(env: str = None) -> Wiring:
"""Get configured Wiring instance."""
spec = load_spec(env)
thread_safe = env == "production"
return Wiring(spec, thread_safe=thread_safe)from app.config import get_wired
def main():
wired = get_wired()
# Access configured services
db = wired.database()
cache = wired.cache()
service = wired.service()
# Run application
service.run()
if __name__ == "__main__":
main()- Basic Usage - Learn the fundamentals
- Advanced Features - Complex patterns
- Examples - Real-world use cases