Settings management using Pydantic with support for multiple configuration sources including environment variables, configuration files, CLI arguments, and cloud secret management services
—
CLI argument parsing and settings generation with support for subcommands, flags, positional arguments, and running Pydantic models as CLI applications. The CLI system provides comprehensive integration with argparse and supports complex argument structures.
Parse command-line arguments and convert them to settings values with full integration with argparse functionality.
class CliSettingsSource(EnvSettingsSource, Generic[T]):
"""Source class for loading settings values from CLI."""
def __init__(
self,
settings_cls: type[BaseSettings],
cli_prog_name: str | None = None,
cli_parse_args: bool | list[str] | tuple[str, ...] | None = None,
cli_parse_none_str: str | None = None,
cli_hide_none_type: bool = False,
cli_avoid_json: bool = False,
cli_enforce_required: bool = False,
cli_use_class_docs_for_groups: bool = False,
cli_exit_on_error: bool = True,
cli_prefix: str = "",
cli_flag_prefix_char: str = "-",
cli_implicit_flags: bool | None = None,
cli_ignore_unknown_args: bool | None = None,
cli_kebab_case: bool | None = None,
cli_shortcuts: Mapping[str, str | list[str]] | None = None,
case_sensitive: bool | None = None,
):
"""
Initialize CLI settings source.
Parameters:
- settings_cls: The settings class
- cli_prog_name: Program name for help text
- cli_parse_args: Arguments to parse (True for sys.argv[1:])
- cli_parse_none_str: String value to parse as None
- cli_hide_none_type: Hide None values in help text
- cli_avoid_json: Avoid complex JSON objects in help
- cli_enforce_required: Enforce required fields at CLI
- cli_use_class_docs_for_groups: Use class docs for group help
- cli_exit_on_error: Exit on parsing errors
- cli_prefix: Root parser arguments prefix
- cli_flag_prefix_char: Flag prefix character
- cli_implicit_flags: Convert bool fields to flags
- cli_ignore_unknown_args: Ignore unknown CLI args
- cli_kebab_case: Use kebab-case for CLI args
- cli_shortcuts: Mapping of field names to alias names
- case_sensitive: Whether CLI args are case-sensitive
"""Utility class for running Pydantic models as CLI applications with support for subcommands and async methods.
class CliApp:
"""Utility class for running Pydantic models as CLI applications."""
@staticmethod
def run(
model_cls: type[T],
cli_args: list[str] | Namespace | SimpleNamespace | dict[str, Any] | None = None,
cli_settings_source: CliSettingsSource[Any] | None = None,
cli_exit_on_error: bool | None = None,
cli_cmd_method_name: str = 'cli_cmd',
**model_init_data: Any,
) -> T:
"""
Run a Pydantic model as a CLI application.
Parameters:
- model_cls: The model class to run
- cli_args: CLI arguments to parse (defaults to sys.argv[1:])
- cli_settings_source: Custom CLI settings source
- cli_exit_on_error: Whether to exit on error
- cli_cmd_method_name: CLI command method name to run
- **model_init_data: Additional model initialization data
Returns:
The model instance after running the CLI command
Raises:
- SettingsError: If model_cls is not a BaseModel or dataclass
- SettingsError: If model_cls lacks the required CLI command method
"""
@staticmethod
def run_subcommand(
model: PydanticModel,
cli_exit_on_error: bool | None = None,
cli_cmd_method_name: str = 'cli_cmd'
) -> PydanticModel:
"""
Run a model subcommand.
Parameters:
- model: The model to run the subcommand from
- cli_exit_on_error: Whether to exit on error
- cli_cmd_method_name: CLI command method name to run
Returns:
The subcommand model after execution
Raises:
- SystemExit: When no subcommand found and cli_exit_on_error=True
- SettingsError: When no subcommand found and cli_exit_on_error=False
"""Type annotations for defining CLI-specific field behavior including subcommands, positional arguments, and flags.
# CLI subcommand annotation
CliSubCommand = Annotated[Union[T, None], _CliSubCommand]
# Positional argument annotation
CliPositionalArg = Annotated[T, _CliPositionalArg]
# Boolean flag annotations
CliImplicitFlag = Annotated[bool, _CliImplicitFlag]
CliExplicitFlag = Annotated[bool, _CliExplicitFlag]
# Suppress CLI argument generation
CliSuppress = Annotated[T, CLI_SUPPRESS]
# Capture unknown CLI arguments
CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode]Model for managing mutually exclusive CLI argument groups.
class CliMutuallyExclusiveGroup(BaseModel):
"""Model for mutually exclusive CLI argument groups."""
pass# Suppress argument generation (from argparse.SUPPRESS)
CLI_SUPPRESS: strfrom pydantic_settings import BaseSettings
from pydantic import Field
class AppSettings(BaseSettings):
name: str = Field(..., description="Application name")
debug: bool = Field(False, description="Enable debug mode")
port: int = Field(8000, description="Server port")
model_config = SettingsConfigDict(
cli_parse_args=True,
cli_prog_name="myapp"
)
# Command line: python app.py --name "My App" --debug --port 3000
settings = AppSettings()
print(f"Running {settings.name} on port {settings.port}, debug={settings.debug}")class ServerSettings(BaseSettings):
server_host: str = Field("localhost", description="Server hostname")
server_port: int = Field(8000, description="Server port")
enable_ssl: bool = Field(False, description="Enable SSL")
model_config = SettingsConfigDict(
cli_parse_args=True,
cli_prefix="server-",
cli_kebab_case=True,
cli_implicit_flags=True
)
# Command line: python app.py --server-server-host prod.example.com --server-enable-ssl
settings = ServerSettings()from pydantic_settings import BaseSettings, CliApp
class CalculatorSettings(BaseSettings):
operation: str = Field(..., description="Math operation to perform")
x: float = Field(..., description="First number")
y: float = Field(..., description="Second number")
def cli_cmd(self):
"""Execute the calculator command."""
if self.operation == "add":
result = self.x + self.y
elif self.operation == "multiply":
result = self.x * self.y
else:
raise ValueError(f"Unknown operation: {self.operation}")
print(f"Result: {result}")
# Run as CLI app
if __name__ == "__main__":
# Command line: python calculator.py --operation add --x 10 --y 5
CliApp.run(CalculatorSettings)from typing import Union
from pydantic import BaseModel
class DatabaseCommand(BaseModel):
host: str = Field("localhost", description="Database host")
def cli_cmd(self):
print(f"Connecting to database at {self.host}")
class ServerCommand(BaseModel):
port: int = Field(8000, description="Server port")
def cli_cmd(self):
print(f"Starting server on port {self.port}")
class AppSettings(BaseSettings):
verbose: bool = Field(False, description="Verbose output")
command: CliSubCommand[Union[DatabaseCommand, ServerCommand]] = None
model_config = SettingsConfigDict(cli_parse_args=True)
# Command line: python app.py --verbose database --host prod-db.com
# Command line: python app.py server --port 3000
settings = AppSettings()
if settings.verbose:
print("Verbose mode enabled")
if settings.command:
CliApp.run_subcommand(settings)class FileProcessor(BaseSettings):
input_file: CliPositionalArg[str] = Field(..., description="Input file path")
output_file: CliPositionalArg[str] = Field(..., description="Output file path")
format: str = Field("json", description="Output format")
verbose: CliImplicitFlag[bool] = Field(False, description="Verbose output")
model_config = SettingsConfigDict(cli_parse_args=True)
def cli_cmd(self):
print(f"Processing {self.input_file} -> {self.output_file} ({self.format})")
# Command line: python processor.py input.txt output.txt --format csv --verbose
CliApp.run(FileProcessor)class BuildSettings(BaseSettings):
clean: CliExplicitFlag[bool] = Field(False, description="Clean before build")
release: CliImplicitFlag[bool] = Field(False, description="Release build")
jobs: int = Field(1, description="Number of parallel jobs")
model_config = SettingsConfigDict(
cli_parse_args=True,
cli_shortcuts={
"jobs": ["j"],
"clean": ["c"],
"release": ["r"]
}
)
# Command line: python build.py --clean --release -j 4
# Command line: python build.py -c -r -j 8
settings = BuildSettings()class FlexibleApp(BaseSettings):
name: str = Field(..., description="App name")
debug: bool = Field(False, description="Debug mode")
extra_args: CliUnknownArgs = Field(default=[])
model_config = SettingsConfigDict(
cli_parse_args=True,
cli_ignore_unknown_args=True
)
def cli_cmd(self):
print(f"App: {self.name}, Debug: {self.debug}")
if self.extra_args:
print(f"Extra arguments: {self.extra_args}")
# Command line: python app.py --name MyApp --debug --custom-flag value --another-arg
# extra_args will contain: ['--custom-flag', 'value', '--another-arg']
CliApp.run(FlexibleApp)import asyncio
class AsyncApp(BaseSettings):
delay: int = Field(1, description="Delay in seconds")
message: str = Field("Hello", description="Message to display")
async def cli_cmd(self):
"""Async CLI command method."""
await asyncio.sleep(self.delay)
print(f"Async message: {self.message}")
# CliApp.run automatically handles async methods
CliApp.run(AsyncApp)class RobustApp(BaseSettings):
"""
A robust application with custom CLI handling.
This app demonstrates error handling and help customization.
"""
config_file: str = Field(..., description="Configuration file path")
strict: bool = Field(False, description="Strict validation mode")
model_config = SettingsConfigDict(
cli_parse_args=True,
cli_prog_name="robust-app",
cli_exit_on_error=False, # Handle errors manually
cli_use_class_docs_for_groups=True
)
def cli_cmd(self):
try:
with open(self.config_file) as f:
config = f.read()
print(f"Loaded config: {len(config)} bytes")
except FileNotFoundError:
if self.strict:
raise
print(f"Config file {self.config_file} not found, using defaults")
# Will handle parsing errors gracefully
try:
CliApp.run(RobustApp)
except SettingsError as e:
print(f"CLI Error: {e}")Install with Tessl CLI
npx tessl i tessl/pypi-pydantic-settings