Configuration & Providers
Table of Contents
- Overview
- Configuration Files
- @Value Injection
- @Configuration Classes
- @Provider Factory Methods
- Environment Variables
- Configuration Source Logging
- Complete Examples
Overview
Mitsuki provides multiple ways to configure your application:
- application.yml - Primary configuration file
- Environment variables - Override YAML values
- @Value injection - Inject config into components
- @Provider factory methods - Create configured objects
- Profile-specific files - Different configs per environment
NOTE: Mitsuki has a defaults.yml file, which provides default configs for most components. They're used when you don't set any higher level of configuration.
Configuration Files
application.yml
Create application.yml in your project root:
# Server configuration
server:
host: 0.0.0.0
port: 8000
type: granian # Options: uvicorn, granian
timeout: 60 # Request timeout in seconds (optional)
max_body_size: 10485760 # 10MB in bytes
ignore_trailing_slash: true
cors:
enabled: false
allowed_origins:
- "*"
# Database configuration
database:
url: sqlite:///mitsuki_app.db
adapter: sqlalchemy
echo: false
# Application settings
app:
name: My Mitsuki Application
debug: false
max_upload_size: 10485760 # 10MB
# Logging
logging:
level: INFO
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
sqlalchemy: false
# Custom settings
email:
smtp_host: smtp.gmail.com
smtp_port: 587
from_address: noreply@example.com
api:
timeout: 30
retry_count: 3
base_url: https://api.example.comNested Configuration
Access nested values with dot notation:
database:
connection:
pool:
size: 10
timeout: 30
retry:
max_attempts: 3
backoff: 1000pool_size: int = Value("${database.connection.pool.size}")
max_attempts: int = Value("${database.connection.retry.max_attempts}")Profile-Specific Files
Create environment-specific configuration:
defaults.yml # Mitsuki-level defaults - overriden by any configs
project/
├── application.yml # Base (all environments)
├── application-development.yml # Development overrides
├── application-test.yml # Test overrides
├── application-staging.yml # Staging overrides
└── application-production.yml # Production overridesWhen MITSUKI_PROFILE=production, Mitsuki loads:
application.yml(base)application-production.yml(overrides)
@Value Injection
Inject configuration values into class properties using @Value.
Basic Injection
from mitsuki import Configuration, Value
@Configuration
class AppConfig:
# Simple value injection
app_name: str = Value("${app.name}")
# With default value
port: int = Value("${server.port:8000}")
# Nested configuration
pool_size: int = Value("${database.connection.pool.size:10}")Syntax
${key} - Get value for key, None if not found:
app_name: str = Value("${app.name}")${key:default} - Get value for key, use default if not found:
port: int = Value("${server.port:8000}")
debug: bool = Value("${app.debug:false}")Type Conversion
Mitsuki automatically converts values to the correct type:
@Configuration
class AppConfig:
# Integer
port: int = Value("${server.port:8000}")
# Float
timeout: float = Value("${api.timeout:30.5}")
# Boolean
debug: bool = Value("${app.debug:false}") # "true", "yes" -> True
enabled: bool = Value("${feature.enabled:true}")
# String
app_name: str = Value("${app.name:My App}")Using in Services
from mitsuki import Service, Value
@Service()
class EmailService:
smtp_host: str = Value("${email.smtp_host}")
smtp_port: int = Value("${email.smtp_port:587}")
from_address: str = Value("${email.from_address}")
async def send_email(self, to: str, subject: str, body: str):
# Use injected configuration
print(f"Sending via {self.smtp_host}:{self.smtp_port}")Using in @Application
from mitsuki import Application, Value
@Application
class MyApp:
# Inject configuration directly into application class
port: int = Value("${server.port:8000}")
host: str = Value("${server.host:127.0.0.1}")
if __name__ == "__main__":
MyApp.run() # Uses injected port value@Configuration Classes
Configuration classes organize related configuration and provider definitions.
Basic Configuration Class
from mitsuki import Configuration, Value
@Configuration
class DatabaseConfig:
# Configuration properties
url: str = Value("${database.url}")
pool_size: int = Value("${database.pool.size:10}")
echo: bool = Value("${database.echo:false}")Injecting Configuration
Configuration classes can be injected into services:
@Service()
class DatabaseService:
def __init__(self, config: DatabaseConfig):
self.config = config
async def connect(self):
print(f"Connecting to {self.config.url}")
print(f"Pool size: {self.config.pool_size}")Configuration with Dependencies
Configuration classes can have dependencies:
@Service()
class SecretManager:
def get_secret(self, key: str) -> str:
# Fetch from vault, AWS Secrets Manager, etc.
return "secret-value"
@Configuration
class ApiConfig:
def __init__(self, secrets: SecretManager):
self.secrets = secrets
@Provider
def api_key(self) -> str:
# Use injected service to get secret
return self.secrets.get_secret("api_key")@Provider Factory Methods
Provider factory methods create and configure complex objects.
Basic Provider
from mitsuki import Configuration, Provider
import httpx
@Configuration
class HttpClientConfig:
@Provider
def http_client(self) -> httpx.AsyncClient:
"""Create configured HTTP client"""
return httpx.AsyncClient(
timeout=30,
headers={"User-Agent": "Mitsuki/1.0"}
)Provider with Configuration
@Configuration
class HttpClientConfig:
timeout: int = Value("${http.timeout:30}")
base_url: str = Value("${http.base_url}")
@Provider
def http_client(self) -> httpx.AsyncClient:
"""Create client using configuration values"""
return httpx.AsyncClient(
timeout=self.timeout,
base_url=self.base_url
)Multiple Providers
@Configuration
class ClientConfig:
@Provider
def internal_api_client(self) -> httpx.AsyncClient:
"""Client for internal API"""
return httpx.AsyncClient(
timeout=10,
base_url="http://internal-api:8080"
)
@Provider
def external_api_client(self) -> httpx.AsyncClient:
"""Client for external API"""
return httpx.AsyncClient(
timeout=60,
base_url="https://api.external.com"
)
@Provider
def payment_client(self) -> httpx.AsyncClient:
"""Client for payment gateway"""
return httpx.AsyncClient(
timeout=120,
base_url="https://payment-gateway.com"
)Provider Injection
Providers are injected by parameter name:
@Service()
class ApiService:
def __init__(
self,
internal_api_client: httpx.AsyncClient,
external_api_client: httpx.AsyncClient
):
# Providers matched by parameter name
self.internal_client = internal_api_client
self.external_client = external_api_client
async def fetch_internal_data(self):
return await self.internal_client.get("/data")
async def fetch_external_data(self):
return await self.external_client.get("/data")Custom Provider Names
@Configuration
class Config:
@Provider(name="primary_db")
def create_primary_connection(self) -> str:
return "postgresql://primary/db"
@Provider(name="replica_db")
def create_replica_connection(self) -> str:
return "postgresql://replica/db"
@Service()
class DatabaseService:
def __init__(self, primary_db: str, replica_db: str):
# Matched by custom provider names
self.primary = primary_db
self.replica = replica_dbProvider Scopes
@Configuration
class Config:
@Provider(scope="singleton") # Default - one instance
def connection_pool(self) -> ConnectionPool:
return ConnectionPool(size=10)
@Provider(scope="prototype") # New instance each time (limited support)
def temp_file(self) -> TempFile:
return TempFile()Note: For @Provider factory methods, prototype scope currently has limitations. The factory method runs once during initialization.
Environment Variables
Fallback Configuration
Environment variables serve as fallback values when a key isn't defined in application.yml or profile-specific files. This is useful for containerized deployments or CI/CD where modifying config files isn't practical.
# Provide fallback for ${server.port} if not in application.yml
MITSUKI_SERVER_PORT=9000 python app.py
# Provide fallback for ${database.url}
MITSUKI_DATABASE_URL=postgresql://localhost/db python app.py
# Provide fallback for ${app.debug}
MITSUKI_APP_DEBUG=true python app.pyNote: If the key exists in application.yml or application-{profile}.yml, those values take precedence over environment variables.
Naming convention:
- Prefix:
MITSUKI_ - Convert dots to underscores:
server.port→SERVER_PORT - Uppercase:
MITSUKI_SERVER_PORT
Direct Environment Variables
Read environment variables directly:
import os
@Configuration
class Config:
@Provider
def api_key(self) -> str:
# Read directly from environment
return os.getenv("API_KEY", "default-key")
@Provider
def database_url(self) -> str:
# Different variable names per environment
if os.getenv("MITSUKI_PROFILE") == "production":
return os.getenv("PROD_DATABASE_URL")
else:
return "sqlite:///dev.db"Configuration Source Logging
Mitsuki can log where each configuration value is loaded from, helping you debug configuration issues and understand which values are being overridden.
Enable Configuration Source Logging
Add to your application.yml:
logging:
log_config_sources: trueOutput Format
When enabled, Mitsuki displays a color-coded table during startup showing all configuration keys grouped by their source:
Configuration sources:
[default configuration]
┌──────────────────────────────────────────────────────────────────────┐
│database.adapter database.echo │
│database.pool.enabled database.pool.max_overflow │
│database.pool.recycle database.pool.size │
│database.pool.timeout logging.format │
│logging.sqlalchemy server.cors.allowed_origins │
│server.max_body_size server.multipart.max_file_size │
│server.multipart.max_request_size server.workers │
└──────────────────────────────────────────────────────────────────────┘
[application.yml]
┌──────────────────────────────────────────────────────────────────────┐
│logging.level logging.log_config_sources │
│server.access_log server.cors.enabled │
│server.ignore_trailing_slash server.type │
└──────────────────────────────────────────────────────────────────────┘
[environment variable (MITSUKI_SERVER_PORT)]
┌──────────────────────────────────────────────────────────────────────┐
│server.port │
└──────────────────────────────────────────────────────────────────────┘Configuration Sources
Mitsuki tracks configuration from four sources (in order of priority, highest to lowest):
- Profile-specific files -
application-{profile}.yml(highest priority) - Application file -
application.yml - Environment variables -
MITSUKI_*prefixed environment variables (fallback) - Default configuration - Framework defaults from
mitsuki/config/defaults.yml
This priority ensures your configuration files are the source of truth, with environment variables serving as a fallback for containerized deployments or CI/CD environments where file modification isn't practical.
Use Cases
Configuration source logging is useful for:
- Debugging - See which file is providing each value
- Environment verification - Confirm environment variables are being applied
- Configuration auditing - Understand the complete configuration state
- Troubleshooting overrides - Identify why a value differs from expectations
Programmatic Access
You can also access configuration sources programmatically:
from mitsuki.config import get_config
config = get_config()
sources = config.get_config_sources()
for key, source in sources.items():
print(f"{key} loaded from {source}")Security Note
Configuration source logging does not display configuration values, only the keys and their sources. This prevents accidentally logging sensitive information like passwords, API keys, or secrets.
Complete Examples
Application with Full Configuration
from mitsuki import Application, Configuration, Provider, Service, Value
from mitsuki import RestController, GetMapping
import httpx
# 1. Base configuration
@Configuration
class AppConfig:
app_name: str = Value("${app.name:Mitsuki App}")
version: str = Value("${app.version:1.0.0}")
debug: bool = Value("${app.debug:false}")
# 2. Database configuration
@Configuration
class DatabaseConfig:
url: str = Value("${database.url:sqlite:///app.db}")
pool_size: int = Value("${database.pool.size:10}")
pool_timeout: int = Value("${database.pool.timeout:30}")
@Provider
def connection_string(self) -> str:
"""Build complete connection string with pool settings"""
return f"{self.url}?pool_size={self.pool_size}&timeout={self.pool_timeout}"
# 3. HTTP client configuration
@Configuration
class HttpConfig:
timeout: int = Value("${http.timeout:30}")
max_retries: int = Value("${http.max_retries:3}")
base_url: str = Value("${http.base_url:https://api.example.com}")
@Provider
def http_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(
timeout=self.timeout,
base_url=self.base_url,
headers={"User-Agent": "Mitsuki/1.0"}
)
# 4. Service using configuration
@Service()
class ApiService:
def __init__(
self,
app_config: AppConfig,
http_client: httpx.AsyncClient
):
self.config = app_config
self.client = http_client
async def fetch_data(self):
response = await self.client.get("/data")
return response.json()
# 5. Controller
@RestController("/api")
class ApiController:
def __init__(self, service: ApiService):
self.service = service
@GetMapping("/data")
async def get_data(self) -> dict:
return await self.service.fetch_data()
@GetMapping("/info")
async def get_info(self) -> dict:
return {
"app_name": self.service.config.app_name,
"version": self.service.config.version,
"debug": self.service.config.debug
}
# 6. Application
@Application
class MyApp:
port: int = Value("${server.port:8000}")
if __name__ == "__main__":
MyApp.run()application.yml:
app:
name: My Application
version: 1.0.0
debug: false
server:
port: 8000
database:
url: sqlite:///app.db
pool:
size: 10
timeout: 30
http:
timeout: 30
max_retries: 3
base_url: https://api.example.comMulti-Environment Configuration
from mitsuki import Configuration, Profile, Provider, Value
import os
# Shared configuration
@Configuration
class SharedConfig:
app_name: str = Value("${app.name}")
max_upload_size: int = Value("${app.max_upload:10485760}")
# Development
@Configuration
@Profile("development")
class DevConfig:
@Provider
def database_url(self) -> str:
return "sqlite:///dev.db"
@Provider
def log_level(self) -> str:
return "DEBUG"
@Provider
def enable_debug_toolbar(self) -> bool:
return True
# Production
@Configuration
@Profile("production")
class ProdConfig:
@Provider
def database_url(self) -> str:
return os.getenv("DATABASE_URL", "postgresql://localhost/prod")
@Provider
def log_level(self) -> str:
return "WARNING"
@Provider
def enable_debug_toolbar(self) -> bool:
return False
@Service()
class ConfigService:
def __init__(
self,
shared: SharedConfig,
database_url: str,
log_level: str,
enable_debug_toolbar: bool
):
self.app_name = shared.app_name
self.max_upload = shared.max_upload_size
self.db_url = database_url
self.log_level = log_level
self.debug_toolbar = enable_debug_toolbarBest Practices
- Use @Value for simple config - Strings, numbers, booleans
- Use @Provider for complex objects that are reusable - HTTP clients, connection pools
- Group related configuration - One @Configuration per concern
- Provide defaults - Use
${key:default}syntax - Don't commit secrets - Use environment variables for sensitive data
- Use profiles for environments - Different configs per environment
Configuration Validation
@Configuration
class ValidatedConfig:
database_url: str = Value("${database.url}")
api_key: str = Value("${api.key}")
def __post_init__(self):
"""Validate configuration after injection"""
if not self.database_url:
raise ValueError("database.url is required")Next Steps
- Profiles - Environment-specific configuration
- Decorators - Complete decorator reference
- Overview - Framework architecture