diff --git a/README.md b/README.md index c852215..046abd5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

- + @@ -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 diff --git a/pyproject.toml b/pyproject.toml index c676dfe..238ac0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/coinhunter/__init__.py b/src/coinhunter/__init__.py index 23f8bf1..0e1d7d4 100644 --- a/src/coinhunter/__init__.py +++ b/src/coinhunter/__init__.py @@ -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" diff --git a/src/coinhunter/cli.py b/src/coinhunter/cli.py index c5b1c32..c19f385 100644 --- a/src/coinhunter/cli.py +++ b/src/coinhunter/cli.py @@ -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: diff --git a/src/coinhunter/runtime.py b/src/coinhunter/runtime.py index 45aaf7c..fdc80ba 100644 --- a/src/coinhunter/runtime.py +++ b/src/coinhunter/runtime.py @@ -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(), + } diff --git a/tests/test_cli.py b/tests/test_cli.py index dd8d234..e9f13dc 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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)