97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
|
|
"""Configuration management — env vars, config file, and defaults."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
import toml
|
||
|
|
from platformdirs import user_config_dir
|
||
|
|
from pydantic import field_validator
|
||
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
|
|
||
|
|
CONFIG_DIR = Path(user_config_dir("freepik-cli"))
|
||
|
|
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
||
|
|
|
||
|
|
|
||
|
|
class FreepikConfig(BaseSettings):
|
||
|
|
"""
|
||
|
|
Configuration with priority (highest to lowest):
|
||
|
|
1. CLI --api-key flag (handled in commands directly)
|
||
|
|
2. FREEPIK_* environment variables
|
||
|
|
3. ~/.config/freepik-cli/config.toml
|
||
|
|
4. Defaults below
|
||
|
|
"""
|
||
|
|
|
||
|
|
model_config = SettingsConfigDict(
|
||
|
|
env_prefix="FREEPIK_",
|
||
|
|
env_file=".env",
|
||
|
|
env_file_encoding="utf-8",
|
||
|
|
extra="ignore",
|
||
|
|
)
|
||
|
|
|
||
|
|
api_key: Optional[str] = None
|
||
|
|
base_url: str = "https://api.freepik.com"
|
||
|
|
default_output_dir: str = "."
|
||
|
|
default_image_model: str = "flux-2-pro"
|
||
|
|
default_video_model: str = "kling-o1-pro"
|
||
|
|
default_upscale_mode: str = "precision-v2"
|
||
|
|
poll_timeout: int = 600
|
||
|
|
poll_max_interval: int = 15
|
||
|
|
show_banner: bool = True
|
||
|
|
|
||
|
|
@field_validator("api_key", mode="before")
|
||
|
|
@classmethod
|
||
|
|
def strip_api_key(cls, v: Optional[str]) -> Optional[str]:
|
||
|
|
return v.strip() if isinstance(v, str) else v
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def load(cls) -> "FreepikConfig":
|
||
|
|
"""Load from config file, then overlay environment variables."""
|
||
|
|
file_data: dict = {}
|
||
|
|
if CONFIG_FILE.exists():
|
||
|
|
try:
|
||
|
|
file_data = toml.load(CONFIG_FILE)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return cls(**file_data)
|
||
|
|
|
||
|
|
def save(self, exclude_keys: set[str] | None = None) -> None:
|
||
|
|
"""Persist non-sensitive config to disk."""
|
||
|
|
exclude_keys = (exclude_keys or set()) | {"api_key"}
|
||
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
data = self.model_dump(exclude=exclude_keys, exclude_none=True)
|
||
|
|
# Stringify paths
|
||
|
|
for k, v in data.items():
|
||
|
|
if isinstance(v, Path):
|
||
|
|
data[k] = str(v)
|
||
|
|
with open(CONFIG_FILE, "w") as f:
|
||
|
|
toml.dump(data, f)
|
||
|
|
|
||
|
|
def to_display_dict(self) -> dict:
|
||
|
|
"""Return all settings as a displayable dict (keeps api_key for masking)."""
|
||
|
|
d = self.model_dump()
|
||
|
|
return {k: v for k, v in d.items()}
|
||
|
|
|
||
|
|
def set_value(self, key: str, value: str) -> None:
|
||
|
|
"""Update a single config key and save."""
|
||
|
|
allowed = {
|
||
|
|
"base_url", "default_output_dir", "default_image_model",
|
||
|
|
"default_video_model", "default_upscale_mode", "poll_timeout",
|
||
|
|
"poll_max_interval", "show_banner",
|
||
|
|
}
|
||
|
|
if key not in allowed:
|
||
|
|
raise ValueError(
|
||
|
|
f"Key '{key}' is not configurable via this command. "
|
||
|
|
f"Use the FREEPIK_API_KEY environment variable to set the API key."
|
||
|
|
)
|
||
|
|
current = self.model_dump()
|
||
|
|
if key in ("poll_timeout", "poll_max_interval"):
|
||
|
|
current[key] = int(value)
|
||
|
|
elif key == "show_banner":
|
||
|
|
current[key] = value.lower() in ("true", "1", "yes")
|
||
|
|
else:
|
||
|
|
current[key] = value
|
||
|
|
updated = FreepikConfig(**{k: v for k, v in current.items() if v is not None})
|
||
|
|
updated.save()
|