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,
    )