You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
5.3 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
from typing import Optional
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# ── Sub-config models ──────────────────────────────────────────────────────────
class RuntimeConfig(BaseModel):
timezone: str = "America/Phoenix"
headless: bool = True
profile_name: str = "JobAgent"
slow_mo_ms: int = 0
default_timeout_ms: int = 15000
navigation_timeout_ms: int = 30000
max_pages_per_site: int = 3
min_delay_ms: int = 1200
max_delay_ms: int = 3500
class SearchDefaults(BaseModel):
keywords: list[str] = Field(default_factory=lambda: ["orthodontist"])
location: str = ""
remote: bool = False
date_mode: str = "today_or_last_24h"
sort: str = "relevance"
max_items_per_site: int = 30
class SummarizationConfig(BaseModel):
ollama_base_url: str = "http://127.0.0.1:11434"
model: str = "qwen3.5:9b"
temperature: float = 0.1
max_input_items: int = 200
class NotificationConfig(BaseModel):
provider: str = "telegram"
fallback_markdown: bool = True
# ── Main global config ─────────────────────────────────────────────────────────
class GlobalConfig(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_nested_delimiter="__",
extra="ignore",
)
runtime: RuntimeConfig = Field(default_factory=RuntimeConfig)
search_defaults: SearchDefaults = Field(default_factory=SearchDefaults)
summarization: SummarizationConfig = Field(default_factory=SummarizationConfig)
notification: NotificationConfig = Field(default_factory=NotificationConfig)
# ── Loader functions ──────────────────────────────────────────────────────────
def load_global_config() -> GlobalConfig:
"""Load global config from sites/global.yaml, with env var overrides."""
from gimme_job.utils.json_io import read_yaml
from gimme_job.utils.paths import sites_dir
yaml_path = sites_dir() / "global.yaml"
if yaml_path.exists():
data = read_yaml(yaml_path)
return GlobalConfig.model_validate(data)
return GlobalConfig()
def load_site_manifest(site_id: str):
"""Load a site manifest from sites/{site_id}.yaml."""
from gimme_job.models.manifest import SiteManifest
from gimme_job.utils.paths import sites_dir
path = sites_dir() / f"{site_id}.yaml"
if not path.exists():
raise FileNotFoundError(f"Site manifest not found: {path}")
return SiteManifest.from_yaml(path)
def list_enabled_sites() -> list[str]:
"""Return site_ids of all enabled (non-repair_needed) sites from sites/*.yaml."""
from gimme_job.models.manifest import SiteManifest
from gimme_job.utils.paths import sites_dir
enabled = []
for yaml_path in sorted(sites_dir().glob("*.yaml")):
if yaml_path.stem == "global":
continue
try:
manifest = SiteManifest.from_yaml(yaml_path)
if manifest.enabled and not manifest.repair_needed:
enabled.append(manifest.site_id)
except Exception:
pass
return enabled
def list_all_sites() -> list[str]:
"""Return all site_ids from sites/*.yaml (including disabled/repair_needed)."""
from gimme_job.models.manifest import SiteManifest
from gimme_job.utils.paths import sites_dir
sites = []
for yaml_path in sorted(sites_dir().glob("*.yaml")):
if yaml_path.stem == "global":
continue
try:
manifest = SiteManifest.from_yaml(yaml_path)
sites.append(manifest.site_id)
except Exception:
pass
return sites
def merge_query(
global_config: GlobalConfig,
manifest,
cli_overrides: Optional[dict] = None,
):
"""Merge search query with priority: CLI > site override > global defaults."""
from gimme_job.models.dto import SearchQuery
# Start from global defaults
base = global_config.search_defaults
merged = {
"keywords": list(base.keywords),
"location": base.location,
"remote": base.remote,
"date_mode": base.date_mode,
"sort": base.sort,
"max_items": base.max_items_per_site,
}
# Apply site-level overrides
override = manifest.search_override
if override.keywords is not None:
merged["keywords"] = override.keywords
if override.location is not None:
merged["location"] = override.location
if override.remote is not None:
merged["remote"] = override.remote
if override.date_mode is not None:
merged["date_mode"] = override.date_mode
if override.sort is not None:
merged["sort"] = override.sort
if override.max_items is not None:
merged["max_items"] = override.max_items
# Apply CLI overrides
if cli_overrides:
for k, v in cli_overrides.items():
if v is not None and k in merged:
merged[k] = v
return SearchQuery(**merged)