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.
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/PKG-INFO +91 -10
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/README.md +89 -9
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/__init__.py +6 -4
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/client.py +249 -104
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/exceptions.py +0 -6
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/helpers.py +1 -1
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/models.py +29 -2
- apple_maps_api-0.2.1/apple_maps_api/py.typed +0 -0
- apple_maps_api-0.2.1/pyproject.toml +74 -0
- apple_maps_api-0.1.0/pyproject.toml → apple_maps_api-0.2.1/pyproject.toml.orig +14 -3
- {apple_maps_api-0.1.0 → apple_maps_api-0.2.1}/apple_maps_api/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: apple-maps-api
|
|
3
|
-
Version: 0.1
|
|
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
|
[](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(
|
|
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",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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:
|
|
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
|
|
367
|
-
|
|
368
|
-
:param
|
|
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":
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
:param
|
|
425
|
-
|
|
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:
|
|
428
|
-
:param search_region:
|
|
429
|
-
:param
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
:param
|
|
433
|
-
:param
|
|
434
|
-
|
|
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":
|
|
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":
|
|
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
|
|
479
|
-
|
|
480
|
-
:param
|
|
481
|
-
:param
|
|
482
|
-
|
|
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:
|
|
485
|
-
:param include_poi_categories:
|
|
486
|
-
|
|
487
|
-
:param
|
|
488
|
-
:param
|
|
489
|
-
:param
|
|
490
|
-
|
|
491
|
-
|
|
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":
|
|
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":
|
|
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(
|
|
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.
|
|
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:
|
|
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
|
|
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.
|
|
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/"]
|
|
File without changes
|