167 lines
5.4 KiB
Python
167 lines
5.4 KiB
Python
"""Typed configuration via pydantic-settings (12-factor).
|
|
|
|
Base `Settings` carries everything shared across services. A service imports
|
|
this and reads only the fields it needs. All values come from environment
|
|
variables (or `.env`); nothing is hardcoded.
|
|
|
|
Per-service entrypoints may subclass `Settings` to add their own fields
|
|
(e.g. the bot adds `BOT_TOKEN`, `API_URL`). See docs/ARCHITECTURE.md §11 for the
|
|
full env reference.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from typing import Literal
|
|
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
Env = Literal["dev", "staging", "prod"]
|
|
RefundPolicy = Literal["all", "infra_only"]
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application configuration. See docs/ARCHITECTURE.md §11."""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
# --- runtime ---
|
|
env: Env = "dev"
|
|
log_level: str = "INFO"
|
|
|
|
# --- datastores / brokers ---
|
|
database_url: str = Field(..., description="async DSN: postgresql+asyncpg://...")
|
|
redis_url: str = "redis://redis:6379/0"
|
|
rabbitmq_url: str = Field(..., description="amqp://user:pass@host:5672//")
|
|
|
|
# --- RabbitMQ tuning ---
|
|
mq_prefetch_extract: int = 1
|
|
mq_prefetch_analyze: int = 3
|
|
mq_prefetch_notify: int = 5
|
|
mq_max_attempts: int = 5
|
|
mq_retry_base_ms: int = 2000
|
|
|
|
# --- object storage (MinIO) ---
|
|
s3_endpoint_url: str
|
|
s3_access_key: str
|
|
s3_secret_key: str
|
|
s3_bucket: str = "contract-check-docs"
|
|
s3_region: str = "us-east-1"
|
|
s3_server_side_encryption: bool = False
|
|
doc_retention_days: int = 7
|
|
text_retention_days: int = 30
|
|
|
|
# --- billing ---
|
|
refund_policy: RefundPolicy = "all"
|
|
|
|
# --- observability (empty disables) ---
|
|
sentry_dsn: str = ""
|
|
otel_exporter_otlp_endpoint: str = ""
|
|
otel_service_name: str = "contract-check"
|
|
|
|
# --- LLM provider ---
|
|
llm_provider: str = "ollama_cloud"
|
|
ollama_host: str = ""
|
|
ollama_api_key: str = ""
|
|
ollama_model: str = "qwen2.5:14b"
|
|
ollama_fallback_model: str = "qwen2.5:7b"
|
|
ollama_temperature: float = 0.2
|
|
ollama_num_predict: int = 3072
|
|
ollama_timeout: float = 120.0
|
|
ollama_max_concurrency: int = 3
|
|
|
|
# --- API ---
|
|
api_host: str = "0.0.0.0"
|
|
api_port: int = 8000
|
|
api_metrics_port: int = 9100
|
|
b2b_default_rate_limit_rps: int = 3
|
|
|
|
# --- auth (JWT + Telegram identity verification) ---
|
|
telegram_bot_token: str = Field(
|
|
"", description="Telegram bot token; used to verify Login Widget / Mini App signatures"
|
|
)
|
|
jwt_secret: str = Field(..., description="HS256 secret for signing user JWTs")
|
|
jwt_algorithm: str = "HS256"
|
|
jwt_access_ttl_minutes: int = 24 * 60 # 24 hours default; tune per env
|
|
jwt_refresh_ttl_days: int = 30 # refresh-token lifetime for webUI auth
|
|
|
|
# --- webUI auth (email + password) ---
|
|
web_auth_enabled: bool = Field(
|
|
default=True,
|
|
description="Toggle for /api/v1/auth/{register,login,...} email-password routes",
|
|
)
|
|
password_reset_ttl_minutes: int = 60 # reset-token validity window
|
|
web_app_base_url: str = Field(
|
|
default="http://localhost:5173",
|
|
description="Base URL of the webUI SPA — used to build password-reset links",
|
|
)
|
|
# Minimum password length enforced at register / reset.
|
|
password_min_length: int = 8
|
|
|
|
# --- admin panel (server-rendered, mounted at /admin in the api) ---
|
|
web_admin_enabled: bool = Field(
|
|
default=True,
|
|
description="Toggle for the /admin management UI (users, future subscriptions)",
|
|
)
|
|
admin_required_role: str = Field(
|
|
default="admin",
|
|
description="users.role value required to enter /admin (must match the DB CHECK constraint)",
|
|
)
|
|
admin_default_email: str = Field(
|
|
default="admin@contract-check.local",
|
|
description="Default admin panel login email (auto-created if no admin exists)",
|
|
)
|
|
admin_default_password: str = Field(
|
|
default="",
|
|
description="Default admin panel password. Empty disables auto-admin creation.",
|
|
)
|
|
|
|
# --- SMTP (notification transport; empty host disables sending) ---
|
|
smtp_host: str = ""
|
|
smtp_port: int = 587
|
|
smtp_username: str = ""
|
|
smtp_password: str = ""
|
|
smtp_from: str = Field(
|
|
default="no-reply@contract-check.local",
|
|
description="From: address used by the notify worker",
|
|
)
|
|
smtp_use_tls: bool = True # STARTTLS on port 587; set false for plain SMTP
|
|
|
|
# --- logging ---
|
|
log_format: str = "json" # json | console
|
|
|
|
# --- analysis ---
|
|
chunk_size_chars: int = 10000
|
|
|
|
@property
|
|
def json_logs(self) -> bool:
|
|
"""JSON logs in staging/prod, pretty console in dev.
|
|
|
|
Override with LOG_FORMAT=json|console.
|
|
"""
|
|
fmt = self.log_format.lower()
|
|
if fmt == "console":
|
|
return False
|
|
if fmt == "json":
|
|
return True
|
|
return self.env != "dev"
|
|
|
|
@property
|
|
def log_format_value(self) -> str:
|
|
"""Resolved log format: json in prod/staging unless explicitly console."""
|
|
fmt = self.log_format.lower()
|
|
if fmt in ("json", "console"):
|
|
return fmt
|
|
return "json" if self.env != "dev" else "console"
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
"""Cached settings singleton. Call `get_settings.cache_clear()` to reset."""
|
|
return Settings() # type: ignore[call-arg]
|