Skip to content

Commit 71c800a

Browse files
yjwyjw
authored andcommitted
fix: support python 3.9 installs
1 parent ed0949c commit 71c800a

11 files changed

Lines changed: 174 additions & 22 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
fail-fast: false
2424
matrix:
25-
python-version: ["3.10", "3.11", "3.12"]
25+
python-version: ["3.9", "3.10", "3.11", "3.12"]
2626
steps:
2727
- uses: actions/checkout@v4
2828
- uses: actions/setup-python@v5

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.17] - 2026-06-14
9+
10+
### Fixed
11+
- Lowered package metadata to Python 3.9+ after verifying the source compiles on Python 3.9, so macOS system `pip3 install young-stock-cli` no longer rejects every release as Python 3.10-only.
12+
- `young update` now prints a concrete Python-version and `python3 -m pip` retry hint when pip fails.
13+
- Removed Python 3.10-only `zip(strict=False)` calls so fund-flow parsing works under Python 3.9.
14+
15+
### Changed
16+
- Install docs now prefer `python3 -m pip install young-stock-cli` and CI covers Python 3.9.
17+
- `young diagnose --json` now prints read-only machine-readable support diagnostics, and tests cover every top-level command's `--help`.
18+
819
## [0.1.16] - 2026-06-04
920

1021
### Changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Thanks for considering a contribution! This project welcomes pull requests, bug
77
```bash
88
git clone https://github.com/AdvancingTitans/young-stock-cli.git
99
cd young-stock-cli
10-
pip install -e ".[dev]"
10+
python3 -m pip install -e ".[dev]"
1111
pytest
1212
ruff check .
1313
```

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ Born out of a real workflow: every trading day after close I wanted the same fiv
1818
## Install
1919

2020
```bash
21-
pip install young-stock-cli
21+
python3 -m pip install young-stock-cli
2222
```
2323

24-
Requires Python 3.10+.
24+
Requires Python 3.9+.
25+
26+
If `pip3 install young-stock-cli` reports that every release requires a different Python version, your
27+
`pip3` is attached to an older Python. Check with `python3 --version`, then install or upgrade with a
28+
Python 3.9+ interpreter:
29+
30+
```bash
31+
python3 -m pip install --upgrade young-stock-cli
32+
```
2533

2634
## Usage
2735

@@ -46,6 +54,7 @@ young daily --format full # full personalized daily report
4654
young daily --only 基金,A股 --quick
4755
young news 3690.HK # multi-source news only
4856
young diagnose # network/source diagnostic
57+
young diagnose --json # machine-readable support diagnostic
4958
young note add "today I reduced chasing"
5059
young alert create 600519 "涨跌幅>5%"
5160
young stock AAPL --no-news
@@ -94,7 +103,7 @@ The internals are being split into focused modules: `young_stock.calendar` handl
94103
- **Short report modes**`young daily --format summary` keeps terminal output compact; `--format key-points` adds a few trend/risk bullets; `--only`, `--order`, and `--quick` trim slower or irrelevant sections.
95104
- **Investment memory management** — list, remove, clear, and group saved stocks/funds with `young profile list`, `remove-stock`, `remove-fund`, `clear`, `clear-stocks`, `clear-funds`, and `profile group create/add`.
96105
- **Local workflow helpers** — lightweight `portfolio`, `alert`, `note`, and `diary` commands store local records for portfolio experiments, reminder rules, investment notes, and saved daily-report text.
97-
- **Diagnostics**`young diagnose` summarizes recent source health and suggests cache/quick-mode fallbacks when public APIs are unstable.
106+
- **Diagnostics**`young diagnose` summarizes recent source health and suggests cache/quick-mode fallbacks when public APIs are unstable; `young diagnose --json` prints read-only, machine-readable support info with Python/version/path/source-health details.
98107
- **Single-stock news**`young news 3690.HK` prints only the news/momentum view, with each item showing source and link status.
99108
- **Smart caching**`~/.young_stock/cache/`, 7-day TTL, auto-pruned. Pass `--refresh` to skip.
100109
- **Trade-day awareness** — nearest-trade-day resolution including weekends and (best-effort) holidays.
@@ -123,7 +132,7 @@ print(get_zt_pool(date))
123132
```bash
124133
git clone https://github.com/AdvancingTitans/young-stock-cli.git
125134
cd young-stock-cli
126-
pip install -e ".[dev]"
135+
python3 -m pip install -e ".[dev]"
127136
pytest
128137
ruff check .
129138
```
@@ -148,7 +157,7 @@ MIT — see [LICENSE](LICENSE).
148157
A 股盘后行情命令行工具。免登录、免 API key、免反爬技巧 — 一行命令把当日涨跌、涨停板、可核验的 A 股资金流向、板块榜全部打到终端。同时支持港股、美股指数和全球快照视图。
149158

150159
```bash
151-
pip install young-stock-cli
160+
python3 -m pip install young-stock-cli
152161
young a # A 股盘后总览(主命令)
153162
young a --no-news # 只看 A 股行情与情绪,跳过新闻
154163
young stock 600519 # 单只股票速览(A股 / 港股 / 美股)

docs/promo-copy.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 推广文案 — young-stock-cli
22

3-
发布时机建议:v0.1.0 在 PyPI 上线后再发,文案里的 `pip install` 才有效。
3+
发布时机建议:v0.1.0 在 PyPI 上线后再发,文案里的 `python3 -m pip install` 才有效。
44

55
---
66

@@ -16,7 +16,7 @@
1616
每天 15:01 都要刷一遍上证、深成指、创业板、涨停板、北向资金…开网页太慢,付费终端没必要,干脆自己写了个命令行。
1717

1818
```
19-
pip install young-stock-cli
19+
python3 -m pip install young-stock-cli
2020
young a
2121
```
2222

@@ -53,7 +53,7 @@ GitHub: github.com/AdvancingTitans/young-stock-cli
5353
每天盘后想看的几个数据:四大指数、涨停板数 / 连板天梯、北向资金、板块热点。开 5 个网页太慢,akshare 装一次拖 200MB 依赖,tushare 要 token 还限流。一气之下自己撸。
5454

5555
### 设计目标
56-
1. `pip install` 装完就能用,无需任何账号配置
56+
1. `python3 -m pip install` 装完就能用,无需任何账号配置
5757
2. 三个运行时依赖:`requests` + `click` + `rich`
5858
3. 数据源单一可控:东方财富公开行情接口
5959
4. 输出对终端友好:rich Table,深浅色都好看
@@ -85,7 +85,7 @@ github.com/AdvancingTitans/young-stock-cli — 欢迎 star。
8585
**正文**
8686

8787
```
88-
pip install young-stock-cli
88+
python3 -m pip install young-stock-cli
8989
young a # A 股盘后总览
9090
young zt-pool # 涨停 / 跌停 / 炸板分析
9191
young flow # 北向资金 + 主力资金

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "young-stock-cli"
7-
version = "0.1.16"
7+
version = "0.1.17"
88
description = "A-share (China stock market) after-hours CLI — no login, no scraping tricks, just data."
99
readme = "README.md"
10-
requires-python = ">=3.10"
10+
requires-python = ">=3.9"
1111
license = { text = "MIT" }
1212
authors = [{ name = "AdvancingTitans" }]
1313
keywords = ["stock", "a-share", "china", "finance", "cli", "eastmoney"]
@@ -19,6 +19,7 @@ classifiers = [
1919
"License :: OSI Approved :: MIT License",
2020
"Operating System :: OS Independent",
2121
"Programming Language :: Python :: 3",
22+
"Programming Language :: Python :: 3.9",
2223
"Programming Language :: Python :: 3.10",
2324
"Programming Language :: Python :: 3.11",
2425
"Programming Language :: Python :: 3.12",
@@ -51,7 +52,7 @@ packages = ["src/young_stock"]
5152

5253
[tool.ruff]
5354
line-length = 110
54-
target-version = "py310"
55+
target-version = "py39"
5556

5657
[tool.ruff.lint]
5758
select = ["E", "F", "W", "I", "B", "UP"]

src/young_stock/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""young-stock-cli: A-share after-hours CLI."""
22

3-
__version__ = "0.1.16"
3+
__version__ = "0.1.17"

src/young_stock/_core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ def fetch_sina_sector_money_flow_snapshot(date_str: str) -> dict[str, str]:
762762
xml = str(data.get("xml") or "")
763763
labels = re.findall(r"<category label='([^']+)'", xml)
764764
values = [_safe_float(v) for v in re.findall(r"<set value='([^']+)'", xml)]
765-
pairs = [(label, value) for label, value in zip(labels, values, strict=False) if value is not None]
765+
pairs = [(label, value) for label, value in zip(labels, values) if value is not None]
766766
if not pairs:
767767
diag("Sina sector money-flow snapshot: empty sector values")
768768
return {}
@@ -1750,7 +1750,7 @@ def get_fund_flow(date_str: str, *, strict_date: bool = True) -> dict[str, str]:
17501750
if klines:
17511751
for row in klines:
17521752
vals = row.split(",")
1753-
result = dict(zip(FUND_FLOW_COLS, vals, strict=False))
1753+
result = dict(zip(FUND_FLOW_COLS, vals))
17541754
result["_source"] = display_source
17551755
result["_scope"] = "A股"
17561756
if latest_result is None:
@@ -1784,7 +1784,7 @@ def get_fund_flow(date_str: str, *, strict_date: bool = True) -> dict[str, str]:
17841784
if klines:
17851785
for row in klines:
17861786
vals = row.split(",")
1787-
result = dict(zip(FUND_FLOW_COLS, vals, strict=False))
1787+
result = dict(zip(FUND_FLOW_COLS, vals))
17881788
result["_source"] = "东财历史日线资金流"
17891789
result["_scope"] = "A股"
17901790
if latest_result is None:

src/young_stock/cli.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"""young-stock-cli command line interface."""
22
from __future__ import annotations
33

4+
import json
5+
import platform
46
import subprocess
57
import sys
68
from datetime import datetime
79

810
import click
911

1012
from . import __version__, _core
11-
from .local_store import load_store, now_label, save_store
13+
from .local_store import load_store, now_label, save_store, young_home
1214
from .profile import (
1315
add_group,
1416
add_group_item,
@@ -107,7 +109,12 @@ def update(pre: bool, user_install: bool) -> None:
107109
click.echo("Running: " + " ".join(cmd))
108110
result = subprocess.run(cmd, check=False)
109111
if result.returncode != 0:
110-
raise click.ClickException(f"update failed with exit code {result.returncode}")
112+
raise click.ClickException(
113+
"update failed with exit code "
114+
f"{result.returncode}. young-stock-cli requires Python 3.9+. "
115+
"Check `python3 --version`, then retry with "
116+
"`python3 -m pip install --upgrade young-stock-cli`."
117+
)
111118

112119

113120
@cli.command(help="Uninstall young-stock-cli from the current Python environment.")
@@ -364,10 +371,42 @@ def profile_group_add(name: str, code: str) -> None:
364371
click.echo(f"Added {code} to group: {name}")
365372

366373

374+
def _diagnostic_payload() -> dict:
375+
sources = []
376+
for name in ["eastmoney", "sina", "tencent", "ths", "futu"]:
377+
snap = _core.SOURCE_HEALTH.snapshot(name)
378+
sources.append({
379+
"name": name,
380+
"success_rate": snap.success_rate,
381+
"average_latency_ms": snap.average_latency_ms,
382+
"should_skip": snap.should_skip,
383+
})
384+
return {
385+
"command": "young diagnose",
386+
"version": __version__,
387+
"python": {
388+
"version": platform.python_version(),
389+
"executable": sys.executable,
390+
},
391+
"paths": {
392+
"profile": str(profile_path()),
393+
"home": str(young_home()),
394+
"cache": str(_core.CACHE_DIR),
395+
},
396+
"sources": sources,
397+
"read_only": True,
398+
}
399+
400+
367401
@cli.command(help="Run a lightweight network/source diagnostic.")
368-
def diagnose() -> None:
402+
@click.option("--json", "as_json", is_flag=True, help="Print machine-readable diagnostics.")
403+
def diagnose(as_json: bool) -> None:
404+
if as_json:
405+
click.echo(json.dumps(_diagnostic_payload(), ensure_ascii=False, indent=2))
406+
return
369407
click.echo("# 网络诊断")
370-
for name in ["eastmoney", "sina", "tencent", "ths", "futu"]:
408+
for source in _diagnostic_payload()["sources"]:
409+
name = source["name"]
371410
snap = _core.SOURCE_HEALTH.snapshot(name)
372411
state = "建议暂缓使用" if snap.should_skip else "可用/未发现近期异常"
373412
click.echo(f"{name}: 成功率 {snap.success_rate:.0%}, 平均延迟 {snap.average_latency_ms:.0f}ms, {state}")

tests/test_cli.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import sys
23
from types import SimpleNamespace
34

@@ -67,6 +68,22 @@ def test_cli_subcommands_registered():
6768
assert sub in result.output, f"subcommand `{sub}` missing from help"
6869

6970

71+
def test_cli_top_level_command_help_is_available():
72+
from click.testing import CliRunner
73+
74+
runner = CliRunner()
75+
commands = [
76+
"a", "hk", "us", "global", "indices", "zt-pool", "flow", "stock", "fund", "news",
77+
"daily", "profile", "portfolio", "alert", "note", "diary", "diagnose", "guide", "example",
78+
"cache-clear", "update", "uninstall",
79+
]
80+
81+
for command in commands:
82+
result = runner.invoke(cli, [command, "--help"])
83+
assert result.exit_code == 0, f"{command} --help failed: {result.output}"
84+
assert "Usage:" in result.output
85+
86+
7087
def test_cli_stock_runs_single_stock_quote(monkeypatch):
7188
from click.testing import CliRunner
7289

@@ -187,6 +204,22 @@ def fake_run(cmd, check):
187204
)]
188205

189206

207+
def test_cli_update_failure_mentions_python_version(monkeypatch):
208+
from click.testing import CliRunner
209+
210+
def fake_run(cmd, check):
211+
return SimpleNamespace(returncode=1)
212+
213+
monkeypatch.setattr(cli_module.subprocess, "run", fake_run)
214+
215+
runner = CliRunner()
216+
result = runner.invoke(cli, ["update"])
217+
218+
assert result.exit_code != 0
219+
assert "Python 3.9+" in result.output
220+
assert "python3 -m pip install --upgrade young-stock-cli" in result.output
221+
222+
190223
def test_cli_uninstall_runs_pip_uninstall(monkeypatch):
191224
from click.testing import CliRunner
192225

@@ -403,3 +436,28 @@ def test_cli_diagnose_outputs_network_guidance(monkeypatch):
403436
assert result.exit_code == 0
404437
assert "网络诊断" in result.output
405438
assert "建议" in result.output
439+
440+
441+
def test_cli_diagnose_json_outputs_machine_readable_support_info(monkeypatch, tmp_path):
442+
from click.testing import CliRunner
443+
444+
monkeypatch.setenv("YOUNG_STOCK_PROFILE", str(tmp_path / "profile.json"))
445+
monkeypatch.setenv("YOUNG_STOCK_HOME", str(tmp_path / "home"))
446+
monkeypatch.setattr(
447+
cli_module._core,
448+
"SOURCE_HEALTH",
449+
SimpleNamespace(snapshot=lambda name: SimpleNamespace(success_rate=1.0, average_latency_ms=0.0, should_skip=False)),
450+
)
451+
452+
result = CliRunner().invoke(cli, ["diagnose", "--json"])
453+
454+
assert result.exit_code == 0
455+
payload = json.loads(result.output)
456+
assert payload["command"] == "young diagnose"
457+
assert payload["version"] == __version__
458+
assert payload["python"]["executable"] == sys.executable
459+
assert payload["paths"]["profile"].endswith("profile.json")
460+
assert payload["paths"]["home"].endswith("home")
461+
assert payload["paths"]["cache"]
462+
assert payload["read_only"] is True
463+
assert {source["name"] for source in payload["sources"]} >= {"eastmoney", "sina", "tencent", "ths", "futu"}

0 commit comments

Comments
 (0)