groundapi-cli 1.0.0__py3-none-any.whl

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.
cli/__init__.py ADDED
File without changes
cli/main.py ADDED
@@ -0,0 +1,262 @@
1
+ """
2
+ GroundAPI CLI — A股行情、天气、汇率、快递、搜索、短信、网页抓取、邮件、IP定位
3
+ 安装后使用: groundapi <command> <args>
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import httpx
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich.panel import Panel
16
+ from rich import print as rprint
17
+
18
+ app = typer.Typer(
19
+ name="groundapi",
20
+ help="GroundAPI CLI — Real-time Data API for AI Agents",
21
+ no_args_is_help=True,
22
+ )
23
+ console = Console()
24
+
25
+ CONFIG_DIR = Path.home() / ".groundapi"
26
+ CONFIG_FILE = CONFIG_DIR / "config.json"
27
+
28
+ API_BASE = os.getenv("GROUNDAPI_BASE_URL", "https://api.groundapi.net")
29
+
30
+
31
+ def _load_config() -> dict:
32
+ if CONFIG_FILE.exists():
33
+ return json.loads(CONFIG_FILE.read_text())
34
+ return {}
35
+
36
+
37
+ def _save_config(data: dict):
38
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
39
+ CONFIG_FILE.write_text(json.dumps(data, indent=2))
40
+
41
+
42
+ def _get_api_key() -> str:
43
+ key = os.getenv("GROUNDAPI_API_KEY") or _load_config().get("api_key", "")
44
+ if not key:
45
+ console.print("[red]未设置 API Key。请先运行: groundapi config set-key <your-key>[/red]")
46
+ raise typer.Exit(1)
47
+ return key
48
+
49
+
50
+ def _call(path: str, params: dict) -> dict:
51
+ api_key = _get_api_key()
52
+ try:
53
+ resp = httpx.get(
54
+ f"{API_BASE}{path}",
55
+ params=params,
56
+ headers={"X-API-Key": api_key},
57
+ timeout=30,
58
+ )
59
+ return resp.json()
60
+ except httpx.HTTPError as e:
61
+ console.print(f"[red]请求失败: {e}[/red]")
62
+ raise typer.Exit(1)
63
+
64
+
65
+ def _print_result(data: dict):
66
+ if not data.get("success"):
67
+ err = data.get("error", {})
68
+ console.print(f"[red]错误 [{err.get('code', 'UNKNOWN')}]: {err.get('message', '未知错误')}[/red]")
69
+ if err.get("docs"):
70
+ console.print(f"[dim]文档: {err['docs']}[/dim]")
71
+ raise typer.Exit(1)
72
+
73
+ rprint(Panel(json.dumps(data["data"], indent=2, ensure_ascii=False), title="结果", border_style="blue"))
74
+
75
+
76
+ # ─── config ─────────────────────────────────────────────
77
+
78
+ config_app = typer.Typer(help="配置管理")
79
+ app.add_typer(config_app, name="config")
80
+
81
+
82
+ @config_app.command("set-key")
83
+ def config_set_key(api_key: str = typer.Argument(..., help="你的 GroundAPI API Key")):
84
+ """保存 API Key 到本地配置"""
85
+ cfg = _load_config()
86
+ cfg["api_key"] = api_key
87
+ _save_config(cfg)
88
+ console.print(f"[green]API Key 已保存到 {CONFIG_FILE}[/green]")
89
+
90
+
91
+ @config_app.command("show")
92
+ def config_show():
93
+ """显示当前配置"""
94
+ cfg = _load_config()
95
+ if cfg.get("api_key"):
96
+ key = cfg["api_key"]
97
+ masked = key[:12] + "..." + key[-4:] if len(key) > 16 else "***"
98
+ console.print(f"API Key: {masked}")
99
+ else:
100
+ console.print("[yellow]未设置 API Key[/yellow]")
101
+ console.print(f"API Base: {API_BASE}")
102
+
103
+
104
+ # ─── stock ──────────────────────────────────────────────
105
+
106
+ @app.command()
107
+ def stock(
108
+ symbol: str = typer.Argument(..., help="股票代码,如 600519"),
109
+ ):
110
+ """查询 A 股实时行情"""
111
+ result = _call("/v1/stock/quote", {"symbol": symbol})
112
+ if result.get("success"):
113
+ d = result["data"]
114
+ table = Table(title=f"{d.get('name', symbol)} ({symbol})")
115
+ table.add_column("指标", style="cyan")
116
+ table.add_column("值", style="bold")
117
+ table.add_row("当前价", str(d.get("price", "-")))
118
+ table.add_row("涨跌额", str(d.get("change", "-")))
119
+ table.add_row("涨跌幅", f"{d.get('change_percent', '-')}%")
120
+ table.add_row("开盘", str(d.get("open", "-")))
121
+ table.add_row("最高", str(d.get("high", "-")))
122
+ table.add_row("最低", str(d.get("low", "-")))
123
+ table.add_row("成交量", str(d.get("volume", "-")))
124
+ console.print(table)
125
+ else:
126
+ _print_result(result)
127
+
128
+
129
+ # ─── weather ────────────────────────────────────────────
130
+
131
+ @app.command()
132
+ def weather(
133
+ city: str = typer.Argument(..., help="城市名称,如 北京、Tokyo"),
134
+ ):
135
+ """查询实时天气"""
136
+ result = _call("/v1/weather/current", {"city": city})
137
+ _print_result(result)
138
+
139
+
140
+ # ─── forex ──────────────────────────────────────────────
141
+
142
+ @app.command()
143
+ def forex(
144
+ from_currency: str = typer.Argument(..., help="源货币,如 USD"),
145
+ to_currency: str = typer.Argument(..., help="目标货币,如 CNY"),
146
+ ):
147
+ """查询实时汇率"""
148
+ result = _call("/v1/forex/rate", {"from": from_currency, "to": to_currency})
149
+ if result.get("success"):
150
+ d = result["data"]
151
+ console.print(
152
+ f"[bold]1 {d.get('from', from_currency)} = {d.get('rate', '?')} {d.get('to', to_currency)}[/bold]"
153
+ )
154
+ else:
155
+ _print_result(result)
156
+
157
+
158
+ # ─── logistics ──────────────────────────────────────────
159
+
160
+ @app.command()
161
+ def logistics(
162
+ number: str = typer.Argument(..., help="快递运单号"),
163
+ carrier: str = typer.Option("", help="快递公司编码,如 sf、yd,不填自动识别"),
164
+ ):
165
+ """追踪快递物流"""
166
+ params: dict = {"number": number}
167
+ if carrier:
168
+ params["carrier"] = carrier
169
+ result = _call("/v1/logistics/track", params)
170
+ if result.get("success"):
171
+ d = result["data"]
172
+ console.print(f"[bold]{d.get('carrier_name', '')}[/bold] — {d.get('status_text', '')}")
173
+ for track in d.get("tracks", []):
174
+ console.print(f" [dim]{track.get('time', '')}[/dim] {track.get('description', '')}")
175
+ else:
176
+ _print_result(result)
177
+
178
+
179
+ # ─── search ────────────────────────────────────────────
180
+
181
+ @app.command()
182
+ def search(
183
+ query: str = typer.Argument(..., help="搜索关键词"),
184
+ engine: str = typer.Option("pro", help="搜索引擎:std/pro/sogou/quark"),
185
+ count: int = typer.Option(10, help="返回结果数量,1-50"),
186
+ recency: str = typer.Option("noLimit", help="时间范围:oneDay/oneWeek/oneMonth/oneYear/noLimit"),
187
+ ):
188
+ """网络搜索"""
189
+ result = _call("/v1/search/query", {"q": query, "engine": engine, "count": count, "recency": recency})
190
+ if result.get("success"):
191
+ d = result["data"]
192
+ console.print(f"[bold]搜索: {d.get('query', query)}[/bold] — {d.get('result_count', 0)} 条结果\n")
193
+ for i, item in enumerate(d.get("results", []), 1):
194
+ console.print(f"[cyan]{i}.[/cyan] [bold]{item.get('title', '')}[/bold]")
195
+ console.print(f" [blue underline]{item.get('link', '')}[/blue underline]")
196
+ snippet = item.get("snippet", "")
197
+ if snippet:
198
+ console.print(f" [dim]{snippet[:200]}[/dim]")
199
+ source = item.get("source", "")
200
+ date = item.get("publish_date", "")
201
+ if source or date:
202
+ console.print(f" [dim]{source} {date}[/dim]")
203
+ console.print()
204
+ else:
205
+ _print_result(result)
206
+
207
+
208
+ # ─── scrape ────────────────────────────────────────────
209
+
210
+ @app.command()
211
+ def scrape(
212
+ url: str = typer.Argument(..., help="要抓取的网页 URL"),
213
+ format: str = typer.Option("markdown", help="返回格式:markdown 或 text"),
214
+ max_lines: int = typer.Option(50, help="终端显示最大行数"),
215
+ ):
216
+ """抓取网页内容"""
217
+ result = _call("/v1/scrape/read", {"url": url, "format": format})
218
+ if result.get("success"):
219
+ d = result["data"]
220
+ title = d.get("title", "")
221
+ if title:
222
+ console.print(f"[bold]{title}[/bold]\n")
223
+ console.print(f"[dim]URL: {d.get('url', url)} | {d.get('content_length', 0)} 字符[/dim]\n")
224
+ content = d.get("content", "")
225
+ lines = content.split("\n")
226
+ if len(lines) > max_lines:
227
+ console.print("\n".join(lines[:max_lines]))
228
+ console.print(f"\n[dim]... 还有 {len(lines) - max_lines} 行,使用 --max-lines 查看更多[/dim]")
229
+ else:
230
+ console.print(content)
231
+ else:
232
+ _print_result(result)
233
+
234
+
235
+ # ─── ip ────────────────────────────────────────────────
236
+
237
+ @app.command()
238
+ def ip(
239
+ address: str = typer.Argument(..., help="要查询的 IP 地址,如 8.8.8.8"),
240
+ ):
241
+ """IP 地理定位"""
242
+ result = _call("/v1/ip/locate", {"ip": address})
243
+ if result.get("success"):
244
+ d = result["data"]
245
+ table = Table(title=f"IP 定位: {d.get('ip', address)}")
246
+ table.add_column("属性", style="cyan")
247
+ table.add_column("值", style="bold")
248
+ table.add_row("国家", f"{d.get('country', '')} ({d.get('country_code', '')})")
249
+ table.add_row("地区", d.get("region", ""))
250
+ table.add_row("城市", d.get("city", ""))
251
+ table.add_row("邮编", d.get("zip", ""))
252
+ table.add_row("经纬度", f"{d.get('latitude', '')}, {d.get('longitude', '')}")
253
+ table.add_row("时区", d.get("timezone", ""))
254
+ table.add_row("ISP", d.get("isp", ""))
255
+ table.add_row("组织", d.get("org", ""))
256
+ console.print(table)
257
+ else:
258
+ _print_result(result)
259
+
260
+
261
+ if __name__ == "__main__":
262
+ app()
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.2
2
+ Name: groundapi-cli
3
+ Version: 1.0.0
4
+ Summary: GroundAPI CLI — Real-time Data API for AI Agents
5
+ Project-URL: Homepage, https://groundapi.net
6
+ Project-URL: Documentation, https://groundapi.net/documentation
7
+ Keywords: groundapi,cli,api,ai-agent,real-time-data
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.28.0
13
+ Requires-Dist: typer>=0.15.0
14
+ Requires-Dist: rich>=13.9.0
15
+
16
+ # GroundAPI CLI
17
+
18
+ 专为 AI Agent 设计的实时数据命令行工具。一个 API Key,在终端查询 A 股行情、天气、汇率、快递、搜索、网页抓取、IP 定位。
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ pip install groundapi-cli
24
+ ```
25
+
26
+ ## 配置
27
+
28
+ ```bash
29
+ groundapi config set-key sk_gapi_你的密钥
30
+ ```
31
+
32
+ 或使用环境变量:
33
+
34
+ ```bash
35
+ export GROUNDAPI_API_KEY=sk_gapi_你的密钥
36
+ ```
37
+
38
+ ## 使用
39
+
40
+ ```bash
41
+ groundapi stock 600519 # A 股实时行情
42
+ groundapi weather 北京 # 天气查询
43
+ groundapi forex USD CNY # 汇率查询
44
+ groundapi logistics SF1234567 # 快递追踪
45
+ groundapi search "AI Agent" # 网络搜索
46
+ groundapi scrape https://... # 网页抓取
47
+ groundapi ip 8.8.8.8 # IP 定位
48
+ ```
49
+
50
+ ## 获取 API Key
51
+
52
+ 前往 [groundapi.net](https://groundapi.net) 注册,每月 500 次免费调用。
@@ -0,0 +1,7 @@
1
+ cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli/main.py,sha256=_OsEcllgK4q3A37RFZJJCrfQ2ZIWOHzLmgJGtedORu0,9718
3
+ groundapi_cli-1.0.0.dist-info/METADATA,sha256=Cy8sBLnS0qdD4meTYb3kx0n7BX2fkspxVYnPsMs_CBw,1374
4
+ groundapi_cli-1.0.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
5
+ groundapi_cli-1.0.0.dist-info/entry_points.txt,sha256=bAwifWg6gywgBNwM1IyejBbYjA9WloVMyKxaVikSgv4,43
6
+ groundapi_cli-1.0.0.dist-info/top_level.txt,sha256=2ImG917oaVHlm0nP9oJE-Qrgs-fq_fGWgba2H1f8fpE,4
7
+ groundapi_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ groundapi = cli.main:app
@@ -0,0 +1 @@
1
+ cli