visicom-api-client 0.1.0__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.
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install package with dev dependencies
20
+ run: pip install -e ".[dev]"
21
+ - name: Lint
22
+ run: ruff check .
23
+ - name: Type check
24
+ run: mypy src
25
+ - name: Run tests
26
+ run: pytest
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+ .coverage
@@ -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,70 @@
1
+ # visicom-api-client
2
+
3
+ Неофіційний Python-клієнт для [Visicom Data API](https://api.visicom.ua/uk/products/data-api/data-api-references) (геокодування, маршрутизація, буфери, прив'язка до доріг).
4
+
5
+ ## Встановлення
6
+
7
+ ```bash
8
+ pip install -e ".[dev]"
9
+ ```
10
+
11
+ ## Використання
12
+
13
+ Синхронно:
14
+
15
+ ```python
16
+ from visicom_api import VisicomClient
17
+
18
+ with VisicomClient(api_key="YOUR_API_KEY") as client:
19
+ result = client.geocode(text="Київ, Хрещатик, 26")
20
+ print(result)
21
+
22
+ route = client.distance((30.42612, 50.45111), (30.44946, 50.45682))
23
+ print(route["distance"])
24
+ ```
25
+
26
+ Асинхронно:
27
+
28
+ ```python
29
+ import asyncio
30
+ from visicom_api import AsyncVisicomClient
31
+
32
+
33
+ async def main() -> None:
34
+ async with AsyncVisicomClient(api_key="YOUR_API_KEY") as client:
35
+ result = await client.geocode(categories="poi_restaurant", near=(30.5113, 50.4550), radius=300)
36
+ print(result)
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ Точки (`origin`, `near`, `waypoints`, ...) можна передавати як:
43
+ - ідентифікатор об'єкта: `"POIA1KIGKN"`
44
+ - координати: `(lng, lat)` кортеж
45
+ - список точок для `waypoints`/`locks`/`origins`/`destinations`/`points`: `[(lng, lat), ...]` або список id
46
+ - заздалегідь відформатований рядок (наприклад, WKT-геометрія)
47
+
48
+ ## Обробка помилок
49
+
50
+ ```python
51
+ from visicom_api import VisicomAuthError, VisicomRateLimitError, VisicomAPIError
52
+
53
+ try:
54
+ client.geocode(text="...")
55
+ except VisicomAuthError:
56
+ ... # невірний або відсутній ключ
57
+ except VisicomRateLimitError:
58
+ ... # перевищено ліміт запитів
59
+ except VisicomAPIError as e:
60
+ ... # інша помилка API, e.status_code
61
+ ```
62
+
63
+ ## Розробка
64
+
65
+ ```bash
66
+ pip install -e ".[dev]"
67
+ ruff check .
68
+ mypy src
69
+ pytest
70
+ ```
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "visicom-api-client"
7
+ version = "0.1.0"
8
+ description = "Unofficial sync/async Python client for the Visicom Data API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Your Name" }]
13
+ keywords = ["visicom", "geocoding", "gis", "api-client"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = ["httpx>=0.24"]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=7.0",
26
+ "pytest-asyncio>=0.21",
27
+ "respx>=0.20",
28
+ "ruff>=0.4",
29
+ "mypy>=1.8",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://api.visicom.ua/uk/products/data-api/data-api-references"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/visicom_api"]
37
+
38
+ [tool.pytest.ini_options]
39
+ asyncio_mode = "auto"
40
+ testpaths = ["tests"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py39"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I", "UP", "B"]
48
+
49
+ [tool.mypy]
50
+ python_version = "3.9"
51
+ strict = true
52
+ packages = ["visicom_api"]
@@ -0,0 +1,46 @@
1
+ """Manual smoke test against the real Visicom Data API.
2
+
3
+ Not part of the pytest suite (network + real API key required). Run with:
4
+ VISICOM_API_KEY=... python scripts/smoke_test.py
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
14
+
15
+ from visicom_api import VisicomClient # noqa: E402
16
+
17
+
18
+ def main() -> None:
19
+ api_key = os.environ.get("VISICOM_API_KEY")
20
+ if not api_key:
21
+ raise SystemExit("Set VISICOM_API_KEY environment variable first")
22
+
23
+ with VisicomClient(api_key=api_key) as client:
24
+ print("=== geocode (single expected result, limit=1) ===")
25
+ geocode_result = client.geocode(text="Київ, Хрещатик, 26", limit=1)
26
+ print("type:", type(geocode_result).__name__)
27
+ print("top-level keys:", list(geocode_result.keys()))
28
+ print(json.dumps(geocode_result, ensure_ascii=False, indent=2)[:1500])
29
+
30
+ print("\n=== geocode (multiple results, no limit) ===")
31
+ many_result = client.geocode(
32
+ categories="poi_restaurant", near=(30.5113, 50.4550), radius=300
33
+ )
34
+ print("type:", type(many_result).__name__)
35
+ print("top-level keys:", list(many_result.keys()))
36
+
37
+ print("\n=== distancematrix (check for 'status' field) ===")
38
+ matrix = client.distancematrix(
39
+ origins=[(30.36277, 50.51605), (30.49667, 50.49508)],
40
+ destinations=[(30.36277, 50.51605), (30.49667, 50.49508)],
41
+ )
42
+ print(json.dumps(matrix, ensure_ascii=False, indent=2))
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -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"
@@ -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