- Add AI code generation requirements to AGENTS.md - Add module-level docstrings to all 46 Python modules - Add detailed Google-style docstrings to all classes and functions - Remove all inline comments following self-documenting code principle - Include Args, Returns, Raises sections in function docstrings - Add Attributes and Examples sections to class docstrings
286 lines
7.6 KiB
Python
286 lines
7.6 KiB
Python
"""Application settings with composition pattern.
|
|
|
|
This module defines the application configuration using pydantic-settings.
|
|
Provides typed configuration for database, Keycloak, security, and app settings.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from functools import cached_property
|
|
|
|
from pydantic import Field, PostgresDsn, field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Environment(str, Enum):
|
|
"""Application environment modes.
|
|
|
|
Defines the available deployment environments.
|
|
Each environment may have different configuration defaults.
|
|
|
|
Attributes:
|
|
DEV: Development environment with debug features.
|
|
PROD: Production environment with strict security.
|
|
|
|
Example:
|
|
>>> if settings.environment == Environment.PROD:
|
|
... enable_strict_security()
|
|
"""
|
|
|
|
DEV = "dev"
|
|
PROD = "prod"
|
|
|
|
|
|
class AppConfig(BaseSettings):
|
|
"""Application configuration.
|
|
|
|
Contains general application settings like name, host, and port.
|
|
|
|
Attributes:
|
|
name: Application display name.
|
|
debug: Debug mode flag.
|
|
host: Server bind host.
|
|
port: Server bind port.
|
|
|
|
Example:
|
|
>>> config = AppConfig(name="My API", port=8000)
|
|
"""
|
|
|
|
name: str = "Blog API"
|
|
debug: bool = False
|
|
host: str = "0.0.0.0"
|
|
port: int = 8000
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="APP_",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
|
|
class DBConfig(BaseSettings):
|
|
"""Database configuration.
|
|
|
|
Contains database connection settings. Supports both SQLite for
|
|
development and PostgreSQL for production.
|
|
|
|
Attributes:
|
|
url: Full database URL (optional, can build from components).
|
|
echo: Enable SQL query logging.
|
|
host: Database server host.
|
|
port: Database server port.
|
|
user: Database username.
|
|
password: Database password.
|
|
name: Database name.
|
|
|
|
Example:
|
|
>>> db_config = DBConfig(host="localhost", name="blog")
|
|
"""
|
|
|
|
url: str | None = None
|
|
echo: bool = False
|
|
|
|
host: str = "localhost"
|
|
port: int = 5432
|
|
user: str = "postgres"
|
|
password: str = "postgres"
|
|
name: str = "blog"
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="DB_",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
@field_validator("url")
|
|
@classmethod
|
|
def validate_url(cls, v: str | None) -> str | None:
|
|
"""Validate database URL if provided.
|
|
|
|
Args:
|
|
v: Database URL string to validate.
|
|
|
|
Returns:
|
|
Validated URL string.
|
|
|
|
Raises:
|
|
ValueError: If URL does not start with supported prefix.
|
|
"""
|
|
if v is None:
|
|
return v
|
|
if not any(v.startswith(prefix) for prefix in ("sqlite+", "postgresql+")):
|
|
raise ValueError("Database URL must start with 'sqlite+' or 'postgresql+'")
|
|
return v
|
|
|
|
|
|
class KCConfig(BaseSettings):
|
|
"""Keycloak configuration.
|
|
|
|
Contains Keycloak authentication server settings.
|
|
|
|
Attributes:
|
|
server_url: Keycloak server base URL.
|
|
realm: Keycloak realm name.
|
|
client_id: OAuth client identifier.
|
|
client_secret: OAuth client secret.
|
|
token_cache_ttl: Token cache time-to-live in seconds.
|
|
|
|
Example:
|
|
>>> kc = KCConfig(server_url="http://localhost:8080", realm="blog")
|
|
"""
|
|
|
|
server_url: str = "http://localhost:8080"
|
|
realm: str = "blog"
|
|
client_id: str = "blog-api"
|
|
client_secret: str = Field(
|
|
default="",
|
|
description="Keycloak client secret - must be set via env in production",
|
|
)
|
|
token_cache_ttl: int = 60
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="KC_",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
"""Check if Keycloak is properly configured.
|
|
|
|
Returns:
|
|
True if client_secret is set.
|
|
"""
|
|
return bool(self.client_secret)
|
|
|
|
|
|
class SecurityConfig(BaseSettings):
|
|
"""Security configuration.
|
|
|
|
Contains security-related settings for JWT and authentication.
|
|
|
|
Attributes:
|
|
secret_key: Secret key for JWT signing.
|
|
access_token_expire_minutes: Token expiration time in minutes.
|
|
|
|
Example:
|
|
>>> security = SecurityConfig(secret_key="super-secret-key")
|
|
"""
|
|
|
|
secret_key: str = Field(
|
|
default="", description="Secret key for JWT - must be set via env in production"
|
|
)
|
|
access_token_expire_minutes: int = 30
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="SECURITY_",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
"""Check if security is properly configured.
|
|
|
|
Returns:
|
|
True if secret_key is set.
|
|
"""
|
|
return bool(self.secret_key)
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application configuration settings with composition.
|
|
|
|
Main settings class that composes all sub-configurations.
|
|
Validates production settings and provides computed properties.
|
|
|
|
Attributes:
|
|
environment: Current deployment environment.
|
|
app: Application configuration.
|
|
db: Database configuration.
|
|
kc: Keycloak configuration.
|
|
security: Security configuration.
|
|
|
|
Raises:
|
|
ValueError: If required production settings are missing.
|
|
|
|
Example:
|
|
>>> settings = Settings()
|
|
>>> print(settings.database_url)
|
|
"""
|
|
|
|
environment: Environment = Environment.DEV
|
|
|
|
app: AppConfig = Field(default_factory=AppConfig)
|
|
db: DBConfig = Field(default_factory=DBConfig)
|
|
kc: KCConfig = Field(default_factory=KCConfig)
|
|
security: SecurityConfig = Field(default_factory=SecurityConfig)
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
env_nested_delimiter="__",
|
|
)
|
|
|
|
def model_post_init(self, __context: object) -> None:
|
|
"""Validate settings after initialization.
|
|
|
|
Checks that required settings are configured for production mode.
|
|
|
|
Raises:
|
|
ValueError: If required production settings are missing.
|
|
"""
|
|
if self.is_prod:
|
|
if not self.security.is_configured:
|
|
raise ValueError("SECURITY_SECRET_KEY must be set in production mode")
|
|
if not self.kc.is_configured:
|
|
raise ValueError("KC_CLIENT_SECRET must be set in production mode")
|
|
|
|
@cached_property
|
|
def database_url(self) -> str:
|
|
"""Get database URL based on environment.
|
|
|
|
Returns configured URL or builds one from components.
|
|
Uses SQLite for development, PostgreSQL for production.
|
|
|
|
Returns:
|
|
Complete database URL string.
|
|
"""
|
|
if self.db.url:
|
|
return self.db.url
|
|
|
|
if self.environment == Environment.PROD:
|
|
return str(
|
|
PostgresDsn.build(
|
|
scheme="postgresql+asyncpg",
|
|
username=self.db.user,
|
|
password=self.db.password,
|
|
host=self.db.host,
|
|
port=self.db.port,
|
|
path=self.db.name,
|
|
)
|
|
)
|
|
|
|
return "sqlite+aiosqlite:///./blog.db"
|
|
|
|
@property
|
|
def is_dev(self) -> bool:
|
|
"""Check if running in development mode.
|
|
|
|
Returns:
|
|
True if environment is DEV.
|
|
"""
|
|
return self.environment == Environment.DEV
|
|
|
|
@property
|
|
def is_prod(self) -> bool:
|
|
"""Check if running in production mode.
|
|
|
|
Returns:
|
|
True if environment is PROD.
|
|
"""
|
|
return self.environment == Environment.PROD
|
|
|
|
|
|
settings = Settings()
|