Obsah
1. Validace dat v Pythonu s využitím knihovny Pydantic (2. část)
2. Načtení konfigurace z konfiguračního souboru s konstrukcí modelu
3. Přidání nové závislosti do projektového souboru pyproject.toml
4. Realizace načtení konfiguračního souboru s inicializací a validací modelu
5. Export modelu do formátu JSON
6. Chování modelu v případě, že konfigurační soubor obsahuje neznámé prvky
7. Detekce nadbytečných atributů při validaci modelu
8. Demonstrační příklad: detekce neznámých prvků v konfiguračním souboru
9. Model s komplikovanější konfigurační strukturou
10. Úplný zdrojový kód příkladu s rozšířeným modelem
11. Konfigurace připojení k databázi jako součást konfigurace služby
12. Načtení hodnot vybraných konfiguračních parametrů z proměnných prostředí
13. Instalace balíčku pyaml-env
14. Realizace načtení konfiguračního souboru s náhradou hodnot za obsah proměnných prostředí
15. Načtení hodnoty parametrů ze souboru
16. Demonstrační příklad: načtení konfigurace se získáním hesla ze samostatného souboru
17. Kontrola, zda parametr typu řetězec obsahuje očekávanou hodnotu
18. Demonstrační příklad: kontrola obsahu vybraných parametrů typu řetězec
19. Repositář s demonstračními příklady
1. Validace dat v Pythonu s využitím knihovny Pydantic (2. část)
Na úvodní článek o knihovně Pydantic dnes navážeme. Zatímco minule jsme si ukazovali především pouze relativně jednoduché (a také do jisté míry umělé) demonstrační příklady, dnes se zaměříme více prakticky. Řekneme si totiž, jakým způsobem je možné realizovat načtení konfigurace z konfiguračních souborů (v dnes populárním formátu YAML) s plnou validací načítaných dat (což je velmi důležité), možností doplnění hodnot z proměnných prostředí (environment variables) nebo ze zadaných souborů do načítané konfigurace atd. Všechny popisované vlastnosti knihovny Pydantic budou vyzkoušeny na konfiguračním souboru, jehož základní struktura do značné míry odpovídá konfiguraci reálného projektu; model však byl pro účely tohoto článku do značné míry zjednodušen.
2. Načtení konfigurace z konfiguračního souboru s konstrukcí modelu
Ve vývojářské praxi se velmi často setkáme s požadavkem na načtení konfigurace nějakého programu (resp. služby) z konfiguračního souboru. To většinou zahrnuje i nutnost validace dat (včetně validace povolených hodnot atd.), ale i nutnost načtení hodnot ze souborů zmíněných v konfiguračním souboru (hesla, certifikáty) nebo načtení některých hodnot z proměnných prostředí (environment variables). Právě těmito požadavky a způsobem jejich řešení se budeme zabývat v dnešním článku. Ukážeme si, jak načíst a zvalidovat konfigurační soubory s touto strukturou:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080 # povoleny jen hodnoty od 0 do 65535
auth_enabled: false
workers: 5 # povoleny jen kladné hodnoty
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER} # hodnota je uložena v proměnné prostředí
password_file: password.txt # hodnota je uložena v souboru
ssl_mode: verify-ca # povoleny jen některé hodnoty
gss_encmode: disable
3. Přidání nové závislosti do projektového souboru pyproject.toml
Podobně, jako tomu bylo v úvodním článku, budeme i dnes zkoumat všechny popisované vlastnosti knihovny Pydantic na krátkých demonstračních příkladech. Vzhledem k tomu, že budeme načítat obsah souborů ve formátu YAML, budeme muset do projektového souboru přidat další závislost, a to konkrétně balíček pyaml.
Původně vypadal projektový soubor pyproject.toml následovně:
[project]
name = "pydantic-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.11.7",
]
Příkazem uv add pyaml nebo pdm add pyaml do něj přidáme další závislost:
$ uv add pyaml
⠙ pydantic-demo==0.1.0
Resolved 8 packages in 130ms
░░░░░░░░░░░░░░░░░░░░ [0/2] Installing wheels... warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
If the cache and target directories are on different filesystems, hardlinking may not be supported.
If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
Installed 2 packages in 14ms
+ pyaml==25.7.0
+ pyyaml==6.0.2
Nyní by měl mít projektový soubor tento obsah:
[project]
name = "pydantic-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pyaml>=25.7.0",
"pydantic>=2.11.7",
]
4. Realizace načtení konfiguračního souboru s inicializací a validací modelu
V dnešním prvním demonstračním příkladu je implementována funkce pro načtení konfiguračního souboru i pro inicializaci modelu s jeho validací. Využívá se zde toho, že funkce yaml.safe_load() vrací slovník (dictionary) s obsahem souboru YAML a slovník můžeme s využitím ** při volání funkce „rozložit“ na pojmenované parametry funkce. Celé načtení s validací a inicializací modelu je tedy záležitostí několika programových řádků:
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
Úplný zdrojový kód dnešního prvního demonstračního příkladu vypadá následovně:
"""Global configuration."""
import yaml
from pydantic import (
BaseModel,
)
class Configuration(BaseModel):
"""Global configuration."""
name: str
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_01.yaml")
print(configuration)
Samotný konfigurační soubor má jen jediný řádek:
name: Service configuration #1
Otestujme si, že příklad lze spustit a že skutečně konfigurační soubor načte a zpracuje:
$ uv run config_01.py name='Service configuration'
5. Export modelu do formátu JSON
V praxi se mi osvědčila realizace exportu celého modelu (po všech jeho validacích, načtení obsahu proměnných prostředí atd.) do formátu JSON. Může se například jednat o přepínač na příkazové řádce, po jehož zadání se provede export (dump je možná lepší výraz) celého modelu do JSONu, který je následně možné prozkoumat atd., a to bez nutnosti spouštět debugger nebo analyzovat logy. Samotný export je vlastně triviální, protože ho již známe z úvodního článku o knihovně Pydantic; realizace je provedena formou metody modelu:
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
Zdrojový kód demonstračního příkladu se rozšířil do této podoby:
"""Global configuration."""
import yaml
from pydantic import (
BaseModel,
)
class Configuration(BaseModel):
"""Global configuration."""
name: str
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_02.yaml")
configuration.dump("config_02.json")
Pro tento vstupní konfigurační soubor:
name: Service configuration #1
…by měl po spuštění příkladu vzniknout soubor config02.json s tímto obsahem:
{
"name": "Service configuration"
}
6. Chování modelu v případě, že konfigurační soubor obsahuje neznámé prvky
Náš výchozí model Configuration je (alespoň prozatím) odvozen od třídy BaseModel:
class Configuration(BaseModel):
...
...
...
Při inicializaci takového modelu se ignorují neznámé prvky, což je poněkud problematické chování, které později opravíme (problematické je proto, že se mohou ignorovat například přepisy v názvu atributu v případě, že je definována jeho výchozí hodnota). Nicméně se nejdříve podívejme, jak to vypadá v praxi. Pokusme se například načíst následující konfigurační soubor config03.yaml:
name: Service configuration #1 foo: 1 bar: "*" baz: true
Náš demonstrační příklad tento soubor bez problému načte, provede inicializaci a validaci modelu a následně model vyexportuje do formátu JSON. Zde pochopitelně budou uvedeny jen známé prvky (atributy):
{
"name": "Service configuration"
}
Jen pro úplnost si uveďme úplný zdrojový kód tohoto demonstračního příkladu:
"""Global configuration."""
import yaml
from pydantic import (
BaseModel,
)
class Configuration(BaseModel):
"""Global configuration."""
name: str
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_03.yaml")
configuration.dump("config_03.json")
7. Detekce nadbytečných atributů při validaci modelu
Ve skutečnosti je relativně snadné „donutit“ knihovnu Pydantic k tomu, aby validace modelu vyhodila výjimku v případě, že se na vstupu naleznou neznámé prvky. Musíme změnit konfiguraci modelu specifikací atributu model_config. Ovšem abychom nemuseli tento řádek přidávat do každé třídy odvozené od BaseModel, vytvoříme ve třídní hierarchii novou třídu nazvanou například ConfigurationBase, od které budeme odvozovat všechny další modely:
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
Náš model reprezentovaný třídou Configuration nyní nebude odvozen přímo od třídy BaseModel, ale od výše uvedené třídy nazvané ConfigurationBase:
class Configuration(ConfigurationBase):
...
...
...
8. Demonstrační příklad: detekce neznámých prvků v konfiguračním souboru
Výše uvedenou úpravu definice modelu provedeme v dalším demonstračním příkladu. Ten se od předchozího příkladu odlišuje pouze v tom, že třída Configuration je odvozena od třídy ConfigurationBase a nikoli přímo od třídy BaseModel:
"""Global configuration."""
import yaml
from pydantic import (
BaseModel,
ConfigDict,
)
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_04.yaml")
configuration.dump("config_04.json")
Příklad se pokusíme spustit s tím, že vstupní konfigurační soubor config04.yaml bude vypadat takto:
foo: 1 bar: "*" baz: true
Po spuštění dojde při validaci modelu k detekci nadbytečných prvků a skript (podle očekávání) zhavaruje. Podtržené řádky obsahují jména nadbytečných atributů:
Traceback (most recent call last):
File "/tmp/ramdisk/pydantic/pydantic-demo/config_04.py", line 36, in <module>
configuration = load_configuration("config_04.yaml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/ramdisk/pydantic/pydantic-demo/config_04.py", line 33, in load_configuration
return Configuration(**config_dict)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/ramdisk/pydantic/pydantic-demo/.venv/lib64/python3.12/site-packages/pydantic/main.py", line 253, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 3 validation errors for Configuration
foo
Extra inputs are not permitted [type=extra_forbidden, input_value=1, input_type=int]
For further information visit https://errors.pydantic.dev/2.11/v/extra_forbidden
bar
Extra inputs are not permitted [type=extra_forbidden, input_value='*', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/extra_forbidden
baz
Extra inputs are not permitted [type=extra_forbidden, input_value=True, input_type=bool]
For further information visit https://errors.pydantic.dev/2.11/v/extra_forbidden
9. Model s komplikovanější konfigurační strukturou
Naše konfigurace, kterou jsme načítali a zpracovávali, byla prozatím značně triviální, protože obsahovala pouze jméno služby, ale už žádné další atributy. Takže si v rámci dalšího kroku konfiguraci rozšíříme, například tak, že do ní přidáme podsekci s informacemi o webové službě, která se má spustit na zadaném portu. Specifikovat bude možné i to, zda se má povolit autentizace (a popř. i autorizace), v kolika procesech se má služba spustit atd. Konfigurační soubor může vypadat například následovně:
name: Service configuration #1 service: host: 127.0.0.1 port: 8080 auth_enabled: false workers: 5
Do zdrojového kódu přidáme další třídu představující model. Povšimněte si, že kontrolujeme i rozsah hodnot portu. Stejně tak by bylo možné omezit počet procesů atd.:
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
Základní model rozšíříme o další atribut nazvaný service:
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
10. Úplný zdrojový kód příkladu s rozšířeným modelem
To je vše – výsledný demonstrační příklad bude vypadat následovně:
"""Global configuration."""
import yaml
from typing_extensions import Self
from pydantic import (
BaseModel,
ConfigDict,
PositiveInt,
model_validator,
)
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_05.yaml")
configuration.dump("config_05.json")
Po spuštění tohoto skriptu by se měl vygenerovat soubor config05.json s tímto obsahem:
{
"name": "Service configuration",
"service": {
"host": "127.0.0.1",
"port": 8080,
"auth_enabled": false,
"workers": 5
}
}
11. Konfigurace připojení k databázi jako součást konfigurace služby
Konfiguraci služby dále rozšíříme o další konfigurační volby. Ty se budou týkat připojení k databázi; v tomto konkrétním případě k PostgreSQL. Konfigurovat bude možné jméno počítače a port, na kterém databáze přijímá požadavky na připojení. Poté pochopitelně jméno databáze, jméno uživatele i jeho heslo. A konečně umožníme zadat i režimy SSL a GSS. Konfigurační soubor může vypadat následovně:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080
auth_enabled: false
workers: 5
database:
host: db1.pg.com
db: test
user: test
password: 123qwe
ssl_mode: prefer
gss_encmode: prefer
Model s konfigurací databáze lze realizovat relativně snadno (konstanty POSTGRES_DEFAULT_SSL_MODE a POSTGRES_DEFAULT_GSS_ENCMODE obsahují výchozí nastavení ve formě řetězců):
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str
ssl_mode: str = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: str = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
Úplný zdrojový kód skriptu, který akceptuje i konfiguraci připojení k databázi, by mohl vypadat následovně:
"""Global configuration."""
import yaml
from typing_extensions import Self, Optional
from pydantic import (
BaseModel,
ConfigDict,
PositiveInt,
model_validator,
FilePath,
)
# PostgreSQL connection constants
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
POSTGRES_DEFAULT_SSL_MODE = "prefer"
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
POSTGRES_DEFAULT_GSS_ENCMODE = "prefer"
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str
ssl_mode: str = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: str = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
database: DatabaseConfiguration
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
configuration = load_configuration("config_06.yaml")
configuration.dump("config_06.json")
Po spuštění tohoto skriptu se načte konfigurační soubor config06.yaml a aktuální konfigurace se vyexportuje do souboru se jménem config06.json:
{
"name": "Service configuration",
"service": {
"host": "127.0.0.1",
"port": 8080,
"auth_enabled": false,
"workers": 5,
"database": {
"host": "db1.pg.com",
"port": 5432,
"db": "test",
"user": "test",
"password": "123qwe",
"ssl_mode": "prefer",
"gss_encmode": "prefer",
"ca_cert_path": null
}
}
}
12. Načtení hodnot vybraných konfiguračních parametrů z proměnných prostředí
V praxi se poměrně často setkáme s požadavkem na to, aby se hodnoty některých konfiguračních parametrů načítaly z proměnných prostředí – tj. aby nebyly přímo součástí konfiguračního souboru. Typicky se jedná o údaje, které musí být z nějakých důvodů utajené (hesla, certifikáty, klíče) nebo o údaje, které se liší podle toho, na jakém počítači je služba nasazena (typ a verze operačního systému apod.). Příklad jsme si již ukazovali ve druhé kapitole, takže jen krátce:
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER} # hodnota je uložena v proměnné prostředí
database:
host: db1.pg.com
db: test
user: ${env.DB_USER} # hodnota je uložena v proměnné prostředí
13. Instalace balíčku pyaml-env
Načtení hodnot některých konfiguračních parametrů z proměnných prostředí je možné realizovat různými způsoby, například rekurzivním průchodem celým modelem a náhradou vhodně označených parametrů za obsah proměnných prostředí. Popř. lze využít již existujících knihoven, které tuto funkcionalitu nabízí. Jedna z těchto knihoven se jmenuje pyaml-env. Dokáže načíst konfiguraci uloženou ve formátu YAML a nahradit označené parametry za obsah proměnných prostředí (popř. pokud proměnná prostředí neexistuje se doplní N/A apod.).
Nejdříve je pochopitelně nutné tuto knihovnu nainstalovat, což je v praxi při použití pdm nebo uv snadné:
$ uv add pyaml-env Resolved 9 packages in 413ms Prepared 1 package in 94ms Installed 1 package in 4ms + pyaml-env==1.2.2
Projektový soubor pyproject.toml by nyní měl obsahovat tři primární závislosti na knihovnách pyaml, pyaml-env a pydantic:
[project]
name = "pydantic-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pyaml>=25.7.0",
"pyaml-env>=1.2.2",
"pydantic>=2.11.7",
]
14. Realizace načtení konfiguračního souboru s náhradou hodnot za obsah proměnných prostředí
Původně vypadala funkce pro načtení konfigurace ze souboru ve formátu YAML následovně:
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
with open(filename, encoding="utf-8") as fin:
config_dict = yaml.safe_load(fin)
return Configuration(**config_dict)
Nyní provedeme nepatrnou změnu jediného řádku (viz podtrženou část kódu):
from pyaml_env import parse_config
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
config_dict = parse_config(filename)
return Configuration(**config_dict)
Po této nepatrné úpravě vyexportujeme dvě proměnné prostředí:
$ export DB_USER=tester $ export DB_PASSWORD=top_secret
A pokusíme se načíst tuto konfiguraci:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080
auth_enabled: false
workers: 5
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER}
password: !ENV ${DB_PASSWORD}
Výsledek by měl vypadat takto:
{
"name": "Service configuration",
"service": {
"host": "127.0.0.1",
"port": 8080,
"auth_enabled": false,
"workers": 5,
"database": {
"host": "db1.pg.com",
"port": 5432,
"db": "test",
"user": "tester",
"password": "top_secret",
"ssl_mode": "prefer",
"gss_encmode": "prefer",
"ca_cert_path": null
}
}
}
Pro úplnost si ještě ukažme celý zdrojový kód takto upraveného demonstračního příkladu:
"""Global configuration."""
from pyaml_env import parse_config
from typing_extensions import Self, Optional
from pydantic import (
BaseModel,
ConfigDict,
PositiveInt,
model_validator,
FilePath,
)
# PostgreSQL connection constants
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
POSTGRES_DEFAULT_SSL_MODE = "prefer"
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
POSTGRES_DEFAULT_GSS_ENCMODE = "prefer"
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str
ssl_mode: str = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: str = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
database: DatabaseConfiguration
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
config_dict = parse_config(filename)
return Configuration(**config_dict)
configuration = load_configuration("config_07.yaml")
configuration.dump("config_07.json")
15. Načtení hodnoty parametrů ze souboru
Kromě načtení některých konfiguračních parametrů z proměnných prostředí se ještě v některých případech setkáme s dalším požadavkem – hodnoty (hesla, certifikáty atd.) jsou uloženy v samostatných souborech a konfigurační soubor pouze obsahuje jména těchto souborů. Příkladem může být možnost uložení hesla pro připojení do databáze v samostatném souboru, takže konfigurační soubor obsahuje jen jméno (a cestu) k tomuto souboru:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080
auth_enabled: false
workers: 5
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER}
password_file: password.txt
Implementace pomocné třídy a dvou pomocných funkcí pro načítání parametrů ze souborů může vypadat takto:
class InvalidConfigurationError(Exception):
"""Lightspeed configuration is invalid."""
def get_attribute_from_file(file_path: FilePath) -> Optional[str]:
"""Retrieve value of an attribute from a file."""
with open(file_path, encoding="utf-8") as f:
return f.read().rstrip()
def file_check(file_path: FilePath) -> None:
"""Check that path is a readable regular file."""
if not os.path.isfile(file_path):
raise InvalidConfigurationError(f"{desc} '{path}' is not a file")
if not os.access(file_path, os.R_OK):
raise InvalidConfigurationError(f"{desc} '{path}' is not readable")
Samotné načtení hesla může být součástí modelu, ze konkrétně modelu DatabaseConfiguration:
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
file_check(self.password_file)
self.password = get_attribute_from_file(self.password_file)
return self
16. Demonstrační příklad: načtení konfigurace se získáním hesla ze samostatného souboru
V dalším, dnes již předposledním demonstračním příkladu, je realizováno načtení hesla použitého pro připojení k databázi ze samostatného souboru, jehož jméno je uloženo v konfiguračním souboru:
"""Global configuration."""
import os
from pyaml_env import parse_config
from typing_extensions import Self, Optional
from pydantic import (
BaseModel,
ConfigDict,
PositiveInt,
model_validator,
FilePath,
)
# PostgreSQL connection constants
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
POSTGRES_DEFAULT_SSL_MODE = "prefer"
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
POSTGRES_DEFAULT_GSS_ENCMODE = "prefer"
class InvalidConfigurationError(Exception):
"""Lightspeed configuration is invalid."""
def get_attribute_from_file(file_path: FilePath) -> Optional[str]:
"""Retrieve value of an attribute from a file."""
with open(file_path, encoding="utf-8") as f:
return f.read().rstrip()
def file_check(file_path: FilePath) -> None:
"""Check that path is a readable regular file."""
if not os.path.isfile(file_path):
raise InvalidConfigurationError(f"{desc} '{path}' is not a file")
if not os.access(file_path, os.R_OK):
raise InvalidConfigurationError(f"{desc} '{path}' is not readable")
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str = None
password_file: FilePath
ssl_mode: str = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: str = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
file_check(self.password_file)
self.password = get_attribute_from_file(self.password_file)
return self
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
database: DatabaseConfiguration
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
config_dict = parse_config(filename)
return Configuration(**config_dict)
configuration = load_configuration("config_08.yaml")
configuration.dump("config_08.json")
{
"name": "Service configuration",
"service": {
"host": "127.0.0.1",
"port": 8080,
"auth_enabled": false,
"workers": 5,
"database": {
"host": "db1.pg.com",
"port": 5432,
"db": "test",
"user": "N/A",
"password": "123qwe",
"password_file": "password.txt",
"ssl_mode": "prefer",
"gss_encmode": "prefer",
"ca_cert_path": null
}
}
}
Vstupní konfigurační soubor:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080
auth_enabled: false
workers: 5
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER}
password_file: password.txt
ssl_mode: verify-ca
gss_encmode: disable
Po spuštění skriptu by se měl vygenerovat tento výsledek:
{
"name": "Service configuration",
"service": {
"host": "127.0.0.1",
"port": 8080,
"auth_enabled": false,
"workers": 5,
"database": {
"host": "db1.pg.com",
"port": 5432,
"db": "test",
"user": "N/A",
"password": "123qwe",
"password_file": "password.txt",
"ssl_mode": "prefer",
"gss_encmode": "prefer",
"ca_cert_path": null
}
}
}
17. Kontrola, zda parametr typu řetězec obsahuje očekávanou hodnotu
Podívejme se na následující konfigurační soubor. Ten je po stránce syntaxe i s přihlédnutím k použitým datovým typům zcela korektní. Problém ovšem spočívá v tom, že zvýrazněné konfigurační volby neobsahují platné hodnoty:
name: Service configuration #1
service:
host: 127.0.0.1
port: 8080
auth_enabled: false
workers: 5
database:
host: db1.pg.com
db: test
user: !ENV ${DB_USER}
password_file: password.txt
ssl_mode: false
gss_encmode: yes
Jakým způsobem se však provádí validace modelu, pokud jsou (například) řetězcové hodnoty omezeny jen na několik možných variant? Řešení spočívá ve využití typového systému jazyka Python, konkrétně ve specifikaci typu Literal:
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str = None
password_file: FilePath
ssl_mode: Literal[
"disable", "allow", "prefer", "require", "verify-ca", "verify-full"
] = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: Literal["disable", "prefer", "require"] = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
18. Demonstrační příklad: kontrola obsahu vybraných parametrů typu řetězec
Vyzkoušejme si nyní, co se stane v případě, že do konfiguračních parametrů ssl_mode a gss_encmode zadáme nekorektní hodnoty:
Traceback (most recent call last):
File "/tmp/ramdisk/pydantic/pydantic-demo/config_09.py", line 113, in <module>
configuration = load_configuration("config_09_wrong.yaml")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/ramdisk/pydantic/pydantic-demo/config_09.py", line 106, in load_configuration
return Configuration(**config_dict)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/ramdisk/pydantic/pydantic-demo/.venv/lib64/python3.12/site-packages/pydantic/main.py", line 253, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Configuration
service.database.ssl_mode
Input should be 'disable', 'allow', 'prefer', 'require', 'verify-ca' or 'verify-full' [type=literal_error, input_value=False, input_type=bool]
For further information visit https://errors.pydantic.dev/2.11/v/literal_error
service.database.gss_encmode
Input should be 'disable', 'prefer' or 'require' [type=literal_error, input_value=True, input_type=bool]
For further information visit https://errors.pydantic.dev/2.11/v/literal_error
Pro úplnost si ještě uvedeme celý zdrojový kód dnešního posledního demonstračního příkladu:
"""Global configuration."""
import os
from pyaml_env import parse_config
from typing_extensions import Self, Optional, Literal
from pydantic import (
BaseModel,
ConfigDict,
PositiveInt,
model_validator,
FilePath,
)
# PostgreSQL connection constants
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
POSTGRES_DEFAULT_SSL_MODE = "prefer"
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
POSTGRES_DEFAULT_GSS_ENCMODE = "prefer"
class InvalidConfigurationError(Exception):
"""Lightspeed configuration is invalid."""
def get_attribute_from_file(file_path: FilePath) -> Optional[str]:
"""Retrieve value of an attribute from a file."""
with open(file_path, encoding="utf-8") as f:
return f.read().rstrip()
def file_check(file_path: FilePath) -> None:
"""Check that path is a readable regular file."""
if not os.path.isfile(file_path):
raise InvalidConfigurationError(f"{desc} '{path}' is not a file")
if not os.access(file_path, os.R_OK):
raise InvalidConfigurationError(f"{desc} '{path}' is not readable")
class ConfigurationBase(BaseModel):
"""Base class for all configuration models that rejects unknown fields."""
model_config = ConfigDict(extra="forbid")
class DatabaseConfiguration(ConfigurationBase):
"""Database configuration."""
host: str = "localhost"
port: PositiveInt = 5432
db: str
user: str
password: str = None
password_file: FilePath
ssl_mode: Literal[
"disable", "allow", "prefer", "require", "verify-ca", "verify-full"
] = POSTGRES_DEFAULT_SSL_MODE
gss_encmode: Literal["disable", "prefer", "require"] = POSTGRES_DEFAULT_GSS_ENCMODE
ca_cert_path: Optional[FilePath] = None
@model_validator(mode="after")
def check_postgres_configuration(self) -> Self:
"""Check PostgreSQL configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
file_check(self.password_file)
self.password = get_attribute_from_file(self.password_file)
return self
class ServiceConfiguration(ConfigurationBase):
"""Service configuration."""
host: str = "localhost"
port: PositiveInt = 8080
auth_enabled: bool = False
workers: PositiveInt = 1
database: DatabaseConfiguration
@model_validator(mode="after")
def check_service_configuration(self) -> Self:
"""Check service configuration."""
if self.port > 65535:
raise ValueError("Port value should be less than 65536")
return self
class Configuration(ConfigurationBase):
"""Global configuration."""
name: str
service: ServiceConfiguration
def dump(self, filename: str = "configuration.json") -> None:
"""Dump actual configuration into JSON file."""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(self.model_dump_json(indent=4))
def load_configuration(filename: str) -> Configuration:
"""Load configuration from YAML file."""
config_dict = parse_config(filename)
return Configuration(**config_dict)
configuration = load_configuration("config_09.yaml")
configuration.dump("config_09.json")
configuration = load_configuration("config_09_wrong.yaml")
19. Repositář s demonstračními příklady
Demonstrační příklady vytvořené v Pythonu a popsané v minulém i v dnešním článku najdete v repositáři https://github.com/tisnik/most-popular-python-libs/. Následují odkazy na jednotlivé příklady:
20. Odkazy na Internetu
- Pydantic: domácí stránka
https://docs.pydantic.dev/latest/ - Pydantic na GitHubu
https://github.com/pydantic/pydantic - Pydantic na PyPi
https://pypi.org/project/pydantic/ - Introduction to Python Pydantic Library
https://www.geeksforgeeks.org/python/introduction-to-python-pydantic-library/ - An introduction to Pydantic (with basic example)
https://www.slingacademy.com/article/an-introduction-to-pydantic-with-basic-example/ - Pydantic: Simplifying Data Validation in Python
https://realpython.com/python-pydantic/ - Pydantic: A Guide With Practical Examples
https://www.datacamp.com/tutorial/pydantic - Pydantic validators
https://docs.pydantic.dev/latest/concepts/validators/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-2-cast/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-3/ - Novinky v typovém systému přidané do Pythonu 3.12
https://www.root.cz/clanky/novinky-v-typovem-systemu-pridane-do-pythonu-3–12/ - Mastering Pydantic – A Guide for Python Developers
https://dev.to/devasservice/mastering-pydantic-a-guide-for-python-developers-3kan - 7 Best Python Libraries for Validating Data
https://www.yeahhub.com/7-best-python-libraries-validating-data/ - Universally unique identifier (Wikipedia)
https://en.wikipedia.org/wiki/Universally_unique_identifier - UUID objects according to RFC 4122 (knihovna pro Python)
https://docs.python.org/3.5/library/uuid.html#uuid.uuid4 - Object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Object_identifier - Digital object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Digital_object_identifier - voluptuous na (na PyPi)
https://pypi.python.org/pypi/voluptuous - voluptuous (na GitHubu)
https://github.com/alecthomas/voluptuous - pytest-voluptuous 1.0.2 (na PyPi)
https://pypi.org/project/pytest-voluptuous/ - pytest-voluptuous (na GitHubu)
https://github.com/F-Secure/pytest-voluptuous - schemagic 0.9.1 (na PyPi)
https://pypi.python.org/pypi/schemagic/0.9.1 - Schemagic / Schemagic.web (na GitHubu)
https://github.com/Mechrophile/schemagic - schema 0.6.7 (na PyPi)
https://pypi.python.org/pypi/schema - schema (na GitHubu)
https://github.com/keleshev/schema - XML Schema validator and data conversion library for Python
https://github.com/brunato/xmlschema - xmlschema 0.9.7
https://pypi.python.org/pypi/xmlschema/0.9.7 - jsonschema 2.6.0
https://pypi.python.org/pypi/jsonschema - Tired of Pydantic? Try These 5 Game-Changing Python Libraries
https://developer-service.blog/tired-of-pydantic-try-these-5-game-changing-python-libraries/ - PostgreSQL: 32.1. Database Connection Control Functions
https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE - Python type hints: Literals
https://typing.python.org/en/latest/spec/literal.html - YAML
https://yaml.org/