apple-maps-api 0.1.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: apple-maps-api
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A modern Python client for the Apple Maps Server API with automatic JWT management and type safety
5
5
  Keywords: apple-maps,geocoding,search,geolocation
6
6
  Author: Michael Bianco
@@ -12,6 +12,7 @@ Requires-Dist: httpx2>=2.7.0
12
12
  Requires-Dist: funcy>=2.0
13
13
  Requires-Python: >=3.11
14
14
  Project-URL: Repository, https://github.com/iloveitaly/apple-maps-api
15
+ Project-URL: Documentation, https://iloveitaly.github.io/python-apple-maps-api/
15
16
  Description-Content-Type: text/markdown
16
17
 
17
18
  [![Release Notes](https://img.shields.io/github/release/iloveitaly/apple-maps-api)](https://github.com/iloveitaly/apple-maps-api/releases)
@@ -32,6 +33,13 @@ The goal of this library is to provide similar functionality to the radar python
32
33
  - **Resilient**: Automatic retries with exponential backoff for transient network and server errors.
33
34
  - **Modern**: Uses `httpx` for synchronous requests, with a structure that's ready for future async support.
34
35
 
36
+ ## Full-stack Example
37
+
38
+ See [`examples/full-stack/`](examples/full-stack/) for a runnable FastAPI + React/MapKit JS app that demonstrates:
39
+
40
+ - Minting MapKit JS tokens server-side (`create_mapkit_token`) and rendering an interactive map
41
+ - Address autocomplete via this client, proxied through a small FastAPI API
42
+
35
43
  ## Installation
36
44
 
37
45
  ```bash
@@ -42,7 +50,7 @@ uv add apple-maps-api
42
50
 
43
51
  ### Basic Setup
44
52
 
45
- You'll need your Team ID, Key ID, and the private key from your Apple Developer account.
53
+ You'll need your Team ID, Key ID, and the private key from your Apple Developer account. Step-by-step portal setup (Maps ID, MapKit JS key, domains/localhost): [`docs/mapkit-js-credential-setup.md`](docs/mapkit-js-credential-setup.md).
46
54
 
47
55
  ```python
48
56
  from apple_maps_api import AppleMapsClient
@@ -52,7 +60,7 @@ client = AppleMapsClient(
52
60
  key_id="YOUR_KEY_ID",
53
61
  private_key="""-----BEGIN PRIVATE KEY-----
54
62
  ...
55
- -----END PRIVATE KEY-----"""
63
+ -----END PRIVATE KEY-----""",
56
64
  )
57
65
  ```
58
66
 
@@ -73,6 +81,29 @@ from apple_maps_api import AppleMapsClient
73
81
  client = AppleMapsClient.from_env()
74
82
  ```
75
83
 
84
+ ### Generating a Long-lived Frontend Token
85
+
86
+ MapKit JS needs a signed JWT (not the Server API access token). Use `create_mapkit_token()`:
87
+
88
+ ```python
89
+ from apple_maps_api import AppleMapsClient
90
+
91
+ client = AppleMapsClient.from_env()
92
+
93
+ # default TTL is 1 hour — fine for production token endpoints
94
+ token = client.create_mapkit_token()
95
+
96
+ # long-lived token for local FE work (e.g. 30 days)
97
+ token = client.create_mapkit_token(ttl_seconds=30 * 24 * 60 * 60)
98
+ print(token)
99
+ ```
100
+
101
+ Paste the printed JWT into MapKit JS / [mapkit-react](https://github.com/Nicolapps/mapkit-react) during development so you do not need a backend token endpoint.
102
+
103
+ Optional domain lock: set `origin` on the client (or `APPLE_MAPS_ORIGIN`) so the token only works from that origin (e.g. `http://localhost:5173`). Omit `origin` for unrestricted local play.
104
+
105
+ Do not ship long-lived tokens in production frontend builds — mint short-lived tokens from your server instead (see [`examples/full-stack/`](examples/full-stack/)).
106
+
76
107
  ### Geocoding
77
108
 
78
109
  Convert an address string to coordinates:
@@ -95,7 +126,7 @@ for place in results.results:
95
126
  Convert coordinates back to a structured address:
96
127
 
97
128
  ```python
98
- results = client.reverse_geocode((37.3346, -122.0090))
129
+ results = client.reverse_geocode(lat=37.3346, lng=-122.0090)
99
130
 
100
131
  if results.results:
101
132
  print(results.results[0].formattedAddressLines)
@@ -110,8 +141,7 @@ if results.results:
110
141
  Search for points of interest near a location:
111
142
 
112
143
  ```python
113
- results = client.search("pizza", near="37.3346,-122.0090")
114
- # or: client.search("pizza", lat=37.3346, lng=-122.0090)
144
+ results = client.search("pizza", lat=37.3346, lng=-122.0090)
115
145
 
116
146
  for place in results.results:
117
147
  print(f"{place.name} - {place.formattedAddressLines}")
@@ -127,7 +157,12 @@ for place in results.results:
127
157
  Provide search completions for a partial query and filter by country:
128
158
 
129
159
  ```python
130
- results = client.autocomplete("1 Apple Park", limit_to_countries="US", lat=37.3346, lng=-122.0090)
160
+ results = client.autocomplete(
161
+ "1 Apple Park",
162
+ limit_to_countries=["US"],
163
+ lat=37.3346,
164
+ lng=-122.0090,
165
+ )
131
166
 
132
167
  for completion in results.results:
133
168
  print(completion.displayLines)
@@ -137,6 +172,47 @@ for completion in results.results:
137
172
  # results.results[0].completionUrl => "/v1/search?q=1%20Apple%20Park%20Way..."
138
173
  ```
139
174
 
175
+ Multi-value filters (countries, categories, result types, …) accept a list; a single string still works.
176
+
177
+ ### Importance of Lat/Lng Hinting for Result Relevance
178
+
179
+ Always pass `lat`/`lng`. Autocomplete ranking is heavily biased by location — without it you get scattered results across the country for common street names. For server-side autocomplete, resolve the user's approximate location from their IP:
180
+
181
+ 1. Get the client public IP with [fastapi-ipware](https://github.com/iloveitaly/fastapi-ipware)
182
+ 2. Look up lat/lng via [observabilitystack/geoip-api](https://github.com/observabilitystack/geoip-api) (`ghcr.io/observabilitystack/geoip-api:latest`)
183
+
184
+ ### Location Bias Options
185
+
186
+ Search, autocomplete, and geocode share the same location-hint shape:
187
+
188
+ - **`lat` / `lng`** — app-defined search bias (Apple `searchLocation`). Prefer nearby results around this map point.
189
+ - **`user_lat` / `user_lng`** — the user's current position (Apple `userLocation`). Used for ranking; if `lat`/`lng` are omitted, some endpoints may fall back to this.
190
+ - **`search_region`** — bounding-box hint as a `MapRegion` (Apple `searchRegion`).
191
+ - **`search_region_priority`** — `SearchRegionPriority.default` or `.required` (or the strings `"default"` / `"required"`).
192
+
193
+ ```python
194
+ from apple_maps_api import MapRegion, SearchRegionPriority
195
+
196
+ region = MapRegion(
197
+ northLatitude=37.5,
198
+ eastLongitude=-121.7,
199
+ southLatitude=37.1,
200
+ westLongitude=-122.5,
201
+ )
202
+
203
+ results = client.search(
204
+ "coffee",
205
+ lat=37.3346,
206
+ lng=-122.0090,
207
+ user_lat=37.3317,
208
+ user_lng=-122.0307,
209
+ search_region=region,
210
+ search_region_priority=SearchRegionPriority.required,
211
+ categories=["Cafe"],
212
+ limit_to_countries=["US"],
213
+ )
214
+ ```
215
+
140
216
  ### Helper Functions
141
217
 
142
218
  The library includes high-level helpers for common geocoding operations:
@@ -207,7 +283,7 @@ result = geocode_coordinates(client, lat=40.7589, lon=-73.9851)
207
283
  Apple Park, Cupertino CA (`37.3346, -122.0090`):
208
284
 
209
285
  ```python
210
- place = client.reverse_geocode((37.3346, -122.0090)).results[0]
286
+ place = client.reverse_geocode(lat=37.3346, lng=-122.0090).results[0]
211
287
  # place.name => "Apple Park"
212
288
  # place.formattedAddressLines => ["Apple Park", "1 Apple Park Way", "Cupertino, CA 95014", "United States"]
213
289
  # place.structuredAddress.administrativeArea => "California"
@@ -226,7 +302,7 @@ place = client.reverse_geocode((37.3346, -122.0090)).results[0]
226
302
  Times Square, NYC (`40.7589, -73.9851`):
227
303
 
228
304
  ```python
229
- place = client.reverse_geocode((40.7589, -73.9851)).results[0]
305
+ place = client.reverse_geocode(lat=40.7589, lng=-73.9851).results[0]
230
306
  # place.name => "Times Square"
231
307
  # place.formattedAddressLines => ["Times Square", "1552–1568 Broadway", "New York, NY 10036", "United States"]
232
308
  # place.structuredAddress.administrativeArea => "New York"
@@ -245,7 +321,7 @@ place = client.reverse_geocode((40.7589, -73.9851)).results[0]
245
321
  Beverly Hills, CA (`34.1025226, -118.4167959`) — note `locality` vs formatted address mismatch:
246
322
 
247
323
  ```python
248
- place = client.reverse_geocode((34.1025226, -118.4167959)).results[0]
324
+ place = client.reverse_geocode(lat=34.1025226, lng=-118.4167959).results[0]
249
325
  # place.name => "1731 N Franklin Canyon Dr"
250
326
  # place.formattedAddressLines => ["1731 N Franklin Canyon Dr", "Beverly Hills, CA 90210", "United States"]
251
327
  # place.structuredAddress.administrativeArea => "California"
@@ -261,6 +337,11 @@ place = client.reverse_geocode((34.1025226, -118.4167959)).results[0]
261
337
  # place.structuredAddress.dependentLocalities => ["Beverly Crest"]
262
338
  ```
263
339
 
340
+ ## Related Projects
341
+
342
+ - [fastapi-ipware](https://github.com/iloveitaly/fastapi-ipware) — client IP extraction for FastAPI (useful for lat/lng hinting via GeoIP)
343
+ - [mapkit-react](https://github.com/Nicolapps/mapkit-react) — React bindings for MapKit JS (used in the full-stack example)
344
+
264
345
  ## [MIT License](LICENSE.md)
265
346
 
266
347
  ---
@@ -16,6 +16,13 @@ The goal of this library is to provide similar functionality to the radar python
16
16
  - **Resilient**: Automatic retries with exponential backoff for transient network and server errors.
17
17
  - **Modern**: Uses `httpx` for synchronous requests, with a structure that's ready for future async support.
18
18
 
19
+ ## Full-stack Example
20
+
21
+ See [`examples/full-stack/`](examples/full-stack/) for a runnable FastAPI + React/MapKit JS app that demonstrates:
22
+
23
+ - Minting MapKit JS tokens server-side (`create_mapkit_token`) and rendering an interactive map
24
+ - Address autocomplete via this client, proxied through a small FastAPI API
25
+
19
26
  ## Installation
20
27
 
21
28
  ```bash
@@ -26,7 +33,7 @@ uv add apple-maps-api
26
33
 
27
34
  ### Basic Setup
28
35
 
29
- You'll need your Team ID, Key ID, and the private key from your Apple Developer account.
36
+ You'll need your Team ID, Key ID, and the private key from your Apple Developer account. Step-by-step portal setup (Maps ID, MapKit JS key, domains/localhost): [`docs/mapkit-js-credential-setup.md`](docs/mapkit-js-credential-setup.md).
30
37
 
31
38
  ```python
32
39
  from apple_maps_api import AppleMapsClient
@@ -36,7 +43,7 @@ client = AppleMapsClient(
36
43
  key_id="YOUR_KEY_ID",
37
44
  private_key="""-----BEGIN PRIVATE KEY-----
38
45
  ...
39
- -----END PRIVATE KEY-----"""
46
+ -----END PRIVATE KEY-----""",
40
47
  )
41
48
  ```
42
49
 
@@ -57,6 +64,29 @@ from apple_maps_api import AppleMapsClient
57
64
  client = AppleMapsClient.from_env()
58
65
  ```
59
66
 
67
+ ### Generating a Long-lived Frontend Token
68
+
69
+ MapKit JS needs a signed JWT (not the Server API access token). Use `create_mapkit_token()`:
70
+
71
+ ```python
72
+ from apple_maps_api import AppleMapsClient
73
+
74
+ client = AppleMapsClient.from_env()
75
+
76
+ # default TTL is 1 hour — fine for production token endpoints
77
+ token = client.create_mapkit_token()
78
+
79
+ # long-lived token for local FE work (e.g. 30 days)
80
+ token = client.create_mapkit_token(ttl_seconds=30 * 24 * 60 * 60)
81
+ print(token)
82
+ ```
83
+
84
+ Paste the printed JWT into MapKit JS / [mapkit-react](https://github.com/Nicolapps/mapkit-react) during development so you do not need a backend token endpoint.
85
+
86
+ Optional domain lock: set `origin` on the client (or `APPLE_MAPS_ORIGIN`) so the token only works from that origin (e.g. `http://localhost:5173`). Omit `origin` for unrestricted local play.
87
+
88
+ Do not ship long-lived tokens in production frontend builds — mint short-lived tokens from your server instead (see [`examples/full-stack/`](examples/full-stack/)).
89
+
60
90
  ### Geocoding
61
91
 
62
92
  Convert an address string to coordinates:
@@ -79,7 +109,7 @@ for place in results.results:
79
109
  Convert coordinates back to a structured address:
80
110
 
81
111
  ```python
82
- results = client.reverse_geocode((37.3346, -122.0090))
112
+ results = client.reverse_geocode(lat=37.3346, lng=-122.0090)
83
113
 
84
114
  if results.results:
85
115
  print(results.results[0].formattedAddressLines)
@@ -94,8 +124,7 @@ if results.results:
94
124
  Search for points of interest near a location:
95
125
 
96
126
  ```python
97
- results = client.search("pizza", near="37.3346,-122.0090")
98
- # or: client.search("pizza", lat=37.3346, lng=-122.0090)
127
+ results = client.search("pizza", lat=37.3346, lng=-122.0090)
99
128
 
100
129
  for place in results.results:
101
130
  print(f"{place.name} - {place.formattedAddressLines}")
@@ -111,7 +140,12 @@ for place in results.results:
111
140
  Provide search completions for a partial query and filter by country:
112
141
 
113
142
  ```python
114
- results = client.autocomplete("1 Apple Park", limit_to_countries="US", lat=37.3346, lng=-122.0090)
143
+ results = client.autocomplete(
144
+ "1 Apple Park",
145
+ limit_to_countries=["US"],
146
+ lat=37.3346,
147
+ lng=-122.0090,
148
+ )
115
149
 
116
150
  for completion in results.results:
117
151
  print(completion.displayLines)
@@ -121,6 +155,47 @@ for completion in results.results:
121
155
  # results.results[0].completionUrl => "/v1/search?q=1%20Apple%20Park%20Way..."
122
156
  ```
123
157
 
158
+ Multi-value filters (countries, categories, result types, …) accept a list; a single string still works.
159
+
160
+ ### Importance of Lat/Lng Hinting for Result Relevance
161
+
162
+ Always pass `lat`/`lng`. Autocomplete ranking is heavily biased by location — without it you get scattered results across the country for common street names. For server-side autocomplete, resolve the user's approximate location from their IP:
163
+
164
+ 1. Get the client public IP with [fastapi-ipware](https://github.com/iloveitaly/fastapi-ipware)
165
+ 2. Look up lat/lng via [observabilitystack/geoip-api](https://github.com/observabilitystack/geoip-api) (`ghcr.io/observabilitystack/geoip-api:latest`)
166
+
167
+ ### Location Bias Options
168
+
169
+ Search, autocomplete, and geocode share the same location-hint shape:
170
+
171
+ - **`lat` / `lng`** — app-defined search bias (Apple `searchLocation`). Prefer nearby results around this map point.
172
+ - **`user_lat` / `user_lng`** — the user's current position (Apple `userLocation`). Used for ranking; if `lat`/`lng` are omitted, some endpoints may fall back to this.
173
+ - **`search_region`** — bounding-box hint as a `MapRegion` (Apple `searchRegion`).
174
+ - **`search_region_priority`** — `SearchRegionPriority.default` or `.required` (or the strings `"default"` / `"required"`).
175
+
176
+ ```python
177
+ from apple_maps_api import MapRegion, SearchRegionPriority
178
+
179
+ region = MapRegion(
180
+ northLatitude=37.5,
181
+ eastLongitude=-121.7,
182
+ southLatitude=37.1,
183
+ westLongitude=-122.5,
184
+ )
185
+
186
+ results = client.search(
187
+ "coffee",
188
+ lat=37.3346,
189
+ lng=-122.0090,
190
+ user_lat=37.3317,
191
+ user_lng=-122.0307,
192
+ search_region=region,
193
+ search_region_priority=SearchRegionPriority.required,
194
+ categories=["Cafe"],
195
+ limit_to_countries=["US"],
196
+ )
197
+ ```
198
+
124
199
  ### Helper Functions
125
200
 
126
201
  The library includes high-level helpers for common geocoding operations:
@@ -191,7 +266,7 @@ result = geocode_coordinates(client, lat=40.7589, lon=-73.9851)
191
266
  Apple Park, Cupertino CA (`37.3346, -122.0090`):
192
267
 
193
268
  ```python
194
- place = client.reverse_geocode((37.3346, -122.0090)).results[0]
269
+ place = client.reverse_geocode(lat=37.3346, lng=-122.0090).results[0]
195
270
  # place.name => "Apple Park"
196
271
  # place.formattedAddressLines => ["Apple Park", "1 Apple Park Way", "Cupertino, CA 95014", "United States"]
197
272
  # place.structuredAddress.administrativeArea => "California"
@@ -210,7 +285,7 @@ place = client.reverse_geocode((37.3346, -122.0090)).results[0]
210
285
  Times Square, NYC (`40.7589, -73.9851`):
211
286
 
212
287
  ```python
213
- place = client.reverse_geocode((40.7589, -73.9851)).results[0]
288
+ place = client.reverse_geocode(lat=40.7589, lng=-73.9851).results[0]
214
289
  # place.name => "Times Square"
215
290
  # place.formattedAddressLines => ["Times Square", "1552–1568 Broadway", "New York, NY 10036", "United States"]
216
291
  # place.structuredAddress.administrativeArea => "New York"
@@ -229,7 +304,7 @@ place = client.reverse_geocode((40.7589, -73.9851)).results[0]
229
304
  Beverly Hills, CA (`34.1025226, -118.4167959`) — note `locality` vs formatted address mismatch:
230
305
 
231
306
  ```python
232
- place = client.reverse_geocode((34.1025226, -118.4167959)).results[0]
307
+ place = client.reverse_geocode(lat=34.1025226, lng=-118.4167959).results[0]
233
308
  # place.name => "1731 N Franklin Canyon Dr"
234
309
  # place.formattedAddressLines => ["1731 N Franklin Canyon Dr", "Beverly Hills, CA 90210", "United States"]
235
310
  # place.structuredAddress.administrativeArea => "California"
@@ -245,6 +320,11 @@ place = client.reverse_geocode((34.1025226, -118.4167959)).results[0]
245
320
  # place.structuredAddress.dependentLocalities => ["Beverly Crest"]
246
321
  ```
247
322
 
323
+ ## Related Projects
324
+
325
+ - [fastapi-ipware](https://github.com/iloveitaly/fastapi-ipware) — client IP extraction for FastAPI (useful for lat/lng hinting via GeoIP)
326
+ - [mapkit-react](https://github.com/Nicolapps/mapkit-react) — React bindings for MapKit JS (used in the full-stack example)
327
+
248
328
  ## [MIT License](LICENSE.md)
249
329
 
250
330
  ---
@@ -18,6 +18,7 @@ from .models import (
18
18
  SearchACResultType,
19
19
  SearchAutocompleteResponse,
20
20
  SearchPlace,
21
+ SearchRegionPriority,
21
22
  SearchResponse,
22
23
  SearchResultType,
23
24
  StructuredAddress,
@@ -26,11 +27,8 @@ from .models import (
26
27
  from .version import __version__
27
28
 
28
29
  __all__ = [
29
- "__version__",
30
- "AppleMapsClient",
31
- "geocode_coordinates",
32
- "geocode_postal_code",
33
30
  "AddressCategory",
31
+ "AppleMapsClient",
34
32
  "AutocompleteResult",
35
33
  "GeocodeResult",
36
34
  "Location",
@@ -41,8 +39,12 @@ __all__ = [
41
39
  "SearchACResultType",
42
40
  "SearchAutocompleteResponse",
43
41
  "SearchPlace",
42
+ "SearchRegionPriority",
44
43
  "SearchResponse",
45
44
  "SearchResultType",
46
45
  "StructuredAddress",
47
46
  "TokenResponse",
47
+ "__version__",
48
+ "geocode_coordinates",
49
+ "geocode_postal_code",
48
50
  ]
@@ -10,7 +10,7 @@ import logging
10
10
  import os
11
11
  import time
12
12
  from collections.abc import Sequence
13
- from typing import TypedDict, Unpack
13
+ from typing import Required, TypedDict, Unpack, overload
14
14
  from urllib.parse import parse_qs, urlparse
15
15
 
16
16
  import funcy as f
@@ -27,11 +27,12 @@ from tenacity import (
27
27
  from apple_maps_api.models import (
28
28
  AddressCategory,
29
29
  AutocompleteResult,
30
- Location,
30
+ MapRegion,
31
31
  PlaceResults,
32
32
  PoiCategory,
33
33
  SearchACResultType,
34
34
  SearchAutocompleteResponse,
35
+ SearchRegionPriority,
35
36
  SearchResponse,
36
37
  SearchResultType,
37
38
  TokenResponse,
@@ -41,7 +42,7 @@ log = logging.getLogger(__name__)
41
42
 
42
43
 
43
44
  def _csv(value: str | Sequence | None) -> str | None:
44
- """Serialize a plain string or a sequence of StrEnum values to a comma-separated string."""
45
+ """Serialize a string or sequence of values to Apple's comma-separated form."""
45
46
  if value is None:
46
47
  return None
47
48
  if isinstance(value, str):
@@ -49,17 +50,12 @@ def _csv(value: str | Sequence | None) -> str | None:
49
50
  return ",".join(str(v) for v in value)
50
51
 
51
52
 
52
- def _search_location(
53
+ def _lat_lng_to_apple_str(
53
54
  *,
54
- near: str | None = None,
55
55
  lat: float | None = None,
56
56
  lng: float | None = None,
57
57
  ) -> str | None:
58
- """Resolve searchLocation from near="lat,lng" or separate lat/lng floats."""
59
- if near is not None:
60
- assert lat is None and lng is None, "pass near or lat/lng, not both"
61
- return near
62
-
58
+ """Serialize an optional lat/lng pair to Apple's 'lat,lng' string form."""
63
59
  if lat is None and lng is None:
64
60
  return None
65
61
 
@@ -67,6 +63,17 @@ def _search_location(
67
63
  return f"{lat},{lng}"
68
64
 
69
65
 
66
+ def _search_region(region: MapRegion | None) -> str | None:
67
+ """Serialize MapRegion to Apple's searchRegion form: northLat,eastLng,southLat,westLng."""
68
+ if region is None:
69
+ return None
70
+
71
+ return (
72
+ f"{region.northLatitude},{region.eastLongitude},"
73
+ f"{region.southLatitude},{region.westLongitude}"
74
+ )
75
+
76
+
70
77
  # shared timeout: 10s connect, 30s read
71
78
  _TIMEOUT = httpx2.Timeout(connect=10, read=30, write=10, pool=10)
72
79
 
@@ -103,50 +110,119 @@ _retry_policy = retry(
103
110
  )
104
111
 
105
112
 
106
- class GeocodeOptions(TypedDict, total=False):
107
- limit_to_countries: str
108
- lang: str
109
- search_location: str
110
- search_region: str
111
- user_location: str
113
+ class _GeocodeOptionsBase(TypedDict, total=False):
114
+ """Shared optional kwargs for :meth:`AppleMapsClient.geocode`.
112
115
 
116
+ Location params (all optional; different roles):
117
+
118
+ - ``lat`` / ``lng``: app-defined point hint; sent as Apple's
119
+ ``searchLocation`` (must pass both). Primary geographic bias.
120
+ - ``search_region``: app-defined bounding-box hint as :class:`MapRegion`.
121
+ - ``user_lat`` / ``user_lng``: the *user's* current position (must pass both).
122
+ Used for ranking/relevance; if ``lat``/``lng`` are omitted, some Apple
123
+ endpoints may fall back to user position as the search hint.
124
+ """
113
125
 
114
- class ReverseGeocodeOptions(TypedDict, total=False):
126
+ limit_to_countries: str | Sequence[str]
115
127
  lang: str
128
+ search_region: MapRegion
129
+ user_lat: float
130
+ user_lng: float
116
131
 
117
132
 
118
- class SearchOptions(TypedDict, total=False):
119
- near: str
133
+ class GeocodeOptionsLatLng(_GeocodeOptionsBase, total=False):
134
+ """Geocode options with required ``lat`` and ``lng`` location bias."""
135
+
136
+ lat: Required[float]
137
+ lng: Required[float]
138
+
139
+
140
+ class GeocodeOptions(_GeocodeOptionsBase, total=False):
141
+ """All optional kwargs for :meth:`AppleMapsClient.geocode`."""
142
+
120
143
  lat: float
121
144
  lng: float
122
- categories: str | Sequence[PoiCategory]
123
- exclude_categories: str | Sequence[PoiCategory]
124
- limit_to_countries: str
145
+
146
+
147
+ # Location bias is optional; when set both lat and lng are required.
148
+ # Overloads below enforce that for pyright; runtime asserts in _lat_lng_to_apple_str.
149
+ class _SearchOptionsBase(TypedDict, total=False):
150
+ """Shared optional kwargs for :meth:`AppleMapsClient.search`.
151
+
152
+ Location params (all optional; different roles):
153
+
154
+ - ``lat`` / ``lng``: app-defined point hint; sent as Apple's
155
+ ``searchLocation`` (must pass both). Primary geographic bias —
156
+ "search near this map point".
157
+ - ``search_region``: app-defined bounding-box hint as :class:`MapRegion`.
158
+ - ``user_lat`` / ``user_lng``: the *user's* current position (must pass both).
159
+ Used for ranking/relevance; Search may fall back to it as
160
+ ``searchLocation`` when ``lat``/``lng`` are omitted.
161
+ """
162
+
163
+ categories: str | Sequence[str | PoiCategory]
164
+ exclude_categories: str | Sequence[str | PoiCategory]
165
+ limit_to_countries: str | Sequence[str]
125
166
  lang: str
126
- result_type_filter: str | Sequence[SearchResultType]
127
- search_region: str
128
- user_location: str
129
- search_region_priority: str
167
+ result_type_filter: str | Sequence[str | SearchResultType]
168
+ search_region: MapRegion
169
+ user_lat: float
170
+ user_lng: float
171
+ search_region_priority: SearchRegionPriority | str
130
172
  enable_pagination: bool
131
173
  page_token: str
132
- include_address_categories: str | Sequence[AddressCategory]
133
- exclude_address_categories: str | Sequence[AddressCategory]
134
-
135
-
136
- class AutocompleteOptions(TypedDict, total=False):
137
- near: str | None
138
- lat: float | None
139
- lng: float | None
140
- limit_to_countries: str | None
141
- lang: str | None
142
- result_type_filter: str | Sequence[SearchACResultType] | None
143
- include_poi_categories: str | Sequence[PoiCategory] | None
144
- exclude_poi_categories: str | Sequence[PoiCategory] | None
145
- search_region: str | None
146
- user_location: str | None
147
- search_region_priority: str | None
148
- include_address_categories: str | Sequence[AddressCategory] | None
149
- exclude_address_categories: str | Sequence[AddressCategory] | None
174
+ include_address_categories: str | Sequence[str | AddressCategory]
175
+ exclude_address_categories: str | Sequence[str | AddressCategory]
176
+
177
+
178
+ class SearchOptionsLatLng(_SearchOptionsBase, total=False):
179
+ """Search options with required ``lat`` and ``lng`` location bias."""
180
+
181
+ lat: Required[float]
182
+ lng: Required[float]
183
+
184
+
185
+ class SearchOptions(_SearchOptionsBase, total=False):
186
+ """All optional kwargs for :meth:`AppleMapsClient.search`."""
187
+
188
+ lat: float
189
+ lng: float
190
+
191
+
192
+ class _AutocompleteOptionsBase(TypedDict, total=False):
193
+ """Shared optional kwargs for :meth:`AppleMapsClient.autocomplete`.
194
+
195
+ Location params match search (see :class:`_SearchOptionsBase`):
196
+ ``lat``/``lng`` → ``searchLocation`` bias; ``search_region`` as MapRegion;
197
+ ``user_lat``/``user_lng`` is the user's position (ranking; may fall back as
198
+ ``searchLocation`` when lat/lng are omitted).
199
+ """
200
+
201
+ limit_to_countries: str | Sequence[str]
202
+ lang: str
203
+ result_type_filter: str | Sequence[str | SearchACResultType]
204
+ include_poi_categories: str | Sequence[str | PoiCategory]
205
+ exclude_poi_categories: str | Sequence[str | PoiCategory]
206
+ search_region: MapRegion
207
+ user_lat: float
208
+ user_lng: float
209
+ search_region_priority: SearchRegionPriority | str
210
+ include_address_categories: str | Sequence[str | AddressCategory]
211
+ exclude_address_categories: str | Sequence[str | AddressCategory]
212
+
213
+
214
+ class AutocompleteOptionsLatLng(_AutocompleteOptionsBase, total=False):
215
+ """Autocomplete options with required ``lat`` and ``lng`` location bias."""
216
+
217
+ lat: Required[float]
218
+ lng: Required[float]
219
+
220
+
221
+ class AutocompleteOptions(_AutocompleteOptionsBase, total=False):
222
+ """All optional kwargs for :meth:`AppleMapsClient.autocomplete`."""
223
+
224
+ lat: float
225
+ lng: float
150
226
 
151
227
 
152
228
  class AppleMapsClient:
@@ -355,17 +431,33 @@ class AppleMapsClient:
355
431
 
356
432
  return response.json()
357
433
 
434
+ @overload
435
+ def geocode(
436
+ self, query: str, **kwargs: Unpack[GeocodeOptionsLatLng]
437
+ ) -> PlaceResults: ...
438
+
439
+ @overload
440
+ def geocode(
441
+ self, query: str, **kwargs: Unpack[_GeocodeOptionsBase]
442
+ ) -> PlaceResults: ...
443
+
358
444
  def geocode(self, query: str, **kwargs: Unpack[GeocodeOptions]) -> PlaceResults:
359
445
  """Convert an address string to coordinates.
360
446
 
361
447
  Maps to GET /v1/geocode.
362
448
 
363
449
  :param query: Address to geocode (e.g., "1 Apple Park Way").
364
- :param limit_to_countries: Comma-separated ISO 3166-1 alpha-2 country codes to limit results.
450
+ :param limit_to_countries: ISO 3166-1 alpha-2 country codes to limit results
451
+ (e.g. ``["US", "CA"]``).
365
452
  :param lang: BCP 47 language code (default: "en-US").
366
- :param search_location: Lat/lng hint for search bias (e.g., "37.78,-122.42").
367
- :param search_region: Bounding box hint as "northLat,eastLng,southLat,westLng".
368
- :param user_location: User's current location as "lat,lng".
453
+ :param lat: Latitude for app-defined search bias (must pass with lng).
454
+ Sent as Apple's ``searchLocation``.
455
+ :param lng: Longitude for app-defined search bias (must pass with lat).
456
+ :param search_region: App-defined bounding-box hint as :class:`MapRegion`.
457
+ :param user_lat: Latitude of the user's position (must pass with user_lng).
458
+ Used for ranking/relevance; if ``lat``/``lng`` are omitted, some
459
+ endpoints may fall back to this as the search hint.
460
+ :param user_lng: Longitude of the user's position (must pass with user_lat).
369
461
  """
370
462
  if not query:
371
463
  raise ValueError("query must be provided.")
@@ -374,11 +466,17 @@ class AppleMapsClient:
374
466
  f.compact(
375
467
  {
376
468
  "q": query,
377
- "limitToCountries": kwargs.get("limit_to_countries"),
469
+ "limitToCountries": _csv(kwargs.get("limit_to_countries")),
378
470
  "lang": kwargs.get("lang"),
379
- "searchLocation": kwargs.get("search_location"),
380
- "searchRegion": kwargs.get("search_region"),
381
- "userLocation": kwargs.get("user_location"),
471
+ "searchLocation": _lat_lng_to_apple_str(
472
+ lat=kwargs.get("lat"),
473
+ lng=kwargs.get("lng"),
474
+ ),
475
+ "searchRegion": _search_region(kwargs.get("search_region")),
476
+ "userLocation": _lat_lng_to_apple_str(
477
+ lat=kwargs.get("user_lat"),
478
+ lng=kwargs.get("user_lng"),
479
+ ),
382
480
  }
383
481
  )
384
482
  )
@@ -388,70 +486,98 @@ class AppleMapsClient:
388
486
 
389
487
  def reverse_geocode(
390
488
  self,
391
- coordinates: tuple[float, float] | Location,
392
- **kwargs: Unpack[ReverseGeocodeOptions],
489
+ *,
490
+ lat: float,
491
+ lng: float,
492
+ lang: str | None = None,
393
493
  ) -> PlaceResults:
394
494
  """Convert coordinates to an address.
395
495
 
396
496
  Maps to GET /v1/reverseGeocode.
397
497
 
398
- :param coordinates: (latitude, longitude) tuple or Location object.
498
+ :param lat: Latitude of the point to reverse geocode.
499
+ :param lng: Longitude of the point to reverse geocode.
399
500
  :param lang: BCP 47 language code (default: "en-US").
400
501
  """
401
- if isinstance(coordinates, Location):
402
- loc_str = f"{coordinates.latitude},{coordinates.longitude}"
403
- else:
404
- loc_str = f"{coordinates[0]},{coordinates[1]}"
405
-
406
- params: dict[str, str] = dict(
407
- f.compact({"loc": loc_str, "lang": kwargs.get("lang")})
408
- )
502
+ params: dict[str, str] = dict(f.compact({"loc": f"{lat},{lng}", "lang": lang}))
409
503
 
410
504
  raw = self._make_request("/v1/reverseGeocode", params)
411
505
  return PlaceResults.model_validate(raw)
412
506
 
413
- def search(self, query: str, **kwargs: Unpack[SearchOptions]) -> SearchResponse:
507
+ @overload
508
+ def search(
509
+ self, query: str, **kwargs: Unpack[SearchOptionsLatLng]
510
+ ) -> SearchResponse: ...
511
+
512
+ @overload
513
+ def search(
514
+ self, query: str = "", **kwargs: Unpack[_SearchOptionsBase]
515
+ ) -> SearchResponse: ...
516
+
517
+ def search(
518
+ self, query: str = "", **kwargs: Unpack[SearchOptions]
519
+ ) -> SearchResponse:
414
520
  """Search for places by name or category.
415
521
 
416
522
  Maps to GET /v1/search.
417
523
 
418
- :param query: Search query (e.g., "coffee", "Apple Park").
419
- :param near: Location bias as "lat,lng" (maps to searchLocation).
420
- Mutually exclusive with lat/lng.
421
- :param lat: Latitude for location bias (requires lng).
422
- :param lng: Longitude for location bias (requires lat).
423
- :param categories: Comma-separated POI categories to include (e.g., "MovieTheater").
424
- :param exclude_categories: Comma-separated POI categories to exclude.
425
- :param limit_to_countries: Comma-separated ISO 3166-1 alpha-2 country codes to limit results.
524
+ Location bias (optional): pass ``lat=`` and ``lng=`` together.
525
+
526
+ For page 2+, pass only ``page_token`` from a prior response's
527
+ ``paginationInfo.nextPageToken``. Apple rejects other search params
528
+ (including ``q`` and ``enablePagination``) on token requests.
529
+
530
+ :param query: Search query (e.g., "coffee", "Apple Park"). Required
531
+ unless ``page_token`` is set.
532
+ :param lat: Latitude for app-defined search bias (must pass with lng).
533
+ Sent as Apple's ``searchLocation`` — "search near this map point".
534
+ :param lng: Longitude for app-defined search bias (must pass with lat).
535
+ :param categories: POI categories to include (e.g. ``["MovieTheater", "Cafe"]``).
536
+ :param exclude_categories: POI categories to exclude (e.g. ``["Parking"]``).
537
+ :param limit_to_countries: ISO 3166-1 alpha-2 country codes
538
+ (e.g. ``["US", "CA"]``).
426
539
  :param lang: BCP 47 language code (default: "en-US").
427
- :param result_type_filter: Comma-separated result types (e.g., "Poi", "Address").
428
- :param search_region: Bounding box hint as "northLat,eastLng,southLat,westLng".
429
- :param user_location: User's current location as "lat,lng".
430
- :param search_region_priority: "default" or "required".
431
- :param enable_pagination: Request paginated results.
432
- :param page_token: Token identifying which page of results to return.
433
- :param include_address_categories: Comma-separated AddressCategory values to include.
434
- :param exclude_address_categories: Comma-separated AddressCategory values to exclude.
540
+ :param result_type_filter: Result types (e.g. ``["Poi", "Address"]``).
541
+ :param search_region: App-defined bounding-box hint as :class:`MapRegion`.
542
+ :param user_lat: Latitude of the user's position (must pass with user_lng).
543
+ Used for ranking/relevance; Search may fall back to it as
544
+ ``searchLocation`` when ``lat``/``lng`` are omitted.
545
+ :param user_lng: Longitude of the user's position (must pass with user_lat).
546
+ :param search_region_priority: Importance of ``search_region``
547
+ (:class:`SearchRegionPriority` or ``"default"`` / ``"required"``).
548
+ :param enable_pagination: Request paginated results (first page only).
549
+ :param page_token: Token from ``paginationInfo`` for a subsequent page.
550
+ When set, sent alone — do not combine with query or other filters.
551
+ :param include_address_categories: Address categories to include
552
+ (e.g. ``["AdministrativeArea"]``).
553
+ :param exclude_address_categories: Address categories to exclude.
435
554
  """
555
+ page_token = kwargs.get("page_token")
556
+ if page_token:
557
+ # Apple: pageToken requests must not include q, enablePagination, etc.
558
+ raw = self._make_request("/v1/search", {"pageToken": page_token})
559
+ return SearchResponse.model_validate(raw)
560
+
436
561
  if not query:
437
562
  raise ValueError("query must be provided.")
438
563
 
439
564
  raw_params: dict[str, object] = {
440
565
  "q": query,
441
- "searchLocation": _search_location(
442
- near=kwargs.get("near"),
566
+ "searchLocation": _lat_lng_to_apple_str(
443
567
  lat=kwargs.get("lat"),
444
568
  lng=kwargs.get("lng"),
445
569
  ),
446
570
  "includePoiCategories": _csv(kwargs.get("categories")),
447
571
  "excludePoiCategories": _csv(kwargs.get("exclude_categories")),
448
- "limitToCountries": kwargs.get("limit_to_countries"),
572
+ "limitToCountries": _csv(kwargs.get("limit_to_countries")),
449
573
  "lang": kwargs.get("lang"),
450
574
  "resultTypeFilter": _csv(kwargs.get("result_type_filter")),
451
- "searchRegion": kwargs.get("search_region"),
452
- "userLocation": kwargs.get("user_location"),
575
+ "searchRegion": _search_region(kwargs.get("search_region")),
576
+ "userLocation": _lat_lng_to_apple_str(
577
+ lat=kwargs.get("user_lat"),
578
+ lng=kwargs.get("user_lng"),
579
+ ),
453
580
  "searchRegionPriority": kwargs.get("search_region_priority"),
454
- "pageToken": kwargs.get("page_token"),
455
581
  "includeAddressCategories": _csv(kwargs.get("include_address_categories")),
456
582
  "excludeAddressCategories": _csv(kwargs.get("exclude_address_categories")),
457
583
  }
@@ -463,6 +589,16 @@ class AppleMapsClient:
463
589
  raw = self._make_request("/v1/search", params)
464
590
  return SearchResponse.model_validate(raw)
465
591
 
592
+ @overload
593
+ def autocomplete(
594
+ self, query: str, **kwargs: Unpack[AutocompleteOptionsLatLng]
595
+ ) -> SearchAutocompleteResponse: ...
596
+
597
+ @overload
598
+ def autocomplete(
599
+ self, query: str, **kwargs: Unpack[_AutocompleteOptionsBase]
600
+ ) -> SearchAutocompleteResponse: ...
601
+
466
602
  def autocomplete(
467
603
  self, query: str, **kwargs: Unpack[AutocompleteOptions]
468
604
  ) -> SearchAutocompleteResponse:
@@ -470,25 +606,32 @@ class AppleMapsClient:
470
606
 
471
607
  Maps to GET /v1/searchAutocomplete.
472
608
 
609
+ Location bias (optional): pass ``lat=`` and ``lng=`` together.
610
+
473
611
  Result count is fixed by Apple; the API has no limit/maxResults parameter.
474
612
  For more results, use search() (supports enable_pagination) or
475
613
  search_completion() to expand a single autocomplete hit.
476
614
 
477
615
  :param query: Partial address or place name to autocomplete.
478
- :param near: Location bias as "lat,lng" to prefer nearby results.
479
- Mutually exclusive with lat/lng.
480
- :param lat: Latitude for location bias (requires lng).
481
- :param lng: Longitude for location bias (requires lat).
482
- :param limit_to_countries: Comma-separated ISO 3166-1 alpha-2 country codes to limit results.
616
+ :param lat: Latitude for app-defined search bias (must pass with lng).
617
+ Sent as Apple's ``searchLocation`` — "search near this map point".
618
+ :param lng: Longitude for app-defined search bias (must pass with lat).
619
+ :param limit_to_countries: ISO 3166-1 alpha-2 country codes
620
+ (e.g. ``["US", "CA"]``).
483
621
  :param lang: BCP 47 language code (default: "en-US").
484
- :param result_type_filter: Comma-separated result types (e.g., "Address", "Poi").
485
- :param include_poi_categories: Comma-separated POI categories to include.
486
- :param exclude_poi_categories: Comma-separated POI categories to exclude.
487
- :param search_region: Bounding box hint as "northLat,eastLng,southLat,westLng".
488
- :param user_location: User's current location as "lat,lng".
489
- :param search_region_priority: "default" or "required".
490
- :param include_address_categories: Comma-separated AddressCategory values to include.
491
- :param exclude_address_categories: Comma-separated AddressCategory values to exclude.
622
+ :param result_type_filter: Result types (e.g. ``["Address", "Poi"]``).
623
+ :param include_poi_categories: POI categories to include
624
+ (e.g. ``["Cafe"]``).
625
+ :param exclude_poi_categories: POI categories to exclude.
626
+ :param search_region: App-defined bounding-box hint as :class:`MapRegion`.
627
+ :param user_lat: Latitude of the user's position (must pass with user_lng).
628
+ Used for ranking/relevance; may fall back as ``searchLocation``
629
+ when ``lat``/``lng`` are omitted.
630
+ :param user_lng: Longitude of the user's position (must pass with user_lat).
631
+ :param search_region_priority: Importance of ``search_region``
632
+ (:class:`SearchRegionPriority` or ``"default"`` / ``"required"``).
633
+ :param include_address_categories: Address categories to include.
634
+ :param exclude_address_categories: Address categories to exclude.
492
635
  """
493
636
  if not query:
494
637
  raise ValueError("query must be provided.")
@@ -497,19 +640,21 @@ class AppleMapsClient:
497
640
  f.compact(
498
641
  {
499
642
  "q": query,
500
- "searchLocation": _search_location(
501
- near=kwargs.get("near"),
643
+ "searchLocation": _lat_lng_to_apple_str(
502
644
  lat=kwargs.get("lat"),
503
645
  lng=kwargs.get("lng"),
504
646
  ),
505
647
  # Note: Apple maps documentation says to use 'limitToCountries' param but it seems to work only with almost full address
506
- "limitToCountries": kwargs.get("limit_to_countries"),
648
+ "limitToCountries": _csv(kwargs.get("limit_to_countries")),
507
649
  "lang": kwargs.get("lang"),
508
650
  "resultTypeFilter": _csv(kwargs.get("result_type_filter")),
509
651
  "includePoiCategories": _csv(kwargs.get("include_poi_categories")),
510
652
  "excludePoiCategories": _csv(kwargs.get("exclude_poi_categories")),
511
- "searchRegion": kwargs.get("search_region"),
512
- "userLocation": kwargs.get("user_location"),
653
+ "searchRegion": _search_region(kwargs.get("search_region")),
654
+ "userLocation": _lat_lng_to_apple_str(
655
+ lat=kwargs.get("user_lat"),
656
+ lng=kwargs.get("user_lng"),
657
+ ),
513
658
  "searchRegionPriority": kwargs.get("search_region_priority"),
514
659
  "includeAddressCategories": _csv(
515
660
  kwargs.get("include_address_categories")
@@ -1,14 +1,10 @@
1
1
  class AppleMapsError(Exception):
2
2
  """Base exception for all Apple Maps API errors."""
3
3
 
4
- pass
5
-
6
4
 
7
5
  class AppleMapsAuthError(AppleMapsError):
8
6
  """Raised when authentication fails (e.g., invalid JWT, expired token)."""
9
7
 
10
- pass
11
-
12
8
 
13
9
  class AppleMapsRequestError(AppleMapsError):
14
10
  """Raised when the API returns a client error (4xx besides 429)."""
@@ -22,8 +18,6 @@ class AppleMapsRequestError(AppleMapsError):
22
18
  class AppleMapsRateLimitError(AppleMapsRequestError):
23
19
  """Raised when the API returns a 429 Too Many Requests error."""
24
20
 
25
- pass
26
-
27
21
 
28
22
  class AppleMapsServerError(AppleMapsError):
29
23
  """Raised when the API returns a 5xx server error."""
@@ -119,7 +119,7 @@ def geocode_coordinates(
119
119
  GeocodeResult with lat, lon, zip_code, city, and state information.
120
120
  Returns None if geocoding fails.
121
121
  """
122
- location_result = client.reverse_geocode((lat, lon))
122
+ location_result = client.reverse_geocode(lat=lat, lng=lon)
123
123
 
124
124
  if len(location_result.results) == 0:
125
125
  _log_geocode_event(
@@ -119,6 +119,16 @@ class SearchACResultType(StrEnum):
119
119
  query = "query"
120
120
 
121
121
 
122
+ class SearchRegionPriority(StrEnum):
123
+ """Importance of searchRegion for /v1/search and /v1/searchAutocomplete.
124
+
125
+ Apple possible values: default, required.
126
+ """
127
+
128
+ default = "default"
129
+ required = "required"
130
+
131
+
122
132
  class Location(BaseModel):
123
133
  """A coordinate pair from Apple Maps API.
124
134
 
@@ -195,10 +205,11 @@ class Place(BaseModel):
195
205
  class SearchPlace(Place):
196
206
  """Extended Place returned by /v1/search with POI category info.
197
207
 
198
- poiCategory: point-of-interest category (e.g. "Restaurant", "MovieTheater")
208
+ poiCategory: point-of-interest category (e.g. Restaurant, MovieTheater).
209
+ Unknown values from Apple fail validation so we notice and add support.
199
210
  """
200
211
 
201
- poiCategory: str | None = None
212
+ poiCategory: PoiCategory | None = None
202
213
 
203
214
 
204
215
  class PlaceResults(BaseModel):
@@ -210,15 +221,31 @@ class PlaceResults(BaseModel):
210
221
  results: list[Place]
211
222
 
212
223
 
224
+ class PaginationInfo(BaseModel):
225
+ """Pagination metadata from /v1/search when enablePagination is true.
226
+
227
+ nextPageToken / prevPageToken: opaque tokens for pageToken on the next request
228
+ totalPageCount: total pages available
229
+ totalResults: total matching results
230
+ """
231
+
232
+ nextPageToken: str | None = None
233
+ prevPageToken: str | None = None
234
+ totalPageCount: int | float | None = None
235
+ totalResults: int | float | None = None
236
+
237
+
213
238
  class SearchResponse(BaseModel):
214
239
  """Response from /v1/search.
215
240
 
216
241
  results: list of SearchPlace objects with optional POI category
217
242
  displayMapRegion: suggested map viewport encompassing all results
243
+ paginationInfo: present when the request set enablePagination
218
244
  """
219
245
 
220
246
  results: list[SearchPlace]
221
247
  displayMapRegion: MapRegion | None = None
248
+ paginationInfo: PaginationInfo | None = None
222
249
 
223
250
 
224
251
  class AutocompleteResult(BaseModel):
File without changes
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "apple-maps-api"
3
+ version = "0.2.1"
4
+ description = "A modern Python client for the Apple Maps Server API with automatic JWT management and type safety"
5
+ keywords = [
6
+ "apple-maps",
7
+ "geocoding",
8
+ "search",
9
+ "geolocation",
10
+ ]
11
+ readme = "README.md"
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "pydantic>=2.12.3",
15
+ "tenacity>=9.1.2",
16
+ "PyJWT[crypto]>=2.9.0",
17
+ "httpx2>=2.7.0",
18
+ "funcy>=2.0",
19
+ ]
20
+
21
+ [[project.authors]]
22
+ name = "Michael Bianco"
23
+ email = "mike@mikebian.co"
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/iloveitaly/apple-maps-api"
27
+ Documentation = "https://iloveitaly.github.io/python-apple-maps-api/"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.11.17,<0.13"]
31
+ build-backend = "uv_build"
32
+
33
+ [tool.uv.build-backend]
34
+ module-root = ""
35
+
36
+ [tool.pyright]
37
+ exclude = [
38
+ "examples/",
39
+ "playground/",
40
+ "tmp/",
41
+ ".venv/",
42
+ ]
43
+ reportCallIssue = "error"
44
+
45
+ [tool.pytest.ini_options]
46
+ addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
47
+
48
+ [tool.coverage.run]
49
+ plugins = ["covdefaults"]
50
+ source = ["apple_maps_api"]
51
+
52
+ [tool.coverage.report]
53
+ fail_under = 50
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "pytest>=8.3.4",
58
+ "pyright[nodejs]>=1.1.408",
59
+ "ruff>=0.15.15",
60
+ "coverage>=7.14.1",
61
+ "pytest-cov>=7.0.0",
62
+ "covdefaults>=2.3.0",
63
+ "pytest-httpx2>=1.0.0",
64
+ ]
65
+ docs = [
66
+ "sphinx>=7.1.2",
67
+ "shibuya",
68
+ "myst-parser>=2.0.0",
69
+ "sphinx-design>=0.5.0",
70
+ "sphinx-copybutton>=0.5.2",
71
+ "sphinx-autobuild",
72
+ "sphinx-autoapi",
73
+ "sphinx-llm",
74
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "apple-maps-api"
3
- version = "0.1.0"
3
+ version = "0.2.1"
4
4
  description = "A modern Python client for the Apple Maps Server API with automatic JWT management and type safety"
5
5
  keywords = ["apple-maps", "geocoding", "search", "geolocation"]
6
6
  readme = "README.md"
@@ -14,10 +14,10 @@ dependencies = [
14
14
  ]
15
15
 
16
16
  authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
17
- urls = { "Repository" = "https://github.com/iloveitaly/apple-maps-api" }
17
+ urls = { "Repository" = "https://github.com/iloveitaly/apple-maps-api", "Documentation" = "https://iloveitaly.github.io/python-apple-maps-api/" }
18
18
 
19
19
  [build-system]
20
- requires = ["uv_build>=0.11.17,<0.12"]
20
+ requires = ["uv_build>=0.11.17,<0.13"]
21
21
  build-backend = "uv_build"
22
22
 
23
23
  [tool.uv.build-backend]
@@ -34,6 +34,17 @@ dev = [
34
34
  "covdefaults>=2.3.0",
35
35
  "pytest-httpx2>=1.0.0",
36
36
  ]
37
+ # Sphinx toolchain for local/CI docs builds — not a published package extra
38
+ docs = [
39
+ "sphinx>=7.1.2",
40
+ "shibuya",
41
+ "myst-parser>=2.0.0",
42
+ "sphinx-design>=0.5.0",
43
+ "sphinx-copybutton>=0.5.2",
44
+ "sphinx-autobuild",
45
+ "sphinx-autoapi",
46
+ "sphinx-llm",
47
+ ]
37
48
 
38
49
  [tool.pyright]
39
50
  exclude = ["examples/", "playground/", "tmp/", ".venv/"]