Structum Dynaconf (structum-dynaconf)¶
Structum Dynaconf integra Dynaconf per una gestione avanzata, multi-source e type-safe della configurazione.
Feature |
Stato |
Versione |
|---|---|---|
Stato |
Alpha |
0.1.0 |
Namespace |
|
|
Core |
|
Indice¶
1. Cos’è Dynaconf Plugin¶
structum-dynaconf è un configuration engine che porta capacità enterprise in Structum:
1.1 Problema Risolto¶
Prima (senza plugin):
# ❌ Configurazione hard-coded
DB_HOST = "localhost"
DB_PORT = 5432
# ❌ Nessuna separazione dev/prod
# ❌ Secrets in codice
# ❌ Zero validazione tipi
Dopo (con plugin):
# ✅ Configurazione multi-layer
# Layer 1: config/app/database.toml (defaults)
# Layer 2: .secrets.toml (passwords)
# Layer 3: STRUCTUM_DATABASE__HOST env var (infra)
# Layer 4: ~/.structum/database_saved.json (user prefs)
config = get_config()
db_host = config.get("database.host") # Risolve layer in ordine priorità
1.2 Caratteristiche Principali¶
Feature |
Descrizione |
|---|---|
Multi-Layer Configuration |
Defaults → Secrets → Env Vars → Runtime Overrides |
Strong Typing |
Validazione automatica con Pydantic Models |
Convention over Configuration |
Auto-discover basato su struttura directory |
Hot Reload |
Ricarica automatica su modifica file (watchdog) |
Secrets Isolation |
File |
Environment Parity |
Stessa config per dev/staging/prod con override ENV |
2. Concetti Fondamentali¶
2.1 Namespace¶
Un namespace è un contenitore logico per configurazione correlata.
Esempio:
# config/app/database.toml
[default]
host = "localhost"
port = 5432
pool_size = 10
Il namespace qui è "database". Lo usi così:
config.get("database.host") # "localhost"
config.get("database.port") # 5432
config.get("database.pool_size") # 10
2.2 Configuration Layers (Priorità)¶
Quando richiedi config.get("database.host"), il plugin cerca in ordine:
┌─────────────────────────────────────────────────┐
│ 1. Runtime Layer (MASSIMA PRIORITÀ) │
│ ~/.structum/database_saved.json │
│ (Valori impostati via config.set()) │
├─────────────────────────────────────────────────┤
│ 2. Environment Variables Layer │
│ STRUCTUM_DATABASE__HOST=prod.db.com │
├─────────────────────────────────────────────────┤
│ 3. Secrets Layer │
│ config/.secrets.toml │
│ (Password, API keys) │
├─────────────────────────────────────────────────┤
│ 4. Application Layer │
│ config/app/database.toml │
│ (Defaults dichiarati dallo sviluppatore) │
└─────────────────────────────────────────────────┘
Il primo valore trovato vince.
2.3 Configuration Builder¶
Un Builder è un oggetto che dice al plugin:
Cosa caricare: Quali file TOML leggere
Come chiamarlo: Il nome del namespace
Come validarlo: (Opzionale) Lo schema Pydantic
Tre modalità di creazione Builder:
Modalità |
Quando Usarla |
|---|---|
Auto-Discovery |
Setup standard con convenzioni (90% dei casi) |
Shortcut ( |
File singolo custom fuori convenzione |
Manual Builder |
Logica complessa (multi-file merge, custom validation) |
3. Quick Start (5 Minuti)¶
Step 1: Installazione¶
pip install structum-dynaconf
Step 2: Struttura Directory¶
mkdir -p config/app config/models
Step 3: Creare Configurazione¶
File: config/app/database.toml
[default]
host = "localhost"
port = 5432
database = "myapp_dev"
pool_size = 5
[default.auth]
user = "dev_user"
# ⚠️ NON mettere password qui!
[production]
host = "db.prod.example.com"
database = "myapp_prod"
pool_size = 50
Step 4: Setup Plugin¶
File: src/main.py
from structum_lab.config import set_config_provider, get_config
from structum_lab.plugins.dynaconf import DynaconfConfigProvider
def setup_config():
"""
Inizializza il configuration system.
Chiamare PRIMA di qualsiasi altro codice.
"""
provider = DynaconfConfigProvider(
root_path=".", # Project root (dove si trova config/)
env_prefix="STRUCTUM", # Prefix per env vars
environments=True, # Supporta [default], [production], etc.
current_env="development" # O da ENV: os.getenv("APP_ENV", "development")
)
# 🔍 Auto-discover: scansiona config/app/*.toml
provider.auto_discover()
# Registra come provider globale
set_config_provider(provider)
if __name__ == "__main__":
setup_config()
# Ora puoi usare la config ovunque
config = get_config()
db_host = config.get("database.host")
db_port = config.get("database.port")
print(f"Connessione a {db_host}:{db_port}")
Step 5: Test¶
# Default (development)
python src/main.py
# Output: Connessione a localhost:5432
# Override con env var
export STRUCTUM_DATABASE__HOST=10.0.0.50
python src/main.py
# Output: Connessione a 10.0.0.50:5432
# Cambia environment
export APP_ENV=production
python src/main.py
# Output: Connessione a db.prod.example.com:5432
4. Struttura Directory e Convention¶
4.1 Layout Raccomandato¶
my_project/
├── config/
│ ├── app/ # Configurazione applicazione (versionata)
│ │ ├── database.toml # namespace: "database"
│ │ ├── api_client.toml # namespace: "api_client"
│ │ └── features.toml # namespace: "features"
│ │
│ ├── models/ # Pydantic schemas (opzionale, versionato)
│ │ ├── database.py # class DatabaseConfig(BaseModel)
│ │ └── api_client.py # class ApiClientConfig(BaseModel)
│ │
│ └── .secrets.toml # Secrets (NON versionare, .gitignore)
│
├── ~/.structum/ # User runtime config (auto-creato)
│ ├── database_saved.json # Modifiche runtime di "database"
│ └── api_client_saved.json # Modifiche runtime di "api_client"
│
└── src/
└── main.py
4.2 Convenzioni Auto-Discovery¶
File |
Namespace Rilevato |
Descrizione |
|---|---|---|
|
|
Nome file (senza |
|
|
Underscore preservato |
|
|
Trattini → underscore |
|
|
Deve matchare nome namespace |
⚠️ Regole Importanti:
Path relativi:
root_pathnel provider determina dove inizia la ricercaNome namespace: Derivato dal filename, normalizzato (
-→_, lowercase)Model matching: Se esiste
config/models/{namespace}.py, viene usato per validazione
4.3 File .gitignore Raccomandato¶
# Secrets (CRITICO)
config/.secrets.toml
**/.secrets.toml
# User runtime config
.structum/
**/*_saved.json
# Environment-specific (se usi override locali)
config/local.toml
5. Auto-Discovery vs Manual Loading¶
5.1 Auto-Discovery (Metodo Raccomandato)¶
Quando usarlo:
Setup standard con file in
config/app/Progetti nuovi o refactoring verso convenzioni
Vuoi ridurre boilerplate
Come funziona:
provider = DynaconfConfigProvider(root_path=".")
provider.auto_discover()
# Dopo questa chiamata:
# ✅ Tutti i file in config/app/*.toml sono registrati
# ✅ I modelli in config/models/*.py sono linked automaticamente
# ✅ .secrets.toml (se esiste) è mergiato in ogni namespace
⚠️ Nota Critica:
auto_discover() è esplicito, non automatico. Devi chiamarlo manualmente dopo aver creato il provider. Questo ti dà controllo su quando avviene la scansione del filesystem (importante per testing/mocking).
Esempio con gestione errori:
from structum_lab.plugins.dynaconf import DynaconfConfigProvider
from structum_lab.plugins.dynaconf.exceptions import (
ConfigDiscoveryError,
ConfigValidationError
)
provider = DynaconfConfigProvider(root_path=".")
try:
discovered = provider.auto_discover()
print(f"✅ Scoperti {len(discovered)} namespace:")
for ns in discovered:
print(f" - {ns}")
except ConfigDiscoveryError as e:
print(f"❌ Errore discovery: {e}")
# Directory config/ mancante o permessi insufficienti
except ConfigValidationError as e:
print(f"❌ Validazione fallita: {e}")
# File TOML malformato o schema Pydantic non rispettato
5.2 Manual Loading (Shortcut)¶
Quando usarlo:
File legacy fuori convenzione
Config temporanea per testing
File di terze parti da caricare
Esempio:
provider = DynaconfConfigProvider(root_path=".")
# Carica file custom
provider.load(
namespace="legacy_system",
config_file="external/old_config.toml"
)
# Ora accessibile come:
config = get_config()
value = config.get("legacy_system.some_key")
⚠️ Limitazioni:
Non fa auto-match con Pydantic models
Non supporta multi-file merge (usa
GenericConfigBuilderper quello)
5.3 Confronto¶
Feature |
Auto-Discovery |
Manual Load |
Manual Builder |
|---|---|---|---|
Setup Complexity |
🟢 Basso |
🟡 Medio |
🔴 Alto |
Convention Required |
Sì |
No |
No |
Pydantic Auto-Link |
✅ |
❌ |
✅ (manuale) |
Multi-File Merge |
❌ |
❌ |
✅ |
Custom Env Prefix |
❌ (usa globale) |
❌ |
✅ |
6. Validazione con Pydantic¶
6.1 Perché Validare?¶
Senza validazione:
# ❌ Runtime errors nascosti
pool_size = config.get("database.pool_size") # Cosa torna? str? int? None?
connections = [None] * pool_size # 💥 TypeError se pool_size è stringa
Con validazione:
# ✅ Garanzie statiche
pool_size: int = config.get("database.pool_size") # Type-safe
# Se TOML contiene pool_size = "invalid", fallisce subito all'init
6.2 Creare uno Schema Pydantic¶
File: config/models/database.py
from pydantic import BaseModel, Field, field_validator
from typing import Literal
class AuthConfig(BaseModel):
"""Autenticazione database."""
user: str = Field(..., min_length=1, description="Username")
password: str = Field(..., min_length=8, description="Password (da secrets)")
class DatabaseConfig(BaseModel):
"""Configurazione database principale."""
host: str = Field(default="localhost", description="Hostname DB")
port: int = Field(default=5432, ge=1, le=65535, description="Porta DB")
database: str = Field(..., min_length=1, description="Nome database")
pool_size: int = Field(default=10, ge=1, le=1000, description="Connessioni pool")
auth: AuthConfig
# Validatori custom
@field_validator('host')
@classmethod
def validate_production_host(cls, v, info):
"""In production, blocca localhost."""
if info.context and info.context.get('env') == 'production':
if v in ('localhost', '127.0.0.1'):
raise ValueError("localhost non permesso in production")
return v
@field_validator('pool_size')
@classmethod
def validate_pool_size(cls, v, info):
"""Pool size deve essere proporzionale all'environment."""
env = info.context.get('env', 'development') if info.context else 'development'
if env == 'production' and v < 20:
raise ValueError(f"Pool size troppo basso per production: {v} (min 20)")
return v
6.3 Linking Automatico (Auto-Discovery)¶
Se segui la convenzione:
File config:
config/app/database.tomlFile model:
config/models/database.pyClasse model:
DatabaseConfig(deve essereBaseModel)
Il plugin linka automaticamente:
provider = DynaconfConfigProvider(root_path=".")
provider.auto_discover() # Trova database.toml e DatabaseConfig automaticamente
# La validazione è ora attiva
config = get_config()
port = config.get("database.port") # Garantito essere int tra 1-65535
6.4 Linking Manuale¶
Se non usi auto-discovery o hai strutture custom:
from config.models.database import DatabaseConfig
from structum_lab.plugins.dynaconf import GenericConfigBuilder
builder = GenericConfigBuilder(
name="database",
files=["config/app/database.toml"],
model=DatabaseConfig # Link esplicito
)
provider.register_builder("database", builder)
6.5 Modalità Validazione¶
Strict Mode (default):
# ❌ Se TOML ha chiavi extra non nel model, fallisce
[default]
host = "localhost"
unknown_key = "value" # 💥 ValidationError: Extra fields not permitted
Lenient Mode:
class DatabaseConfig(BaseModel):
model_config = {"extra": "ignore"} # Ignora campi extra
host: str
port: int
Warn Mode (custom):
import logging
class DatabaseConfig(BaseModel):
@model_validator(mode='before')
@classmethod
def warn_extra_fields(cls, values):
allowed = set(cls.model_fields.keys())
extra = set(values.keys()) - allowed
if extra:
logging.warning(f"Chiavi extra ignorate: {extra}")
return values
7. Environment Variables Override¶
7.1 Convenzione di Naming¶
Formula:
{ENV_PREFIX}__{NAMESPACE}__{PATH__TO__KEY}
Componenti:
ENV_PREFIX: DefaultSTRUCTUM, configurabile nel providerNAMESPACE: Nome del namespace (es.DATABASE)PATH__TO__KEY: Percorso dot-notation, con.→__
⚠️ Importante: Doppio underscore __ separa i livelli, singolo _ fa parte del nome.
7.2 Esempi¶
Config originale:
# config/app/database.toml
[default]
host = "localhost"
port = 5432
[default.pool]
min_size = 5
max_size = 20
Override via ENV:
# Singolo valore
export STRUCTUM_DATABASE__HOST=prod.db.com
export STRUCTUM_DATABASE__PORT=3306
# Valore annidato
export STRUCTUM_DATABASE__POOL__MAX_SIZE=100
# Namespace con underscore (api_client)
export STRUCTUM_API_CLIENT__TIMEOUT=60
Verifica:
config = get_config()
print(config.get("database.host")) # "prod.db.com" (ENV vince)
print(config.get("database.pool.max_size")) # 100 (ENV vince)
7.3 Tipi Supportati¶
Scalari:
export STRUCTUM_DATABASE__PORT=5432 # int
export STRUCTUM_DATABASE__ENABLED=true # bool
export STRUCTUM_DATABASE__TIMEOUT=30.5 # float
export STRUCTUM_DATABASE__HOST=localhost # str
Liste (separatore: virgola):
export STRUCTUM_DATABASE__HOSTS=db1.com,db2.com,db3.com
# In Python:
hosts = config.get("database.hosts") # ["db1.com", "db2.com", "db3.com"]
JSON Embedded (per strutture complesse):
export STRUCTUM_DATABASE__AUTH='{"user":"admin","password":"secret"}'
# In Python:
auth = config.get("database.auth") # {"user": "admin", "password": "secret"}
7.4 Custom Env Prefix¶
# Usa MYAPP_ invece di STRUCTUM_
provider = DynaconfConfigProvider(
root_path=".",
env_prefix="MYAPP"
)
# Ora le env vars sono:
# MYAPP_DATABASE__HOST=...
# MYAPP_API_CLIENT__TIMEOUT=...
7.5 Disabilitare Env Vars (Testing)¶
provider = DynaconfConfigProvider(
root_path=".",
load_env_vars=False # Ignora completamente le ENV
)
8. Persistenza Runtime¶
8.1 Come Funziona¶
Quando chiami config.set(), il valore viene salvato nel Runtime Layer:
config = get_config()
# Modifica in-memory
config.set("database.host", "10.0.0.100")
# Salva su disco (opzionale ma raccomandato)
config.save()
# File creato: ~/.structum/database_saved.json
# {
# "host": "10.0.0.100"
# }
⚠️ Nota Critica:
set()modifica solo la cache in-memorysave()è necessario per persistenza tra riavviiSolo le chiavi modificate via
set()finiscono nel file*_saved.json
8.2 Location dei File Runtime¶
Default:
~/.structum/
├── database_saved.json
├── api_client_saved.json
└── features_saved.json
Custom path:
provider = DynaconfConfigProvider(
root_path=".",
runtime_config_dir="/var/lib/myapp/config"
)
8.3 Esempio Completo: Salvare Preferenze Utente¶
from structum_lab.config import get_config
def save_user_preferences(theme: str, font_size: int):
"""Salva preferenze UI persistenti."""
config = get_config()
try:
# Modifica valori
config.set("ui.theme", theme)
config.set("ui.font_size", font_size)
# Commit su disco
config.save()
print("✅ Preferenze salvate")
except Exception as e:
print(f"❌ Errore salvataggio: {e}")
# In caso di fallimento, rollback (ricarica da disco)
config.reload()
# Uso
save_user_preferences("dark_mode", 14)
# Al prossimo avvio:
config = get_config()
theme = config.get("ui.theme") # "dark_mode" (dal file saved.json)
8.4 Thread Safety¶
⚠️ Limitazione Nota:
save() NON è thread-safe di default. In ambienti multi-threaded:
Soluzione 1: Lock Applicativo
import threading
_config_lock = threading.Lock()
def safe_save_config():
with _config_lock:
config = get_config()
config.set("key", "value")
config.save()
Soluzione 2: File Locking (Unix)
import fcntl
def atomic_save_config():
config = get_config()
config.set("key", "value")
runtime_file = Path.home() / ".structum" / "namespace_saved.json"
with open(runtime_file, "r+") as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX) # Lock esclusivo
config.save()
fcntl.flock(f.fileno(), fcntl.LOCK_UN) # Unlock
9. Secrets Management¶
9.1 File .secrets.toml¶
Location:
config/.secrets.toml
⚠️ CRITICO: Aggiungi a .gitignore:
config/.secrets.toml
**/.secrets.toml
Contenuto esempio:
# config/.secrets.toml
[database.auth]
password = "SuperSecretPassword123"
[api_client]
api_key = "sk-proj-abc123xyz"
api_secret = "shhh-very-secret"
9.2 Auto-Merge nei Namespace¶
Il plugin mergia automaticamente .secrets.toml con i file config/app/*.toml:
Prima del merge:
# config/app/database.toml
[default.auth]
user = "admin"
# password mancante
# config/.secrets.toml
[database.auth]
password = "secret123"
Dopo il merge (in-memory):
config.get("database.auth.user") # "admin" (da database.toml)
config.get("database.auth.password") # "secret123" (da .secrets.toml)
9.3 Validazione Secrets con Pydantic¶
from pydantic import BaseModel, Field, SecretStr
class DatabaseConfig(BaseModel):
host: str
port: int
class AuthConfig(BaseModel):
user: str
password: SecretStr = Field(..., min_length=12) # Valida lunghezza minima
auth: AuthConfig
# Se password in .secrets.toml è < 12 caratteri, fallisce all'init
Accesso a SecretStr:
password_secret = config.get("database.auth.password") # SecretStr object
password_plain = password_secret.get_secret_value() # str effettiva
9.4 Permessi File (Security Best Practice)¶
from pathlib import Path
import os
def secure_secrets_file():
"""Assicura che .secrets.toml abbia permessi corretti."""
secrets_file = Path("config/.secrets.toml")
if secrets_file.exists():
# Imposta permessi: solo owner può leggere/scrivere
os.chmod(secrets_file, 0o600) # rw-------
# Verifica
stat_info = secrets_file.stat()
if stat_info.st_mode & 0o077: # Se altri utenti hanno accesso
raise RuntimeError(
f"⚠️ SECURITY: {secrets_file} ha permessi insicuri! "
f"Eseguire: chmod 600 {secrets_file}"
)
9.5 Alternative a File Locale¶
Per produzione, considera:
Opzione 1: Environment Variables
# Più sicuro di .secrets.toml versionato
export STRUCTUM_DATABASE__AUTH__PASSWORD=secret123
Opzione 2: HashiCorp Vault
# Richiede structum-vault plugin
from structum_vault import VaultProvider
vault_provider = VaultProvider(
url="https://vault.company.com",
token=os.getenv("VAULT_TOKEN"),
mount_point="secret",
path="myapp/database"
)
Opzione 3: Kubernetes Secrets
# k8s-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-db-secrets
type: Opaque
data:
password: U3VwZXJTZWNyZXQxMjM= # Base64 encoded
Poi monta come env var nel Pod.
10. Hot Reload¶
10.1 Setup¶
Prerequisito:
pip install watchdog
Attivazione:
from structum_lab.plugins.dynaconf import DynaconfConfigProvider
provider = DynaconfConfigProvider(root_path=".")
provider.auto_discover()
# Abilita watching su tutti i file registrati
provider.enable_hot_reload(
watch_secrets=True, # Include .secrets.toml
debounce_seconds=2.0 # Delay prima di reload (evita reload multipli)
)
10.2 Comportamento¶
Quando un file TOML cambia:
Watchdog rileva la modifica
Debounce timer aspetta
debounce_secondsReload avviene: tutti i layer vengono ri-mergiati
Callback (opzionale) viene eseguito
⚠️ Importante:
Il reload è async (non blocca il thread principale)
Cache in-memory viene invalidata
Connessioni/risorse esistenti non vengono ricreate automaticamente
10.3 Callback Custom¶
def on_config_reloaded(namespace: str, changed_keys: list[str]):
"""
Chiamato dopo ogni reload.
Args:
namespace: Quale config è cambiata (es. "database")
changed_keys: Lista di chiavi modificate (es. ["host", "port"])
"""
print(f"🔄 Config '{namespace}' ricaricata")
print(f" Chiavi cambiate: {changed_keys}")
# Esempio: riconnetti al DB se config DB è cambiata
if namespace == "database" and "host" in changed_keys:
reconnect_database()
provider.enable_hot_reload(callback=on_config_reloaded)
10.4 Disabilitare Hot Reload¶
provider.disable_hot_reload()
10.5 Limiti e Gotchas¶
❌ Non funziona per:
File eliminati (solo modifiche)
Permessi filesystem insufficienti
File su network filesystems (NFS/SMB) - polling limitato
⚠️ Race conditions: Se l’app legge config durante un reload:
# Thread 1 (main)
host = config.get("database.host") # "old-host.com"
# Thread 2 (watchdog)
# [Reload avviene QUI - file cambiato]
# Thread 1 (continua)
port = config.get("database.port") # Nuovo valore! (inconsistenza)
Soluzione:
# Leggi config atomicamente
db_config = config.get("database") # Snapshot intero namespace
host = db_config["host"]
port = db_config["port"]
11. Advanced: GenericConfigBuilder¶
11.1 Quando Usarlo¶
Usa GenericConfigBuilder se serve:
Merge di multipli file TOML in un namespace
Env prefix custom per-namespace (override del globale)
Validazione custom oltre Pydantic
Caricamento lazy (caricare config solo quando richiesta)
11.2 Esempio: Multi-File Merge¶
Scenario: Database config diviso in base + overlay
from structum_lab.plugins.dynaconf import GenericConfigBuilder
builder = GenericConfigBuilder(
name="database",
files=[
"config/base_database.toml", # Defaults globali
"config/overlay_database.toml" # Override specifici
],
merge_strategy="deep" # Deep merge (default), "shallow" per last-wins
)
provider.register_builder("database", builder)
File esempio:
# config/base_database.toml
[default]
host = "localhost"
port = 5432
pool_size = 10
# config/overlay_database.toml
[default]
host = "override-host.com" # Sovrascrive base
# port e pool_size vengono ereditati da base
Risultato merge:
config.get("database.host") # "override-host.com"
config.get("database.port") # 5432 (da base)
config.get("database.pool_size") # 10 (da base)
11.3 Custom Env Prefix per Namespace¶
# Globale usa STRUCTUM_, ma "legacy" usa OLDAPP_
builder = GenericConfigBuilder(
name="legacy",
files=["config/legacy.toml"],
env_prefix="OLDAPP" # Override per questo namespace
)
# Ora accetti:
# OLDAPP_LEGACY__TIMEOUT=60
# invece di STRUCTUM_LEGACY__TIMEOUT
11.4 Validazione Custom¶
from pydantic import BaseModel, field_validator
class CustomDatabaseConfig(BaseModel):
host: str
port: int
@field_validator("host", "port")
@classmethod
def validate_connectivity(cls, v, info):
"""Valida che host:port sia raggiungibile."""
import socket
if info.field_name == "port":
return v # Valida solo quando abbiamo entrambi
# Custom logic: prova connessione
host = v
port = info.data.get("port", 5432)
try:
sock = socket.create_connection((host, port), timeout=2)
sock.close()
except Exception:
raise ValueError(f"Host {host}:{port} non raggiungibile")
return v
builder = GenericConfigBuilder(
name="database",
files=["config/database.toml"],
model=CustomDatabaseConfig
)
11.5 API Completa¶
class GenericConfigBuilder:
def __init__(
self,
name: str, # Namespace unico
files: list[str | Path], # File TOML da caricare
model: type[BaseModel] | None = None, # Schema Pydantic
env_prefix: str | None = None, # Override globale
merge_strategy: Literal["deep", "shallow"] = "deep",
validate_on_access: bool = False, # Valida ogni get()
lazy_load: bool = False # Carica solo al primo uso
):
...
def load(self) -> dict[str, Any]:
"""Carica e mergia tutti i file."""
...
def validate(self, data: dict) -> dict:
"""Valida data contro il model."""
...
def add_file(self, path: str | Path) -> None:
"""Aggiunge file dopo init."""
...
def reload(self) -> None:
"""Ricarica tutti i file (invalida cache)."""
12. Troubleshooting¶
12.1 “Config provider non inizializzato”¶
Errore:
ConfigNotInitializedError: Config provider non inizializzato. Chiamare set_config_provider() prima di get_config().
Causa: get_config() chiamato prima di setup.
Fix:
# ❌ Ordine sbagliato
config = get_config() # Fallisce
setup_config()
# ✅ Ordine corretto
setup_config()
config = get_config()
12.2 “Auto-discovery non trova file”¶
Sintomo: provider.auto_discover() ritorna lista vuota.
Checklist:
Path corretto?
provider = DynaconfConfigProvider(root_path="/path/to/project") # Verifica che /path/to/project/config/app/ esistaDirectory config/app/ esiste?
ls -la config/app/ # Se non esiste: mkdir -p config/appFile hanno estensione .toml?
# ✅ Corretto config/app/database.toml # ❌ Ignorato config/app/database.yaml config/app/database.jsonPermessi di lettura?
chmod 644 config/app/*.toml
12.3 “Validazione Pydantic fallisce”¶
Errore:
ValidationError: 1 validation error for DatabaseConfig
port
value is not a valid integer (type=type_error.integer)
Debug:
# Stampa config raw prima della validazione
import toml
raw_data = toml.load("config/app/database.toml")
print("Raw TOML:", raw_data)
# Verifica che il tipo sia corretto
print(type(raw_data["default"]["port"])) # Dovrebbe essere int, non str
Fix comune: TOML quote vs no quote
# ❌ Errato (stringa)
port = "5432"
# ✅ Corretto (intero)
port = 5432
12.4 “Env var non sovrascrive valore”¶
Sintomo:
export STRUCTUM_DATABASE__HOST=new-host.com
python app.py
# Ancora usa "localhost" dal TOML
Checklist:
Env var presente?
echo $STRUCTUM_DATABASE__HOST # Dovrebbe stampare "new-host.com"Doppio underscore corretto?
# ❌ Errato (singolo) STRUCTUM_DATABASE_HOST=... # ✅ Corretto (doppio) STRUCTUM_DATABASE__HOST=...Namespace match?
# Se il namespace è "db_client" export STRUCTUM_DB_CLIENT__HOST=... # ✅ export STRUCTUM_DATABASE__HOST=... # ❌ (namespace diverso)Provider carica env vars?
provider = DynaconfConfigProvider( load_env_vars=True # Default, ma verifica )
12.5 “Hot reload non funziona”¶
Sintomo: Modifico config/app/database.toml ma config non si aggiorna.
Checklist:
Watchdog installato?
pip show watchdog # Deve essere installatoHot reload abilitato?
provider.enable_hot_reload() # Devi chiamarlo esplicitamenteFile watched?
# Debug: vedi cosa viene monitorato print(provider.get_watched_files())Filesystem supportato?
# NFS/SMB hanno polling limitato # Soluzione: usa filesystem locale o polling manuale
12.6 “Secrets non vengono caricati”¶
Sintomo: config.get("database.auth.password") ritorna None.
Checklist:
File esiste e ha nome corretto?
ls -la config/.secrets.toml # Deve esistere esattamente cosìNamespace match nel file?
# config/.secrets.toml # ✅ Corretto (namespace = database) [database.auth] password = "secret" # ❌ Errato (namespace mancante) [auth] password = "secret"Provider configurato per caricare secrets?
provider = DynaconfConfigProvider( load_secrets=True # Default True, ma verifica )
13. API Reference¶
13.1 DynaconfConfigProvider¶
class DynaconfConfigProvider:
"""
Provider configuration engine basato su Dynaconf.
"""
def __init__(
self,
root_path: str | Path = ".",
env_prefix: str = "STRUCTUM",
environments: bool = True,
current_env: str = "development",
load_env_vars: bool = True,
load_secrets: bool = True,
runtime_config_dir: str | Path | None = None
):
"""
Args:
root_path: Directory root del progetto (dove si trova config/)
env_prefix: Prefisso per environment variables
environments: Supporta [default], [production], etc in TOML
current_env: Environment attivo ("development", "production", ...)
load_env_vars: Carica environment variables come override
load_secrets: Carica config/.secrets.toml
runtime_config_dir: Path per *_saved.json (default: ~/.structum)
"""
def auto_discover(
self,
app_dir: str = "config/app",
models_dir: str = "config/models"
) -> list[str]:
"""
Scansiona directory e registra automaticamente namespace.
Args:
app_dir: Directory con file .toml (relativo a root_path)
models_dir: Directory con modelli Pydantic (relativo a root_path)
Returns:
Lista di namespace scoperti
Raises:
ConfigDiscoveryError: Se directory non esistono o non leggibili
"""
def load(
self,
namespace: str,
config_file: str | Path
) -> None:
"""
Carica singolo file in un namespace (shortcut).
Args:
namespace: Nome del namespace (es. "legacy_system")
config_file: Path al file TOML
Raises:
FileNotFoundError: Se config_file non esiste
ConfigLoadError: Se parsing TOML fallisce
"""
def register_builder(
self,
namespace: str,
builder: GenericConfigBuilder
) -> None:
"""
Registra builder custom per namespace.
Args:
namespace: Nome del namespace
builder: Istanza di GenericConfigBuilder
Raises:
ConfigError: Se namespace già registrato
"""
def enable_hot_reload(
self,
watch_secrets: bool = True,
debounce_seconds: float = 2.0,
callback: Callable[[str, list[str]], None] | None = None
) -> None:
"""
Abilita ricaricamento automatico su modifica file.
Args:
watch_secrets: Include .secrets.toml nel watching
debounce_seconds: Delay prima di reload (evita reload multipli)
callback: Funzione chiamata dopo reload
Requires:
watchdog package installato
"""
def disable_hot_reload(self) -> None:
"""Disabilita watching dei file."""
def get_watched_files(self) -> list[Path]:
"""Ritorna lista di file attualmente monitorati."""
13.2 GenericConfigBuilder¶
class GenericConfigBuilder:
"""
Builder avanzato per configurazioni complesse.
"""
def __init__(
self,
name: str,
files: list[str | Path],
model: type[BaseModel] | None = None,
env_prefix: str | None = None,
merge_strategy: Literal["deep", "shallow"] = "deep",
validate_on_access: bool = False,
lazy_load: bool = False
):
"""
Args:
name: Namespace unico
files: Lista di file TOML da mergeare
model: Schema Pydantic per validazione
env_prefix: Override del prefisso ENV globale
merge_strategy: "deep" (ricorsivo) o "shallow" (last-wins)
validate_on_access: Valida ogni get() (overhead performance)
lazy_load: Carica file solo al primo accesso
"""
def load(self) -> dict[str, Any]:
"""Carica e mergia file."""
def validate(self, data: dict) -> dict:
"""Valida data contro il model."""
...
def add_file(self, path: str | Path, priority: int = 0) -> None:
"""
Aggiunge file dinamicamente.
Args:
path: Path al file TOML
priority: Ordine di merge (più alto = più priorità)
"""
def reload(self) -> None:
"""Ricarica tutti i file (invalida cache)."""
13.3 Eccezioni¶
from structum_lab.plugins.dynaconf.exceptions import (
ConfigError, # Base exception
ConfigDiscoveryError, # auto_discover() fallito
ConfigLoadError, # Parsing TOML fallito
ConfigValidationError, # Pydantic validation fallita
ConfigPersistenceError, # save() fallito
NamespaceNotFoundError # Namespace non registrato
)
Changelog¶
v2.1.0 (2025-01-11)¶
📝 Documentazione: revisione completa con esempi funzionanti
🐛 Fix: chiarito comportamento auto_discover() (esplicito, non automatico)
✨ Feature: aggiunto
get_watched_files()per debugging hot reload🔒 Security: aggiunta validazione permessi per .secrets.toml
v2.0.0 (2024-11-15)¶
✨ Feature: introdotto GenericConfigBuilder
⚡ Performance: lazy loading dei namespace
🔄 Breaking: rimosso metodo
load_all()(usareauto_discover())
v1.5.0 (2024-09-01)¶
✨ Feature: hot reload con watchdog
✨ Feature: secrets management con .secrets.toml
v0.1.0 (2025-01-12)¶
🎉 Alpha Release
Licenza¶
Apache 2.0 License - Copyright (c) 2025 PythonWoods
Maintainer: Structum Plugins Team
Repository: https://github.com/structum-lab/structum-dynaconf
Issues: https://github.com/structum-lab/structum-dynaconf/issues
Documentazione Core: https://docs.structum-lab.io/config