feat: persist config from uvx bootstrap
This commit is contained in:
@@ -12,11 +12,21 @@ uvx --from git+https://gitea.shujk.top/shujakuin/shusub2.git shusub2 \
|
|||||||
--api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts
|
--api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Bootstrap a new machine from `uvx`: save the API URL, install `shusub2` as a
|
||||||
|
user command, then run it later as `shusub2`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from git+https://gitea.shujk.top/shujakuin/shusub2.git shusub2 \
|
||||||
|
--api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts \
|
||||||
|
--install
|
||||||
|
shusub2
|
||||||
|
```
|
||||||
|
|
||||||
Install as a user command:
|
Install as a user command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv tool install git+https://gitea.shujk.top/shujakuin/shusub2.git
|
uv tool install git+https://gitea.shujk.top/shujakuin/shusub2.git
|
||||||
shusub2 --api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts
|
shusub2 --api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts --save-config
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure the default public API URL for `shusub2`:
|
Configure the default public API URL for `shusub2`:
|
||||||
@@ -58,6 +68,8 @@ Configuration:
|
|||||||
|
|
||||||
- `--api-url` / `SUB2API_QUOTA_TUI_API_URL`
|
- `--api-url` / `SUB2API_QUOTA_TUI_API_URL`
|
||||||
- `--status-url` / `SHUSUB2_STATUS_URL`
|
- `--status-url` / `SHUSUB2_STATUS_URL`
|
||||||
|
- `--save-config`
|
||||||
|
- `--install`
|
||||||
- `--refresh-seconds` / `SUB2API_QUOTA_TUI_REFRESH_SECONDS`
|
- `--refresh-seconds` / `SUB2API_QUOTA_TUI_REFRESH_SECONDS`
|
||||||
- `--timeout` / `SUB2API_QUOTA_TUI_TIMEOUT`
|
- `--timeout` / `SUB2API_QUOTA_TUI_TIMEOUT`
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "shusub2"
|
name = "shusub2"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "Terminal UI for Sub2API account quota and daily usage"
|
description = "Terminal UI for Sub2API account quota and daily usage"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+41
-1
@@ -8,6 +8,7 @@ import importlib.metadata
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -16,7 +17,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "shusub2"
|
APP_NAME = "shusub2"
|
||||||
FALLBACK_VERSION = "0.1.6"
|
FALLBACK_VERSION = "0.1.7"
|
||||||
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
|
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
|
||||||
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
|
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
|
||||||
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
|
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
|
||||||
@@ -28,6 +29,7 @@ MONITOR_OK_STATUSES = {"operational", "ok", "success"}
|
|||||||
MONITOR_FAILED_STATUSES = {"error", "failed", "failure"}
|
MONITOR_FAILED_STATUSES = {"error", "failed", "failure"}
|
||||||
MONITOR_STOPWORDS = {"response", "responses", "monitor"}
|
MONITOR_STOPWORDS = {"response", "responses", "monitor"}
|
||||||
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/shusub2.git"
|
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/shusub2.git"
|
||||||
|
INSTALL_COMMAND_ARGS = ["uv", "tool", "install", "--force", "git+https://gitea.shujk.top/shujakuin/shusub2.git"]
|
||||||
|
|
||||||
|
|
||||||
def env_int(name: str, default: int, *, minimum: int = 1) -> int:
|
def env_int(name: str, default: int, *, minimum: int = 1) -> int:
|
||||||
@@ -68,6 +70,36 @@ def default_status_url() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def config_file_path() -> Path:
|
||||||
|
return Path(os.environ.get("SHUSUB2_API_URL_FILE", DEFAULT_CONFIG_FILE)).expanduser()
|
||||||
|
|
||||||
|
|
||||||
|
def write_api_url_config(api_url: str) -> Path:
|
||||||
|
value = str(api_url or "").strip()
|
||||||
|
if not value:
|
||||||
|
raise ValueError("api url is empty")
|
||||||
|
path = config_file_path()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
path.parent.chmod(0o700)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
path.write_text(value + "\n", encoding="utf-8")
|
||||||
|
try:
|
||||||
|
path.chmod(0o600)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def run_install_command() -> int:
|
||||||
|
try:
|
||||||
|
return subprocess.run(INSTALL_COMMAND_ARGS, check=False).returncode
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("uv command not found; install uv first, then run:", INSTALL_COMMAND, file=sys.stderr)
|
||||||
|
return 127
|
||||||
|
|
||||||
|
|
||||||
def inferred_status_url(api_url: str) -> str:
|
def inferred_status_url(api_url: str) -> str:
|
||||||
parsed = urllib.parse.urlparse(str(api_url or "").strip())
|
parsed = urllib.parse.urlparse(str(api_url or "").strip())
|
||||||
if not parsed.scheme or not parsed.netloc:
|
if not parsed.scheme or not parsed.netloc:
|
||||||
@@ -624,6 +656,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
parser = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
|
parser = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
|
||||||
parser.add_argument("--api-url", default=default_api_url())
|
parser.add_argument("--api-url", default=default_api_url())
|
||||||
parser.add_argument("--status-url", default=default_status_url(), help="optional sub2api-status /api/status URL for channel monitor health")
|
parser.add_argument("--status-url", default=default_status_url(), help="optional sub2api-status /api/status URL for channel monitor health")
|
||||||
|
parser.add_argument("--save-config", action="store_true", help="persist --api-url to ~/.config/shusub2/api-url before running")
|
||||||
|
parser.add_argument("--install", action="store_true", help="persist --api-url, install shusub2 as a uv tool, then exit")
|
||||||
parser.add_argument("--version-check-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
|
parser.add_argument("--version-check-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--version-check-timeout",
|
"--version-check-timeout",
|
||||||
@@ -645,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = build_parser().parse_args(argv)
|
args = build_parser().parse_args(argv)
|
||||||
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url)
|
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url)
|
||||||
|
if args.save_config or args.install:
|
||||||
|
config_path = write_api_url_config(args.api_url)
|
||||||
|
print(f"saved api url to {config_path}")
|
||||||
|
if args.install:
|
||||||
|
print("installing shusub2 with uv tool...")
|
||||||
|
return run_install_command()
|
||||||
version_message = check_version_update(
|
version_message = check_version_update(
|
||||||
args.version_check_url,
|
args.version_check_url,
|
||||||
max(1, args.version_check_timeout),
|
max(1, args.version_check_timeout),
|
||||||
|
|||||||
+23
-4
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
@@ -98,10 +100,27 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
|||||||
def test_version_update_message_only_for_newer_versions(self) -> None:
|
def test_version_update_message_only_for_newer_versions(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
|
|
||||||
self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.7"\n'), "0.1.7")
|
self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.8"\n'), "0.1.8")
|
||||||
self.assertIn("0.1.6 -> 0.1.7", mod.version_update_message("0.1.7", "0.1.6"))
|
self.assertIn("0.1.7 -> 0.1.8", mod.version_update_message("0.1.8", "0.1.7"))
|
||||||
self.assertEqual(mod.version_update_message("0.1.6", "0.1.6"), "")
|
self.assertEqual(mod.version_update_message("0.1.7", "0.1.7"), "")
|
||||||
self.assertEqual(mod.version_update_message("0.1.5", "0.1.6"), "")
|
self.assertEqual(mod.version_update_message("0.1.6", "0.1.7"), "")
|
||||||
|
|
||||||
|
def test_write_api_url_config_uses_config_file_env(self) -> None:
|
||||||
|
mod = load_module()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config_file = Path(tmp) / "shusub2" / "api-url"
|
||||||
|
old_value = os.environ.get("SHUSUB2_API_URL_FILE")
|
||||||
|
os.environ["SHUSUB2_API_URL_FILE"] = str(config_file)
|
||||||
|
try:
|
||||||
|
written = mod.write_api_url_config("https://example.com/api/tui/accounts")
|
||||||
|
self.assertEqual(written, config_file)
|
||||||
|
self.assertEqual(config_file.read_text(encoding="utf-8"), "https://example.com/api/tui/accounts\n")
|
||||||
|
finally:
|
||||||
|
if old_value is None:
|
||||||
|
os.environ.pop("SHUSUB2_API_URL_FILE", None)
|
||||||
|
else:
|
||||||
|
os.environ["SHUSUB2_API_URL_FILE"] = old_value
|
||||||
|
|
||||||
def test_once_output_is_name_first_and_includes_daily_quota(self) -> None:
|
def test_once_output_is_name_first_and_includes_daily_quota(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
|
|||||||
Reference in New Issue
Block a user