1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
| # config.py
import os
from pathlib import Path
from typing import Any, Self
from pydantic import BaseModel
from pydantic_settings import (
BaseSettings,
InitSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
TomlConfigSettingsSource,
YamlConfigSettingsSource,
)
class EnvVarFileConfigSettingsSource(InitSettingsSource):
"""
A source that loads configuration from a file specified in an environment variable.
It automatically selects the TOML or YAML parser based on the file extension.
"""
def __init__(
self,
settings_cls: type[BaseSettings],
env_var: str = "MYAPP_CONFIG_FILE",
env_file_encoding: str | None = None,
):
"""
Args:
settings_cls: The settings class.
env_var: The name of the environment variable to read the file path from.
env_file_encoding: The encoding to use for YAML files.
"""
self.env_var = env_var
self.file_path_str = os.getenv(env_var)
self.encoding = env_file_encoding
file_data: dict[str, Any] = {}
if not self.file_path_str:
super().__init__(settings_cls, file_data)
return
file_path = Path(self.file_path_str)
if not file_path.exists():
print(
f"Warning: The file '{file_path}' pointed to by the environment variable '{self.env_var}' does not exist."
)
# Reuse existing source logic based on file extension
suffix = file_path.suffix.lower()
if suffix == ".toml":
# Internally create a TomlConfigSettingsSource instance to load the file
file_data = TomlConfigSettingsSource(
settings_cls, toml_file=file_path
).toml_data
elif suffix in (".yaml", ".yml"):
# Internally create a YamlConfigSettingsSource instance to load the file
file_data = YamlConfigSettingsSource(
settings_cls, yaml_file=file_path, yaml_file_encoding=self.encoding
).yaml_data
else:
print(f"Warning: Unsupported file type '{suffix}'. Ignored.")
# Call InitSettingsSource's __init__, passing the data loaded from the file
super().__init__(settings_cls, file_data)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(env_var={self.env_var}, file_path={self.file_path_str!r})"
class DatabaseConfig(BaseModel):
postgres_user: str = "veno"
postgres_password: str = "12345678"
postgres_host: str = "localhost"
postgres_port: int = 5432
postgres_db: str = "veno_db"
class Settings(BaseSettings):
model_config = SettingsConfigDict(
toml_file=Path("myapp.toml"),
yaml_file=Path("myapp.yaml") or Path("myapp.yml"),
yaml_file_encoding="utf-8",
env_prefix="MYAPP_",
env_nested_delimiter="__",
)
debug: bool = False
formal: bool = True
database: DatabaseConfig = DatabaseConfig()
@classmethod
def settings_customise_sources(
cls: type[Self],
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""
Define the priority of different configuration sources.
See: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#customise-settings-sources
"""
return (
init_settings,
# Custom Source
EnvVarFileConfigSettingsSource(settings_cls),
# env_settings,
dotenv_settings,
# See: https://docs.pydantic.dev/latest/concepts/pydantic_settings/#other-settings-source
TomlConfigSettingsSource(settings_cls),
YamlConfigSettingsSource(settings_cls),
file_secret_settings,
)
settings = Settings()
# main.py
import os
from typing import Annotated
import typer
import playground.config as config_module
cli_app = typer.Typer()
env_config_file = "MYAPP_CONFIG_FILE"
@cli_app.command()
def hello(name: Annotated[str, typer.Option()] = "Veno"):
print(f"Hello, {name}")
if config_module.settings.debug:
print(f"cli_app: {cli_app}")
@cli_app.command()
def bye():
if config_module.settings.formal:
print("Goodbye!")
else:
print("See ya!")
@cli_app.callback()
def main(config: Annotated[str | None, typer.Option()] = None):
if config:
os.environ[env_config_file] = str(config)
config_module.settings.__init__()
if __name__ == "__main__":
cli_app()
|