Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/pydantic_models/example_006_boolean_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Example demonstrating boolean field handling in pydantic models."""

from typing import Annotated

import pydantic
import typer

import pydantic_typer


class Settings(pydantic.BaseModel):
"""Settings with boolean fields."""

enable_feature: Annotated[
bool, pydantic.Field(description="Enable the feature.")
] = True
debug_mode: Annotated[bool, pydantic.Field(description="Enable debug mode.")] = (
False
)
verbose: bool = False


def main(settings: Settings):
"""Main function that uses settings with boolean fields."""
typer.echo(f"enable_feature={settings.enable_feature}")
typer.echo(f"debug_mode={settings.debug_mode}")
typer.echo(f"verbose={settings.verbose}")


if __name__ == "__main__":
pydantic_typer.run(main)
151 changes: 151 additions & 0 deletions examples/pydantic_types/example_011_annotated_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Examples of Annotated validators (BeforeValidator, AfterValidator, etc.)."""

from datetime import timedelta
from typing import Annotated, Any

import typer
from pydantic import (
AfterValidator,
BaseModel,
BeforeValidator,
EmailStr,
Field,
HttpUrl,
)

from pydantic_typer import Typer


# BeforeValidator example with timedelta
def parse_timedelta(value: Any) -> timedelta:
"""Parse timedelta from seconds (int/float/str) or ISO 8601 format."""
if isinstance(value, timedelta):
return value
if isinstance(value, (int, float)):
return timedelta(seconds=value)
if isinstance(value, str):
# Try to parse as a number first
try:
return timedelta(seconds=float(value))
except ValueError:
# If it fails, it might be ISO 8601 format
# Let pydantic's default parser handle it
pass
return value


FlexibleTimedelta = Annotated[timedelta, BeforeValidator(parse_timedelta)]


class Settings(BaseModel):
"""Settings with a flexible timedelta field."""

min_duration: Annotated[
FlexibleTimedelta,
Field(description="The minimum testing duration"),
] = timedelta(seconds=5)


# AfterValidator example with temperature
def validate_temperature(value: float) -> float:
"""Validate temperature is in reasonable range (AfterValidator)."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
if value > 1000:
raise ValueError("Temperature is unreasonably high (max 1000°C)")
return value


Temperature = Annotated[float, AfterValidator(validate_temperature)]


class TemperatureConfig(BaseModel):
"""Config with temperature validation using AfterValidator."""

temp: Annotated[
Temperature,
Field(description="Temperature in Celsius"),
] = 20.0


# Combined BeforeValidator and AfterValidator examples
def normalize_url(value: Any) -> str:
"""Add https:// scheme if missing (BeforeValidator for HttpUrl)."""
if isinstance(value, str) and not value.startswith(("http://", "https://")):
return f"https://{value}"
return value


def validate_domain(value: HttpUrl) -> HttpUrl:
"""Validate URL has allowed domain (AfterValidator)."""
allowed_domains = ["example.com", "test.com", "localhost"]
if value.host and not any(
value.host.endswith(domain) for domain in allowed_domains
):
raise ValueError(f"Domain must be one of: {', '.join(allowed_domains)}")
return value


FlexibleHttpUrl = Annotated[
HttpUrl, BeforeValidator(normalize_url), AfterValidator(validate_domain)
]


class WebConfig(BaseModel):
"""Config with flexible URL handling."""

api_url: Annotated[
FlexibleHttpUrl,
Field(description="API endpoint URL"),
]


def normalize_email(value: Any) -> str:
"""Normalize email to lowercase (BeforeValidator for EmailStr)."""
if isinstance(value, str):
return value.lower().strip()
return value


NormalizedEmail = Annotated[EmailStr, BeforeValidator(normalize_email)]


class UserConfig(BaseModel):
"""Config with email normalization."""

email: Annotated[
NormalizedEmail,
Field(description="User email address"),
] = "[email protected]"


# CLI applications
app = Typer()


@app.command(name="settings")
def settings_command(settings: Settings):
"""Process settings with flexible timedelta."""
typer.echo(f"Duration: {settings.min_duration}")


@app.command(name="temperature")
def temperature_command(config: TemperatureConfig):
"""Process temperature configuration."""
typer.echo(f"Temperature: {config.temp}°C")


@app.command(name="web")
def web_command(config: WebConfig):
"""Process web configuration with URL validation."""
typer.echo(f"API URL: {config.api_url}")


@app.command(name="user")
def user_command(config: UserConfig):
"""Process user configuration with email normalization."""
typer.echo(f"Email: {config.email}")


if __name__ == "__main__":
app()
138 changes: 138 additions & 0 deletions examples/pydantic_types/example_012_field_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Examples of field_validator decorators with pydantic-typer."""

from datetime import timedelta
from ipaddress import IPv4Address
from typing import Annotated, Any

import typer
from pydantic import BaseModel, Field, HttpUrl, field_validator

from pydantic_typer import Typer


class SettingsWithFieldValidator(BaseModel):
"""Settings using field_validator decorator."""

min_duration: Annotated[
timedelta,
Field(description="The minimum testing duration"),
] = timedelta(seconds=5)

@field_validator("min_duration", mode="before")
@classmethod
def parse_min_duration(cls, v: Any) -> timedelta:
"""Parse timedelta from seconds or ISO 8601 format."""
if isinstance(v, timedelta):
return v
if isinstance(v, (int, float)):
return timedelta(seconds=v)
if isinstance(v, str):
try:
return timedelta(seconds=float(v))
except ValueError:
pass
return v


class ConfigWithAfterValidator(BaseModel):
"""Config using field_validator with mode='after'."""

temperature: Annotated[
float,
Field(description="Temperature in Celsius"),
] = 20.0

@field_validator("temperature", mode="after")
@classmethod
def validate_temperature_range(cls, v: float) -> float:
"""Ensure temperature is in valid range after conversion."""
if v < -273.15:
raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
if v > 1000:
raise ValueError("Temperature is unreasonably high (max 1000°C)")
return v


class ServerConfig(BaseModel):
"""Server configuration with HttpUrl and field validators."""

api_url: Annotated[
HttpUrl,
Field(description="API server URL"),
]

@field_validator("api_url", mode="before")
@classmethod
def normalize_url(cls, v: Any) -> str:
"""Add https:// if scheme is missing."""
if isinstance(v, str) and not v.startswith(("http://", "https://")):
return f"https://{v}"
return v

@field_validator("api_url", mode="after")
@classmethod
def validate_port(cls, v: HttpUrl) -> HttpUrl:
"""Ensure port is >= 1024 (non-privileged) if specified."""
if v.port and v.port < 1024:
raise ValueError(f"Port {v.port} is reserved (must be >= 1024)")
return v


class NetworkConfig(BaseModel):
"""Network configuration with IPv4Address and validators."""

server_ip: Annotated[
IPv4Address,
Field(description="Server IP address"),
] = IPv4Address("127.0.0.1")

@field_validator("server_ip", mode="before")
@classmethod
def parse_ip(cls, v: Any) -> str:
"""Allow 'localhost' as alias for 127.0.0.1."""
if isinstance(v, str):
if v.lower() == "localhost":
return "127.0.0.1"
# Strip whitespace
return v.strip()
return v

@field_validator("server_ip", mode="after")
@classmethod
def validate_not_zero(cls, v: IPv4Address) -> IPv4Address:
"""Ensure IP is not 0.0.0.0."""
if str(v) == "0.0.0.0":
raise ValueError("IP address cannot be 0.0.0.0")
return v


# CLI applications
app = Typer()


@app.command(name="settings")
def settings_command(settings: SettingsWithFieldValidator):
"""Process settings with field validator."""
typer.echo(f"Duration: {settings.min_duration}")


@app.command(name="temperature")
def temperature_command(config: ConfigWithAfterValidator):
"""Process temperature configuration."""
typer.echo(f"Temperature: {config.temperature}°C")


@app.command(name="server")
def server_command(config: ServerConfig):
"""Process server configuration."""
typer.echo(f"API: {config.api_url}")


@app.command(name="network")
def network_command(config: NetworkConfig):
"""Process network configuration."""
typer.echo(f"Server IP: {config.server_ip}")


if __name__ == "__main__":
app()
Loading