visicom-api-client 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.
- visicom_api/__init__.py +59 -0
- visicom_api/_base.py +56 -0
- visicom_api/_endpoints.py +166 -0
- visicom_api/async_client.py +197 -0
- visicom_api/client.py +195 -0
- visicom_api/exceptions.py +49 -0
- visicom_api/models.py +329 -0
- visicom_api/py.typed +0 -0
- visicom_api_client-0.1.0.dist-info/METADATA +93 -0
- visicom_api_client-0.1.0.dist-info/RECORD +11 -0
- visicom_api_client-0.1.0.dist-info/WHEEL +4 -0
visicom_api/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Unofficial Python client for the Visicom Data API."""
|
|
2
|
+
|
|
3
|
+
from .async_client import AsyncVisicomClient
|
|
4
|
+
from .client import VisicomClient
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
VisicomAPIError,
|
|
7
|
+
VisicomAuthError,
|
|
8
|
+
VisicomError,
|
|
9
|
+
VisicomRateLimitError,
|
|
10
|
+
)
|
|
11
|
+
from .models import (
|
|
12
|
+
AdmCountryProperties,
|
|
13
|
+
AdmDistrictProperties,
|
|
14
|
+
AdmLevel1Properties,
|
|
15
|
+
AdmLevel2Properties,
|
|
16
|
+
AdmLevel3Properties,
|
|
17
|
+
AdmSettlementProperties,
|
|
18
|
+
AdrAddressProperties,
|
|
19
|
+
AdrStreetProperties,
|
|
20
|
+
BufferResponse,
|
|
21
|
+
DistanceMatrixResponse,
|
|
22
|
+
DistanceResult,
|
|
23
|
+
FeatureCollection,
|
|
24
|
+
GeocodeResult,
|
|
25
|
+
GeoFeature,
|
|
26
|
+
LocationResponse,
|
|
27
|
+
PoiProperties,
|
|
28
|
+
SnapToRoadResult,
|
|
29
|
+
TspResponse,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"VisicomClient",
|
|
34
|
+
"AsyncVisicomClient",
|
|
35
|
+
"VisicomError",
|
|
36
|
+
"VisicomAPIError",
|
|
37
|
+
"VisicomAuthError",
|
|
38
|
+
"VisicomRateLimitError",
|
|
39
|
+
"GeoFeature",
|
|
40
|
+
"FeatureCollection",
|
|
41
|
+
"GeocodeResult",
|
|
42
|
+
"AdmCountryProperties",
|
|
43
|
+
"AdmDistrictProperties",
|
|
44
|
+
"AdmLevel1Properties",
|
|
45
|
+
"AdmLevel2Properties",
|
|
46
|
+
"AdmLevel3Properties",
|
|
47
|
+
"AdmSettlementProperties",
|
|
48
|
+
"AdrAddressProperties",
|
|
49
|
+
"AdrStreetProperties",
|
|
50
|
+
"PoiProperties",
|
|
51
|
+
"DistanceResult",
|
|
52
|
+
"DistanceMatrixResponse",
|
|
53
|
+
"TspResponse",
|
|
54
|
+
"SnapToRoadResult",
|
|
55
|
+
"BufferResponse",
|
|
56
|
+
"LocationResponse",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
__version__ = "0.1.0"
|
visicom_api/_base.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Shared helpers for query-parameter formatting.
|
|
2
|
+
|
|
3
|
+
Not part of the public API; import from `visicom_api` instead.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from typing import Any, Union
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://api.visicom.ua/data-api/5.0"
|
|
12
|
+
|
|
13
|
+
# A point is either an object id (or WKT geometry) string, or an (lng, lat) tuple.
|
|
14
|
+
Point = Union[str, tuple[float, float]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _point_to_str(point: Point) -> str:
|
|
18
|
+
if isinstance(point, str):
|
|
19
|
+
return point
|
|
20
|
+
lng, lat = point
|
|
21
|
+
return f"{lng},{lat}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _points_param(points: str | Iterable[Point]) -> str:
|
|
25
|
+
"""Format a list of points for a query parameter.
|
|
26
|
+
|
|
27
|
+
A pre-formatted string is passed through unchanged. Otherwise, the API
|
|
28
|
+
uses '|' to separate coordinate pairs and ',' to separate object ids, so
|
|
29
|
+
an iterable must be all-coordinates or all-ids; mixed lists must be
|
|
30
|
+
formatted by the caller and passed in as a raw string.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(points, str):
|
|
33
|
+
return points
|
|
34
|
+
items = list(points)
|
|
35
|
+
if not items:
|
|
36
|
+
raise ValueError("points must not be empty")
|
|
37
|
+
if all(isinstance(p, str) for p in items):
|
|
38
|
+
return ",".join(items) # type: ignore[arg-type]
|
|
39
|
+
if all(not isinstance(p, str) for p in items):
|
|
40
|
+
return "|".join(_point_to_str(p) for p in items)
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"cannot mix object ids and (lng, lat) tuples in one list; "
|
|
43
|
+
"pass a pre-formatted string instead"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _clean_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
"""Drop None values and render booleans the way the API expects."""
|
|
49
|
+
cleaned: dict[str, Any] = {}
|
|
50
|
+
for key, value in params.items():
|
|
51
|
+
if value is None:
|
|
52
|
+
continue
|
|
53
|
+
if isinstance(value, bool):
|
|
54
|
+
value = "true" if value else "false"
|
|
55
|
+
cleaned[key] = value
|
|
56
|
+
return cleaned
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Query-parameter builders for every Visicom Data API endpoint.
|
|
2
|
+
|
|
3
|
+
This mixin is shared by the sync and async clients so the endpoint naming,
|
|
4
|
+
point formatting, and None-stripping logic lives in exactly one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterable
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from ._base import Point, _clean_params, _point_to_str, _points_param
|
|
13
|
+
|
|
14
|
+
Format = Literal["json", "csv"]
|
|
15
|
+
Lang = Literal["uk", "ru", "en"]
|
|
16
|
+
Mode = Literal["driving", "driving-shortest", "direct"]
|
|
17
|
+
SnapMode = Literal["driving", "walking"]
|
|
18
|
+
Order = Literal["relevance", "distance"]
|
|
19
|
+
|
|
20
|
+
PathParams = tuple[str, dict[str, Any]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Endpoints:
|
|
24
|
+
default_lang: Lang
|
|
25
|
+
|
|
26
|
+
def _geocode_path_params(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
lang: Lang | None,
|
|
30
|
+
format: Format,
|
|
31
|
+
text: str | None,
|
|
32
|
+
word_text: str | None,
|
|
33
|
+
categories: str | None,
|
|
34
|
+
categories_exclude: str | None,
|
|
35
|
+
near: Point | None,
|
|
36
|
+
radius: float | None,
|
|
37
|
+
intersect: Point | None,
|
|
38
|
+
contains: Point | None,
|
|
39
|
+
order: Order | None,
|
|
40
|
+
zoom: int | None,
|
|
41
|
+
limit: int | None,
|
|
42
|
+
country: str | None,
|
|
43
|
+
boost_country: str | None,
|
|
44
|
+
) -> PathParams:
|
|
45
|
+
path = f"{lang or self.default_lang}/geocode.{format}"
|
|
46
|
+
params = _clean_params(
|
|
47
|
+
{
|
|
48
|
+
"text": text,
|
|
49
|
+
"word_text": word_text,
|
|
50
|
+
"categories": categories,
|
|
51
|
+
"categories_exclude": categories_exclude,
|
|
52
|
+
"near": _point_to_str(near) if near is not None else None,
|
|
53
|
+
"radius": radius,
|
|
54
|
+
"intersect": _point_to_str(intersect) if intersect is not None else None,
|
|
55
|
+
"contains": _point_to_str(contains) if contains is not None else None,
|
|
56
|
+
"order": order,
|
|
57
|
+
"zoom": zoom,
|
|
58
|
+
"limit": limit,
|
|
59
|
+
"country": country,
|
|
60
|
+
"boost_country": boost_country,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
return path, params
|
|
64
|
+
|
|
65
|
+
def _feature_path_params(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
ids: str | Iterable[str],
|
|
69
|
+
lang: Lang | None,
|
|
70
|
+
format: Format,
|
|
71
|
+
geometry: bool,
|
|
72
|
+
) -> PathParams:
|
|
73
|
+
id_param = ids if isinstance(ids, str) else ",".join(ids)
|
|
74
|
+
path = f"{lang or self.default_lang}/feature/{id_param}.{format}"
|
|
75
|
+
params = _clean_params({"geometry": None if geometry else "no"})
|
|
76
|
+
return path, params
|
|
77
|
+
|
|
78
|
+
def _distance_path_params(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
origin: Point,
|
|
82
|
+
destination: Point,
|
|
83
|
+
waypoints: str | Iterable[Point] | None,
|
|
84
|
+
locks: str | Iterable[Point] | None,
|
|
85
|
+
mode: Mode,
|
|
86
|
+
geometry: Literal["no", "path"],
|
|
87
|
+
accuracy: float | None,
|
|
88
|
+
) -> PathParams:
|
|
89
|
+
params = _clean_params(
|
|
90
|
+
{
|
|
91
|
+
"origin": _point_to_str(origin),
|
|
92
|
+
"destination": _point_to_str(destination),
|
|
93
|
+
"waypoints": _points_param(waypoints) if waypoints is not None else None,
|
|
94
|
+
"locks": _points_param(locks) if locks is not None else None,
|
|
95
|
+
"mode": mode,
|
|
96
|
+
"geometry": geometry,
|
|
97
|
+
"accuracy": accuracy,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
return "core/distance.json", params
|
|
101
|
+
|
|
102
|
+
def _distancematrix_path_params(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
origins: str | Iterable[Point],
|
|
106
|
+
destinations: str | Iterable[Point],
|
|
107
|
+
locks: str | Iterable[Point] | None,
|
|
108
|
+
mode: Mode,
|
|
109
|
+
) -> PathParams:
|
|
110
|
+
params = _clean_params(
|
|
111
|
+
{
|
|
112
|
+
"origins": _points_param(origins),
|
|
113
|
+
"destinations": _points_param(destinations),
|
|
114
|
+
"locks": _points_param(locks) if locks is not None else None,
|
|
115
|
+
"mode": mode,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
return "core/distancematrix.json", params
|
|
119
|
+
|
|
120
|
+
def _tsp_path_params(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
waypoints: str | Iterable[Point],
|
|
124
|
+
round_trip: bool,
|
|
125
|
+
locks: str | Iterable[Point] | None,
|
|
126
|
+
mode: Mode,
|
|
127
|
+
) -> PathParams:
|
|
128
|
+
params = _clean_params(
|
|
129
|
+
{
|
|
130
|
+
"waypoints": _points_param(waypoints),
|
|
131
|
+
"round_trip": round_trip,
|
|
132
|
+
"locks": _points_param(locks) if locks is not None else None,
|
|
133
|
+
"mode": mode,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
return "core/tsp.json", params
|
|
137
|
+
|
|
138
|
+
def _snaptoroad_path_params(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
points: str | Iterable[Point],
|
|
142
|
+
interpolate: bool,
|
|
143
|
+
mode: SnapMode,
|
|
144
|
+
separate: bool,
|
|
145
|
+
) -> PathParams:
|
|
146
|
+
params = _clean_params(
|
|
147
|
+
{
|
|
148
|
+
"points": _points_param(points),
|
|
149
|
+
"interpolate": interpolate,
|
|
150
|
+
"mode": mode,
|
|
151
|
+
"separate": separate,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return "core/snaptoroad.json", params
|
|
155
|
+
|
|
156
|
+
def _location_path_params(
|
|
157
|
+
self, *, start: Point, azimut: float, distance: float, format: Format
|
|
158
|
+
) -> PathParams:
|
|
159
|
+
params = _clean_params(
|
|
160
|
+
{"start": _point_to_str(start), "azimut": azimut, "distance": distance}
|
|
161
|
+
)
|
|
162
|
+
return f"core/location.{format}", params
|
|
163
|
+
|
|
164
|
+
def _buffer_path_params(self, *, near: Point, radius: float, format: Format) -> PathParams:
|
|
165
|
+
params = _clean_params({"near": _point_to_str(near), "radius": radius})
|
|
166
|
+
return f"core/buffer.{format}", params
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Asynchronous client for the Visicom Data API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._base import BASE_URL, Point
|
|
12
|
+
from ._endpoints import Format, Lang, Mode, Order, SnapMode, _Endpoints
|
|
13
|
+
from .exceptions import raise_for_status
|
|
14
|
+
from .models import (
|
|
15
|
+
BufferResponse,
|
|
16
|
+
DistanceMatrixResponse,
|
|
17
|
+
DistanceResult,
|
|
18
|
+
GeocodeResult,
|
|
19
|
+
LocationResponse,
|
|
20
|
+
SnapToRoadResult,
|
|
21
|
+
TspResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncVisicomClient(_Endpoints):
|
|
26
|
+
"""Asynchronous client for the Visicom Data API.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
async with AsyncVisicomClient(api_key="YOUR_API_KEY") as client:
|
|
30
|
+
result = await client.geocode(text="Київ, Хрещатик, 26")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str,
|
|
36
|
+
*,
|
|
37
|
+
lang: Lang = "uk",
|
|
38
|
+
base_url: str = BASE_URL,
|
|
39
|
+
timeout: float = 10.0,
|
|
40
|
+
client: httpx.AsyncClient | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
self.default_lang: Lang = lang
|
|
44
|
+
self._client = client or httpx.AsyncClient(base_url=base_url, timeout=timeout)
|
|
45
|
+
|
|
46
|
+
async def aclose(self) -> None:
|
|
47
|
+
await self._client.aclose()
|
|
48
|
+
|
|
49
|
+
async def __aenter__(self) -> AsyncVisicomClient:
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
async def __aexit__(
|
|
53
|
+
self,
|
|
54
|
+
exc_type: type[BaseException] | None,
|
|
55
|
+
exc: BaseException | None,
|
|
56
|
+
tb: TracebackType | None,
|
|
57
|
+
) -> None:
|
|
58
|
+
await self.aclose()
|
|
59
|
+
|
|
60
|
+
async def _get(self, path: str, params: dict[str, Any]) -> Any:
|
|
61
|
+
response = await self._client.get(path, params={**params, "key": self.api_key})
|
|
62
|
+
raise_for_status(response)
|
|
63
|
+
return response.json()
|
|
64
|
+
|
|
65
|
+
async def geocode(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
text: str | None = None,
|
|
69
|
+
word_text: str | None = None,
|
|
70
|
+
categories: str | None = None,
|
|
71
|
+
categories_exclude: str | None = None,
|
|
72
|
+
near: Point | None = None,
|
|
73
|
+
radius: float | None = None,
|
|
74
|
+
intersect: Point | None = None,
|
|
75
|
+
contains: Point | None = None,
|
|
76
|
+
order: Order | None = None,
|
|
77
|
+
zoom: int | None = None,
|
|
78
|
+
limit: int | None = None,
|
|
79
|
+
country: str | None = None,
|
|
80
|
+
boost_country: str | None = None,
|
|
81
|
+
lang: Lang | None = None,
|
|
82
|
+
format: Format = "json",
|
|
83
|
+
) -> GeocodeResult:
|
|
84
|
+
"""Search objects by name, or reverse-geocode near a point. See /geocode docs."""
|
|
85
|
+
path, params = self._geocode_path_params(
|
|
86
|
+
lang=lang,
|
|
87
|
+
format=format,
|
|
88
|
+
text=text,
|
|
89
|
+
word_text=word_text,
|
|
90
|
+
categories=categories,
|
|
91
|
+
categories_exclude=categories_exclude,
|
|
92
|
+
near=near,
|
|
93
|
+
radius=radius,
|
|
94
|
+
intersect=intersect,
|
|
95
|
+
contains=contains,
|
|
96
|
+
order=order,
|
|
97
|
+
zoom=zoom,
|
|
98
|
+
limit=limit,
|
|
99
|
+
country=country,
|
|
100
|
+
boost_country=boost_country,
|
|
101
|
+
)
|
|
102
|
+
return cast(GeocodeResult, await self._get(path, params))
|
|
103
|
+
|
|
104
|
+
async def feature(
|
|
105
|
+
self,
|
|
106
|
+
ids: str | Iterable[str],
|
|
107
|
+
*,
|
|
108
|
+
lang: Lang | None = None,
|
|
109
|
+
format: Format = "json",
|
|
110
|
+
geometry: bool = True,
|
|
111
|
+
) -> GeocodeResult:
|
|
112
|
+
"""Fetch full geometry and attributes for up to 250 object ids."""
|
|
113
|
+
path, params = self._feature_path_params(
|
|
114
|
+
ids=ids, lang=lang, format=format, geometry=geometry
|
|
115
|
+
)
|
|
116
|
+
return cast(GeocodeResult, await self._get(path, params))
|
|
117
|
+
|
|
118
|
+
async def distance(
|
|
119
|
+
self,
|
|
120
|
+
origin: Point,
|
|
121
|
+
destination: Point,
|
|
122
|
+
*,
|
|
123
|
+
waypoints: str | Iterable[Point] | None = None,
|
|
124
|
+
locks: str | Iterable[Point] | None = None,
|
|
125
|
+
mode: Mode = "driving",
|
|
126
|
+
geometry: Literal["no", "path"] = "no",
|
|
127
|
+
accuracy: float | None = None,
|
|
128
|
+
) -> DistanceResult:
|
|
129
|
+
"""Compute a route/distance between two points."""
|
|
130
|
+
path, params = self._distance_path_params(
|
|
131
|
+
origin=origin,
|
|
132
|
+
destination=destination,
|
|
133
|
+
waypoints=waypoints,
|
|
134
|
+
locks=locks,
|
|
135
|
+
mode=mode,
|
|
136
|
+
geometry=geometry,
|
|
137
|
+
accuracy=accuracy,
|
|
138
|
+
)
|
|
139
|
+
return cast(DistanceResult, await self._get(path, params))
|
|
140
|
+
|
|
141
|
+
async def distancematrix(
|
|
142
|
+
self,
|
|
143
|
+
origins: str | Iterable[Point],
|
|
144
|
+
destinations: str | Iterable[Point],
|
|
145
|
+
*,
|
|
146
|
+
locks: str | Iterable[Point] | None = None,
|
|
147
|
+
mode: Mode = "driving",
|
|
148
|
+
) -> DistanceMatrixResponse:
|
|
149
|
+
"""Compute a distance matrix between up to 25x25 points."""
|
|
150
|
+
path, params = self._distancematrix_path_params(
|
|
151
|
+
origins=origins, destinations=destinations, locks=locks, mode=mode
|
|
152
|
+
)
|
|
153
|
+
return cast(DistanceMatrixResponse, await self._get(path, params))
|
|
154
|
+
|
|
155
|
+
async def tsp(
|
|
156
|
+
self,
|
|
157
|
+
waypoints: str | Iterable[Point],
|
|
158
|
+
*,
|
|
159
|
+
round_trip: bool = True,
|
|
160
|
+
locks: str | Iterable[Point] | None = None,
|
|
161
|
+
mode: Mode = "driving",
|
|
162
|
+
) -> TspResponse:
|
|
163
|
+
"""Solve the travelling-salesman ordering for up to 50 waypoints."""
|
|
164
|
+
path, params = self._tsp_path_params(
|
|
165
|
+
waypoints=waypoints, round_trip=round_trip, locks=locks, mode=mode
|
|
166
|
+
)
|
|
167
|
+
return cast(TspResponse, await self._get(path, params))
|
|
168
|
+
|
|
169
|
+
async def snaptoroad(
|
|
170
|
+
self,
|
|
171
|
+
points: str | Iterable[Point],
|
|
172
|
+
*,
|
|
173
|
+
interpolate: bool = False,
|
|
174
|
+
mode: SnapMode = "driving",
|
|
175
|
+
separate: bool = False,
|
|
176
|
+
) -> SnapToRoadResult:
|
|
177
|
+
"""Snap up to 250 GNSS points onto the road network."""
|
|
178
|
+
path, params = self._snaptoroad_path_params(
|
|
179
|
+
points=points, interpolate=interpolate, mode=mode, separate=separate
|
|
180
|
+
)
|
|
181
|
+
return cast(SnapToRoadResult, await self._get(path, params))
|
|
182
|
+
|
|
183
|
+
async def location(
|
|
184
|
+
self, start: Point, azimut: float, distance: float, *, format: Format = "json"
|
|
185
|
+
) -> LocationResponse:
|
|
186
|
+
"""Direct geodesic problem: find a point given a start, azimuth, and distance."""
|
|
187
|
+
path, params = self._location_path_params(
|
|
188
|
+
start=start, azimut=azimut, distance=distance, format=format
|
|
189
|
+
)
|
|
190
|
+
return cast(LocationResponse, await self._get(path, params))
|
|
191
|
+
|
|
192
|
+
async def buffer(
|
|
193
|
+
self, near: Point, radius: float, *, format: Format = "json"
|
|
194
|
+
) -> BufferResponse:
|
|
195
|
+
"""Build a buffer polygon (up to 10000 m) around a point/geometry/object."""
|
|
196
|
+
path, params = self._buffer_path_params(near=near, radius=radius, format=format)
|
|
197
|
+
return cast(BufferResponse, await self._get(path, params))
|
visicom_api/client.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Synchronous client for the Visicom Data API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._base import BASE_URL, Point
|
|
12
|
+
from ._endpoints import Format, Lang, Mode, Order, SnapMode, _Endpoints
|
|
13
|
+
from .exceptions import raise_for_status
|
|
14
|
+
from .models import (
|
|
15
|
+
BufferResponse,
|
|
16
|
+
DistanceMatrixResponse,
|
|
17
|
+
DistanceResult,
|
|
18
|
+
GeocodeResult,
|
|
19
|
+
LocationResponse,
|
|
20
|
+
SnapToRoadResult,
|
|
21
|
+
TspResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VisicomClient(_Endpoints):
|
|
26
|
+
"""Synchronous client for the Visicom Data API.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
with VisicomClient(api_key="YOUR_API_KEY") as client:
|
|
30
|
+
result = client.geocode(text="Київ, Хрещатик, 26")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str,
|
|
36
|
+
*,
|
|
37
|
+
lang: Lang = "uk",
|
|
38
|
+
base_url: str = BASE_URL,
|
|
39
|
+
timeout: float = 10.0,
|
|
40
|
+
client: httpx.Client | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
self.default_lang: Lang = lang
|
|
44
|
+
self._client = client or httpx.Client(base_url=base_url, timeout=timeout)
|
|
45
|
+
|
|
46
|
+
def close(self) -> None:
|
|
47
|
+
self._client.close()
|
|
48
|
+
|
|
49
|
+
def __enter__(self) -> VisicomClient:
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(
|
|
53
|
+
self,
|
|
54
|
+
exc_type: type[BaseException] | None,
|
|
55
|
+
exc: BaseException | None,
|
|
56
|
+
tb: TracebackType | None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.close()
|
|
59
|
+
|
|
60
|
+
def _get(self, path: str, params: dict[str, Any]) -> Any:
|
|
61
|
+
response = self._client.get(path, params={**params, "key": self.api_key})
|
|
62
|
+
raise_for_status(response)
|
|
63
|
+
return response.json()
|
|
64
|
+
|
|
65
|
+
def geocode(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
text: str | None = None,
|
|
69
|
+
word_text: str | None = None,
|
|
70
|
+
categories: str | None = None,
|
|
71
|
+
categories_exclude: str | None = None,
|
|
72
|
+
near: Point | None = None,
|
|
73
|
+
radius: float | None = None,
|
|
74
|
+
intersect: Point | None = None,
|
|
75
|
+
contains: Point | None = None,
|
|
76
|
+
order: Order | None = None,
|
|
77
|
+
zoom: int | None = None,
|
|
78
|
+
limit: int | None = None,
|
|
79
|
+
country: str | None = None,
|
|
80
|
+
boost_country: str | None = None,
|
|
81
|
+
lang: Lang | None = None,
|
|
82
|
+
format: Format = "json",
|
|
83
|
+
) -> GeocodeResult:
|
|
84
|
+
"""Search objects by name, or reverse-geocode near a point. See /geocode docs."""
|
|
85
|
+
path, params = self._geocode_path_params(
|
|
86
|
+
lang=lang,
|
|
87
|
+
format=format,
|
|
88
|
+
text=text,
|
|
89
|
+
word_text=word_text,
|
|
90
|
+
categories=categories,
|
|
91
|
+
categories_exclude=categories_exclude,
|
|
92
|
+
near=near,
|
|
93
|
+
radius=radius,
|
|
94
|
+
intersect=intersect,
|
|
95
|
+
contains=contains,
|
|
96
|
+
order=order,
|
|
97
|
+
zoom=zoom,
|
|
98
|
+
limit=limit,
|
|
99
|
+
country=country,
|
|
100
|
+
boost_country=boost_country,
|
|
101
|
+
)
|
|
102
|
+
return cast(GeocodeResult, self._get(path, params))
|
|
103
|
+
|
|
104
|
+
def feature(
|
|
105
|
+
self,
|
|
106
|
+
ids: str | Iterable[str],
|
|
107
|
+
*,
|
|
108
|
+
lang: Lang | None = None,
|
|
109
|
+
format: Format = "json",
|
|
110
|
+
geometry: bool = True,
|
|
111
|
+
) -> GeocodeResult:
|
|
112
|
+
"""Fetch full geometry and attributes for up to 250 object ids."""
|
|
113
|
+
path, params = self._feature_path_params(
|
|
114
|
+
ids=ids, lang=lang, format=format, geometry=geometry
|
|
115
|
+
)
|
|
116
|
+
return cast(GeocodeResult, self._get(path, params))
|
|
117
|
+
|
|
118
|
+
def distance(
|
|
119
|
+
self,
|
|
120
|
+
origin: Point,
|
|
121
|
+
destination: Point,
|
|
122
|
+
*,
|
|
123
|
+
waypoints: str | Iterable[Point] | None = None,
|
|
124
|
+
locks: str | Iterable[Point] | None = None,
|
|
125
|
+
mode: Mode = "driving",
|
|
126
|
+
geometry: Literal["no", "path"] = "no",
|
|
127
|
+
accuracy: float | None = None,
|
|
128
|
+
) -> DistanceResult:
|
|
129
|
+
"""Compute a route/distance between two points."""
|
|
130
|
+
path, params = self._distance_path_params(
|
|
131
|
+
origin=origin,
|
|
132
|
+
destination=destination,
|
|
133
|
+
waypoints=waypoints,
|
|
134
|
+
locks=locks,
|
|
135
|
+
mode=mode,
|
|
136
|
+
geometry=geometry,
|
|
137
|
+
accuracy=accuracy,
|
|
138
|
+
)
|
|
139
|
+
return cast(DistanceResult, self._get(path, params))
|
|
140
|
+
|
|
141
|
+
def distancematrix(
|
|
142
|
+
self,
|
|
143
|
+
origins: str | Iterable[Point],
|
|
144
|
+
destinations: str | Iterable[Point],
|
|
145
|
+
*,
|
|
146
|
+
locks: str | Iterable[Point] | None = None,
|
|
147
|
+
mode: Mode = "driving",
|
|
148
|
+
) -> DistanceMatrixResponse:
|
|
149
|
+
"""Compute a distance matrix between up to 25x25 points."""
|
|
150
|
+
path, params = self._distancematrix_path_params(
|
|
151
|
+
origins=origins, destinations=destinations, locks=locks, mode=mode
|
|
152
|
+
)
|
|
153
|
+
return cast(DistanceMatrixResponse, self._get(path, params))
|
|
154
|
+
|
|
155
|
+
def tsp(
|
|
156
|
+
self,
|
|
157
|
+
waypoints: str | Iterable[Point],
|
|
158
|
+
*,
|
|
159
|
+
round_trip: bool = True,
|
|
160
|
+
locks: str | Iterable[Point] | None = None,
|
|
161
|
+
mode: Mode = "driving",
|
|
162
|
+
) -> TspResponse:
|
|
163
|
+
"""Solve the travelling-salesman ordering for up to 50 waypoints."""
|
|
164
|
+
path, params = self._tsp_path_params(
|
|
165
|
+
waypoints=waypoints, round_trip=round_trip, locks=locks, mode=mode
|
|
166
|
+
)
|
|
167
|
+
return cast(TspResponse, self._get(path, params))
|
|
168
|
+
|
|
169
|
+
def snaptoroad(
|
|
170
|
+
self,
|
|
171
|
+
points: str | Iterable[Point],
|
|
172
|
+
*,
|
|
173
|
+
interpolate: bool = False,
|
|
174
|
+
mode: SnapMode = "driving",
|
|
175
|
+
separate: bool = False,
|
|
176
|
+
) -> SnapToRoadResult:
|
|
177
|
+
"""Snap up to 250 GNSS points onto the road network."""
|
|
178
|
+
path, params = self._snaptoroad_path_params(
|
|
179
|
+
points=points, interpolate=interpolate, mode=mode, separate=separate
|
|
180
|
+
)
|
|
181
|
+
return cast(SnapToRoadResult, self._get(path, params))
|
|
182
|
+
|
|
183
|
+
def location(
|
|
184
|
+
self, start: Point, azimut: float, distance: float, *, format: Format = "json"
|
|
185
|
+
) -> LocationResponse:
|
|
186
|
+
"""Direct geodesic problem: find a point given a start, azimuth, and distance."""
|
|
187
|
+
path, params = self._location_path_params(
|
|
188
|
+
start=start, azimut=azimut, distance=distance, format=format
|
|
189
|
+
)
|
|
190
|
+
return cast(LocationResponse, self._get(path, params))
|
|
191
|
+
|
|
192
|
+
def buffer(self, near: Point, radius: float, *, format: Format = "json") -> BufferResponse:
|
|
193
|
+
"""Build a buffer polygon (up to 10000 m) around a point/geometry/object."""
|
|
194
|
+
path, params = self._buffer_path_params(near=near, radius=radius, format=format)
|
|
195
|
+
return cast(BufferResponse, self._get(path, params))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Exceptions raised by the Visicom API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VisicomError(Exception):
|
|
9
|
+
"""Base exception for all errors raised by this library."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VisicomAPIError(VisicomError):
|
|
13
|
+
"""Raised when the Visicom API returns a non-success response."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, message: str, status_code: int, response: httpx.Response | None = None
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(f"[{status_code}] {message}")
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.response = response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VisicomAuthError(VisicomAPIError):
|
|
24
|
+
"""Raised on 401/403 responses - invalid, missing, or expired API key."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VisicomRateLimitError(VisicomAPIError):
|
|
28
|
+
"""Raised on 429 responses - request quota exceeded."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
32
|
+
"""Translate an httpx response into a VisicomAPIError subclass if needed."""
|
|
33
|
+
if response.is_success:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
message = response.text
|
|
37
|
+
try:
|
|
38
|
+
payload = response.json()
|
|
39
|
+
if isinstance(payload, dict):
|
|
40
|
+
message = str(payload.get("message") or payload.get("error") or message)
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
status = response.status_code
|
|
45
|
+
if status in (401, 403):
|
|
46
|
+
raise VisicomAuthError(message, status, response)
|
|
47
|
+
if status == 429:
|
|
48
|
+
raise VisicomRateLimitError(message, status, response)
|
|
49
|
+
raise VisicomAPIError(message, status, response)
|
visicom_api/models.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Typed response shapes for the Visicom Data API, derived from the official
|
|
2
|
+
OAS3 spec (https://api.visicom.ua/data-api/5.0/openapi.yaml).
|
|
3
|
+
|
|
4
|
+
These are TypedDicts, not runtime-validated models: they document the shape
|
|
5
|
+
of the parsed JSON so callers get autocomplete/mypy checking without adding a
|
|
6
|
+
validation dependency. Fields are `total=False` throughout because the spec's
|
|
7
|
+
own `required` lists are inconsistent with its `properties` lists in several
|
|
8
|
+
places (e.g. AdmDistrict requires `settlement_class` but never defines it).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Literal, TypedDict, Union
|
|
14
|
+
|
|
15
|
+
BBox = list[float] # [min_lng, min_lat, max_lng, max_lat]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PointGeometry(TypedDict, total=False):
|
|
19
|
+
type: Literal["Point"]
|
|
20
|
+
coordinates: list[float] # [lng, lat]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LineStringGeometry(TypedDict, total=False):
|
|
24
|
+
type: Literal["LineString"]
|
|
25
|
+
coordinates: list[list[float]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PolygonGeometry(TypedDict, total=False):
|
|
29
|
+
type: Literal["Polygon"]
|
|
30
|
+
coordinates: list[list[list[float]]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MultiPolygonGeometry(TypedDict, total=False):
|
|
34
|
+
type: Literal["MultiPolygon"]
|
|
35
|
+
coordinates: list[list[list[list[float]]]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Geometry = Union[PointGeometry, LineStringGeometry, PolygonGeometry, MultiPolygonGeometry]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NamedRef(TypedDict, total=False):
|
|
42
|
+
"""An `{id, name}` reference to another object, e.g. an admin subdivision."""
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
name: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- /geocode and /feature `properties`, one per `categories` value ---------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AdmCountryProperties(TypedDict, total=False):
|
|
52
|
+
name: str
|
|
53
|
+
categories: Literal["adm_country"]
|
|
54
|
+
lang: str
|
|
55
|
+
country_code: str
|
|
56
|
+
admin_center: str
|
|
57
|
+
admin_center_id: str
|
|
58
|
+
admin_center_url: str
|
|
59
|
+
copyright: str
|
|
60
|
+
levels1: list[NamedRef]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AdmDistrictProperties(TypedDict, total=False):
|
|
64
|
+
name: str
|
|
65
|
+
type: str
|
|
66
|
+
categories: Literal["adm_district"]
|
|
67
|
+
country_code: str
|
|
68
|
+
country: str
|
|
69
|
+
lang: str
|
|
70
|
+
level1: str
|
|
71
|
+
level1_id: str
|
|
72
|
+
level2: str
|
|
73
|
+
level2_id: str
|
|
74
|
+
level3: str
|
|
75
|
+
level3_id: str
|
|
76
|
+
settlement: str
|
|
77
|
+
settlement_id: str
|
|
78
|
+
settlement_type: str
|
|
79
|
+
settlement_class: str
|
|
80
|
+
copyright: str
|
|
81
|
+
settlement_url: str
|
|
82
|
+
level1_url: str
|
|
83
|
+
level2_url: str
|
|
84
|
+
level3_url: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AdmLevel1Properties(TypedDict, total=False):
|
|
88
|
+
name: str
|
|
89
|
+
categories: Literal["adm_level1"]
|
|
90
|
+
country_code: str
|
|
91
|
+
country: str
|
|
92
|
+
lang: str
|
|
93
|
+
admin_center: str
|
|
94
|
+
admin_center_id: str
|
|
95
|
+
admin_center_url: str
|
|
96
|
+
copyright: str
|
|
97
|
+
levels2: list[NamedRef]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AdmLevel2Properties(TypedDict, total=False):
|
|
101
|
+
name: str
|
|
102
|
+
categories: Literal["adm_level2"]
|
|
103
|
+
country_code: str
|
|
104
|
+
country: str
|
|
105
|
+
lang: str
|
|
106
|
+
level1: str
|
|
107
|
+
level1_id: str
|
|
108
|
+
level1_url: str
|
|
109
|
+
copyright: str
|
|
110
|
+
levels3: list[NamedRef]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AdmLevel3Properties(TypedDict, total=False):
|
|
114
|
+
name: str
|
|
115
|
+
categories: Literal["adm_level3"]
|
|
116
|
+
country_code: str
|
|
117
|
+
country: str
|
|
118
|
+
lang: str
|
|
119
|
+
level1: str
|
|
120
|
+
level1_id: str
|
|
121
|
+
level2: str
|
|
122
|
+
level2_id: str
|
|
123
|
+
level2_url: str
|
|
124
|
+
copyright: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# `class` is a Python keyword, so this one uses the functional TypedDict form.
|
|
128
|
+
AdmSettlementProperties = TypedDict(
|
|
129
|
+
"AdmSettlementProperties",
|
|
130
|
+
{
|
|
131
|
+
"name": str,
|
|
132
|
+
"categories": Literal["adm_settlement"],
|
|
133
|
+
"country_code": str,
|
|
134
|
+
"country": str,
|
|
135
|
+
"lang": str,
|
|
136
|
+
"level1": str,
|
|
137
|
+
"level1_id": str,
|
|
138
|
+
"level2": str,
|
|
139
|
+
"level2_id": str,
|
|
140
|
+
"level3": str,
|
|
141
|
+
"level3_id": str,
|
|
142
|
+
"type": str,
|
|
143
|
+
"class": str,
|
|
144
|
+
"copyright": str,
|
|
145
|
+
},
|
|
146
|
+
total=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AdrAddressProperties(TypedDict, total=False):
|
|
151
|
+
name: str
|
|
152
|
+
categories: Literal["adr_address"]
|
|
153
|
+
street_id: str
|
|
154
|
+
lang: str
|
|
155
|
+
street: str
|
|
156
|
+
street_type: str
|
|
157
|
+
zone: str
|
|
158
|
+
settlement_id: str
|
|
159
|
+
settlement: str
|
|
160
|
+
settlement_type: str
|
|
161
|
+
height: float
|
|
162
|
+
copyright: str
|
|
163
|
+
settlement_url: str
|
|
164
|
+
street_url: str
|
|
165
|
+
# Undocumented in the OAS3 spec, but present in live API responses.
|
|
166
|
+
country: str
|
|
167
|
+
country_code: str
|
|
168
|
+
postal_code: str
|
|
169
|
+
relevance: float
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class AdrStreetProperties(TypedDict, total=False):
|
|
173
|
+
name: str
|
|
174
|
+
name_en: str
|
|
175
|
+
categories: Literal["adr_street"]
|
|
176
|
+
country_code: str
|
|
177
|
+
country: str
|
|
178
|
+
lang: str
|
|
179
|
+
settlement_id: str
|
|
180
|
+
zone: str
|
|
181
|
+
type: str
|
|
182
|
+
settlement: str
|
|
183
|
+
settlement_type: str
|
|
184
|
+
settlement_class: str
|
|
185
|
+
address: list[NamedRef]
|
|
186
|
+
copyright: str
|
|
187
|
+
settlement_url: str
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class PoiProperties(TypedDict, total=False):
|
|
191
|
+
name: str
|
|
192
|
+
categories: str # one of many poi_* ids, see GET /categories
|
|
193
|
+
lang: str
|
|
194
|
+
address: str
|
|
195
|
+
address_info: str
|
|
196
|
+
country_code: str
|
|
197
|
+
creation_data: str
|
|
198
|
+
creator: str
|
|
199
|
+
description: str
|
|
200
|
+
email: str
|
|
201
|
+
phones: list[str]
|
|
202
|
+
site_url: str
|
|
203
|
+
star: float
|
|
204
|
+
vitrine: str
|
|
205
|
+
w24hours: bool
|
|
206
|
+
icon: int
|
|
207
|
+
photo1: int
|
|
208
|
+
photo2: int
|
|
209
|
+
photo3: int
|
|
210
|
+
copyright: str
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
FeatureProperties = Union[
|
|
214
|
+
AdmCountryProperties,
|
|
215
|
+
AdmDistrictProperties,
|
|
216
|
+
AdmLevel1Properties,
|
|
217
|
+
AdmLevel2Properties,
|
|
218
|
+
AdmLevel3Properties,
|
|
219
|
+
AdmSettlementProperties,
|
|
220
|
+
AdrAddressProperties,
|
|
221
|
+
AdrStreetProperties,
|
|
222
|
+
PoiProperties,
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class GeoFeature(TypedDict, total=False):
|
|
227
|
+
"""A single object as returned by /geocode or /feature."""
|
|
228
|
+
|
|
229
|
+
type: Literal["Feature"]
|
|
230
|
+
id: str
|
|
231
|
+
bbox: BBox
|
|
232
|
+
geo_centroid: PointGeometry
|
|
233
|
+
geometry: Geometry
|
|
234
|
+
properties: Any # narrow with FeatureProperties / cast() based on properties["categories"]
|
|
235
|
+
url: str
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class FeatureCollection(TypedDict, total=False):
|
|
239
|
+
"""Wraps multiple results. The OAS3 spec names this field `feature`
|
|
240
|
+
(singular), but real responses use the GeoJSON-standard `features`
|
|
241
|
+
(verified against the live API); the spec is wrong here."""
|
|
242
|
+
|
|
243
|
+
type: Literal["FeatureCollection"]
|
|
244
|
+
features: list[GeoFeature]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
GeocodeResult = Union[GeoFeature, FeatureCollection]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --- /distance ---------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class DistanceNoGeometryResponse(TypedDict, total=False):
|
|
254
|
+
distance: int
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class _RoutePoint(TypedDict, total=False):
|
|
258
|
+
type: Literal["Point"]
|
|
259
|
+
coordinates: list[float]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class DistancePathProperties(TypedDict, total=False):
|
|
263
|
+
origin: _RoutePoint
|
|
264
|
+
destination: _RoutePoint
|
|
265
|
+
distance: float
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class DistancePathResponse(TypedDict, total=False):
|
|
269
|
+
type: Literal["Feature"]
|
|
270
|
+
geometry: LineStringGeometry
|
|
271
|
+
properties: DistancePathProperties
|
|
272
|
+
bbox: BBox
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
DistanceResult = Union[DistanceNoGeometryResponse, DistancePathResponse]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# --- /distancematrix -----------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class DistanceMatrixCell(TypedDict, total=False):
|
|
282
|
+
distance: int
|
|
283
|
+
# Not in the OAS3 schema, but present in the docs' example responses.
|
|
284
|
+
status: str
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class DistanceMatrixResponse(TypedDict, total=False):
|
|
288
|
+
rows: list[list[DistanceMatrixCell]]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# --- /tsp ----------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TspWaypoint(TypedDict, total=False):
|
|
295
|
+
index: int
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TspResponse(TypedDict, total=False):
|
|
299
|
+
list: list[TspWaypoint]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# --- /buffer and /location -------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
BufferResponse = PolygonGeometry
|
|
305
|
+
LocationResponse = PointGeometry
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# --- /snaptoroad -----------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class SnapPointFeature(TypedDict, total=False):
|
|
312
|
+
type: Literal["Feature"]
|
|
313
|
+
geometry: PointGeometry
|
|
314
|
+
bbox: BBox
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SnapLineFeature(TypedDict, total=False):
|
|
318
|
+
type: Literal["Feature"]
|
|
319
|
+
geometry: LineStringGeometry
|
|
320
|
+
bbox: BBox
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class SnapFeatureCollection(TypedDict, total=False):
|
|
324
|
+
type: Literal["FeatureCollection"]
|
|
325
|
+
features: list[SnapPointFeature | SnapLineFeature]
|
|
326
|
+
bbox: BBox
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
SnapToRoadResult = Union[SnapPointFeature, SnapLineFeature, SnapFeatureCollection]
|
visicom_api/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: visicom-api-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial sync/async Python client for the Visicom Data API
|
|
5
|
+
Project-URL: Homepage, https://api.visicom.ua/uk/products/data-api/data-api-references
|
|
6
|
+
Author: Your Name
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: api-client,geocoding,gis,visicom
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: httpx>=0.24
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# visicom-api-client
|
|
25
|
+
|
|
26
|
+
Неофіційний Python-клієнт для [Visicom Data API](https://api.visicom.ua/uk/products/data-api/data-api-references) (геокодування, маршрутизація, буфери, прив'язка до доріг).
|
|
27
|
+
|
|
28
|
+
## Встановлення
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Використання
|
|
35
|
+
|
|
36
|
+
Синхронно:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from visicom_api import VisicomClient
|
|
40
|
+
|
|
41
|
+
with VisicomClient(api_key="YOUR_API_KEY") as client:
|
|
42
|
+
result = client.geocode(text="Київ, Хрещатик, 26")
|
|
43
|
+
print(result)
|
|
44
|
+
|
|
45
|
+
route = client.distance((30.42612, 50.45111), (30.44946, 50.45682))
|
|
46
|
+
print(route["distance"])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Асинхронно:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import asyncio
|
|
53
|
+
from visicom_api import AsyncVisicomClient
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def main() -> None:
|
|
57
|
+
async with AsyncVisicomClient(api_key="YOUR_API_KEY") as client:
|
|
58
|
+
result = await client.geocode(categories="poi_restaurant", near=(30.5113, 50.4550), radius=300)
|
|
59
|
+
print(result)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Точки (`origin`, `near`, `waypoints`, ...) можна передавати як:
|
|
66
|
+
- ідентифікатор об'єкта: `"POIA1KIGKN"`
|
|
67
|
+
- координати: `(lng, lat)` кортеж
|
|
68
|
+
- список точок для `waypoints`/`locks`/`origins`/`destinations`/`points`: `[(lng, lat), ...]` або список id
|
|
69
|
+
- заздалегідь відформатований рядок (наприклад, WKT-геометрія)
|
|
70
|
+
|
|
71
|
+
## Обробка помилок
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from visicom_api import VisicomAuthError, VisicomRateLimitError, VisicomAPIError
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
client.geocode(text="...")
|
|
78
|
+
except VisicomAuthError:
|
|
79
|
+
... # невірний або відсутній ключ
|
|
80
|
+
except VisicomRateLimitError:
|
|
81
|
+
... # перевищено ліміт запитів
|
|
82
|
+
except VisicomAPIError as e:
|
|
83
|
+
... # інша помилка API, e.status_code
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Розробка
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
ruff check .
|
|
91
|
+
mypy src
|
|
92
|
+
pytest
|
|
93
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
visicom_api/__init__.py,sha256=wheLVj2FJ-KV_5TqJ8fhVSePhjy_XEMrtwVyQjvbXEc,1322
|
|
2
|
+
visicom_api/_base.py,sha256=ctnMWf9JgT0cEl74LRefFdQSuXiDVUqZXHKQ4VYVAR0,1832
|
|
3
|
+
visicom_api/_endpoints.py,sha256=mM1nPFnrdnQmaEXKwd3zcIhk8S7XmXsLkE52QvMYWCY,5336
|
|
4
|
+
visicom_api/async_client.py,sha256=1zBaWNsCXgC1WFb9Uxht21krp6sSTWQa3lMl9TZ6oMM,6597
|
|
5
|
+
visicom_api/client.py,sha256=QvFFvPqi2wNvTtotFC81D-zmf5NfzYzDMjEdQ73lo_s,6401
|
|
6
|
+
visicom_api/exceptions.py,sha256=ds2MP3ojH6Fvsk8QTG2t7oKZm4RMrNjf-Z9ullFXlSE,1455
|
|
7
|
+
visicom_api/models.py,sha256=k5bsKKw6a5-DuPNlP62_pPy33QsGbVUIQLgdMQQOUwI,7789
|
|
8
|
+
visicom_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
visicom_api_client-0.1.0.dist-info/METADATA,sha256=eC7sf3fgCGCDtRON0v5rSa9EXAe4QIzPiogLJK_X7n0,2804
|
|
10
|
+
visicom_api_client-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
visicom_api_client-0.1.0.dist-info/RECORD,,
|