- SourceCraftClient (httpx): list_runs, get_run, get_cube_logs (постранично), run_commit_sha; ретраи на сетевые ошибки и 5xx с экспоненциальной задержкой, 4xx — SourceCraftError без повторов - модели: RunStatus/EventType из swagger, «1970-01-01» = не задано, failed_cubes без служебных #-кубов - у прогона в API нет sha → куб check/meta в tgit-uploads печатает TGIT_COMMIT_SHA (ADR-014) - CLI: python -m app.sourcecraft.cli runs - тесты на реальных ответах API (фикстуры), порог покрытия 45 → 80 Refs #10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Typed configuration from the environment / `.env` — one settings class per subsystem (ARCHITECTURE §2.11)."""
|
|
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
from pydantic import Field, SecretStr, field_validator
|
|
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
|
|
|
_ENV = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
|
|
|
|
class TelegramSettings(BaseSettings):
|
|
model_config = SettingsConfigDict(**_ENV, env_prefix="TELEGRAM_")
|
|
|
|
bot_token: SecretStr
|
|
|
|
|
|
class AccessSettings(BaseSettings):
|
|
model_config = _ENV
|
|
|
|
# Bootstrap admins (SPEC A-8). Comma-separated in the env: ADMIN_IDS=1,2
|
|
admin_ids: Annotated[frozenset[int], NoDecode] = frozenset()
|
|
|
|
@field_validator("admin_ids", mode="before")
|
|
@classmethod
|
|
def _split_ids(cls, value: object) -> object:
|
|
if isinstance(value, str):
|
|
return frozenset(int(part) for part in value.replace(" ", "").split(",") if part)
|
|
return value
|
|
|
|
|
|
class SourceCraftSettings(BaseSettings):
|
|
model_config = SettingsConfigDict(**_ENV, env_prefix="SOURCECRAFT_")
|
|
|
|
token: SecretStr = SecretStr("")
|
|
api_url: str = "https://api.sourcecraft.tech"
|
|
uploads_repo: str = "rogachevegorushka/tgit-uploads"
|
|
# Web link to a CI run. The SPA answers 200 on any path, so the format is unverified — adjust if it 404s in UI.
|
|
run_url_template: str = "https://sourcecraft.dev/{repo}/cicd/runs/{run}"
|
|
timeout: float = Field(default=30.0, gt=0)
|
|
max_attempts: int = Field(default=5, ge=1)
|
|
|
|
|
|
class RuntimeSettings(BaseSettings):
|
|
model_config = _ENV
|
|
|
|
log_level: str = "INFO"
|
|
# Non-secret configs (texts, thresholds, prompts). Relative to the working dir: /srv/tgit in Docker.
|
|
config_dir: Path = Path("config")
|
|
# The heartbeat file is touched while the event loop is alive; the Docker healthcheck reads its age.
|
|
heartbeat_file: Path = Path("/tmp/tgit-heartbeat")
|
|
heartbeat_interval: float = Field(default=10.0, gt=0)
|
|
heartbeat_max_age: float = Field(default=30.0, gt=0)
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = _ENV
|
|
|
|
telegram: TelegramSettings = Field(default_factory=TelegramSettings)
|
|
access: AccessSettings = Field(default_factory=AccessSettings)
|
|
sourcecraft: SourceCraftSettings = Field(default_factory=SourceCraftSettings)
|
|
runtime: RuntimeSettings = Field(default_factory=RuntimeSettings)
|