Spaces:
Sleeping
Sleeping
File size: 4,276 Bytes
d5dc912 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | # 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,
)
|