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.
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/PKG-INFO +1 -1
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/__init__.py +1 -1
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/cli.py +142 -25
- akshare_cli-0.2.4/akshare_cli/core/catalog.py +91 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/registry.py +41 -3
- akshare_cli-0.2.4/akshare_cli/data/__init__.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/PKG-INFO +1 -1
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/SOURCES.txt +2 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/setup.py +1 -1
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/README.md +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/__init__.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/cache.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/docsource.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/export.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/fallback.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/core/session.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/__init__.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/conftest.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_cache.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_core.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_doc_examples.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_fallback.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/tests/test_full_e2e.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/utils/__init__.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli/utils/formatting.py +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/dependency_links.txt +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/entry_points.txt +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/requires.txt +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/akshare_cli.egg-info/top_level.txt +0 -0
- {akshare_cli-0.2.2 → akshare_cli-0.2.4}/setup.cfg +0 -0
|
@@ -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
|
|
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
|
|
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,
|
|
91
|
-
_output_config.apply(use_json, use_csv, output_file, limit, no_header
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|