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
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 = "kakaotalk"
|
|
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)
|