akshare-cli 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/PKG-INFO +1 -1
  2. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/__init__.py +1 -1
  3. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/cli.py +142 -25
  4. akshare_cli-0.2.4/akshare_cli/core/catalog.py +91 -0
  5. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/registry.py +41 -3
  6. akshare_cli-0.2.4/akshare_cli/data/__init__.py +0 -0
  7. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/PKG-INFO +1 -1
  8. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/SOURCES.txt +2 -0
  9. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/setup.py +1 -1
  10. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/README.md +0 -0
  11. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/__init__.py +0 -0
  12. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/cache.py +0 -0
  13. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/docsource.py +0 -0
  14. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/export.py +0 -0
  15. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/fallback.py +0 -0
  16. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/session.py +0 -0
  17. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/__init__.py +0 -0
  18. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/conftest.py +0 -0
  19. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_cache.py +0 -0
  20. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_core.py +0 -0
  21. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_doc_examples.py +0 -0
  22. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_fallback.py +0 -0
  23. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_full_e2e.py +0 -0
  24. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/utils/__init__.py +0 -0
  25. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/utils/formatting.py +0 -0
  26. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/dependency_links.txt +0 -0
  27. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/entry_points.txt +0 -0
  28. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/requires.txt +0 -0
  29. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/top_level.txt +0 -0
  30. {akshare_cli-0.2.2 → akshare_cli-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akshare-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: CLI harness for the AKShare financial data library
5
5
  Author: cli-anything
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """AKShare CLI Harness - Sub-package"""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -25,6 +25,12 @@ import click
25
25
  import pandas as pd
26
26
 
27
27
  from akshare_cli.core.cache import _result_cache
28
+ from akshare_cli.core.catalog import (
29
+ get_direct_functions,
30
+ get_function_path,
31
+ get_functions,
32
+ list_categories,
33
+ )
28
34
  from akshare_cli.core.registry import (
29
35
  call_function,
30
36
  get_all_functions,
@@ -57,9 +63,8 @@ class OutputConfig:
57
63
  self.max_rows = None
58
64
  self.show_index = False
59
65
  self.no_header = False
60
- self.sort_order = "desc"
61
66
 
62
- def apply(self, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False, sort=None):
67
+ def apply(self, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False):
63
68
  """Apply output options, only overriding if explicitly set."""
64
69
  if use_json:
65
70
  self.format = "json"
@@ -71,24 +76,21 @@ class OutputConfig:
71
76
  self.max_rows = limit
72
77
  if no_header:
73
78
  self.no_header = no_header
74
- if sort is not None:
75
- self.sort_order = sort
76
79
 
77
80
 
78
81
  _output_config = OutputConfig()
79
82
 
80
83
 
81
84
  def output_options(func):
82
- """Decorator that adds --json/--csv/--output/--limit/--no-header/--sort to any command."""
85
+ """Decorator that adds --json/--csv/--output/--limit/--no-header to any command."""
83
86
  @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
84
87
  @click.option("--csv", "use_csv", is_flag=True, help="Output as CSV.")
85
88
  @click.option("--output", "-o", "output_file", default=None, help="Save output to file.")
86
89
  @click.option("--limit", "-n", "limit", type=int, default=None, help="Limit number of rows.")
87
90
  @click.option("--no-header", "no_header", is_flag=True, help="Suppress table headers.")
88
- @click.option("--sort", "sort", type=click.Choice(["asc", "desc"]), default=None, help="Sort order: desc (newest first, default) or asc (oldest first).")
89
91
  @functools.wraps(func)
90
- def wrapper(*args, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False, sort=None, **kwargs):
91
- _output_config.apply(use_json, use_csv, output_file, limit, no_header, sort)
92
+ def wrapper(*args, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False, **kwargs):
93
+ _output_config.apply(use_json, use_csv, output_file, limit, no_header)
92
94
  return func(*args, **kwargs)
93
95
  return wrapper
94
96
 
@@ -107,10 +109,6 @@ def _handle_result(result, func_name: str = "", kwargs: dict = None):
107
109
  click.echo("No data returned (empty DataFrame).")
108
110
  return
109
111
 
110
- # Apply sort order before display/limit
111
- if _output_config.sort_order == "desc":
112
- result = result.iloc[::-1].reset_index(drop=True)
113
-
114
112
  output = display_result(
115
113
  result,
116
114
  output_format=_output_config.format,
@@ -138,16 +136,15 @@ def _handle_result(result, func_name: str = "", kwargs: dict = None):
138
136
  @click.option("--output", "-o", "output_file", help="Save output to file.")
139
137
  @click.option("--limit", "-n", type=int, help="Limit number of rows.")
140
138
  @click.option("--no-header", is_flag=True, help="Suppress table headers.")
141
- @click.option("--sort", type=click.Choice(["asc", "desc"]), default=None, help="Sort order: desc (newest first, default) or asc (oldest first).")
142
139
  @click.option("--no-cache", is_flag=True, help="禁用结果缓存,强制从 API 获取最新数据。")
143
140
  @click.option("--retry", type=int, default=0, help="网络失败时自动重试次数 (指数退避,默认 0 不重试)")
144
141
  @click.version_option(version="0.1.0", prog_name="akshare-cli")
145
142
  @click.pass_context
146
- def cli(ctx, use_json, use_csv, output_file, limit, no_header, sort, no_cache, retry):
143
+ def cli(ctx, use_json, use_csv, output_file, limit, no_header, no_cache, retry):
147
144
  """AKShare CLI - 命令行金融数据工具
148
145
 
149
146
  支持调用 akshare 库的 1090+ 个函数,获取股票、基金、期货、债券、外汇、宏观等金融数据。
150
- 输出选项 (--json, --csv, --output, --limit, --sort) 可以放在子命令的前面或后面。
147
+ 输出选项 (--json, --csv, --output, --limit) 可以放在子命令的前面或后面。
151
148
 
152
149
  \b
153
150
  基本用法:
@@ -168,7 +165,7 @@ def cli(ctx, use_json, use_csv, output_file, limit, no_header, sort, no_cache, r
168
165
  """
169
166
  ctx.ensure_object(dict)
170
167
 
171
- _output_config.apply(use_json, use_csv, output_file, limit, no_header, sort)
168
+ _output_config.apply(use_json, use_csv, output_file, limit, no_header)
172
169
 
173
170
  if no_cache:
174
171
  _result_cache.enabled = False
@@ -227,7 +224,6 @@ def cmd_call(ctx, params):
227
224
  call_output_csv = False
228
225
  call_output_file = None
229
226
  call_limit = None
230
- call_sort = None
231
227
  full_help = False
232
228
 
233
229
  filtered = []
@@ -252,10 +248,6 @@ def cmd_call(ctx, params):
252
248
  except ValueError:
253
249
  pass
254
250
  i += 2
255
- elif p == "--sort" and i + 1 < len(remaining):
256
- if remaining[i + 1] in ("asc", "desc"):
257
- call_sort = remaining[i + 1]
258
- i += 2
259
251
  elif p == "--no-header":
260
252
  _output_config.no_header = True
261
253
  i += 1
@@ -269,7 +261,7 @@ def cmd_call(ctx, params):
269
261
  filtered.append(p)
270
262
  i += 1
271
263
 
272
- _output_config.apply(call_output_json, call_output_csv, call_output_file, call_limit, sort=call_sort)
264
+ _output_config.apply(call_output_json, call_output_csv, call_output_file, call_limit)
273
265
 
274
266
  # Second pass: the first non-flag token is the function name
275
267
  func_name = None
@@ -282,7 +274,22 @@ def cmd_call(ctx, params):
282
274
 
283
275
  # Handle --full-help
284
276
  if full_help:
285
- _print_full_help(func_name)
277
+ # 支持批量查询:多个函数名用空格或逗号分隔
278
+ func_names = []
279
+ if func_name:
280
+ # 处理逗号分隔:call --full-help fn1,fn2,fn3
281
+ for tok in [func_name] + [t for t in func_params if not t.startswith("--")]:
282
+ for part in tok.split(","):
283
+ part = part.strip()
284
+ if part:
285
+ func_names.append(part)
286
+ if len(func_names) <= 1:
287
+ _print_full_help(func_names[0] if func_names else None)
288
+ else:
289
+ for i, fn in enumerate(func_names):
290
+ if i > 0:
291
+ click.echo("\n" + "─" * 60 + "\n")
292
+ _print_full_help(fn, exit_on_error=False)
286
293
  return
287
294
 
288
295
  if not func_name:
@@ -333,7 +340,7 @@ def _clean_docstring(docstring: str) -> str:
333
340
  return "\n".join(lines) if lines else ""
334
341
 
335
342
 
336
- def _print_full_help(func_name: Optional[str] = None) -> None:
343
+ def _print_full_help(func_name: Optional[str] = None, exit_on_error: bool = True) -> None:
337
344
  """Print detailed usage help for one function or list popular examples."""
338
345
  if func_name:
339
346
  info = get_function_info(func_name)
@@ -344,7 +351,9 @@ def _print_full_help(func_name: Optional[str] = None) -> None:
344
351
  click.echo("\n你是不是想找:")
345
352
  for s in suggestions[:5]:
346
353
  click.echo(f" {s}")
347
- sys.exit(1)
354
+ if exit_on_error:
355
+ sys.exit(1)
356
+ return
348
357
 
349
358
  source = get_function_source(info["name"])
350
359
  clean_desc = _clean_docstring(info["docstring"])
@@ -591,10 +600,17 @@ def cmd_info(func_name):
591
600
  sys.exit(1)
592
601
 
593
602
  if _output_config.format == "json":
603
+ # 注入分类路径到 JSON 输出
604
+ cat_path = get_function_path(func_name)
605
+ if cat_path:
606
+ info["category"] = " > ".join(cat_path)
594
607
  click.echo(json.dumps(info, indent=2, ensure_ascii=False, default=str))
595
608
  else:
596
609
  click.echo(f"Function: {info['name']}")
597
610
  click.echo(f"Module: {info['module']}")
611
+ cat_path = get_function_path(func_name)
612
+ if cat_path:
613
+ click.echo(f"分类: {' > '.join(cat_path)}")
598
614
  click.echo()
599
615
  if info["params"]:
600
616
  click.echo("Parameters:")
@@ -610,6 +626,107 @@ def cmd_info(func_name):
610
626
  click.echo(f" {info['docstring']}")
611
627
 
612
628
 
629
+ # ─── BROWSE COMMAND ─────────────────────────────────────────────────────────
630
+
631
+
632
+ @cli.command("browse")
633
+ @click.argument("path", default="", required=False)
634
+ @click.option("--func", "func_name", default=None, help="查询某函数所属分类路径。")
635
+ @output_options
636
+ def cmd_browse(path, func_name):
637
+ """按目录浏览 akshare 函数分类
638
+
639
+ 基于 akshare 官方文档的层级结构,逐级浏览函数分类。
640
+ 使用 '/' 分隔路径层级。
641
+
642
+ \b
643
+ 示例:
644
+ akshare-cli browse # 顶级分类
645
+ akshare-cli browse 股票数据 # 子分类
646
+ akshare-cli browse 股票数据/A股/历史行情数据 # 函数列表
647
+ akshare-cli browse --func stock_zh_a_hist # 查函数归属
648
+ akshare-cli browse --json # JSON 输出
649
+ """
650
+ if func_name:
651
+ _browse_func(func_name)
652
+ return
653
+ _browse_path(path)
654
+
655
+
656
+ def _browse_func(func_name):
657
+ """查询函数所属分类路径。"""
658
+ cat_path = get_function_path(func_name)
659
+ if cat_path is None:
660
+ if _output_config.format == "json":
661
+ click.echo(json.dumps({"error": f"Function '{func_name}' not found in catalog"}, indent=2))
662
+ else:
663
+ print_error(f"Function '{func_name}' not found in catalog.")
664
+ sys.exit(1)
665
+
666
+ if _output_config.format == "json":
667
+ click.echo(json.dumps({"function": func_name, "path": cat_path, "display": " > ".join(cat_path)}, indent=2, ensure_ascii=False))
668
+ else:
669
+ click.echo(f"{func_name}")
670
+ click.echo(f" 分类: {' > '.join(cat_path)}")
671
+
672
+
673
+ def _browse_path(path):
674
+ """浏览目录路径。"""
675
+ categories = list_categories(path)
676
+ direct_funcs = get_direct_functions(path) if path else []
677
+
678
+ if not categories and not direct_funcs:
679
+ # 路径不存在
680
+ if _output_config.format == "json":
681
+ click.echo(json.dumps({"error": f"Path '{path}' not found"}, indent=2))
682
+ else:
683
+ print_error(f"Path '{path}' not found.")
684
+ sys.exit(1)
685
+
686
+ if _output_config.format == "json":
687
+ data = {}
688
+ if path:
689
+ data["path"] = path
690
+ if categories:
691
+ data["categories"] = [{"name": name, "count": count} for name, count in categories]
692
+ if direct_funcs:
693
+ data["functions"] = direct_funcs
694
+ click.echo(json.dumps(data, indent=2, ensure_ascii=False))
695
+ return
696
+
697
+ # Table output
698
+ if path:
699
+ click.echo(path.replace("/", " > "))
700
+ click.echo()
701
+
702
+ if categories:
703
+ if path:
704
+ click.echo("子分类:")
705
+ # 计算对齐宽度
706
+ max_width = max(_display_width(name) for name, _ in categories)
707
+ for name, count in categories:
708
+ padding = " " * (max_width - _display_width(name) + 2)
709
+ click.echo(f" {name}{padding}({count} 个函数)")
710
+
711
+ if direct_funcs:
712
+ if categories:
713
+ click.echo()
714
+ click.echo("函数:")
715
+ for fn in direct_funcs:
716
+ click.echo(f" {fn}")
717
+
718
+
719
+ def _display_width(s):
720
+ """计算字符串显示宽度(中文字符宽度为 2)。"""
721
+ width = 0
722
+ for ch in s:
723
+ if '\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef':
724
+ width += 2
725
+ else:
726
+ width += 1
727
+ return width
728
+
729
+
613
730
  # ─── DOMAIN SHORTCUTS ────────────────────────────────────────────────────────
614
731
 
615
732
 
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """目录查询模块 — 基于 catalog.json 提供函数分类浏览。"""
4
+
5
+ import json
6
+ import os
7
+ from typing import Dict, List, Optional, Tuple
8
+
9
+ _catalog = None
10
+
11
+
12
+ def load_catalog() -> dict:
13
+ """加载 catalog.json(模块级缓存)。"""
14
+ global _catalog
15
+ if _catalog is not None:
16
+ return _catalog
17
+ catalog_path = os.path.join(os.path.dirname(__file__), "..", "data", "catalog.json")
18
+ catalog_path = os.path.normpath(catalog_path)
19
+ with open(catalog_path, encoding="utf-8") as f:
20
+ _catalog = json.load(f)
21
+ return _catalog
22
+
23
+
24
+ def _resolve_node(path: str) -> Optional[dict]:
25
+ """根据 '/' 分隔路径定位目录节点,返回 None 表示路径不存在。"""
26
+ catalog = load_catalog()
27
+ node = {"children": catalog["categories"]}
28
+ if not path:
29
+ return node
30
+ parts = [p.strip() for p in path.split("/") if p.strip()]
31
+ for part in parts:
32
+ children = node.get("children", {})
33
+ if part not in children:
34
+ return None
35
+ node = children[part]
36
+ return node
37
+
38
+
39
+ def list_categories(path: str = "") -> List[Tuple[str, int]]:
40
+ """列出子目录及其函数数量。
41
+
42
+ 返回 [(name, func_count), ...],按名称排序。
43
+ """
44
+ node = _resolve_node(path)
45
+ if node is None:
46
+ return []
47
+ children = node.get("children", {})
48
+ result = []
49
+ for name, child in children.items():
50
+ count = _count_functions(child)
51
+ result.append((name, count))
52
+ return sorted(result, key=lambda x: x[0])
53
+
54
+
55
+ def _count_functions(node: dict) -> int:
56
+ """递归统计节点下的函数总数。"""
57
+ count = len(node.get("functions", []))
58
+ for child in node.get("children", {}).values():
59
+ count += _count_functions(child)
60
+ return count
61
+
62
+
63
+ def get_functions(path: str) -> List[str]:
64
+ """返回某目录下所有函数(含子目录递归),排序。"""
65
+ node = _resolve_node(path)
66
+ if node is None:
67
+ return []
68
+ funcs = []
69
+ _collect_functions(node, funcs)
70
+ return sorted(funcs)
71
+
72
+
73
+ def _collect_functions(node: dict, result: list):
74
+ """递归收集函数名。"""
75
+ result.extend(node.get("functions", []))
76
+ for child in node.get("children", {}).values():
77
+ _collect_functions(child, result)
78
+
79
+
80
+ def get_function_path(func_name: str) -> Optional[List[str]]:
81
+ """查函数所属目录路径,返回 None 表示未找到。"""
82
+ catalog = load_catalog()
83
+ return catalog["func_to_path"].get(func_name)
84
+
85
+
86
+ def get_direct_functions(path: str) -> List[str]:
87
+ """返回某目录直接包含的函数(不递归子目录)。"""
88
+ node = _resolve_node(path)
89
+ if node is None:
90
+ return []
91
+ return sorted(node.get("functions", []))
@@ -84,13 +84,51 @@ def get_function(name: str) -> Optional[Callable]:
84
84
 
85
85
 
86
86
  def search_functions(keyword: str) -> List[str]:
87
- """Search function names containing the keyword (case-insensitive)."""
87
+ """Search functions by name, docstring, or catalog category path.
88
+
89
+ Matches against:
90
+ 1. Function name (case-insensitive)
91
+ 2. First line of docstring (supports Chinese)
92
+ 3. Catalog category path (Chinese category names)
93
+ """
88
94
  keyword_lower = keyword.lower()
89
- return sorted(
90
- name for name in get_all_functions()
95
+ all_funcs = get_all_functions()
96
+
97
+ # Phase 1: name match (always fast)
98
+ name_matches = set(
99
+ name for name in all_funcs
91
100
  if keyword_lower in name.lower()
92
101
  )
93
102
 
103
+ # Phase 2: docstring match (for Chinese keywords or broader search)
104
+ doc_matches = set()
105
+ for name in all_funcs:
106
+ if name in name_matches:
107
+ continue
108
+ func = get_function(name)
109
+ if func and func.__doc__:
110
+ first_line = func.__doc__.strip().split("\n")[0]
111
+ if keyword_lower in first_line.lower():
112
+ doc_matches.add(name)
113
+
114
+ # Phase 3: catalog category path match
115
+ cat_matches = set()
116
+ try:
117
+ from akshare_cli.core.catalog import load_catalog
118
+ catalog = load_catalog()
119
+ func_to_path = catalog.get("func_to_path", {})
120
+ for fname, path_list in func_to_path.items():
121
+ if fname in name_matches or fname in doc_matches:
122
+ continue
123
+ path_str = "/".join(path_list).lower()
124
+ if keyword_lower in path_str:
125
+ if fname in all_funcs:
126
+ cat_matches.add(fname)
127
+ except Exception:
128
+ pass
129
+
130
+ return sorted(name_matches | doc_matches | cat_matches)
131
+
94
132
 
95
133
  def list_functions_by_domain(domain: str) -> List[str]:
96
134
  """List functions whose name starts with a given domain prefix."""
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akshare-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: CLI harness for the AKShare financial data library
5
5
  Author: cli-anything
6
6
  License: MIT
@@ -10,11 +10,13 @@ akshare_cli.egg-info/requires.txt
10
10
  akshare_cli.egg-info/top_level.txt
11
11
  akshare_cli/core/__init__.py
12
12
  akshare_cli/core/cache.py
13
+ akshare_cli/core/catalog.py
13
14
  akshare_cli/core/docsource.py
14
15
  akshare_cli/core/export.py
15
16
  akshare_cli/core/fallback.py
16
17
  akshare_cli/core/registry.py
17
18
  akshare_cli/core/session.py
19
+ akshare_cli/data/__init__.py
18
20
  akshare_cli/tests/__init__.py
19
21
  akshare_cli/tests/conftest.py
20
22
  akshare_cli/tests/test_cache.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
 
5
5
  setup(
6
6
  name="akshare-cli",
7
- version="0.2.2",
7
+ version="0.2.4",
8
8
  description="CLI harness for the AKShare financial data library",
9
9
  long_description=open("README.md", encoding="utf-8").read(),
10
10
  long_description_content_type="text/markdown",
File without changes
File without changes