Context Presets¶
Context presets provide a powerful way to package and reuse combinations of state objects and disposable resources in Haiway applications. They enable you to create reusable configurations that can be applied consistently across different parts of your application, promoting modularity and reducing code duplication.
Overview¶
Context presets allow you to:
- Package state and disposables into reusable configurations
- Apply presets directly to context scopes without registry setup
- Override preset state with explicit parameters when needed
- Compose complex configurations from simpler building blocks
- Maintain consistency across different execution contexts
Basic Usage¶
Creating Context Presets¶
Define presets by combining state objects and disposables:
from haiway import State, ctx
from haiway.context import ContextPreset
class DatabaseConfig(State):
host: str
port: int = 5432
database: str = "app"
class ApiConfig(State):
base_url: str
timeout: int = 30
api_key: str
# Create a preset for development environment
dev_preset = ContextPreset(
name="development",
state=[
DatabaseConfig(host="localhost", database="app_dev"),
ApiConfig(
base_url="https://dev-api.example.com",
timeout=60,
api_key="dev-key-123"
)
]
)
# Create a preset for production environment
prod_preset = ContextPreset(
name="production",
state=[
DatabaseConfig(host="prod-db.example.com", database="app_prod"),
ApiConfig(
base_url="https://api.example.com",
timeout=30,
api_key="prod-key-456"
)
]
)
Using Presets Directly¶
Pass presets directly to ctx.scope()
instead of string names:
async def run_with_development():
# Use development preset directly
async with ctx.scope(dev_preset):
db_config = ctx.state(DatabaseConfig)
api_config = ctx.state(ApiConfig)
print(f"Database: {db_config.host}:{db_config.port}/{db_config.database}")
print(f"API: {api_config.base_url} (timeout: {api_config.timeout}s)")
async def run_with_production():
# Use production preset directly
async with ctx.scope(prod_preset):
db_config = ctx.state(DatabaseConfig)
api_config = ctx.state(ApiConfig)
print(f"Database: {db_config.host}:{db_config.port}/{db_config.database}")
print(f"API: {api_config.base_url} (timeout: {api_config.timeout}s)")
Overriding Preset State¶
Override specific state objects from presets with explicit parameters:
async def run_with_custom_timeout():
# Use development preset but override API config
async with ctx.scope(
dev_preset,
ApiConfig(
base_url="https://dev-api.example.com",
timeout=60,
api_key="dev-key-123"
)
):
db_config = ctx.state(DatabaseConfig) # From preset
api_config = ctx.state(ApiConfig) # Overridden
print(f"Database: {db_config.host} (from preset)")
print(f"API timeout: {api_config.timeout}s (overridden)")
print(f"API URL: {api_config.base_url} (overridden)")
Advanced Features¶
Presets with Disposables¶
Include disposable resources in presets for complete environment setup:
from contextlib import asynccontextmanager
@asynccontextmanager
async def database_connection():
print("Opening database connection")
try:
# Setup database connection
yield DatabaseConnection(pool=create_connection_pool())
finally:
print("Closing database connection")
class DatabaseConnection(State):
pool: Any
# Preset with both state and disposables
full_dev_preset = ContextPreset(
name="development_full",
state=[
DatabaseConfig(host="localhost", database="app_dev"),
],
disposables=[
database_connection(),
]
)
async def run_with_resources():
async with ctx.scope(full_dev_preset):
# State from preset
db_config = ctx.state(DatabaseConfig)
# Disposable resources from preset
db_conn = ctx.state(DatabaseConnection)
# Use resources
await perform_database_operation(db_conn.pool)
Nested Presets and Composition¶
Compose complex configurations by combining presets:
# Base configuration preset
base_preset = ContextPreset(
name="base",
state=[
LoggingConfig(level="INFO", format="json"),
MetricsConfig(enabled=True, interval=60)
]
)
# Database-specific preset
database_preset = ContextPreset(
name="database",
state=[
DatabaseConfig(host="localhost", port=5432),
ConnectionPoolConfig(min_size=5, max_size=20)
]
)
async def run_composed_setup():
# First apply base configuration
async with ctx.scope(base_preset):
# Then add database configuration
async with ctx.scope(database_preset):
# Both presets' state is available
logging_config = ctx.state(LoggingConfig) # From base_preset
db_config = ctx.state(DatabaseConfig) # From database_preset
print(f"Logging level: {logging_config.level}")
print(f"Database: {db_config.host}:{db_config.port}")
Dynamic Preset Creation¶
Create presets dynamically based on runtime conditions:
def create_environment_preset(env: str) -> ContextPreset:
if env == "development":
return ContextPreset(
name=f"dynamic_{env}",
state=[
DatabaseConfig(host="localhost", database="app_dev"),
ApiConfig(base_url="https://dev-api.example.com", timeout=60),
DebugConfig(enabled=True, verbose=True)
]
)
elif env == "production":
return ContextPreset(
name=f"dynamic_{env}",
state=[
DatabaseConfig(host="prod-db.example.com", database="app_prod"),
ApiConfig(base_url="https://api.example.com", timeout=30),
DebugConfig(enabled=False, verbose=False)
]
)
else:
raise ValueError(f"Unknown environment: {env}")
async def run_dynamic_environment():
import os
env = os.getenv("APP_ENV", "development")
# Create preset based on environment
env_preset = create_environment_preset(env)
async with ctx.scope(env_preset):
db_config = ctx.state(DatabaseConfig)
debug_config = ctx.state(DebugConfig)
print(f"Running in {env} mode")
print(f"Debug enabled: {debug_config.enabled}")
State Priority System¶
Priority order (highest to lowest):
1. Explicit state - passed directly to ctx.scope()
2. Explicit disposables - from disposables=
parameter
3. Preset state - from preset's state and disposables
4. Contextual state - inherited from parent contexts
Preset Registry¶
Instead of directly providing presets you can use the preset registry approach allowing to resolve presets using scope names:
# Register multiple presets by name
with ctx.presets(dev_preset, prod_preset, staging_preset):
# Use by name lookup
async with ctx.scope("development"): # Matches dev_preset
db_config = ctx.state(DatabaseConfig)
async with ctx.scope("production"): # Matches prod_preset
db_config = ctx.state(DatabaseConfig)
Best Practices¶
1. Descriptive Preset Names¶
Use clear, descriptive names that indicate the preset's purpose:
# Good: Clear purpose
api_client_preset = ContextPreset(name="api_client", ...)
database_readonly_preset = ContextPreset(name="database_readonly", ...)
# Avoid: Generic names
config_preset = ContextPreset(name="config", ...)
preset1 = ContextPreset(name="preset1", ...)
2. Environment-Specific Presets¶
Create separate presets for different environments:
dev_api_preset = ContextPreset(
name="api_development",
state=[ApiConfig(base_url="https://dev-api.example.com", debug=True)]
)
prod_api_preset = ContextPreset(
name="api_production",
state=[ApiConfig(base_url="https://api.example.com", debug=False)]
)
3. Minimal Preset Scope¶
Keep presets focused on specific concerns:
# Good: Focused on database concerns
db_preset = ContextPreset(
name="database",
state=[DatabaseConfig(...), ConnectionPoolConfig(...)]
)
# Good: Focused on API concerns
api_preset = ContextPreset(
name="api_client",
state=[ApiConfig(...), RetryConfig(...)]
)
# Avoid: Mixed concerns
everything_preset = ContextPreset(
name="everything",
state=[DatabaseConfig(...), ApiConfig(...), LoggingConfig(...)]
)
4. Validation and Defaults¶
Ensure preset state objects have sensible defaults:
class DatabaseConfig(State):
host: str
port: int = 5432 # Sensible default
database: str = "app" # Sensible default
ssl: bool = True # Secure by default
# Preset can rely on defaults
minimal_db_preset = ContextPreset(
name="minimal_database",
state=[DatabaseConfig(host="localhost")] # Other fields use defaults
)
5. Testing with Presets¶
Create test-specific presets for consistent testing:
test_preset = ContextPreset(
name="testing",
state=[
DatabaseConfig(host="localhost", database="test_db"),
ApiConfig(base_url="https://mock-api.test", timeout=5),
LoggingConfig(level="DEBUG")
]
)
async def test_user_service():
async with ctx.scope(test_preset):
# Test with consistent configuration
user_service = ctx.state(UserService)
result = await user_service.fetch_user("test-id")
assert result is not None
Performance Considerations¶
- Preset Creation: Create presets once and reuse them - avoid creating new preset instances in hot paths
- State Objects: Preset state objects are shared (immutable), so there's no memory overhead for reuse
- Disposables: Each preset usage creates new disposable instances, so consider the cost of resource creation
- Nested Contexts: Deeply nested contexts with many presets may have slight overhead - profile if performance is critical