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.
@@ -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)
@@ -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()