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/geocode.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Shared geocoding helpers for CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from amap_cli.api import AmapApiClient
|
|
9
|
+
from amap_cli.errors import ApiResponseError
|
|
10
|
+
from amap_cli.params import Coordinate, parse_coordinate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class GeocodeResult:
|
|
15
|
+
"""Normalized geocoding result for CLI commands."""
|
|
16
|
+
|
|
17
|
+
coordinate: Coordinate
|
|
18
|
+
formatted_address: str | None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def geocode_text(
|
|
22
|
+
value: str,
|
|
23
|
+
*,
|
|
24
|
+
client: AmapApiClient,
|
|
25
|
+
city: str | None = None,
|
|
26
|
+
) -> GeocodeResult:
|
|
27
|
+
"""Convert a place name to a coordinate with the geocoding API."""
|
|
28
|
+
params: dict[str, Any] = {"address": value}
|
|
29
|
+
if city is not None:
|
|
30
|
+
params["city"] = city
|
|
31
|
+
|
|
32
|
+
payload = client.get("/v3/geocode/geo", params=params)
|
|
33
|
+
geocodes = payload.get("geocodes")
|
|
34
|
+
if not isinstance(geocodes, list) or not geocodes:
|
|
35
|
+
raise ApiResponseError(
|
|
36
|
+
f"未找到地点:{value}",
|
|
37
|
+
details={"address": value, "city": city},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
first = geocodes[0]
|
|
41
|
+
if not isinstance(first, dict):
|
|
42
|
+
raise ApiResponseError(
|
|
43
|
+
"地理编码响应格式无效。",
|
|
44
|
+
details={"address": value, "city": city},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
location_value = first.get("location")
|
|
48
|
+
if not isinstance(location_value, str) or not location_value.strip():
|
|
49
|
+
raise ApiResponseError(
|
|
50
|
+
"地理编码结果缺少坐标信息。",
|
|
51
|
+
details={"address": value, "city": city},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
formatted_address = first.get("formatted_address")
|
|
55
|
+
if not isinstance(formatted_address, str) or not formatted_address.strip():
|
|
56
|
+
formatted_address = None
|
|
57
|
+
|
|
58
|
+
return GeocodeResult(
|
|
59
|
+
coordinate=parse_coordinate(location_value, "geocode.location"),
|
|
60
|
+
formatted_address=formatted_address,
|
|
61
|
+
)
|
amap_cli/output.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Helpers for unified JSON output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, TextIO
|
|
8
|
+
|
|
9
|
+
from amap_cli.errors import AmapCliError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_success_payload(data: Any) -> dict[str, Any]:
|
|
13
|
+
"""Build the standard success payload."""
|
|
14
|
+
return {
|
|
15
|
+
"success": True,
|
|
16
|
+
"data": data,
|
|
17
|
+
"error": None,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_error_payload(error: AmapCliError) -> dict[str, Any]:
|
|
22
|
+
"""Build the standard error payload."""
|
|
23
|
+
return {
|
|
24
|
+
"success": False,
|
|
25
|
+
"data": None,
|
|
26
|
+
"error": error.to_dict(),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_json(payload: dict[str, Any], *, stream: TextIO | None = None) -> None:
|
|
31
|
+
"""Write a JSON payload as a single CLI response."""
|
|
32
|
+
if stream is None:
|
|
33
|
+
stream = sys.stdout
|
|
34
|
+
json.dump(payload, stream, ensure_ascii=False, indent=2)
|
|
35
|
+
stream.write("\n")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def emit_success(data: Any) -> None:
|
|
39
|
+
"""Print a success payload to standard output."""
|
|
40
|
+
write_json(build_success_payload(data))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def emit_error(error: AmapCliError) -> None:
|
|
44
|
+
"""Print an error payload to standard output."""
|
|
45
|
+
write_json(build_error_payload(error))
|
amap_cli/params.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared argument parsing helpers for future business commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from amap_cli.errors import ValidationError
|
|
9
|
+
|
|
10
|
+
COORDINATE_PATTERN = re.compile(
|
|
11
|
+
r"^\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([-+]?\d+(?:\.\d+)?)\s*$"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class Coordinate:
|
|
17
|
+
"""A validated longitude/latitude pair."""
|
|
18
|
+
|
|
19
|
+
longitude: float
|
|
20
|
+
latitude: float
|
|
21
|
+
|
|
22
|
+
def to_amap_value(self) -> str:
|
|
23
|
+
"""Convert to the `lng,lat` format expected by Amap APIs."""
|
|
24
|
+
return f"{self.longitude:.6f},{self.latitude:.6f}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class LocationInput:
|
|
29
|
+
"""A location argument that may be coordinates or a place name."""
|
|
30
|
+
|
|
31
|
+
raw: str
|
|
32
|
+
kind: str
|
|
33
|
+
coordinate: Coordinate | None = None
|
|
34
|
+
name: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalize_text_argument(value: str, field_name: str) -> str:
|
|
38
|
+
"""Trim and validate a text argument."""
|
|
39
|
+
normalized = value.strip()
|
|
40
|
+
if not normalized:
|
|
41
|
+
raise ValidationError(f"`{field_name}` 不能为空。")
|
|
42
|
+
return normalized
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_coordinate(value: str, field_name: str = "location") -> Coordinate:
|
|
46
|
+
"""Parse a `lng,lat` string into a validated coordinate object."""
|
|
47
|
+
normalized = normalize_text_argument(value, field_name)
|
|
48
|
+
matched = COORDINATE_PATTERN.match(normalized)
|
|
49
|
+
if matched is None:
|
|
50
|
+
raise ValidationError(
|
|
51
|
+
f"`{field_name}` 必须是 `经度,纬度` 格式,例如 `116.397,39.909`。"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
longitude = float(matched.group(1))
|
|
55
|
+
latitude = float(matched.group(2))
|
|
56
|
+
|
|
57
|
+
if not -180 <= longitude <= 180:
|
|
58
|
+
raise ValidationError(f"`{field_name}` 中的经度超出范围 [-180, 180]。")
|
|
59
|
+
if not -90 <= latitude <= 90:
|
|
60
|
+
raise ValidationError(f"`{field_name}` 中的纬度超出范围 [-90, 90]。")
|
|
61
|
+
|
|
62
|
+
return Coordinate(longitude=longitude, latitude=latitude)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_location_input(value: str, field_name: str) -> LocationInput:
|
|
66
|
+
"""Parse a future route/search location argument."""
|
|
67
|
+
normalized = normalize_text_argument(value, field_name)
|
|
68
|
+
|
|
69
|
+
if COORDINATE_PATTERN.match(normalized):
|
|
70
|
+
coordinate = parse_coordinate(normalized, field_name)
|
|
71
|
+
return LocationInput(
|
|
72
|
+
raw=normalized,
|
|
73
|
+
kind="coordinate",
|
|
74
|
+
coordinate=coordinate,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return LocationInput(raw=normalized, kind="text", name=normalized)
|