Metadata-Version: 2.4
Name: navvo
Version: 0.1.0
Summary: Official Python client for the Navvo Web Service — geocoding, search, places, routing, traffic, weather, EV & transit.
Author-email: Navvo <developers@navvo.io>
License: MIT
Project-URL: Homepage, https://navvo.io
Project-URL: Documentation, https://docs.navvo.io
Project-URL: API Reference, https://docs.navvo.io/en/api-reference
Project-URL: Source, https://github.com/navvo/navvo-python
Keywords: navvo,maps,geocoding,routing,places,directions,traffic,isochrone,optimization,ev,transit,jordan,mena
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: httpx
Requires-Dist: httpx>=0.23; extra == "httpx"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: wheel; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# Navvo Python SDK

Official Python client for the [Navvo](https://navvo.io) Web Service — geocoding,
search, places, routing, traffic, incidents, weather, EV charging, and transit.

It is a **pure server-side** client: typed, retry-aware, and dependency-free. By
default it uses only the Python standard library (`urllib`), so it installs and
runs anywhere; if [`httpx`](https://www.python-httpx.org) is installed it is used
automatically for connection pooling. There is **no map UI or tile fetching** —
for rendering a map use Navvo Maps in the browser.

- API reference: <https://docs.navvo.io/en/api-reference>
- Base URL: `https://navvo.io/api/v1`
- Python 3.8+

## Install

```bash
pip install navvo
# optional pooled HTTP transport:
pip install "navvo[httpx]"
```

## Authentication

Every call is authenticated with an API key of the form `nvvo_<48 hex>`, sent as
the `x-api-key` header. The SDK never puts the key in a URL — **except** for
`style_url()`, which builds a browser-fetchable map style URL where the key must
ride as `?api_key=` (browsers can't set headers on tile/style requests).

The key is read from `NAVVO_API_KEY` by default, or pass it explicitly:

```python
from navvo import NavvoClient

navvo = NavvoClient()                         # uses $NAVVO_API_KEY
navvo = NavvoClient("nvvo_your_key_here")     # explicit
```

Keys carry one or more of 11 **scopes** (`search`, `geocode`, `places`,
`routing`, `traffic`, `telemetry`, `tiles`, `imagery`, `geo`, `insights`,
`optimize`). A missing scope surfaces as a `NavvoForbiddenError`.

## Quickstart

```python
from navvo import NavvoClient, LatLng

with NavvoClient() as navvo:
    # Forward geocode (string coords are parsed to floats for you)
    hits = navvo.geocode("Abdali Hospital, Amman", limit=5)
    print(hits[0].lat, hits[0].lng, hits[0].display_name)

    # Plan a route (you pass lat/lng; the SDK serialises lon/lat on the wire)
    plan = navvo.route(locations=[
        {"lat": 31.95, "lng": 35.91, "name": "A"},
        {"lat": 31.99, "lng": 35.95, "name": "B"},
    ], costing="auto")
    print("ETA seconds:", plan.trip["summary"]["time"])
    path = plan.geometry()          # decoded polyline -> list[LatLng]
```

## Coordinates — you never get them backwards

You always supply points as `LatLng(lat=..., lng=...)` (or a `{"lat":.., "lng":..}`
dict / `(lat, lng)` tuple). The SDK owns every wire-format difference for you:

| Surface | Wire format |
| --- | --- |
| `route()`, `traffic_route()` | `{lon, lat, name?}` objects |
| `simple_route()`/`matrix()`/`isochrone()`/`map_match()`/`optimize()` payloads | `{lat, lon}` |
| reverse geocode | separate `lat` & `lon` query params |
| `bbox` params | `west,south,east,north` (use `BBox(west, south, east, north)`) |
| GeoJSON outputs | `[lng, lat]` |
| place `center` / entrances | WKT `POINT(lng lat)` (use `place.center_latlng`) |

```python
from navvo import LatLng, BBox, parse_wkt_point

amman = LatLng(lat=31.9539, lng=35.9106)
box = BBox(west=35.7, south=31.8, east=36.1, north=32.1)
center = parse_wkt_point("POINT(35.913 31.953)")   # -> LatLng(lat=31.953, lng=35.913)
```

## Configuration

```python
from navvo import NavvoClient, RetryConfig

navvo = NavvoClient(
    api_key="nvvo_...",
    base_url="https://navvo.io/api/v1",   # override for staging/self-hosted
    timeout=15.0,                          # per-attempt seconds
    retry=RetryConfig(retries=3, base_delay=0.5, factor=2.0,
                      max_delay=20.0, jitter=True, respect_retry_after=True),
    user_agent_suffix="acme-dispatch/2.3",
    default_headers={"x-trace-id": "..."},
)
```

Transient failures — `5xx`, `429`/quota, network blips, and envelope
`available:false` (when `retry_unavailable=True`) — are retried with exponential
backoff and full jitter, honouring `Retry-After`. Client errors (`400`/`401`/
`403`/`404`) are **never** retried.

## Error handling

All errors subclass `NavvoError`:

```python
from navvo.errors import (
    NavvoRateLimitError, NavvoForbiddenError, NavvoServiceUnavailableError,
)

try:
    r = navvo.geocode("Abdali Hospital, Amman")
except NavvoRateLimitError as e:
    print("rate limited, retry after", e.retry_after, "s")
except NavvoForbiddenError as e:
    print("scope/quota problem:", e.reason or e.message)   # e.scope when known
except NavvoServiceUnavailableError:
    print("geocoder upstream is down — fall back")
```

| Exception | Maps from |
| --- | --- |
| `NavvoConfigError` | bad/missing key or option (client-side) |
| `NavvoValidationError` | `400` / `422` |
| `NavvoAuthError` | `401` (no/unknown key) |
| `NavvoForbiddenError` | `403` (revoked key, missing scope, quota) |
| `NavvoRateLimitError` | `429` |
| `NavvoNotFoundError` | true `404` |
| `NavvoServiceUnavailableError` | envelope `available:false`, `502`/`503` |
| `NavvoServerError` | other `5xx` |
| `NavvoTimeoutError` | per-attempt timeout, retries exhausted |
| `NavvoNetworkError` | connection failure |

The geocoder/routing **envelope** (`{provider, available, data}`) is unwrapped
for you. `place(slug)` is a **soft-null getter**: an unknown slug answers with a
true `404`, which the SDK catches and returns as `None` (not a raised error), so
you can simply check `if place is None`.

## Method reference

### Tiles & map config
- `styles()` — `GET /tiles/styles`
- `style_url(style="standard", version=None, terrain=None)` — builds a browser style URL with `?api_key=`
- `map_config()` — `GET /map/config`

### Search & places
- `search(q, limit=, lang=, page=, bbox=)` — `GET /search`
  ```python
  s = navvo.search("coffee in Abdoun", limit=10)
  for item in s["places"]["items"]: print(item["nameEn"])
  ```
- `geocode(q, limit=, locale=, near=, bbox=)` — `GET /geocode/search` → `list[GeocodeResult]`
- `reverse_geocode(lat, lon)` — `GET /geocode/reverse` → `GeocodeResult | None`
- `geocode_batch(queries, locale=, bbox=)` — `POST /geocode/batch` (auto-chunked ≤50)
- `reverse_geocode_batch(points)` — `POST /geocode/reverse-batch` (auto-chunked ≤50)
- `places(q=, page=, limit=, category=, amenities=, open_now=, sort=, min_rating=, near=, radius_km=, sponsored=)` — `GET /places` → `PlacePage`
- `iter_places(limit=50, **filters)` — auto-paginate every matching place
  ```python
  for p in navvo.iter_places(category="restaurants", limit=100):
      print(p.name_en)
  ```
- `place(slug)` — `GET /places/{slug}` → `Place | None` (soft-null: an unknown slug returns `None`, never raises)
  ```python
  place = navvo.place("jordan-beans-cafe")
  if place is None:
      ...                                              # unknown slug
  dest = place.route_target() or place.center_latlng   # route to the entrance
  ```
- `place_thumb(slug)` — `GET /places/thumb`
- `place_categories()` — `GET /places/categories`
- `place_facets(q=)` — `GET /places/facets`
- `place_hours(slug, at=)` — `GET /places/{slug}/hours`
- `place_popular_times(slug)` — `GET /places/{slug}/popular-times`
- `place_attributes(slug)` — `GET /places/{slug}/attributes`
- `place_reviews_summary(slug)` — `GET /places/{slug}/reviews-summary`
- `place_photos(slug, page=, limit=)` — `GET /places/{slug}/photos`
- `place_nearby(slug, radius_km=, same_category=, limit=)` — `GET /places/{slug}/nearby`
- `places_reachable(origin, minutes=, costing=, q=, categories=, amenities=, limit=)` — `POST /places/reachable`

### Routing
- `route(locations=[...], costing=, preference=, avoid=, optimize=, avoid_incidents=, avoid_reported=, disregard=, constraints_text=, include_labels=, language=, vehicle=, truck=, departure_time=)` — `POST /routing/plan` → `RoutePlan`
- `simple_route(payload)` — `POST /routing/route` (raw engine passthrough)
- `matrix(payload)` — `POST /routing/matrix` → `Matrix` with `.cell(i, j)`
  ```python
  m = navvo.matrix({"sources": [{"lat":31.95,"lon":35.91}],
                    "targets": [{"lat":31.9,"lon":35.93}], "costing": "auto"})
  print(m.cell(0, 0)["time"])
  ```
- `isochrone(payload)` — `POST /routing/isochrone` (GeoJSON)
- `map_match(payload)` — `POST /routing/map-match`
- `optimize(payload)` — `POST /routing/optimize` (scope `optimize`)
- `routing_profiles()` — `GET /routing/profiles`

### Traffic & incidents
- `traffic_route(locations=[...], costing=, departure_time=, language=)` — `POST /traffic/route` (scopes `routing`+`traffic`)
- `traffic_summary(bbox=)` — `GET /traffic/summary`
- `incidents(bbox=, type=, limit=, zoom=)` — `GET /incidents/geojson`
- `incident_types()` — `GET /incidents/types`
- `report_incident(type=, lat=, lng=, title=, description=, severity=, ttl_minutes=, dedup_meters=)` — `POST /incidents/report`
- `road_edits(bbox=)` — `GET /road-edits/geojson`
- `route_risk(geometry, route_id=, time_offset=)` — `POST /map/route-risk`
  ```python
  risk = navvo.route_risk([[35.91,31.95],[35.93,31.97],[35.95,31.99]], time_offset="+1h")
  print(risk["scores"]["overall"], risk["severity"])
  ```

### Weather & air quality
- `weather_point(lat, lng, units=)` — `GET /map/weather/point`
- `weather_forecast(lat, lng, hours=, units=)` — `GET /map/weather/forecast`
- `weather_cities(bbox, zoom, units=, limit=)` — `GET /map/weather/cities`
- `air_quality_cities(bbox, zoom, limit=)` — `GET /map/airquality/cities`

### EV & transit
- `ev_stations(bbox, min_kw=, connector=, available=, network=, limit=)` — `GET /ev/stations`
- `transit_stops(lat, lng, radius=)` — `GET /transit/stops`
- `gbfs(lat, lng, radius=, type=, min_bikes=, min_docks=)` — `GET /transit/gbfs`

## Typed models

`geocode`, `places`, `place`, and `route` return typed objects
(`GeocodeResult`, `PlacePage`/`Place`, `RoutePlan`, `Matrix`) with the common
fields parsed and the full payload preserved on `.raw`. Methods whose shapes are
highly variable return the parsed `dict`/`list` directly so no field is ever
hidden from you.

## License

MIT © Navvo. See [LICENSE](LICENSE).
