twitch-byte-bot / cli /config.py
JuanCS-Dev's picture
Deploying CLI completion and HF Token auth support
d5dc912 verified
Raw History Blame Contribute Delete
4.28 kB
# SPDX-License-Identifier: MIT
"""Configuration loader for the Byte Agent CLI.
Resolution order (highest priority first):
1. CLI flags (--url, --token)
2. Environment variables (BYTE_API_URL, BYTE_ADMIN_TOKEN)
3. Config file (~/.byterc)
4. Defaults (http://localhost:7860, empty token)
"""
from __future__ import annotations
import configparser
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Final
DEFAULT_API_URL: Final[str] = "http://localhost:7860"
DEFAULT_CONFIG_PATH: Final[Path] = Path.home() / ".byterc"
DEFAULT_PROFILE: Final[str] = "default"
_ENV_API_URL: Final[str] = "BYTE_API_URL"
_ENV_ADMIN_TOKEN: Final[str] = "BYTE_ADMIN_TOKEN"
# Fallback: read from the same env var the bot uses
_ENV_DASHBOARD_TOKEN: Final[str] = "BYTE_DASHBOARD_ADMIN_TOKEN"
_ENV_HF_TOKEN: Final[str] = "HF_TOKEN"
@dataclass(frozen=True, slots=True)
class CLIConfig:
"""Immutable configuration for a CLI session."""
url: str = DEFAULT_API_URL
token: str = ""
hf_token: str = ""
channel: str = "default"
json_output: bool = False
def with_overrides(
self,
*,
url: str | None = None,
token: str | None = None,
hf_token: str | None = None,
channel: str | None = None,
json_output: bool | None = None,
) -> CLIConfig:
"""Return a new config with selective overrides (non-None values win)."""
return CLIConfig(
url=url if url is not None else self.url,
token=token if token is not None else self.token,
hf_token=hf_token if hf_token is not None else self.hf_token,
channel=channel if channel is not None else self.channel,
json_output=json_output if json_output is not None else self.json_output,
)
def _read_config_file(
path: Path = DEFAULT_CONFIG_PATH,
profile: str = DEFAULT_PROFILE,
) -> dict[str, str]:
"""Parse an INI-style config file and return the profile section as a dict.
Returns an empty dict if the file doesn't exist or the profile is missing.
"""
if not path.is_file():
return {}
parser = configparser.ConfigParser()
try:
parser.read(str(path), encoding="utf-8")
except configparser.Error:
return {}
if not parser.has_section(profile):
return {}
return dict(parser.items(profile))
def _read_env() -> dict[str, str]:
"""Read CLI-relevant environment variables."""
result: dict[str, str] = {}
url = os.environ.get(_ENV_API_URL, "").strip()
if url:
result["url"] = url
token = os.environ.get(_ENV_ADMIN_TOKEN, "").strip()
if not token:
token = os.environ.get(_ENV_DASHBOARD_TOKEN, "").strip()
if token:
result["token"] = token
hf_token = os.environ.get(_ENV_HF_TOKEN, "").strip()
if hf_token:
result["hf_token"] = hf_token
return result
def load_config(
*,
cli_url: str | None = None,
cli_token: str | None = None,
cli_channel: str | None = None,
cli_json: bool = False,
config_path: Path = DEFAULT_CONFIG_PATH,
profile: str = DEFAULT_PROFILE,
) -> CLIConfig:
"""Build a CLIConfig by merging file → env → CLI flags (ascending priority)."""
file_values = _read_config_file(config_path, profile)
env_values = _read_env()
# Start from defaults, layer file, then env, then CLI flags
url = file_values.get("url", DEFAULT_API_URL)
url = env_values.get("url", url)
if cli_url:
url = cli_url
token = file_values.get("token", "")
token = env_values.get("token", token)
if cli_token:
token = cli_token
hf_token = file_values.get("hf_token", "")
hf_token = env_values.get("hf_token", hf_token)
# cli_hf_token logic added in main.py will be set via with_overrides if needed,
# but let's just add it to config.py load_config signature to be clean:
channel = file_values.get("channel", "default")
if cli_channel:
channel = cli_channel
# Normalize URL: strip trailing slash
url = url.rstrip("/")
return CLIConfig(
url=url,
token=token,
hf_token=hf_token,
channel=channel.strip().lower() or "default",
json_output=cli_json,
)