Modulo Configurazione (structum.config)

Documentation Source Code License: Apache-2.0

Facade e Interfacce per il sistema di gestione della configurazione unificata.

Feature

Dettagli

Namespace

structum_lab.config

Ruolo

Configuration Facade


Indice

  1. Architettura e Pattern

  2. API Pubblica

  3. Provider Implementati

  4. Setup e Configurazione

  5. Gestione Errori

  6. Sicurezza e Best Practices

  7. API Reference Completa

  8. Estendere il Sistema


1. Architettura e Pattern

1.1 Principio: Dependency Injection per la Configurazione

Structum implementa il Strategy Pattern per disaccoppiare la business logic dalla sorgente di configurazione:

┌─────────────────────────────────────────┐
│   Application Code                      │
│   config.get("database.host")           │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│   ConfigFacade (Singleton)              │
│   • Risolve dot-notation (a.b.c)        │
│   • Gestisce layer priority             │
│   • Cache e validazione                 │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│   ConfigProvider (Interface)            │
│   ┌───────────┬──────────┬────────────┐ │
│   │ JSON      │ Dynaconf │ Custom     │ │
│   │ Provider  │ Provider │ Provider   │ │
│   └───────────┴──────────┴────────────┘ │
└─────────────────────────────────────────┘

1.2 Vantaggi Architetturali

Beneficio

Descrizione

Testabilità

Mock del provider senza modificare application code

Zero Refactoring

Switch da file locali a Vault/Consul senza code changes

Ambienti Multipli

Provider diversi per dev/staging/production

Type Safety

Validazione opzionale con Pydantic (provider-specific)

1.3 Layer di Configurazione

I valori vengono risolti con precedenza (dal più alto al più basso):

  1. Runtime Layerconfig.set() chiamato dall’applicazione

  2. Environment Layer → Variabili d’ambiente (STRUCTUM_*)

  3. User Layer → File persistente utente (~/.structum/config.json)

  4. Application Layer → File di progetto (config/settings.toml)

  5. Defaults Layer → Valori hard-coded nel codice


2. API Pubblica

2.1 Inizializzazione

from structum_lab.config import get_config, set_config_provider, JSONConfigProvider

# Setup del provider (una sola volta all'avvio)
provider = JSONConfigProvider() # Default: usa ~/.structum/config.json
set_config_provider(provider)

# Accesso al singleton
config = get_config()

2.2 Lettura con Dot-Notation

Il metodo get() supporta la navigazione di strutture nested:

# Struttura dati ipotetica:
# {
#   "database": {
#     "primary": {
#       "host": "db.example.com",
#       "port": 5432
#     }
#   }
# }

# Accesso flat
log_level = config.get("log_level", default="INFO")

# Accesso nested (dot-notation)
db_host = config.get("database.primary.host", default="localhost")
db_port = config.get("database.primary.port", default=5432)

# Type hints (opzionale, dipende dall'uso)
timeout: int = config.get("network.timeout", default=30)

⚠️ Nota sui Default:
Il valore default viene restituito se:

  • La chiave non esiste

  • Un nodo intermedio nel path non è un dict

  • Si verifica un errore di tipo (es. "database" è una stringa invece di un dict)

2.3 Scrittura e Persistenza

# Modifica semplice
config.set("ui.theme", "dark_mode")

# Modifica nested
config.set("database.primary.max_connections", 100)

# IMPORTANTE: Salvataggio esplicito
config.save()  # Persiste su disco (~/.structum/config.json)

⚠️ Comportamento di set():

  • set() modifica solo la cache in-memory

  • save() è richiesto per rendere le modifiche persistenti (nel Core JSONProvider)

  • Solo i valori modificati via set() vengono salvati nel layer utente

Esempio completo con gestione errori:

from structum_lab.config import get_config

config = get_config()

try:
    # Lettura sicura
    db_host = config.get("database.host", default="localhost")
    
    # Modifica 
    config.set("database.max_connections", 150)
    
    # Commit su disco
    config.save()
    
except Exception as e:
    # Errore di I/O durante save()
    print(f"Impossibile salvare: {e}")

2.4 Metodi di Utilità

# Verifica esistenza chiave
if config.has("feature_flags.new_ui"):
    # ...

# Ricarica da sorgente (utile se il file cambia su disco)
config.reload()

3. Provider Implementati

3.1 Confronto Provider

Feature

JSONConfigProvider

DynaconfProvider

Sorgente Dati

File JSON singolo (~/.structum/config.json)

TOML Project Files + Env Vars

Validazione

❌ Nessuna (dict libero)

Pydantic (Auto o Strict)

Layer Support

Single layer (User Persistence)

Multi-layer (5 livelli)

Hot Reload

❌ No

✅ Watchdog (Opzionale)

Produzione

⚠️ Solo User Prefs

✅ Raccomandato

Dipendenze

Core built-in

structum-dynaconf

3.2 JSON Provider (Built-in)

Caso d’Uso: Salvare preferenze utente persistenti (tema, ultimi file aperti, dimensioni finestre) senza dipendenze esterne.

Limiti:

  • Non supporta file di progetto (es. config/app/settings.toml)

  • Nessuna separazione tra layer (tutto appiattito)

Esempio:

from structum_lab.config import set_config_provider, get_config, JSONConfigProvider

# Setup
provider = JSONConfigProvider() # Path default: ~/.structum/config.json
set_config_provider(provider)

# Uso
config = get_config()
config.set("window.width", 1920)
config.save()  # Scrive su file

3.3 Dynaconf Provider (Raccomandato)

Caso d’Uso: Applicazioni di produzione con configurazione complessa su più ambienti.

Setup completo: Vedi Plugin Dynaconf Documentation.

Quick Start (Zero-Config):

from structum_lab.plugins.dynaconf import DynaconfConfigProvider
from structum_lab.config import set_config_provider

provider = DynaconfConfigProvider()
provider.auto_discover() # Scans config/app/*.toml
set_config_provider(provider)

4. Setup e Configurazione Raccomandata

4.1 Struttura Directory “Industry Standard”

my_project/
├── config/
│   ├── app/
│   │   ├── setting.toml       # Configurazione applicazione
│   │   └── database.toml
│   └── models/
│       └── database.py        # Pydantic models (opzionale)
├── src/
│   └── main.py
└── .env                       # Secrets locali (NON committare)

5. Gestione Errori

5.1 Eccezioni

L’interfaccia definisce contratti, ma i metodi possono sollevare eccezioni standard o specifiche del provider. Il metodo get_config() solleva RuntimeError se il provider non è inizializzato.

5.2 Edge Cases della Dot-Notation

config_data = {
    "database": "not_a_dict",  # Errore: ci aspettiamo un dict
    "app.name": "MyApp"        # Chiave con punto letterale
}

# ❌ Fallisce: "database" non è un dict
config.get("database.host")  # Restituisce default (o errore a seconda del provider)

# ✅ Corretto: usa default per gestire l'errore
config.get("database.host", default="localhost")

6. Sicurezza e Best Practices

6.1 Gestione Secrets

❌ MAI fare questo:

# settings.toml (versionato in Git)
[production.database]
password = "SuperSecret123"  # ❌ ESPOSTO IN VERSION CONTROL

✅ Approcci corretti:

Opzione 1: Environment Variables

export STRUCTUM_DATABASE__PASSWORD=SuperSecret123

Opzione 2: File .secrets (Dynaconf) Configura Dynaconf per caricare .secrets.toml (aggiunto a .gitignore).

6.2 Permessi File

I file di configurazione contenenti segreti devono avere permessi 600 (rw——-).


7. API Reference Completa

7.1 Interfaccia ConfigProviderInterface

class ConfigProviderInterface(Protocol):
    
    def get(self, key: str, default: Any = None) -> Any:
        ...
    
    def set(self, key: str, value: Any) -> None:
        """
        Imposta un valore (solo in-memory).
        Richiede save() per persistenza.
        """
        ...
    
    def has(self, key: str) -> bool:
        ...
    
    def save(self) -> None:
        """
        Persiste le modifiche allo storage sottostante.
        """
        ...
    
    def reload(self) -> None:
        ...

7.2 Funzioni Pubbliche

def get_config() -> ConfigProviderInterface:
    """
    Ottiene il singleton della configurazione.
    Raises RuntimeError se set_config_provider() non è stato chiamato.
    """
    ...

def set_config_provider(provider: ConfigProviderInterface | type[ConfigProviderInterface]) -> None:
    """
    Imposta il provider globale (chiamare una sola volta all'avvio).
    """
    ...

8. Estendere il Sistema

8.1 Creare un Custom Provider

Basta creare una classe che rispetti il protocollo ConfigProviderInterface.

class InMemoryProvider:
    def __init__(self): self._data = {}
    def get(self, key, default=None): return self._data.get(key, default)
    def set(self, key, value): self._data[key] = value
    def has(self, key): return key in self._data
    def save(self): pass
    def reload(self): pass