amap-cli 0.1.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.
- amap_cli/__init__.py +5 -0
- amap_cli/__main__.py +9 -0
- amap_cli/api.py +90 -0
- amap_cli/cli.py +129 -0
- amap_cli/config.py +155 -0
- amap_cli/distance.py +194 -0
- amap_cli/errors.py +87 -0
- amap_cli/geocode.py +61 -0
- amap_cli/output.py +45 -0
- amap_cli/params.py +77 -0
- amap_cli/route.py +625 -0
- amap_cli/search_poi.py +281 -0
- amap_cli-0.1.0.dist-info/METADATA +290 -0
- amap_cli-0.1.0.dist-info/RECORD +16 -0
- amap_cli-0.1.0.dist-info/WHEEL +4 -0
- amap_cli-0.1.0.dist-info/entry_points.txt +2 -0
amap_cli/search_poi.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""POI search command registration and handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from amap_cli.api import AmapApiClient
|
|
10
|
+
from amap_cli.errors import ValidationError
|
|
11
|
+
from amap_cli.params import Coordinate, normalize_text_argument, parse_coordinate
|
|
12
|
+
|
|
13
|
+
DEFAULT_RADIUS = 3000
|
|
14
|
+
DEFAULT_PAGE_SIZE = 10
|
|
15
|
+
DEFAULT_PAGE_INDEX = 1
|
|
16
|
+
MAX_PAGE_SIZE = 25
|
|
17
|
+
MAX_RADIUS = 50000
|
|
18
|
+
TEXT_SEARCH_PATH = "/v3/place/text"
|
|
19
|
+
AROUND_SEARCH_PATH = "/v3/place/around"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class SearchPoiRequest:
|
|
24
|
+
"""Normalized search-poi command input."""
|
|
25
|
+
|
|
26
|
+
keyword: str
|
|
27
|
+
city: str | None
|
|
28
|
+
center: Coordinate | None
|
|
29
|
+
radius: int
|
|
30
|
+
page_size: int
|
|
31
|
+
page_index: int
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_nearby_search(self) -> bool:
|
|
35
|
+
"""Return whether the request should use nearby search."""
|
|
36
|
+
return self.center is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_args(cls, args: argparse.Namespace) -> "SearchPoiRequest":
|
|
40
|
+
"""Build a validated request from parsed CLI arguments."""
|
|
41
|
+
keyword = normalize_text_argument(args.keyword, "keyword")
|
|
42
|
+
city = (
|
|
43
|
+
normalize_text_argument(args.city, "city")
|
|
44
|
+
if args.city is not None
|
|
45
|
+
else None
|
|
46
|
+
)
|
|
47
|
+
center = (
|
|
48
|
+
parse_coordinate(args.center, "center")
|
|
49
|
+
if args.center is not None
|
|
50
|
+
else None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
radius = _validate_positive_int(
|
|
54
|
+
args.radius,
|
|
55
|
+
"radius",
|
|
56
|
+
min_value=1,
|
|
57
|
+
max_value=MAX_RADIUS,
|
|
58
|
+
)
|
|
59
|
+
page_size = _validate_positive_int(
|
|
60
|
+
args.pageSize,
|
|
61
|
+
"pageSize",
|
|
62
|
+
min_value=1,
|
|
63
|
+
max_value=MAX_PAGE_SIZE,
|
|
64
|
+
)
|
|
65
|
+
page_index = _validate_positive_int(
|
|
66
|
+
args.pageIndex,
|
|
67
|
+
"pageIndex",
|
|
68
|
+
min_value=1,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if center is None and getattr(args, "radius", None) != DEFAULT_RADIUS:
|
|
72
|
+
raise ValidationError("仅在提供 `--center` 时允许传入 `--radius`。")
|
|
73
|
+
|
|
74
|
+
return cls(
|
|
75
|
+
keyword=keyword,
|
|
76
|
+
city=city,
|
|
77
|
+
center=center,
|
|
78
|
+
radius=radius,
|
|
79
|
+
page_size=page_size,
|
|
80
|
+
page_index=page_index,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def to_api_request(self) -> tuple[str, dict[str, Any]]:
|
|
84
|
+
"""Convert the normalized request to Amap REST API arguments."""
|
|
85
|
+
params: dict[str, Any] = {
|
|
86
|
+
"keywords": self.keyword,
|
|
87
|
+
"offset": self.page_size,
|
|
88
|
+
"page": self.page_index,
|
|
89
|
+
"extensions": "all",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if self.is_nearby_search:
|
|
93
|
+
params["location"] = self.center.to_amap_value() if self.center else None
|
|
94
|
+
params["radius"] = self.radius
|
|
95
|
+
return AROUND_SEARCH_PATH, params
|
|
96
|
+
|
|
97
|
+
if self.city is not None:
|
|
98
|
+
params["city"] = self.city
|
|
99
|
+
return TEXT_SEARCH_PATH, params
|
|
100
|
+
|
|
101
|
+
def build_state(self, total: int) -> dict[str, Any]:
|
|
102
|
+
"""Build CLI-facing state metadata."""
|
|
103
|
+
state: dict[str, Any] = {
|
|
104
|
+
"keyword": self.keyword,
|
|
105
|
+
"pageSize": self.page_size,
|
|
106
|
+
"pageIndex": self.page_index,
|
|
107
|
+
"extensions": "all",
|
|
108
|
+
"resultCount": total,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if self.is_nearby_search:
|
|
112
|
+
state["center"] = (
|
|
113
|
+
[self.center.longitude, self.center.latitude]
|
|
114
|
+
if self.center is not None
|
|
115
|
+
else None
|
|
116
|
+
)
|
|
117
|
+
state["radius"] = self.radius
|
|
118
|
+
elif self.city is not None:
|
|
119
|
+
state["city"] = self.city
|
|
120
|
+
|
|
121
|
+
return state
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def register_search_poi_command(
|
|
125
|
+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
126
|
+
) -> argparse.ArgumentParser:
|
|
127
|
+
"""Register the `search-poi` command on a subparser collection."""
|
|
128
|
+
parser = subparsers.add_parser(
|
|
129
|
+
"search-poi",
|
|
130
|
+
help="搜索 POI",
|
|
131
|
+
description="按关键词或周边范围搜索 POI。",
|
|
132
|
+
)
|
|
133
|
+
add_search_poi_arguments(parser)
|
|
134
|
+
parser.set_defaults(handler=handle_search_poi)
|
|
135
|
+
return parser
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def add_search_poi_arguments(parser: argparse.ArgumentParser) -> None:
|
|
139
|
+
"""Add search-poi arguments to a parser."""
|
|
140
|
+
parser.add_argument("--keyword", required=True, help="搜索关键词")
|
|
141
|
+
parser.add_argument("--city", help="搜索城市")
|
|
142
|
+
parser.add_argument("--center", help="周边搜索中心,格式为 `经度,纬度`")
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--radius",
|
|
145
|
+
type=int,
|
|
146
|
+
default=DEFAULT_RADIUS,
|
|
147
|
+
help=f"周边搜索半径(米),默认 {DEFAULT_RADIUS}",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--pageSize",
|
|
151
|
+
type=int,
|
|
152
|
+
default=DEFAULT_PAGE_SIZE,
|
|
153
|
+
help=f"每页数量,范围 1-{MAX_PAGE_SIZE},默认 {DEFAULT_PAGE_SIZE}",
|
|
154
|
+
)
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--pageIndex",
|
|
157
|
+
type=int,
|
|
158
|
+
default=DEFAULT_PAGE_INDEX,
|
|
159
|
+
help=f"页码,从 1 开始,默认 {DEFAULT_PAGE_INDEX}",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def handle_search_poi(
|
|
164
|
+
args: argparse.Namespace,
|
|
165
|
+
*,
|
|
166
|
+
client: AmapApiClient | None = None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Handle the `search-poi` command and normalize the response."""
|
|
169
|
+
request = SearchPoiRequest.from_args(args)
|
|
170
|
+
api_client = client or AmapApiClient()
|
|
171
|
+
path, params = request.to_api_request()
|
|
172
|
+
payload = api_client.get(path, params=params)
|
|
173
|
+
|
|
174
|
+
total = _parse_total(payload.get("count"))
|
|
175
|
+
return {
|
|
176
|
+
"state": request.build_state(total),
|
|
177
|
+
"pois": _normalize_pois(payload.get("pois")),
|
|
178
|
+
"total": total,
|
|
179
|
+
"pageIndex": request.page_index,
|
|
180
|
+
"pageSize": request.page_size,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _validate_positive_int(
|
|
185
|
+
value: Any,
|
|
186
|
+
field_name: str,
|
|
187
|
+
*,
|
|
188
|
+
min_value: int,
|
|
189
|
+
max_value: int | None = None,
|
|
190
|
+
) -> int:
|
|
191
|
+
"""Validate a positive integer CLI argument."""
|
|
192
|
+
if not isinstance(value, int):
|
|
193
|
+
raise ValidationError(f"`{field_name}` 必须是整数。")
|
|
194
|
+
if value < min_value:
|
|
195
|
+
raise ValidationError(f"`{field_name}` 必须大于等于 {min_value}。")
|
|
196
|
+
if max_value is not None and value > max_value:
|
|
197
|
+
raise ValidationError(f"`{field_name}` 必须小于等于 {max_value}。")
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _parse_total(value: Any) -> int:
|
|
202
|
+
"""Convert the Amap `count` field to an integer."""
|
|
203
|
+
if value in (None, ""):
|
|
204
|
+
return 0
|
|
205
|
+
try:
|
|
206
|
+
return int(value)
|
|
207
|
+
except (TypeError, ValueError):
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _normalize_pois(value: Any) -> list[dict[str, Any]]:
|
|
212
|
+
"""Normalize the raw Amap POI list to the CLI output shape."""
|
|
213
|
+
if not isinstance(value, list):
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
return [_normalize_poi(item) for item in value if isinstance(item, dict)]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _normalize_poi(raw: dict[str, Any]) -> dict[str, Any]:
|
|
220
|
+
"""Normalize a single POI payload."""
|
|
221
|
+
return {
|
|
222
|
+
"id": _normalize_optional_text(raw.get("id")),
|
|
223
|
+
"name": _normalize_optional_text(raw.get("name")),
|
|
224
|
+
"type": _normalize_optional_text(raw.get("type")),
|
|
225
|
+
"location": _parse_location(raw.get("location")),
|
|
226
|
+
"address": _normalize_optional_text(raw.get("address")),
|
|
227
|
+
"distance": _parse_distance(raw.get("distance")),
|
|
228
|
+
"tel": _normalize_optional_text(raw.get("tel")),
|
|
229
|
+
"pname": _normalize_optional_text(raw.get("pname")),
|
|
230
|
+
"cityname": _normalize_optional_text(raw.get("cityname")),
|
|
231
|
+
"adname": _normalize_optional_text(raw.get("adname")),
|
|
232
|
+
"photo": _extract_photo(raw.get("photos")),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_location(value: Any) -> list[float] | None:
|
|
237
|
+
"""Parse API location strings into `[lng, lat]` lists."""
|
|
238
|
+
if not isinstance(value, str) or not value.strip():
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
parts = [part.strip() for part in value.split(",", 1)]
|
|
242
|
+
if len(parts) != 2:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
return [float(parts[0]), float(parts[1])]
|
|
247
|
+
except ValueError:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _parse_distance(value: Any) -> int | None:
|
|
252
|
+
"""Convert the API distance field to an integer when available."""
|
|
253
|
+
if value in (None, ""):
|
|
254
|
+
return None
|
|
255
|
+
try:
|
|
256
|
+
return int(float(value))
|
|
257
|
+
except (TypeError, ValueError):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _extract_photo(value: Any) -> str | None:
|
|
262
|
+
"""Extract the first photo URL from the Amap photo list."""
|
|
263
|
+
if not isinstance(value, list):
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
for item in value:
|
|
267
|
+
if not isinstance(item, dict):
|
|
268
|
+
continue
|
|
269
|
+
url = _normalize_optional_text(item.get("url"))
|
|
270
|
+
if url is not None:
|
|
271
|
+
return url
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _normalize_optional_text(value: Any) -> str | None:
|
|
277
|
+
"""Trim optional text fields and normalize empty strings to None."""
|
|
278
|
+
if not isinstance(value, str):
|
|
279
|
+
return None
|
|
280
|
+
normalized = value.strip()
|
|
281
|
+
return normalized or None
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: amap-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure command-line CLI for Amap APIs.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# amap-cli
|
|
9
|
+
|
|
10
|
+
`amap-cli` 是一个基于高德开放平台 Web API 的纯命令行工具,面向终端环境和 AI Agent 使用。
|
|
11
|
+
|
|
12
|
+
## 功能特性
|
|
13
|
+
|
|
14
|
+
- 纯命令行调用高德地图 API
|
|
15
|
+
- 支持命令行写入和读取高德 API Key
|
|
16
|
+
- 支持两点直线距离计算
|
|
17
|
+
- 支持路径规划
|
|
18
|
+
- 支持 POI 搜索
|
|
19
|
+
- 所有命令统一返回机器可读 JSON
|
|
20
|
+
- 提供配套的 Agent skill,便于在智能体环境中直接调用
|
|
21
|
+
|
|
22
|
+
## 安装与使用
|
|
23
|
+
|
|
24
|
+
### 方式一:直接使用 `uvx` 运行
|
|
25
|
+
|
|
26
|
+
项目发布到 PyPI 后,可以直接执行:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx amap-cli --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
配置高德 API Key:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uvx amap-cli config set --api-key <YOUR_AMAP_KEY>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
路径规划示例:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uvx amap-cli route \
|
|
42
|
+
--from 北京南站 \
|
|
43
|
+
--to 天安门 \
|
|
44
|
+
--type driving
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
直线距离示例:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx amap-cli distance \
|
|
51
|
+
--from 116.397,39.909 \
|
|
52
|
+
--to 116.407,39.904
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
POI 搜索示例:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uvx amap-cli search-poi \
|
|
59
|
+
--keyword 星巴克 \
|
|
60
|
+
--city 北京 \
|
|
61
|
+
--pageSize 5
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 方式二:安装后使用
|
|
65
|
+
|
|
66
|
+
如果希望安装到本机工具目录,执行:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool install amap-cli
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
安装完成后可直接调用:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
amap-cli --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
配置和业务命令示例:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
amap-cli config set --api-key <YOUR_AMAP_KEY>
|
|
82
|
+
amap-cli distance --from 116.397,39.909 --to 116.407,39.904
|
|
83
|
+
amap-cli route --from 北京南站 --to 天安门 --type driving
|
|
84
|
+
amap-cli search-poi --keyword 星巴克 --city 北京
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Skill 使用说明
|
|
88
|
+
|
|
89
|
+
本项目提供了配套的 `amap-cli` skill,适合在支持 skill 的 Agent 环境中使用。
|
|
90
|
+
|
|
91
|
+
- skill 的目标是让 Agent 直接通过 `uvx amap-cli` 调用本工具
|
|
92
|
+
- 使用方式与上面的“直接使用 `uvx` 运行”一致
|
|
93
|
+
- 使用前先配置高德 API Key,之后即可发起路径规划和 POI 搜索
|
|
94
|
+
- `distance` 命令在起终点都为坐标时可直接本地计算,不依赖 API Key
|
|
95
|
+
- 建议 Agent 直接解析命令返回的 JSON 结果
|
|
96
|
+
|
|
97
|
+
## 命令说明
|
|
98
|
+
|
|
99
|
+
### 配置
|
|
100
|
+
|
|
101
|
+
写入 API Key:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
amap-cli config set --api-key <YOUR_AMAP_KEY>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
查看当前配置:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
amap-cli config show
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
配置会按照当前操作系统规范保存在用户配置目录中。macOS 下默认路径为:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
~/Library/Application Support/amap-cli/config.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 路径规划
|
|
120
|
+
|
|
121
|
+
基础格式:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
amap-cli route \
|
|
125
|
+
--from <结构化地址|地标名称|经度,纬度> \
|
|
126
|
+
--to <结构化地址|地标名称|经度,纬度> \
|
|
127
|
+
--type <driving|walking|riding|transit>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
可选参数:
|
|
131
|
+
|
|
132
|
+
- `--from-name`
|
|
133
|
+
- `--to-name`
|
|
134
|
+
- `--waypoints`
|
|
135
|
+
- `--policy`
|
|
136
|
+
- `--strategy`
|
|
137
|
+
- `--city`
|
|
138
|
+
|
|
139
|
+
说明:
|
|
140
|
+
|
|
141
|
+
- `--waypoints` 和 `--policy` 仅 `driving` 支持
|
|
142
|
+
- `--strategy` 和 `--city` 仅 `transit` 支持
|
|
143
|
+
- 地理编码仅支持详细的结构化地址,以及地标性名胜景区、建筑物名称
|
|
144
|
+
- 当起点、终点或途经点使用上述名称并解析成功时,返回结果中的对应节点会附带 `formattedAddress`
|
|
145
|
+
- 对于其他较模糊的名称,建议先通过 `search-poi` 获取目标地点坐标,再调用 `route`
|
|
146
|
+
- 不支持 `--json` 输入
|
|
147
|
+
|
|
148
|
+
### 直线距离
|
|
149
|
+
|
|
150
|
+
基础格式:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
amap-cli distance \
|
|
154
|
+
--from <结构化地址|地标名称|经度,纬度> \
|
|
155
|
+
--to <结构化地址|地标名称|经度,纬度>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
可选参数:
|
|
159
|
+
|
|
160
|
+
- `--from-name`
|
|
161
|
+
- `--to-name`
|
|
162
|
+
|
|
163
|
+
说明:
|
|
164
|
+
|
|
165
|
+
- 当 `--from` 和 `--to` 都是坐标时,直接在本地计算直线距离
|
|
166
|
+
- 当任一输入为结构化地址或地标性名胜景区、建筑物名称时,会先调用高德地理编码接口解析坐标,再做本地计算
|
|
167
|
+
- 当输入为上述名称时,返回结果中的 `state.from.formattedAddress` / `state.to.formattedAddress` 会带上高德解析出的完整地址
|
|
168
|
+
- 对于其他较模糊的名称,建议先通过 `search-poi` 获取目标地点坐标,再调用 `distance`
|
|
169
|
+
- 结果中的 `summary.distance` 单位为米
|
|
170
|
+
- 不支持 `--json` 输入
|
|
171
|
+
|
|
172
|
+
### POI 搜索
|
|
173
|
+
|
|
174
|
+
基础格式:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
amap-cli search-poi --keyword <关键词>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
可选参数:
|
|
181
|
+
|
|
182
|
+
- `--city`
|
|
183
|
+
- `--center`
|
|
184
|
+
- `--radius`
|
|
185
|
+
- `--pageSize`
|
|
186
|
+
- `--pageIndex`
|
|
187
|
+
|
|
188
|
+
说明:
|
|
189
|
+
|
|
190
|
+
- 传入 `--center` 时执行周边搜索
|
|
191
|
+
- 未传入 `--center` 时执行关键词搜索
|
|
192
|
+
- 不支持 `--json` 输入
|
|
193
|
+
|
|
194
|
+
## 输出格式
|
|
195
|
+
|
|
196
|
+
成功时:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"success": true,
|
|
201
|
+
"data": {},
|
|
202
|
+
"error": null
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
失败时:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"success": false,
|
|
211
|
+
"data": null,
|
|
212
|
+
"error": {
|
|
213
|
+
"code": "INVALID_ARGUMENT",
|
|
214
|
+
"message": "错误说明",
|
|
215
|
+
"details": {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 本地开发
|
|
221
|
+
|
|
222
|
+
安装项目依赖:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
uv sync
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
本地运行:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
uv run amap-cli --help
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## PyPI 发布
|
|
235
|
+
|
|
236
|
+
### 1. 确认版本号
|
|
237
|
+
|
|
238
|
+
每次发布前,先更新 `pyproject.toml` 中的 `version`。
|
|
239
|
+
PyPI 不允许重复上传同一个版本号的分发包。
|
|
240
|
+
|
|
241
|
+
### 2. 构建分发包
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
uv build
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
构建成功后会在 `dist/` 目录下生成:
|
|
248
|
+
|
|
249
|
+
- `*.tar.gz`
|
|
250
|
+
- `*.whl`
|
|
251
|
+
|
|
252
|
+
### 3. 准备 PyPI Token
|
|
253
|
+
|
|
254
|
+
登录 PyPI 后创建 API Token,推荐通过环境变量传入:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
export UV_PUBLISH_TOKEN="pypi-你的token"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 4. 先发布到 TestPyPI 验证(推荐)
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
uv publish \
|
|
264
|
+
--publish-url https://test.pypi.org/legacy/ \
|
|
265
|
+
--check-url https://test.pypi.org/simple/
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
如果只是验证包能否被安装,可以执行:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
uvx --index-url https://test.pypi.org/simple/ amap-cli --help
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 5. 发布到正式 PyPI
|
|
275
|
+
|
|
276
|
+
确认 TestPyPI 验证通过后,执行:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
uv publish
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 6. 发布后验证
|
|
283
|
+
|
|
284
|
+
发布完成后,推荐优先使用 `uvx` 做一次快速验证:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
uvx amap-cli --help
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
发布后,推荐优先使用 `uvx amap-cli` 进行一次性调用,或使用 `uv tool install amap-cli` 安装到本地。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
amap_cli/__init__.py,sha256=fIvpzGG20FCyIKhTdrMw2SaUL6l-rBxWPkka7uCLkoQ,74
|
|
2
|
+
amap_cli/__main__.py,sha256=I_7ztHJAX_h8YivkMSLeC8suZmAsBPiJVVq8JvhIwG4,175
|
|
3
|
+
amap_cli/api.py,sha256=tBxSuJIYcK_vTBuFu0JBNPmkfGiWVGfl2aagI-fDj2s,3284
|
|
4
|
+
amap_cli/cli.py,sha256=hiOu7OanlzoVU4gq03vFPSIOftQQWsmslXXf2mdVZxc,4374
|
|
5
|
+
amap_cli/config.py,sha256=7yd9LgVQphxd-BSnpjN6DbBY0vs-Jvx2GqjMyQT6XR4,5040
|
|
6
|
+
amap_cli/distance.py,sha256=d3cRX6_TjK6wEiosAufSb1n5fnM2nXdL76sU5_ltg6k,6035
|
|
7
|
+
amap_cli/errors.py,sha256=3D4pozE-T073FqcnyHg4XhdExEf9K_P8PpKsVu84RVM,2356
|
|
8
|
+
amap_cli/geocode.py,sha256=zjBLaWY2bbNSHG36N48DLfSqcQy3vWLOGeBLszrzm0A,1842
|
|
9
|
+
amap_cli/output.py,sha256=fDGp9FUnBuHNZc94GfNjNnlKPYFjHXMqxATfhVh-Ea0,1132
|
|
10
|
+
amap_cli/params.py,sha256=TF0-AHHSgCn2ORxb55aZtfwOXpPebAr6MBO_wTCZodw,2398
|
|
11
|
+
amap_cli/route.py,sha256=su4a3U0ZPSrfkhujdRXMxvmEdbP71OeOlWNM4vddvv0,20667
|
|
12
|
+
amap_cli/search_poi.py,sha256=CgRsII7mC2pl2wbIy34Ta9Is_XlOOJXcmuZYB88Cs0k,8686
|
|
13
|
+
amap_cli-0.1.0.dist-info/METADATA,sha256=p-El-zCN-bYrfeDYg1GUkm5hpmdkrsHtiBefZj8QVUk,5803
|
|
14
|
+
amap_cli-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
15
|
+
amap_cli-0.1.0.dist-info/entry_points.txt,sha256=CUABuFvXeD0w7roHluAZamzJws04DcezkMnXpxJAFkA,47
|
|
16
|
+
amap_cli-0.1.0.dist-info/RECORD,,
|