Last active
January 26, 2025 21:39
-
-
Save mpkocher/eb11b6807ef5e119b3e1ef5d7b629529 to your computer and use it in GitHub Desktop.
Pydantic Settings. Example of configure a yaml/json file at runtime.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Different workarounds for setting a configuration file path at runtime. | |
https://github.com/pydantic/pydantic-settings/issues/259 | |
There's an explicit step of validating the file path to make the errors more obvious. | |
Otherwise, a cryptic pydantic error will be raised. | |
""" | |
import argparse | |
import sys | |
from typing import Type, Tuple, Self | |
from argparse import ArgumentParser | |
from pathlib import Path | |
from pydantic import BaseModel | |
from pydantic_settings import ( | |
YamlConfigSettingsSource, | |
BaseSettings, | |
PydanticBaseSettingsSource, | |
SettingsConfigDict, | |
) | |
import yaml | |
class Settings(BaseModel): | |
"""For cases that only required 1 data source, it can be | |
useful to avoid using pydantic-settings and wire a simple | |
method or function to load the file/source. | |
This avoids generating an "empty" constructor from BaseSettings | |
of your data model. | |
It also can be more explicit and obvious in communicating if a file is strictly required to exist | |
to initialize the datasource. | |
""" | |
alpha: int | |
beta: int | |
@classmethod | |
def from_yaml(cls, path: Path) -> Self: | |
with open(path, "r") as yaml_file: | |
dx = yaml.safe_load(yaml_file) or {} | |
return cls(**dx) | |
class Settings2(BaseSettings): | |
""" | |
This is similar to #1, but reuses the DataSource machinery of pydantic-settings. | |
This enables mixing/merging multiple settings. | |
There's no layer (e.g., SettingsConfigDict) between you and the source settings. | |
""" | |
alpha: int | |
beta: int | |
@classmethod | |
def from_yaml(cls, path: Path) -> Self: | |
return cls(**YamlConfigSettingsSource(cls, path)()) | |
def to_settings3(yaml_file: Path): | |
""" | |
This method enables configuring a datasource at runtime. | |
""" | |
class Settings3(BaseSettings): | |
model_config = SettingsConfigDict(yaml_file=yaml_file) | |
alpha: int | |
beta: int | |
@classmethod | |
def settings_customise_sources( | |
cls, | |
settings_cls: Type[BaseSettings], | |
init_settings: PydanticBaseSettingsSource, | |
env_settings: PydanticBaseSettingsSource, | |
dotenv_settings: PydanticBaseSettingsSource, | |
file_secret_settings: PydanticBaseSettingsSource, | |
) -> Tuple[PydanticBaseSettingsSource, ...]: | |
return (YamlConfigSettingsSource(settings_cls),) | |
return Settings3 | |
def validate_path(sx: str) -> Path: | |
px = Path(sx) | |
if px.exists(): | |
return px | |
raise argparse.ArgumentError(None, message=f"{sx} not a valid file path.") | |
def to_parser() -> ArgumentParser: | |
p = ArgumentParser() | |
# adding explicit validation here to make the errors more clear. Otherwise, pydantic will | |
# raise an error about an empty data source. | |
p.add_argument( | |
"-f", "--yaml-file", required=True, type=validate_path, help="Path to YAML file" | |
) | |
return p | |
def main(ax: list[str]) -> int: | |
p = to_parser() | |
pargs = p.parse_args(ax) | |
funcs = [Settings.from_yaml, Settings2.from_yaml] | |
for func in funcs: | |
settings = func(pargs.yaml_file) | |
print(f"{func} -> {settings}") | |
klass = to_settings3(pargs.yaml_file) | |
sx = klass() | |
print(sx) | |
return 0 | |
if __name__ == "__main__": | |
sys.exit(main(sys.argv[1:])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment