Skip to content

Instantly share code, notes, and snippets.

@mpkocher
Last active January 26, 2025 21:39
Show Gist options
  • Save mpkocher/eb11b6807ef5e119b3e1ef5d7b629529 to your computer and use it in GitHub Desktop.
Save mpkocher/eb11b6807ef5e119b3e1ef5d7b629529 to your computer and use it in GitHub Desktop.
Pydantic Settings. Example of configure a yaml/json file at runtime.
"""
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