feat: add self-update command and bump to 2.0.1

- Add `coinhunter update` CLI command for pipx/pip upgrade
- README: document update behavior and recommend pipx install
- Dynamic version badge with cacheSeconds=60
- Version bump: 2.0.0 → 2.0.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 18:00:43 +08:00
parent 52cd76a750
commit b78845eb43
6 changed files with 71 additions and 5 deletions

View File

@@ -11,7 +11,7 @@
</p>
<p align="center">
<a href="https://pypi.org/project/coinhunter/"><img src="https://img.shields.io/pypi/v/coinhunter?style=flat-square&color=F7B93E&labelColor=1a1a1a" /></a>
<a href="https://pypi.org/project/coinhunter/"><img src="https://img.shields.io/pypi/v/coinhunter?style=flat-square&color=F7B93E&labelColor=1a1a1a&cacheSeconds=60" /></a>
<a href="#"><img src="https://img.shields.io/badge/python-3.10%2B-3776ab?style=flat-square&logo=python&logoColor=white&labelColor=1a1a1a" /></a>
<a href="#"><img src="https://img.shields.io/badge/tests-passing-22c55e?style=flat-square&labelColor=1a1a1a" /></a>
<a href="#"><img src="https://img.shields.io/badge/lint-ruff%20%2B%20mypy-8b5cf6?style=flat-square&labelColor=1a1a1a" /></a>
@@ -21,11 +21,25 @@
## Install
For end users, install from PyPI with [pipx](https://pipx.pypa.io/) (recommended) to avoid polluting your system Python:
```bash
pip install -e ".[dev]"
pipx install coinhunter
coinhunter --help
```
Check the installed version:
```bash
coinhunter --version
```
To update later:
```bash
pipx upgrade coinhunter
```
## Initialize runtime
```bash
@@ -73,8 +87,13 @@ coinhunter trade futures close BTCUSDT
coinhunter opportunity portfolio
coinhunter opportunity scan
coinhunter opportunity scan --symbols BTCUSDT ETHUSDT SOLUSDT
# Self-update
coinhunter update
```
`update` will try `pipx upgrade coinhunter` first, and fall back to `pip install --upgrade coinhunter` if pipx is not available.
## Architecture
CoinHunter V2 uses a flat, direct architecture:
@@ -106,6 +125,16 @@ Events include:
## Development
Clone the repo and install in editable mode:
```bash
git clone https://git.tacitlab.cc/TacitLab/coinhunter-cli.git
cd coinhunter-cli
pip install -e ".[dev]"
```
Run quality checks:
```bash
pytest tests/ # run tests
ruff check src tests # lint

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "coinhunter"
version = "2.0.0"
version = "2.0.1"
description = "Binance-first trading CLI for balances, market data, opportunity scanning, and execution."
readme = "README.md"
license = {text = "MIT"}

View File

@@ -1,3 +1,8 @@
"""CoinHunter V2."""
__version__ = "2.0.0"
try:
from importlib.metadata import version
__version__ = version("coinhunter")
except Exception: # pragma: no cover
__version__ = "unknown"

View File

@@ -10,7 +10,7 @@ from . import __version__
from .binance.spot_client import SpotBinanceClient
from .binance.um_futures_client import UMFuturesClient
from .config import ensure_init_files, get_binance_credentials, load_config
from .runtime import get_runtime_paths, print_json
from .runtime import get_runtime_paths, print_json, self_update
from .services import account_service, market_service, opportunity_service, trade_service
@@ -102,6 +102,8 @@ def build_parser() -> argparse.ArgumentParser:
scan_parser = opportunity_subparsers.add_parser("scan")
scan_parser.add_argument("--symbols", nargs="*")
subparsers.add_parser("update", help="Upgrade coinhunter to the latest version")
return parser
@@ -231,6 +233,10 @@ def main(argv: list[str] | None = None) -> int:
return 0
parser.error("opportunity requires `portfolio` or `scan`")
if args.command == "update":
print_json(self_update())
return 0
parser.error(f"Unsupported command {args.command}")
return 2
except Exception as exc:

View File

@@ -4,6 +4,9 @@ from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from dataclasses import asdict, dataclass, is_dataclass
from datetime import date, datetime
from pathlib import Path
@@ -50,3 +53,17 @@ def json_default(value: Any) -> Any:
def print_json(payload: Any) -> None:
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True, default=json_default))
def self_update() -> dict[str, Any]:
if shutil.which("pipx"):
cmd = ["pipx", "upgrade", "coinhunter"]
else:
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "coinhunter"]
result = subprocess.run(cmd, capture_output=True, text=True)
return {
"command": " ".join(cmd),
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}

View File

@@ -36,3 +36,12 @@ class CLITestCase(unittest.TestCase):
result = cli.main(["market", "tickers", "BTCUSDT"])
self.assertEqual(result, 1)
self.assertIn("error: boom", stderr.getvalue())
def test_update_dispatches(self):
captured = {}
with patch.object(cli, "self_update", return_value={"command": "pipx upgrade coinhunter", "returncode": 0}), patch.object(
cli, "print_json", side_effect=lambda payload: captured.setdefault("payload", payload)
):
result = cli.main(["update"])
self.assertEqual(result, 0)
self.assertEqual(captured["payload"]["returncode"], 0)