apple-maps-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apple_maps_api/__init__.py +48 -0
- apple_maps_api/client.py +559 -0
- apple_maps_api/exceptions.py +34 -0
- apple_maps_api/helpers.py +145 -0
- apple_maps_api/models.py +288 -0
- apple_maps_api/version.py +34 -0
- apple_maps_api-0.1.0.dist-info/METADATA +268 -0
- apple_maps_api-0.1.0.dist-info/RECORD +9 -0
- apple_maps_api-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Helper functions for common geocoding operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from apple_maps_api.models import GeocodeResult, Place
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from apple_maps_api.client import AppleMapsClient
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _log_geocode_event(message: str, level: str = "info", **extras: object) -> None:
|
|
15
|
+
extra_str = ", ".join(f"{k}={v!r}" for k, v in extras.items())
|
|
16
|
+
full_message = f"{message} ({extra_str})" if extra_str else message
|
|
17
|
+
getattr(log, level)(full_message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _place_to_geocode_result(
|
|
21
|
+
place: Place,
|
|
22
|
+
*,
|
|
23
|
+
lat: float | None = None,
|
|
24
|
+
lon: float | None = None,
|
|
25
|
+
postal_code_override: str | None = None,
|
|
26
|
+
) -> GeocodeResult | None:
|
|
27
|
+
"""Extract a GeocodeResult from an Apple Maps Place object.
|
|
28
|
+
|
|
29
|
+
Returns None if coordinates cannot be determined.
|
|
30
|
+
"""
|
|
31
|
+
coord = place.coordinate
|
|
32
|
+
|
|
33
|
+
if lat is not None and lon is not None:
|
|
34
|
+
result_lat = lat
|
|
35
|
+
result_lon = lon
|
|
36
|
+
elif coord:
|
|
37
|
+
result_lat = coord.latitude
|
|
38
|
+
result_lon = coord.longitude
|
|
39
|
+
else:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
addr = place.structuredAddress
|
|
43
|
+
address1 = addr.fullThoroughfare if addr else None
|
|
44
|
+
city = addr.locality if addr else None
|
|
45
|
+
state_code = addr.administrativeAreaCode if addr else None
|
|
46
|
+
postal_code = postal_code_override or (addr.postCode if addr else None)
|
|
47
|
+
|
|
48
|
+
formatted_address = None
|
|
49
|
+
if place.formattedAddressLines:
|
|
50
|
+
formatted_address = ", ".join(place.formattedAddressLines)
|
|
51
|
+
|
|
52
|
+
return GeocodeResult(
|
|
53
|
+
lat=result_lat,
|
|
54
|
+
lon=result_lon,
|
|
55
|
+
address1=address1,
|
|
56
|
+
postal_code=postal_code,
|
|
57
|
+
city=city,
|
|
58
|
+
state_code=state_code,
|
|
59
|
+
country_code=place.countryCode,
|
|
60
|
+
formatted_address=formatted_address,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def geocode_postal_code(
|
|
65
|
+
client: "AppleMapsClient",
|
|
66
|
+
*,
|
|
67
|
+
postal_code: str,
|
|
68
|
+
country: str = "US",
|
|
69
|
+
) -> GeocodeResult | None:
|
|
70
|
+
"""Geocode a postal code and extract coordinates and address information.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
client: AppleMapsClient instance to use for geocoding
|
|
74
|
+
postal_code: The postal code to geocode
|
|
75
|
+
country: Country code (default: "US")
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
GeocodeResult with lat, lon, city, and state information.
|
|
79
|
+
Returns None if geocoding fails.
|
|
80
|
+
"""
|
|
81
|
+
location_result = client.geocode(postal_code, limit_to_countries=country)
|
|
82
|
+
|
|
83
|
+
if len(location_result.results) == 0:
|
|
84
|
+
_log_geocode_event(
|
|
85
|
+
"no geocoding results for zip code",
|
|
86
|
+
level="info",
|
|
87
|
+
zip=postal_code,
|
|
88
|
+
country=country,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if len(location_result.results) > 1:
|
|
94
|
+
_log_geocode_event(
|
|
95
|
+
"multiple geocoding results for zip code",
|
|
96
|
+
zip=postal_code,
|
|
97
|
+
results=len(location_result.results),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
place = location_result.results[0]
|
|
101
|
+
|
|
102
|
+
return _place_to_geocode_result(place, postal_code_override=postal_code)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def geocode_coordinates(
|
|
106
|
+
client: "AppleMapsClient",
|
|
107
|
+
*,
|
|
108
|
+
lat: float,
|
|
109
|
+
lon: float,
|
|
110
|
+
) -> GeocodeResult | None:
|
|
111
|
+
"""Reverse geocode coordinates and extract address information.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
client: AppleMapsClient instance to use for geocoding
|
|
115
|
+
lat: Latitude
|
|
116
|
+
lon: Longitude
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
GeocodeResult with lat, lon, zip_code, city, and state information.
|
|
120
|
+
Returns None if geocoding fails.
|
|
121
|
+
"""
|
|
122
|
+
location_result = client.reverse_geocode((lat, lon))
|
|
123
|
+
|
|
124
|
+
if len(location_result.results) == 0:
|
|
125
|
+
_log_geocode_event(
|
|
126
|
+
"no geocoding results for coordinates",
|
|
127
|
+
level="info",
|
|
128
|
+
lat=lat,
|
|
129
|
+
lon=lon,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
if len(location_result.results) > 1:
|
|
135
|
+
_log_geocode_event(
|
|
136
|
+
"multiple geocoding results for coordinates",
|
|
137
|
+
lat=lat,
|
|
138
|
+
lon=lon,
|
|
139
|
+
results=len(location_result.results),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
place = location_result.results[0]
|
|
143
|
+
|
|
144
|
+
# preserve the caller's coordinates, not whatever Apple rounded to
|
|
145
|
+
return _place_to_geocode_result(place, lat=lat, lon=lon)
|
apple_maps_api/models.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Pydantic models for Apple Maps Server API responses.
|
|
2
|
+
|
|
3
|
+
Model names and field names match Apple's official API documentation exactly.
|
|
4
|
+
Docstrings explain what each field represents in familiar terms.
|
|
5
|
+
|
|
6
|
+
See: https://developer.apple.com/documentation/applemapsserverapi
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AddressCategory(StrEnum):
|
|
15
|
+
Country = "Country"
|
|
16
|
+
AdministrativeArea = "AdministrativeArea"
|
|
17
|
+
SubAdministrativeArea = "SubAdministrativeArea"
|
|
18
|
+
Locality = "Locality"
|
|
19
|
+
SubLocality = "SubLocality"
|
|
20
|
+
PostalCode = "PostalCode"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PoiCategory(StrEnum):
|
|
24
|
+
Airport = "Airport"
|
|
25
|
+
AirportGate = "AirportGate"
|
|
26
|
+
AirportTerminal = "AirportTerminal"
|
|
27
|
+
AmusementPark = "AmusementPark"
|
|
28
|
+
AnimalService = "AnimalService"
|
|
29
|
+
Aquarium = "Aquarium"
|
|
30
|
+
ATM = "ATM"
|
|
31
|
+
AutomotiveRepair = "AutomotiveRepair"
|
|
32
|
+
Bakery = "Bakery"
|
|
33
|
+
Bank = "Bank"
|
|
34
|
+
Baseball = "Baseball"
|
|
35
|
+
Basketball = "Basketball"
|
|
36
|
+
Beach = "Beach"
|
|
37
|
+
Beauty = "Beauty"
|
|
38
|
+
Bowling = "Bowling"
|
|
39
|
+
Brewery = "Brewery"
|
|
40
|
+
Cafe = "Cafe"
|
|
41
|
+
Campground = "Campground"
|
|
42
|
+
CarRental = "CarRental"
|
|
43
|
+
Castle = "Castle"
|
|
44
|
+
ConventionCenter = "ConventionCenter"
|
|
45
|
+
Distillery = "Distillery"
|
|
46
|
+
EVCharger = "EVCharger"
|
|
47
|
+
Fairground = "Fairground"
|
|
48
|
+
FireStation = "FireStation"
|
|
49
|
+
Fishing = "Fishing"
|
|
50
|
+
FitnessCenter = "FitnessCenter"
|
|
51
|
+
FoodMarket = "FoodMarket"
|
|
52
|
+
Fortress = "Fortress"
|
|
53
|
+
GasStation = "GasStation"
|
|
54
|
+
GoKart = "GoKart"
|
|
55
|
+
Golf = "Golf"
|
|
56
|
+
Hiking = "Hiking"
|
|
57
|
+
Hospital = "Hospital"
|
|
58
|
+
Hotel = "Hotel"
|
|
59
|
+
Kayaking = "Kayaking"
|
|
60
|
+
Landmark = "Landmark"
|
|
61
|
+
Laundry = "Laundry"
|
|
62
|
+
Library = "Library"
|
|
63
|
+
Mailbox = "Mailbox"
|
|
64
|
+
Marina = "Marina"
|
|
65
|
+
MiniGolf = "MiniGolf"
|
|
66
|
+
MovieTheater = "MovieTheater"
|
|
67
|
+
Museum = "Museum"
|
|
68
|
+
MusicVenue = "MusicVenue"
|
|
69
|
+
NationalMonument = "NationalMonument"
|
|
70
|
+
NationalPark = "NationalPark"
|
|
71
|
+
Nightlife = "Nightlife"
|
|
72
|
+
Park = "Park"
|
|
73
|
+
Parking = "Parking"
|
|
74
|
+
Pharmacy = "Pharmacy"
|
|
75
|
+
Planetarium = "Planetarium"
|
|
76
|
+
Playground = "Playground"
|
|
77
|
+
Police = "Police"
|
|
78
|
+
PostOffice = "PostOffice"
|
|
79
|
+
PublicTransport = "PublicTransport"
|
|
80
|
+
ReligiousSite = "ReligiousSite"
|
|
81
|
+
Restaurant = "Restaurant"
|
|
82
|
+
Restroom = "Restroom"
|
|
83
|
+
RockClimbing = "RockClimbing"
|
|
84
|
+
RVPark = "RVPark"
|
|
85
|
+
School = "School"
|
|
86
|
+
SkatePark = "SkatePark"
|
|
87
|
+
Skating = "Skating"
|
|
88
|
+
Skiing = "Skiing"
|
|
89
|
+
Soccer = "Soccer"
|
|
90
|
+
Spa = "Spa"
|
|
91
|
+
Stadium = "Stadium"
|
|
92
|
+
Store = "Store"
|
|
93
|
+
Surfing = "Surfing"
|
|
94
|
+
Swimming = "Swimming"
|
|
95
|
+
Tennis = "Tennis"
|
|
96
|
+
Theater = "Theater"
|
|
97
|
+
University = "University"
|
|
98
|
+
Volleyball = "Volleyball"
|
|
99
|
+
Winery = "Winery"
|
|
100
|
+
Zoo = "Zoo"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SearchResultType(StrEnum):
|
|
104
|
+
"""Result type filter for /v1/search."""
|
|
105
|
+
|
|
106
|
+
poi = "poi"
|
|
107
|
+
address = "address"
|
|
108
|
+
physicalFeature = "physicalFeature"
|
|
109
|
+
pointOfInterest = "pointOfInterest"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SearchACResultType(StrEnum):
|
|
113
|
+
"""Result type filter for /v1/searchAutocomplete."""
|
|
114
|
+
|
|
115
|
+
poi = "poi"
|
|
116
|
+
address = "address"
|
|
117
|
+
physicalFeature = "physicalFeature"
|
|
118
|
+
pointOfInterest = "pointOfInterest"
|
|
119
|
+
query = "query"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Location(BaseModel):
|
|
123
|
+
"""A coordinate pair from Apple Maps API.
|
|
124
|
+
|
|
125
|
+
latitude: north-south position (e.g. 37.334)
|
|
126
|
+
longitude: east-west position (e.g. -122.009)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
latitude: float
|
|
130
|
+
longitude: float
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class StructuredAddress(BaseModel):
|
|
134
|
+
"""Detailed address components of a place.
|
|
135
|
+
|
|
136
|
+
Apple uses different terminology than most geocoding APIs:
|
|
137
|
+
- locality = city
|
|
138
|
+
- administrativeArea = state/province name
|
|
139
|
+
- administrativeAreaCode = state/province short code (e.g. "NY")
|
|
140
|
+
- subAdministrativeArea = county name
|
|
141
|
+
- postCode = postal/ZIP code
|
|
142
|
+
- fullThoroughfare = full street address (number + street, e.g. "841 Broadway")
|
|
143
|
+
- thoroughfare = street name (e.g. "Broadway")
|
|
144
|
+
- subThoroughfare = street/house number (e.g. "841")
|
|
145
|
+
- subLocality = neighborhood or area within the city
|
|
146
|
+
- areasOfInterest = common names for the surrounding area
|
|
147
|
+
- dependentLocalities = neighborhood names
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
administrativeArea: str | None = None
|
|
151
|
+
administrativeAreaCode: str | None = None
|
|
152
|
+
subAdministrativeArea: str | None = None
|
|
153
|
+
areasOfInterest: list[str] | None = None
|
|
154
|
+
dependentLocalities: list[str] | None = None
|
|
155
|
+
fullThoroughfare: str | None = None
|
|
156
|
+
locality: str | None = None
|
|
157
|
+
postCode: str | None = None
|
|
158
|
+
subLocality: str | None = None
|
|
159
|
+
subThoroughfare: str | None = None
|
|
160
|
+
thoroughfare: str | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class MapRegion(BaseModel):
|
|
164
|
+
"""A rectangular bounding box on a map.
|
|
165
|
+
|
|
166
|
+
Expressed as south-west and north-east corners.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
eastLongitude: float
|
|
170
|
+
northLatitude: float
|
|
171
|
+
southLatitude: float
|
|
172
|
+
westLongitude: float
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Place(BaseModel):
|
|
176
|
+
"""A place returned by geocode, reverseGeocode, or place lookup endpoints.
|
|
177
|
+
|
|
178
|
+
coordinate: lat/lng of the place
|
|
179
|
+
formattedAddressLines: human-readable address lines (e.g. ["841 Broadway", "New York, NY 10003"])
|
|
180
|
+
structuredAddress: parsed address components
|
|
181
|
+
displayMapRegion: suggested map viewport for displaying this place
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
id: str | None = None
|
|
185
|
+
name: str | None = None
|
|
186
|
+
coordinate: Location | None = None
|
|
187
|
+
formattedAddressLines: list[str] | None = None
|
|
188
|
+
structuredAddress: StructuredAddress | None = None
|
|
189
|
+
country: str | None = None
|
|
190
|
+
countryCode: str | None = None
|
|
191
|
+
displayMapRegion: MapRegion | None = None
|
|
192
|
+
alternateIds: list[str] | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class SearchPlace(Place):
|
|
196
|
+
"""Extended Place returned by /v1/search with POI category info.
|
|
197
|
+
|
|
198
|
+
poiCategory: point-of-interest category (e.g. "Restaurant", "MovieTheater")
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
poiCategory: str | None = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PlaceResults(BaseModel):
|
|
205
|
+
"""Response from /v1/geocode and /v1/reverseGeocode.
|
|
206
|
+
|
|
207
|
+
results: list of Place objects matching the query
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
results: list[Place]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SearchResponse(BaseModel):
|
|
214
|
+
"""Response from /v1/search.
|
|
215
|
+
|
|
216
|
+
results: list of SearchPlace objects with optional POI category
|
|
217
|
+
displayMapRegion: suggested map viewport encompassing all results
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
results: list[SearchPlace]
|
|
221
|
+
displayMapRegion: MapRegion | None = None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class AutocompleteResult(BaseModel):
|
|
225
|
+
"""A single autocomplete suggestion from /v1/searchAutocomplete.
|
|
226
|
+
|
|
227
|
+
completionUrl: relative URL to /v1/search to fetch full details for this suggestion
|
|
228
|
+
displayLines: raw text lines Apple returns — use completionTitle / completionSubtitle instead
|
|
229
|
+
location: approximate coordinate of the suggestion
|
|
230
|
+
structuredAddress: parsed address components if available
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
completionUrl: str | None = None
|
|
234
|
+
displayLines: list[str] | None = None
|
|
235
|
+
location: Location | None = None
|
|
236
|
+
structuredAddress: StructuredAddress | None = None
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def completionTitle(self) -> str | None:
|
|
240
|
+
"""Primary display text (e.g. place name or street address)."""
|
|
241
|
+
return self.displayLines[0] if self.displayLines else None
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def completionSubtitle(self) -> str | None:
|
|
245
|
+
"""Secondary display text (e.g. city, state)."""
|
|
246
|
+
return (
|
|
247
|
+
self.displayLines[1]
|
|
248
|
+
if self.displayLines and len(self.displayLines) > 1
|
|
249
|
+
else None
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SearchAutocompleteResponse(BaseModel):
|
|
254
|
+
"""Response from /v1/searchAutocomplete.
|
|
255
|
+
|
|
256
|
+
results: list of autocomplete suggestions
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
results: list[AutocompleteResult]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TokenResponse(BaseModel):
|
|
263
|
+
"""Response from /v1/token.
|
|
264
|
+
|
|
265
|
+
accessToken: short-lived Bearer token for API requests
|
|
266
|
+
expiresInSeconds: token lifetime (typically 1800 = 30 minutes)
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
accessToken: str
|
|
270
|
+
expiresInSeconds: int
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class GeocodeResult(BaseModel):
|
|
274
|
+
"""Provider-agnostic geocode result.
|
|
275
|
+
|
|
276
|
+
Identical to radar-mapping-api's GeocodeResult for drop-in replacement.
|
|
277
|
+
This abstraction allows swapping Apple Maps for Radar without changing consuming code.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
lat: float
|
|
281
|
+
lon: float
|
|
282
|
+
address1: str | None = None
|
|
283
|
+
address2: str | None = None
|
|
284
|
+
postal_code: str | None
|
|
285
|
+
city: str | None
|
|
286
|
+
state_code: str | None
|
|
287
|
+
country_code: str | None = None
|
|
288
|
+
formatted_address: str | None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Version handling for apple-maps-api."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_local_source_checkout() -> bool:
|
|
8
|
+
"""Check if the code is running from a local source checkout."""
|
|
9
|
+
package_dir = Path(__file__).resolve().parent
|
|
10
|
+
# Since this is a flat layout (module-root = ""), repo root is the parent of the package dir
|
|
11
|
+
repo_root = package_dir.parent
|
|
12
|
+
|
|
13
|
+
return (repo_root / ".git").exists() and (repo_root / "pyproject.toml").exists()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_version() -> str:
|
|
17
|
+
"""Get the version string, appending .dev if running from source."""
|
|
18
|
+
try:
|
|
19
|
+
# Try to get the version of the installed package
|
|
20
|
+
version = importlib.metadata.version("apple-maps-api")
|
|
21
|
+
except importlib.metadata.PackageNotFoundError:
|
|
22
|
+
# Fallback for local development if not installed
|
|
23
|
+
version = "0.1.0"
|
|
24
|
+
|
|
25
|
+
if not is_local_source_checkout():
|
|
26
|
+
return version
|
|
27
|
+
|
|
28
|
+
if version.endswith(".dev"):
|
|
29
|
+
return version
|
|
30
|
+
|
|
31
|
+
return f"{version}.dev"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = get_version()
|