Skip to content
Closed
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Pallets Projects Website

## What's different in this fork




This is the code and content for https://palletsprojects.com. Static content
from the `content` folder is loaded when the server is started then served
quickly from memory or cache. This allows editing content as plain files like a
Expand Down
103 changes: 103 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import tomllib
from pathlib import Path

from pydantic import BaseModel
from pydantic import Field
from pydantic import ValidationError


class ConfigError(Exception):
"""Custom exception for configuration-related errors."""

pass


class Settings(BaseModel):
"""
Defines the application's configuration structure using Pydantic.
This ensures that all configuration values are of the correct type.
"""

api_key: str = Field(..., description="The API key for the primary service.")
timeout: int = Field(
default=30, gt=0, description="Default timeout for API requests in seconds."
)
log_level: str = Field(
default="INFO",
pattern=r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$",
description="Logging level (e.g., DEBUG, INFO, WARNING).",
)
database_url: str | None = Field(
default=None, description="Optional database connection URL."
)


def load_config(config_path: Path = Path("config.toml")) -> Settings:
"""
Loads and validates configuration from a TOML file.

This function is designed to provide clear, actionable error messages
to help users fix configuration issues quickly.

Args:
config_path: The path to the configuration file.

Returns:
A validated Settings object.

Raises:
ConfigError: If the file is not found, cannot be parsed, or fails validation.
"""
# Pain Point 1: The configuration file doesn't exist.
# The error should be explicit about what's missing and where we looked.
if not config_path.is_file():
raise ConfigError(
f"Configuration file not found at '{config_path}'.\n"
"Hint: Check that the file exists at this path. You may need to create it "
"from a template (e.g., 'config.example.toml') or specify the correct "
"path via a command-line argument or environment variable."
)

# Pain Point 2: The file exists but is malformed (e.g., invalid TOML).
# We'll catch the specific parsing error and provide context.
try:
with open(config_path, "rb") as f:
config_data = tomllib.load(f)
except tomllib.TOMLDecodeError as e:
raise ConfigError(
f"Error parsing '{config_path}':\n"
f" {e}\n\n"
"Hint: This looks like a TOML syntax error. Common issues include "
"unclosed quotes for strings or misplaced brackets. Please double-check "
"the line mentioned in the error above."
)
except OSError as e:
# Also handle cases where the file can't be read due to permissions.
raise ConfigError(
f"Could not read configuration file '{config_path}': {e}\n"
"Hint: Please check the file's permissions and ensure the application "
"has read access."
)

# Pain Points 3 & 4: Missing required keys or values of the wrong type.
# Pydantic handles this validation, but we can format its error for clarity.
try:
return Settings(**config_data)
except ValidationError as e:
# Pydantic's default error is good, but we can make it more user-friendly
# by formatting it as a clear, readable list.
error_messages = []
for error in e.errors():
field = " -> ".join(map(str, error["loc"]))
message = error["msg"]
error_messages.append(f" - Field '{field}': {message}")

formatted_errors = "\n".join(error_messages)
raise ConfigError(
f"Configuration in '{config_path}' is invalid:\n"
f"{formatted_errors}\n\n"
"Hint: Please review the errors above and correct your configuration file. For example:\n"
" - 'api_key' must be a non-empty string.\n"
" - 'timeout' must be a whole number greater than 0.\n"
" - 'log_level' must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL."
)
126 changes: 126 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import sys
from typing import Any


class ConfigError(Exception):
"""A custom exception for user-facing configuration errors."""

pass


def validate_config(config: dict[str, Any]) -> dict[str, Any]:
"""
Validates a configuration dictionary, raising ConfigError with actionable
advice if any checks fail.
"""
_check_for_required_keys(config)
_check_api_key(config)
_check_timeout(config)
_check_mode(config)

return config


def _check_for_required_keys(config: dict[str, Any]):
"""Ensure all mandatory keys are present."""
required_keys = ["api_key", "timeout", "mode"]
missing_keys = [key for key in required_keys if key not in config]

if missing_keys:
key_str = ", ".join(f"'{key}'" for key in missing_keys)
raise ConfigError(
f"Configuration error: Missing required key(s): {key_str}.\n"
f" > Please add the missing key(s) to your configuration file."
)


def _check_api_key(config: dict[str, Any]):
"""Validate the 'api_key' is a non-empty string."""
api_key = config.get("api_key")
if not isinstance(api_key, str) or not api_key:
# A common mistake is an empty string or forgetting quotes in YAML.
# This message helps diagnose both.
raise ConfigError(
"Configuration error: The 'api_key' must be a non-empty string.\n"
f" > We found a value of type '{type(api_key).__name__}'.\n"
' > Please ensure your config looks like: api_key: "your_secret_key"'
)


def _check_timeout(config: dict[str, Any]):
"""Validate the 'timeout' is a positive integer."""
timeout = config.get("timeout")
if not isinstance(timeout, int) or timeout <= 0:
# Guide the user on the expected type and a valid range.
raise ConfigError(
"Configuration error: The 'timeout' must be a positive integer.\n"
f" > We found '{timeout}', which is a '{type(timeout).__name__}'.\n"
" > Please use a whole number greater than zero, like: timeout: 30"
)


def _check_mode(config: dict[str, Any]):
"""Validate the 'mode' is one of the allowed values."""
mode = config.get("mode")
valid_modes = ["fast", "balanced", "high_quality"]
if mode not in valid_modes:
# Show the user the exact value they provided and list the valid options.
# This prevents typos and guesswork.
raise ConfigError(
f"Configuration error: Invalid value for 'mode'.\n"
f" > We received '{mode}', but the only allowed values are: {valid_modes}.\n"
f" > Please update 'mode' in your configuration file."
)


def run_with_config(config_name: str, config_data: dict[str, Any]):
"""A helper to simulate running the app with a given configuration."""
print(f"--- Attempting to load '{config_name}' ---")
try:
validate_config(config_data)
print("✅ Configuration is valid and loaded successfully.\n")
except ConfigError as e:
# This is where the user sees our improved, actionable error message.
# Printing to stderr is standard practice for errors.
print(f"❌ {e}\n", file=sys.stderr)


def main():
"""
Demonstrates configuration validation with user-friendly error messages
by running through several common invalid configuration scenarios.
"""
# A valid configuration to show the success case.
valid_config = {"api_key": "sk-12345abcde", "timeout": 30, "mode": "balanced"}

# --- Example Error Cases ---

# 1. Missing a required key ('api_key')
config_missing_key = {"timeout": 60, "mode": "fast"}

# 2. Incorrect data type for 'timeout' (string instead of int)
config_wrong_type = {
"api_key": "sk-12345abcde",
"timeout": "30", # Should be an integer
"mode": "fast",
}

# 3. Invalid value for 'mode'
config_invalid_value = {
"api_key": "sk-12345abcde",
"timeout": 15,
"mode": "quick", # Not a valid mode
}

# 4. Empty string for 'api_key'
config_empty_key = {"api_key": "", "timeout": 15, "mode": "high_quality"}

run_with_config("Valid Config", valid_config)
run_with_config("Config Missing Key", config_missing_key)
run_with_config("Config Wrong Type", config_wrong_type)
run_with_config("Config Invalid Value", config_invalid_value)
run_with_config("Config Empty Key", config_empty_key)


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