Structum Dynaconf (structum-dynaconf)

Documentation Source Code Python 3.11+ License: Apache-2.0

Structum Dynaconf integra Dynaconf per una gestione avanzata, multi-source e type-safe della configurazione.

Feature

Stato

Versione

Stato

Alpha

0.1.0

Namespace

structum_lab.plugins.dynaconf

Core

dynaconf, pydantic


Indice

  1. Cos’è Dynaconf Plugin

  2. Concetti Fondamentali

  3. Quick Start (5 Minuti)

  4. Struttura Directory e Convention

  5. Auto-Discovery vs Manual Loading

  6. Validazione con Pydantic

  7. Environment Variables Override

  8. Persistenza Runtime

  9. Secrets Management

  10. Hot Reload

  11. Advanced: GenericConfigBuilder

  12. Troubleshooting

  13. API Reference


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 .secrets.toml separato (non versionato)

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 (load)

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

config/app/database.toml

database

Nome file (senza .toml)

config/app/api_client.toml

api_client

Underscore preservato

config/app/api-client.toml

api_client

Trattini → underscore

config/models/database.py

database

Deve matchare nome namespace

⚠️ Regole Importanti:

  1. Path relativi: root_path nel provider determina dove inizia la ricerca

  2. Nome namespace: Derivato dal filename, normalizzato (-_, lowercase)

  3. 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 GenericConfigBuilder per quello)

5.3 Confronto

Feature

Auto-Discovery

Manual Load

Manual Builder

Setup Complexity

🟢 Basso

🟡 Medio

🔴 Alto

Convention Required

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.toml

  • File model: config/models/database.py

  • Classe model: DatabaseConfig (deve essere BaseModel)

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: Default STRUCTUM, configurabile nel provider

  • NAMESPACE: 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-memory

  • save() è necessario per persistenza tra riavvii

  • Solo 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:

  1. Watchdog rileva la modifica

  2. Debounce timer aspetta debounce_seconds

  3. Reload avviene: tutti i layer vengono ri-mergiati

  4. 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:

  1. Path corretto?

    provider = DynaconfConfigProvider(root_path="/path/to/project")
    # Verifica che /path/to/project/config/app/ esista
    
  2. Directory config/app/ esiste?

    ls -la config/app/
    # Se non esiste: mkdir -p config/app
    
  3. File hanno estensione .toml?

    # ✅ Corretto
    config/app/database.toml
    
    # ❌ Ignorato
    config/app/database.yaml
    config/app/database.json
    
  4. Permessi 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:

  1. Env var presente?

    echo $STRUCTUM_DATABASE__HOST  # Dovrebbe stampare "new-host.com"
    
  2. Doppio underscore corretto?

    # ❌ Errato (singolo)
    STRUCTUM_DATABASE_HOST=...
    
    # ✅ Corretto (doppio)
    STRUCTUM_DATABASE__HOST=...
    
  3. Namespace match?

    # Se il namespace è "db_client"
    export STRUCTUM_DB_CLIENT__HOST=...  # ✅
    export STRUCTUM_DATABASE__HOST=...   # ❌ (namespace diverso)
    
  4. 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:

  1. Watchdog installato?

    pip show watchdog  # Deve essere installato
    
  2. Hot reload abilitato?

    provider.enable_hot_reload()  # Devi chiamarlo esplicitamente
    
  3. File watched?

    # Debug: vedi cosa viene monitorato
    print(provider.get_watched_files())
    
  4. 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:

  1. File esiste e ha nome corretto?

    ls -la config/.secrets.toml  # Deve esistere esattamente così
    
  2. Namespace match nel file?

    # config/.secrets.toml
    
    # ✅ Corretto (namespace = database)
    [database.auth]
    password = "secret"
    
    # ❌ Errato (namespace mancante)
    [auth]
    password = "secret"
    
  3. 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() (usare auto_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