bookalimo 0.1.5__py3-none-any.whl → 1.0.1__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.
- bookalimo/__init__.py +17 -24
- bookalimo/_version.py +9 -0
- bookalimo/client.py +310 -0
- bookalimo/config.py +16 -0
- bookalimo/exceptions.py +115 -5
- bookalimo/integrations/__init__.py +1 -0
- bookalimo/integrations/google_places/__init__.py +31 -0
- bookalimo/integrations/google_places/client_async.py +289 -0
- bookalimo/integrations/google_places/client_sync.py +287 -0
- bookalimo/integrations/google_places/common.py +231 -0
- bookalimo/integrations/google_places/proto_adapter.py +224 -0
- bookalimo/integrations/google_places/resolve_airport.py +397 -0
- bookalimo/integrations/google_places/transports.py +98 -0
- bookalimo/{_logging.py → logging.py} +45 -42
- bookalimo/schemas/__init__.py +103 -0
- bookalimo/schemas/base.py +56 -0
- bookalimo/{models.py → schemas/booking.py} +88 -100
- bookalimo/schemas/places/__init__.py +62 -0
- bookalimo/schemas/places/common.py +351 -0
- bookalimo/schemas/places/field_mask.py +221 -0
- bookalimo/schemas/places/google.py +883 -0
- bookalimo/schemas/places/place.py +334 -0
- bookalimo/services/__init__.py +11 -0
- bookalimo/services/pricing.py +191 -0
- bookalimo/services/reservations.py +227 -0
- bookalimo/transport/__init__.py +7 -0
- bookalimo/transport/auth.py +41 -0
- bookalimo/transport/base.py +44 -0
- bookalimo/transport/httpx_async.py +230 -0
- bookalimo/transport/httpx_sync.py +230 -0
- bookalimo/transport/retry.py +102 -0
- bookalimo/transport/utils.py +59 -0
- bookalimo-1.0.1.dist-info/METADATA +370 -0
- bookalimo-1.0.1.dist-info/RECORD +38 -0
- bookalimo-1.0.1.dist-info/licenses/LICENSE +21 -0
- bookalimo/_client.py +0 -420
- bookalimo/wrapper.py +0 -444
- bookalimo-0.1.5.dist-info/METADATA +0 -392
- bookalimo-0.1.5.dist-info/RECORD +0 -12
- bookalimo-0.1.5.dist-info/licenses/LICENSE +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.1.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from os import getenv
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import Any, Optional, TypeVar, cast
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
from google.api_core import exceptions as gexc
|
9
|
+
from google.maps.places_v1 import PlacesAsyncClient
|
10
|
+
from typing_extensions import ParamSpec
|
11
|
+
|
12
|
+
from ...exceptions import BookalimoError
|
13
|
+
from ...logging import get_logger
|
14
|
+
from ...schemas.places import FieldMaskInput
|
15
|
+
from ...schemas.places import google as models
|
16
|
+
from .common import (
|
17
|
+
DEFAULT_PLACE_FIELDS,
|
18
|
+
build_get_place_request,
|
19
|
+
build_search_request_params,
|
20
|
+
derive_effective_query,
|
21
|
+
fmt_exc,
|
22
|
+
mask_header,
|
23
|
+
normalize_place_from_proto,
|
24
|
+
normalize_search_results,
|
25
|
+
validate_autocomplete_inputs,
|
26
|
+
validate_resolve_airport_inputs,
|
27
|
+
)
|
28
|
+
from .proto_adapter import validate_proto_to_model
|
29
|
+
from .resolve_airport import resolve_airport
|
30
|
+
from .transports import GoogleAsyncTransport
|
31
|
+
|
32
|
+
logger = get_logger("places")
|
33
|
+
|
34
|
+
P = ParamSpec("P")
|
35
|
+
R = TypeVar("R")
|
36
|
+
|
37
|
+
|
38
|
+
class AsyncGooglePlaces:
|
39
|
+
"""
|
40
|
+
Google Places API asynchronous client for address validation, geocoding, and autocomplete.
|
41
|
+
Provides location resolution services that integrate seamlessly with
|
42
|
+
Book-A-Limo location factory functions.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
api_key: Optional[str] = None,
|
48
|
+
client: Optional[PlacesAsyncClient] = None,
|
49
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
50
|
+
):
|
51
|
+
"""
|
52
|
+
Initialize Google Places client.
|
53
|
+
Args:
|
54
|
+
api_key: Google Places API key. If not provided, it will be read from the GOOGLE_PLACES_API_KEY environment variable.
|
55
|
+
client: Optional `PlacesAsyncClient` instance.
|
56
|
+
http_client: Optional `httpx.AsyncClient` instance.
|
57
|
+
"""
|
58
|
+
self.http_client = http_client or httpx.AsyncClient()
|
59
|
+
api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
|
60
|
+
if not api_key:
|
61
|
+
raise ValueError("Google Places API key is required.")
|
62
|
+
self.transport = GoogleAsyncTransport(api_key, client)
|
63
|
+
|
64
|
+
async def __aenter__(self) -> AsyncGooglePlaces:
|
65
|
+
return self
|
66
|
+
|
67
|
+
async def __aexit__(
|
68
|
+
self,
|
69
|
+
exc_type: Optional[type[BaseException]],
|
70
|
+
exc_val: Optional[BaseException],
|
71
|
+
exc_tb: Optional[TracebackType],
|
72
|
+
) -> None:
|
73
|
+
await self.aclose()
|
74
|
+
|
75
|
+
async def aclose(self) -> None:
|
76
|
+
"""Close underlying transports safely."""
|
77
|
+
try:
|
78
|
+
await self.transport.close()
|
79
|
+
finally:
|
80
|
+
await self.http_client.aclose()
|
81
|
+
|
82
|
+
async def autocomplete(
|
83
|
+
self,
|
84
|
+
input: Optional[str] = None,
|
85
|
+
*,
|
86
|
+
request: Optional[models.AutocompletePlacesRequest] = None,
|
87
|
+
) -> models.AutocompletePlacesResponse:
|
88
|
+
"""
|
89
|
+
Get autocomplete suggestions for a location query.
|
90
|
+
Args:
|
91
|
+
input: The text string on which to search.
|
92
|
+
request: AutocompletePlacesRequest object.
|
93
|
+
Returns:
|
94
|
+
`AutocompletePlacesResponse` object.
|
95
|
+
Note:
|
96
|
+
If both input and request are provided, request will be used.
|
97
|
+
Raises:
|
98
|
+
ValueError: If neither input nor request is provided, or if both are provided.
|
99
|
+
BookalimoError: If the API request fails.
|
100
|
+
"""
|
101
|
+
request = validate_autocomplete_inputs(input, request)
|
102
|
+
try:
|
103
|
+
proto = await self.transport.autocomplete_places(
|
104
|
+
request=request.model_dump()
|
105
|
+
)
|
106
|
+
return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
|
107
|
+
except gexc.GoogleAPICallError as e:
|
108
|
+
msg = f"Google Places Autocomplete failed: {fmt_exc(e)}"
|
109
|
+
logger.error(msg)
|
110
|
+
raise BookalimoError(msg) from e
|
111
|
+
|
112
|
+
async def geocode(self, request: models.GeocodingRequest) -> dict[str, Any]:
|
113
|
+
try:
|
114
|
+
r = await self.http_client.get(
|
115
|
+
"https://maps.googleapis.com/maps/api/geocode/json",
|
116
|
+
params=request.to_query_params(),
|
117
|
+
)
|
118
|
+
r.raise_for_status()
|
119
|
+
return cast(dict[str, Any], r.json())
|
120
|
+
except httpx.HTTPError as e:
|
121
|
+
msg = f"HTTP geocoding failed: {fmt_exc(e)}"
|
122
|
+
logger.error(msg)
|
123
|
+
raise BookalimoError(msg) from e
|
124
|
+
|
125
|
+
async def search(
|
126
|
+
self,
|
127
|
+
query: Optional[str] = None,
|
128
|
+
*,
|
129
|
+
request: Optional[models.SearchTextRequest] = None,
|
130
|
+
fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
|
131
|
+
**kwargs: Any,
|
132
|
+
) -> list[models.Place]:
|
133
|
+
"""
|
134
|
+
Search for places using a text query or SearchTextRequest.
|
135
|
+
Args:
|
136
|
+
query: Simple text query to search for. Either query or request must be provided.
|
137
|
+
request: SearchTextRequest object with advanced search parameters. Either query or request must be provided.
|
138
|
+
fields: Field mask for response data.
|
139
|
+
**kwargs: Additional parameters for the Text Search API.
|
140
|
+
Returns:
|
141
|
+
list[models.Place]
|
142
|
+
Raises:
|
143
|
+
BookalimoError: If the API request fails.
|
144
|
+
ValueError: If neither query nor request is provided, or if both are provided.
|
145
|
+
"""
|
146
|
+
request_params = build_search_request_params(query, request, **kwargs)
|
147
|
+
metadata = mask_header(fields, prefix="places")
|
148
|
+
|
149
|
+
try:
|
150
|
+
protos = await self.transport.search_text(
|
151
|
+
request=request_params,
|
152
|
+
metadata=metadata,
|
153
|
+
)
|
154
|
+
return normalize_search_results(protos)
|
155
|
+
except gexc.InvalidArgument as e:
|
156
|
+
# Often caused by missing/invalid field mask
|
157
|
+
msg = f"Google Places Text Search invalid argument: {fmt_exc(e)}"
|
158
|
+
logger.error(msg)
|
159
|
+
raise BookalimoError(msg) from e
|
160
|
+
except gexc.GoogleAPICallError as e:
|
161
|
+
msg = f"Google Places Text Search failed: {fmt_exc(e)}"
|
162
|
+
logger.error(msg)
|
163
|
+
raise BookalimoError(msg) from e
|
164
|
+
|
165
|
+
async def get(
|
166
|
+
self,
|
167
|
+
place_id: Optional[str] = None,
|
168
|
+
*,
|
169
|
+
request: Optional[models.GetPlaceRequest] = None,
|
170
|
+
fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
|
171
|
+
) -> Optional[models.Place]:
|
172
|
+
"""
|
173
|
+
Get details for a specific place.
|
174
|
+
Args:
|
175
|
+
place_id: The ID of the place to retrieve details for.
|
176
|
+
request: GetPlaceRequest object with place resource name.
|
177
|
+
fields: Optional field mask for response data.
|
178
|
+
Returns:
|
179
|
+
A models.Place object or None if not found.
|
180
|
+
Raises:
|
181
|
+
ValueError: If neither place_id nor request is provided.
|
182
|
+
BookalimoError: If the API request fails.
|
183
|
+
"""
|
184
|
+
request_params = build_get_place_request(place_id, request)
|
185
|
+
metadata = mask_header(fields)
|
186
|
+
|
187
|
+
try:
|
188
|
+
proto = await self.transport.get_place(
|
189
|
+
request=request_params,
|
190
|
+
metadata=metadata,
|
191
|
+
)
|
192
|
+
return normalize_place_from_proto(proto)
|
193
|
+
except gexc.NotFound:
|
194
|
+
return None
|
195
|
+
except gexc.InvalidArgument as e:
|
196
|
+
msg = f"Google Places Get Place invalid argument: {fmt_exc(e)}"
|
197
|
+
logger.error(msg)
|
198
|
+
raise BookalimoError(msg) from e
|
199
|
+
except gexc.GoogleAPICallError as e:
|
200
|
+
msg = f"Google Places Get Place failed: {fmt_exc(e)}"
|
201
|
+
logger.error(msg)
|
202
|
+
raise BookalimoError(msg) from e
|
203
|
+
|
204
|
+
async def resolve_airport(
|
205
|
+
self,
|
206
|
+
query: Optional[str] = None,
|
207
|
+
place_id: Optional[str] = None,
|
208
|
+
places: Optional[list[models.Place]] = None,
|
209
|
+
max_distance_km: Optional[float] = 100,
|
210
|
+
max_results: Optional[int] = 5,
|
211
|
+
confidence_threshold: Optional[float] = 0.5,
|
212
|
+
text_weight: float = 0.5,
|
213
|
+
) -> list[models.ResolvedAirport]:
|
214
|
+
"""
|
215
|
+
Resolve airport candidates given either a natural language text query, a place_id, or a list of Places.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
query: Text query for airport search (optional)
|
219
|
+
place_id: Google place ID for proximity matching (optional)
|
220
|
+
places: List of existing Place objects for proximity matching (optional)
|
221
|
+
max_distance_km: Maximum distance for proximity matching (default: 100km)
|
222
|
+
max_results: Maximum number of results to return (default: 5)
|
223
|
+
confidence_threshold: Minimum confidence threshold (default: 0.5)
|
224
|
+
text_weight: Weight for text search (default: 0.5) If 0.0, only proximity will be used. If 1.0, only text will be used.
|
225
|
+
|
226
|
+
Rules:
|
227
|
+
- Provide at most one of {place_id, places}. (query may accompany either.)
|
228
|
+
- If nothing but query is given, search for places from the query.
|
229
|
+
- If place_id is given:
|
230
|
+
* Fetch the place.
|
231
|
+
* If no explicit query, derive it from the place's display name.
|
232
|
+
- If places is given:
|
233
|
+
* If len(places) == 0 and no query, error.
|
234
|
+
* If len(places) == 1 and no query, derive query from that place's display name.
|
235
|
+
* If len(places) > 1 and no query, error (need query to disambiguate).
|
236
|
+
- If nothing is provided, error.
|
237
|
+
- If max_distance_km is provided, it must be > 0.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
list[models.ResolvedAirport]
|
241
|
+
Raises:
|
242
|
+
ValueError on invalid inputs.
|
243
|
+
BookalimoError if underlying API requests fail.
|
244
|
+
"""
|
245
|
+
# Validate inputs
|
246
|
+
validate_resolve_airport_inputs(place_id, places, max_distance_km)
|
247
|
+
|
248
|
+
# Establish the authoritative places list
|
249
|
+
effective_places: list[models.Place]
|
250
|
+
|
251
|
+
if place_id is not None:
|
252
|
+
place = await self.get(place_id=place_id)
|
253
|
+
if place is None:
|
254
|
+
raise ValueError(f"Place with id {place_id!r} was not found.")
|
255
|
+
effective_places = [place]
|
256
|
+
|
257
|
+
elif places is not None:
|
258
|
+
if len(places) == 0 and (query is None or not str(query).strip()):
|
259
|
+
raise ValueError(
|
260
|
+
"Empty 'places' and no 'query' provided; nothing to resolve."
|
261
|
+
)
|
262
|
+
effective_places = places
|
263
|
+
|
264
|
+
else:
|
265
|
+
# Neither place_id nor places: fall back to query-driven search
|
266
|
+
if query is None or not str(query).strip():
|
267
|
+
raise ValueError("Either place_id, places, or query must be provided.")
|
268
|
+
effective_places = await self.search(
|
269
|
+
request=models.SearchTextRequest(
|
270
|
+
text_query=str(query).strip(),
|
271
|
+
max_result_count=5,
|
272
|
+
)
|
273
|
+
)
|
274
|
+
|
275
|
+
# Derive effective query
|
276
|
+
effective_query = derive_effective_query(query, effective_places)
|
277
|
+
|
278
|
+
google_places = [
|
279
|
+
p.google_place for p in effective_places if p.google_place is not None
|
280
|
+
]
|
281
|
+
|
282
|
+
return resolve_airport(
|
283
|
+
effective_query,
|
284
|
+
google_places,
|
285
|
+
max_distance_km,
|
286
|
+
max_results,
|
287
|
+
confidence_threshold,
|
288
|
+
text_weight,
|
289
|
+
)
|
@@ -0,0 +1,287 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from os import getenv
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import Any, Optional, TypeVar, cast
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
from google.api_core import exceptions as gexc
|
9
|
+
from google.maps.places_v1 import PlacesClient
|
10
|
+
from typing_extensions import ParamSpec
|
11
|
+
|
12
|
+
from ...exceptions import BookalimoError
|
13
|
+
from ...logging import get_logger
|
14
|
+
from ...schemas.places import FieldMaskInput
|
15
|
+
from ...schemas.places import google as models
|
16
|
+
from .common import (
|
17
|
+
DEFAULT_PLACE_FIELDS,
|
18
|
+
build_get_place_request,
|
19
|
+
build_search_request_params,
|
20
|
+
derive_effective_query,
|
21
|
+
fmt_exc,
|
22
|
+
mask_header,
|
23
|
+
normalize_place_from_proto,
|
24
|
+
normalize_search_results,
|
25
|
+
validate_autocomplete_inputs,
|
26
|
+
validate_resolve_airport_inputs,
|
27
|
+
)
|
28
|
+
from .proto_adapter import validate_proto_to_model
|
29
|
+
from .resolve_airport import resolve_airport
|
30
|
+
from .transports import GoogleSyncTransport
|
31
|
+
|
32
|
+
logger = get_logger("places")
|
33
|
+
|
34
|
+
P = ParamSpec("P")
|
35
|
+
R = TypeVar("R")
|
36
|
+
|
37
|
+
|
38
|
+
class GooglePlaces:
|
39
|
+
"""
|
40
|
+
Google Places API synchronous client for address validation, geocoding, and autocomplete.
|
41
|
+
Provides location resolution services that integrate seamlessly with
|
42
|
+
Book-A-Limo location factory functions.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
api_key: Optional[str] = None,
|
48
|
+
client: Optional[PlacesClient] = None,
|
49
|
+
http_client: Optional[httpx.Client] = None,
|
50
|
+
):
|
51
|
+
"""
|
52
|
+
Initialize Google Places client.
|
53
|
+
Args:
|
54
|
+
api_key: Google Places API key. If not provided, it will be read from the GOOGLE_PLACES_API_KEY environment variable.
|
55
|
+
client: Optional `PlacesClient` instance.
|
56
|
+
http_client: Optional `httpx.Client` instance.
|
57
|
+
"""
|
58
|
+
self.http_client = http_client or httpx.Client()
|
59
|
+
api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
|
60
|
+
if not api_key:
|
61
|
+
raise ValueError("Google Places API key is required.")
|
62
|
+
self.transport = GoogleSyncTransport(api_key, client)
|
63
|
+
|
64
|
+
def __enter__(self) -> GooglePlaces:
|
65
|
+
return self
|
66
|
+
|
67
|
+
def __exit__(
|
68
|
+
self,
|
69
|
+
exc_type: Optional[type[BaseException]],
|
70
|
+
exc_val: Optional[BaseException],
|
71
|
+
exc_tb: Optional[TracebackType],
|
72
|
+
) -> None:
|
73
|
+
self.close()
|
74
|
+
|
75
|
+
def close(self) -> None:
|
76
|
+
"""Close underlying transports safely."""
|
77
|
+
try:
|
78
|
+
self.transport.close()
|
79
|
+
finally:
|
80
|
+
self.http_client.close()
|
81
|
+
|
82
|
+
def autocomplete(
|
83
|
+
self,
|
84
|
+
input: Optional[str] = None,
|
85
|
+
*,
|
86
|
+
request: Optional[models.AutocompletePlacesRequest] = None,
|
87
|
+
) -> models.AutocompletePlacesResponse:
|
88
|
+
"""
|
89
|
+
Get autocomplete suggestions for a location query.
|
90
|
+
Args:
|
91
|
+
input: The text string on which to search.
|
92
|
+
request: AutocompletePlacesRequest object.
|
93
|
+
Returns:
|
94
|
+
`AutocompletePlacesResponse` object.
|
95
|
+
Note:
|
96
|
+
If both input and request are provided, request will be used.
|
97
|
+
Raises:
|
98
|
+
ValueError: If neither input nor request is provided, or if both are provided.
|
99
|
+
BookalimoError: If the API request fails.
|
100
|
+
"""
|
101
|
+
request = validate_autocomplete_inputs(input, request)
|
102
|
+
try:
|
103
|
+
proto = self.transport.autocomplete_places(request=request.model_dump())
|
104
|
+
return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
|
105
|
+
except gexc.GoogleAPICallError as e:
|
106
|
+
msg = f"Google Places Autocomplete failed: {fmt_exc(e)}"
|
107
|
+
logger.error(msg)
|
108
|
+
raise BookalimoError(msg) from e
|
109
|
+
|
110
|
+
def geocode(self, request: models.GeocodingRequest) -> dict[str, Any]:
|
111
|
+
try:
|
112
|
+
r = self.http_client.get(
|
113
|
+
"https://maps.googleapis.com/maps/api/geocode/json",
|
114
|
+
params=request.to_query_params(),
|
115
|
+
)
|
116
|
+
r.raise_for_status()
|
117
|
+
return cast(dict[str, Any], r.json())
|
118
|
+
except httpx.HTTPError as e:
|
119
|
+
msg = f"HTTP geocoding failed: {fmt_exc(e)}"
|
120
|
+
logger.error(msg)
|
121
|
+
raise BookalimoError(msg) from e
|
122
|
+
|
123
|
+
def search(
|
124
|
+
self,
|
125
|
+
query: Optional[str] = None,
|
126
|
+
*,
|
127
|
+
request: Optional[models.SearchTextRequest] = None,
|
128
|
+
fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
|
129
|
+
**kwargs: Any,
|
130
|
+
) -> list[models.Place]:
|
131
|
+
"""
|
132
|
+
Search for places using a text query or SearchTextRequest.
|
133
|
+
Args:
|
134
|
+
query: Simple text query to search for. Either query or request must be provided.
|
135
|
+
request: SearchTextRequest object with advanced search parameters. Either query or request must be provided.
|
136
|
+
fields: Field mask for response data.
|
137
|
+
**kwargs: Additional parameters for the Text Search API.
|
138
|
+
Returns:
|
139
|
+
list[models.Place]
|
140
|
+
Raises:
|
141
|
+
BookalimoError: If the API request fails.
|
142
|
+
ValueError: If neither query nor request is provided, or if both are provided.
|
143
|
+
"""
|
144
|
+
request_params = build_search_request_params(query, request, **kwargs)
|
145
|
+
metadata = mask_header(fields, prefix="places")
|
146
|
+
|
147
|
+
try:
|
148
|
+
protos = self.transport.search_text(
|
149
|
+
request=request_params,
|
150
|
+
metadata=metadata,
|
151
|
+
)
|
152
|
+
return normalize_search_results(protos)
|
153
|
+
except gexc.InvalidArgument as e:
|
154
|
+
# Often caused by missing/invalid field mask
|
155
|
+
msg = f"Google Places Text Search invalid argument: {fmt_exc(e)}"
|
156
|
+
logger.error(msg)
|
157
|
+
raise BookalimoError(msg) from e
|
158
|
+
except gexc.GoogleAPICallError as e:
|
159
|
+
msg = f"Google Places Text Search failed: {fmt_exc(e)}"
|
160
|
+
logger.error(msg)
|
161
|
+
raise BookalimoError(msg) from e
|
162
|
+
|
163
|
+
def get(
|
164
|
+
self,
|
165
|
+
place_id: Optional[str] = None,
|
166
|
+
*,
|
167
|
+
request: Optional[models.GetPlaceRequest] = None,
|
168
|
+
fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
|
169
|
+
) -> Optional[models.Place]:
|
170
|
+
"""
|
171
|
+
Get details for a specific place.
|
172
|
+
Args:
|
173
|
+
place_id: The ID of the place to retrieve details for.
|
174
|
+
request: GetPlaceRequest object with place resource name.
|
175
|
+
fields: Optional field mask for response data.
|
176
|
+
Returns:
|
177
|
+
A models.Place object or None if not found.
|
178
|
+
Raises:
|
179
|
+
ValueError: If neither place_id nor request is provided.
|
180
|
+
BookalimoError: If the API request fails.
|
181
|
+
"""
|
182
|
+
request_params = build_get_place_request(place_id, request)
|
183
|
+
metadata = mask_header(fields)
|
184
|
+
|
185
|
+
try:
|
186
|
+
proto = self.transport.get_place(
|
187
|
+
request=request_params,
|
188
|
+
metadata=metadata,
|
189
|
+
)
|
190
|
+
return normalize_place_from_proto(proto)
|
191
|
+
except gexc.NotFound:
|
192
|
+
return None
|
193
|
+
except gexc.InvalidArgument as e:
|
194
|
+
msg = f"Google Places Get Place invalid argument: {fmt_exc(e)}"
|
195
|
+
logger.error(msg)
|
196
|
+
raise BookalimoError(msg) from e
|
197
|
+
except gexc.GoogleAPICallError as e:
|
198
|
+
msg = f"Google Places Get Place failed: {fmt_exc(e)}"
|
199
|
+
logger.error(msg)
|
200
|
+
raise BookalimoError(msg) from e
|
201
|
+
|
202
|
+
def resolve_airport(
|
203
|
+
self,
|
204
|
+
query: Optional[str] = None,
|
205
|
+
place_id: Optional[str] = None,
|
206
|
+
places: Optional[list[models.Place]] = None,
|
207
|
+
max_distance_km: Optional[float] = 100,
|
208
|
+
max_results: Optional[int] = 5,
|
209
|
+
confidence_threshold: Optional[float] = 0.5,
|
210
|
+
text_weight: float = 0.5,
|
211
|
+
) -> list[models.ResolvedAirport]:
|
212
|
+
"""
|
213
|
+
Resolve airport candidates given either a natural language text query, a place_id, or a list of Places.
|
214
|
+
|
215
|
+
Args:
|
216
|
+
query: Text query for airport search (optional)
|
217
|
+
place_id: Google place ID for proximity matching (optional)
|
218
|
+
places: List of existing Place objects for proximity matching (optional)
|
219
|
+
max_distance_km: Maximum distance for proximity matching (default: 100km)
|
220
|
+
max_results: Maximum number of results to return (default: 5)
|
221
|
+
confidence_threshold: Minimum confidence threshold (default: 0.5)
|
222
|
+
text_weight: Weight for text search (default: 0.5) If 0.0, only proximity will be used. If 1.0, only text will be used.
|
223
|
+
|
224
|
+
Rules:
|
225
|
+
- Provide at most one of {place_id, places}. (query may accompany either.)
|
226
|
+
- If nothing but query is given, search for places from the query.
|
227
|
+
- If place_id is given:
|
228
|
+
* Fetch the place.
|
229
|
+
* If no explicit query, derive it from the place's display name.
|
230
|
+
- If places is given:
|
231
|
+
* If len(places) == 0 and no query, error.
|
232
|
+
* If len(places) == 1 and no query, derive query from that place's display name.
|
233
|
+
* If len(places) > 1 and no query, error (need query to disambiguate).
|
234
|
+
- If nothing is provided, error.
|
235
|
+
- If max_distance_km is provided, it must be > 0.
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
list[models.ResolvedAirport]
|
239
|
+
Raises:
|
240
|
+
ValueError on invalid inputs.
|
241
|
+
BookalimoError if underlying API requests fail.
|
242
|
+
"""
|
243
|
+
# Validate inputs
|
244
|
+
validate_resolve_airport_inputs(place_id, places, max_distance_km)
|
245
|
+
|
246
|
+
# Establish the authoritative places list
|
247
|
+
effective_places: list[models.Place]
|
248
|
+
|
249
|
+
if place_id is not None:
|
250
|
+
place = self.get(place_id=place_id)
|
251
|
+
if place is None:
|
252
|
+
raise ValueError(f"Place with id {place_id!r} was not found.")
|
253
|
+
effective_places = [place]
|
254
|
+
|
255
|
+
elif places is not None:
|
256
|
+
if len(places) == 0 and (query is None or not str(query).strip()):
|
257
|
+
raise ValueError(
|
258
|
+
"Empty 'places' and no 'query' provided; nothing to resolve."
|
259
|
+
)
|
260
|
+
effective_places = places
|
261
|
+
|
262
|
+
else:
|
263
|
+
# Neither place_id nor places: fall back to query-driven search
|
264
|
+
if query is None or not str(query).strip():
|
265
|
+
raise ValueError("Either place_id, places, or query must be provided.")
|
266
|
+
effective_places = self.search(
|
267
|
+
request=models.SearchTextRequest(
|
268
|
+
text_query=str(query).strip(),
|
269
|
+
max_result_count=5,
|
270
|
+
)
|
271
|
+
)
|
272
|
+
|
273
|
+
# Derive effective query
|
274
|
+
effective_query = derive_effective_query(query, effective_places)
|
275
|
+
|
276
|
+
google_places = [
|
277
|
+
p.google_place for p in effective_places if p.google_place is not None
|
278
|
+
]
|
279
|
+
|
280
|
+
return resolve_airport(
|
281
|
+
effective_query,
|
282
|
+
google_places,
|
283
|
+
max_distance_km,
|
284
|
+
max_results,
|
285
|
+
confidence_threshold,
|
286
|
+
text_weight,
|
287
|
+
)
|