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:
33
README.md
33
README.md
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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/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/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>
|
<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
|
## Install
|
||||||
|
|
||||||
|
For end users, install from PyPI with [pipx](https://pipx.pypa.io/) (recommended) to avoid polluting your system Python:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e ".[dev]"
|
pipx install coinhunter
|
||||||
coinhunter --help
|
coinhunter --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Check the installed version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
coinhunter --version
|
||||||
|
```
|
||||||
|
|
||||||
|
To update later:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx upgrade coinhunter
|
||||||
|
```
|
||||||
|
|
||||||
## Initialize runtime
|
## Initialize runtime
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -73,8 +87,13 @@ coinhunter trade futures close BTCUSDT
|
|||||||
coinhunter opportunity portfolio
|
coinhunter opportunity portfolio
|
||||||
coinhunter opportunity scan
|
coinhunter opportunity scan
|
||||||
coinhunter opportunity scan --symbols BTCUSDT ETHUSDT SOLUSDT
|
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
|
## Architecture
|
||||||
|
|
||||||
CoinHunter V2 uses a flat, direct architecture:
|
CoinHunter V2 uses a flat, direct architecture:
|
||||||
@@ -106,6 +125,16 @@ Events include:
|
|||||||
|
|
||||||
## Development
|
## 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
|
```bash
|
||||||
pytest tests/ # run tests
|
pytest tests/ # run tests
|
||||||
ruff check src tests # lint
|
ruff check src tests # lint
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "coinhunter"
|
name = "coinhunter"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
description = "Binance-first trading CLI for balances, market data, opportunity scanning, and execution."
|
description = "Binance-first trading CLI for balances, market data, opportunity scanning, and execution."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
"""CoinHunter V2."""
|
"""CoinHunter V2."""
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
__version__ = version("coinhunter")
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
__version__ = "unknown"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from . import __version__
|
|||||||
from .binance.spot_client import SpotBinanceClient
|
from .binance.spot_client import SpotBinanceClient
|
||||||
from .binance.um_futures_client import UMFuturesClient
|
from .binance.um_futures_client import UMFuturesClient
|
||||||
from .config import ensure_init_files, get_binance_credentials, load_config
|
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
|
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 = opportunity_subparsers.add_parser("scan")
|
||||||
scan_parser.add_argument("--symbols", nargs="*")
|
scan_parser.add_argument("--symbols", nargs="*")
|
||||||
|
|
||||||
|
subparsers.add_parser("update", help="Upgrade coinhunter to the latest version")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -231,6 +233,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return 0
|
return 0
|
||||||
parser.error("opportunity requires `portfolio` or `scan`")
|
parser.error("opportunity requires `portfolio` or `scan`")
|
||||||
|
|
||||||
|
if args.command == "update":
|
||||||
|
print_json(self_update())
|
||||||
|
return 0
|
||||||
|
|
||||||
parser.error(f"Unsupported command {args.command}")
|
parser.error(f"Unsupported command {args.command}")
|
||||||
return 2
|
return 2
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
from dataclasses import asdict, dataclass, is_dataclass
|
from dataclasses import asdict, dataclass, is_dataclass
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -50,3 +53,17 @@ def json_default(value: Any) -> Any:
|
|||||||
|
|
||||||
def print_json(payload: Any) -> None:
|
def print_json(payload: Any) -> None:
|
||||||
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True, default=json_default))
|
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(),
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,3 +36,12 @@ class CLITestCase(unittest.TestCase):
|
|||||||
result = cli.main(["market", "tickers", "BTCUSDT"])
|
result = cli.main(["market", "tickers", "BTCUSDT"])
|
||||||
self.assertEqual(result, 1)
|
self.assertEqual(result, 1)
|
||||||
self.assertIn("error: boom", stderr.getvalue())
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user