bookalimo 1.0.1__py3-none-any.whl → 1.0.2__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.
@@ -5,8 +5,12 @@ Common utilities and shared functionality for Google Places clients.
5
5
  from __future__ import annotations
6
6
 
7
7
  import re
8
- from typing import Any, Optional
8
+ from typing import Any, Awaitable, Callable, Optional, cast
9
9
 
10
+ import httpx
11
+ from google.api_core import exceptions as gexc
12
+
13
+ from ...exceptions import BookalimoError
10
14
  from ...logging import get_logger
11
15
  from ...schemas.places import (
12
16
  AddressComponent,
@@ -15,14 +19,19 @@ from ...schemas.places import (
15
19
  GooglePlace,
16
20
  Place,
17
21
  PlaceType,
22
+ RankPreference,
18
23
  compile_field_mask,
19
24
  )
20
25
  from ...schemas.places import (
21
26
  google as models,
22
27
  )
28
+ from .proto_adapter import validate_proto_to_model
29
+ from .resolve_airport import resolve_airport
23
30
 
24
31
  logger = get_logger("places")
25
32
 
33
+ # Type variables for shared functionality
34
+
26
35
  # Default field mask for places queries
27
36
  DEFAULT_PLACE_FIELDS = (
28
37
  "display_name",
@@ -49,12 +58,12 @@ ADDRESS_TYPES = {
49
58
  }
50
59
 
51
60
 
52
- def fmt_exc(e: BaseException) -> str:
61
+ def _fmt_exc(e: BaseException) -> str:
53
62
  """Format exception for logging without touching non-existent attributes."""
54
63
  return f"{type(e).__name__}: {e}"
55
64
 
56
65
 
57
- def mask_header(
66
+ def _mask_header(
58
67
  fields: FieldMaskInput, prefix: str = ""
59
68
  ) -> tuple[tuple[str, str], ...]:
60
69
  """
@@ -127,7 +136,7 @@ def name_from_place(p: Place) -> Optional[str]:
127
136
  return None
128
137
 
129
138
 
130
- def build_search_request_params(
139
+ def _build_search_request_params(
131
140
  query: Optional[str],
132
141
  request: Optional[models.SearchTextRequest],
133
142
  **kwargs: Any,
@@ -148,9 +157,8 @@ def build_search_request_params(
148
157
  return {"text_query": query, **kwargs}
149
158
 
150
159
 
151
- def normalize_place_from_proto(proto: Any) -> models.Place:
160
+ def _normalize_place_from_proto(proto: Any) -> models.Place:
152
161
  """Convert a proto Place to our normalized Place model."""
153
- from .proto_adapter import validate_proto_to_model
154
162
 
155
163
  model = validate_proto_to_model(proto, GooglePlace)
156
164
  adr_format = getattr(model, "adr_format_address", None)
@@ -170,12 +178,12 @@ def normalize_place_from_proto(proto: Any) -> models.Place:
170
178
  )
171
179
 
172
180
 
173
- def normalize_search_results(proto_response: Any) -> list[models.Place]:
181
+ def _normalize_search_results(proto_response: Any) -> list[models.Place]:
174
182
  """Convert search_text response to list of normalized Place models."""
175
- return [normalize_place_from_proto(proto) for proto in proto_response.places]
183
+ return [_normalize_place_from_proto(proto) for proto in proto_response.places]
176
184
 
177
185
 
178
- def build_get_place_request(
186
+ def _build_get_place_request(
179
187
  place_id: Optional[str], request: Optional[models.GetPlaceRequest]
180
188
  ) -> dict[str, Any]:
181
189
  """Build request parameters for get_place API call."""
@@ -183,12 +191,12 @@ def build_get_place_request(
183
191
  raise ValueError("Either 'place_id' or 'request' must be provided")
184
192
 
185
193
  if request:
186
- return request.model_dump(exclude_none=True)
194
+ return request.model_dump(exclude_none=True, context={"enum_out": "name"})
187
195
  else:
188
196
  return {"name": f"places/{place_id}"}
189
197
 
190
198
 
191
- def derive_effective_query(query: Optional[str], places: list[models.Place]) -> str:
199
+ def _derive_effective_query(query: Optional[str], places: list[models.Place]) -> str:
192
200
  """Derive an effective query string from query and/or places."""
193
201
  effective_query = (query or "").strip()
194
202
 
@@ -206,7 +214,7 @@ def derive_effective_query(query: Optional[str], places: list[models.Place]) ->
206
214
  return effective_query
207
215
 
208
216
 
209
- def validate_resolve_airport_inputs(
217
+ def _validate_resolve_airport_inputs(
210
218
  place_id: Optional[str],
211
219
  places: Optional[list[models.Place]],
212
220
  max_distance_km: Optional[float],
@@ -229,3 +237,273 @@ def validate_autocomplete_inputs(
229
237
  else:
230
238
  request = models.AutocompletePlacesRequest(input=input)
231
239
  return request
240
+
241
+
242
+ def _call_gplaces_sync(
243
+ ctx: str,
244
+ fn: Callable[..., Any],
245
+ *args: Any,
246
+ not_found_ok: bool = False,
247
+ **kwargs: Any,
248
+ ) -> Any:
249
+ try:
250
+ return fn(*args, **kwargs)
251
+ except gexc.NotFound:
252
+ if not_found_ok:
253
+ return None
254
+ raise BookalimoError(f"{ctx} not found") from None
255
+ except gexc.InvalidArgument as e:
256
+ msg = f"{ctx} invalid argument: {_fmt_exc(e)}"
257
+ logger.error(msg)
258
+ raise BookalimoError(msg) from e
259
+ except gexc.GoogleAPICallError as e:
260
+ msg = f"{ctx} failed: {_fmt_exc(e)}"
261
+ logger.error(msg)
262
+ raise BookalimoError(msg) from e
263
+
264
+
265
+ async def _call_gplaces_async(
266
+ ctx: str,
267
+ fn: Callable[..., Awaitable[Any]],
268
+ *args: Any,
269
+ not_found_ok: bool = False,
270
+ **kwargs: Any,
271
+ ) -> Any:
272
+ try:
273
+ return await fn(*args, **kwargs)
274
+ except gexc.NotFound:
275
+ if not_found_ok:
276
+ return None
277
+ raise BookalimoError(f"{ctx} not found") from None
278
+ except gexc.InvalidArgument as e:
279
+ msg = f"{ctx} invalid argument: {_fmt_exc(e)}"
280
+ logger.error(msg)
281
+ raise BookalimoError(msg) from e
282
+ except gexc.GoogleAPICallError as e:
283
+ msg = f"{ctx} failed: {_fmt_exc(e)}"
284
+ logger.error(msg)
285
+ raise BookalimoError(msg) from e
286
+
287
+
288
+ def handle_autocomplete_impl(
289
+ transport_call: Callable[[dict[str, Any]], Any],
290
+ request: models.AutocompletePlacesRequest,
291
+ ) -> models.AutocompletePlacesResponse:
292
+ """Shared implementation for autocomplete method."""
293
+
294
+ proto = _call_gplaces_sync(
295
+ "Google Places Autocomplete", transport_call, request.model_dump()
296
+ )
297
+ return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
298
+
299
+
300
+ def prepare_geocode_params(
301
+ request: models.GeocodingRequest,
302
+ api_key: str,
303
+ ) -> Any:
304
+ """Prepare geocode parameters for both sync/async."""
305
+ params = request.to_query_params()
306
+ params = params.add("key", api_key)
307
+ return params
308
+
309
+
310
+ def handle_geocode_response(response: Any) -> dict[str, Any]:
311
+ """Handle geocode response for both sync/async."""
312
+ try:
313
+ response.raise_for_status()
314
+ return cast(dict[str, Any], response.json())
315
+ except httpx.HTTPError as e:
316
+ msg = f"HTTP geocoding failed: {_fmt_exc(e)}"
317
+ logger.error(msg)
318
+ raise BookalimoError(msg) from e
319
+
320
+
321
+ async def handle_autocomplete_impl_async(
322
+ transport_call: Callable[[dict[str, Any]], Awaitable[Any]],
323
+ request: models.AutocompletePlacesRequest,
324
+ ) -> models.AutocompletePlacesResponse:
325
+ """Shared implementation for async autocomplete method."""
326
+
327
+ proto = await _call_gplaces_async(
328
+ "Google Places Autocomplete", transport_call, request.model_dump()
329
+ )
330
+ return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
331
+
332
+
333
+ def handle_search_impl(
334
+ transport_call: Callable[[dict[str, Any], tuple[tuple[str, str], ...]], Any],
335
+ query: Optional[str] = None,
336
+ request: Optional[models.SearchTextRequest] = None,
337
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
338
+ **kwargs: Any,
339
+ ) -> list[models.Place]:
340
+ """Shared implementation for search method."""
341
+ request_params, metadata = _prepare_search_params_and_metadata(
342
+ query, request, fields, **kwargs
343
+ )
344
+
345
+ protos = _call_gplaces_sync(
346
+ "Google Places Text Search", transport_call, request_params, metadata
347
+ )
348
+ return _normalize_search_results(protos)
349
+
350
+
351
+ async def handle_search_impl_async(
352
+ transport_call: Callable[
353
+ [dict[str, Any], tuple[tuple[str, str], ...]], Awaitable[Any]
354
+ ],
355
+ query: Optional[str] = None,
356
+ request: Optional[models.SearchTextRequest] = None,
357
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
358
+ **kwargs: Any,
359
+ ) -> list[models.Place]:
360
+ """Shared implementation for async search method."""
361
+ request_params, metadata = _prepare_search_params_and_metadata(
362
+ query, request, fields, **kwargs
363
+ )
364
+
365
+ protos = await _call_gplaces_async(
366
+ "Google Places Text Search", transport_call, request_params, metadata
367
+ )
368
+ return _normalize_search_results(protos)
369
+
370
+
371
+ def handle_get_place_impl(
372
+ transport_call: Callable[[dict[str, Any], tuple[tuple[str, str], ...]], Any],
373
+ place_id: Optional[str] = None,
374
+ request: Optional[models.GetPlaceRequest] = None,
375
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
376
+ ) -> Optional[models.Place]:
377
+ """Shared implementation for get place method."""
378
+ request_params, metadata = _prepare_get_params_and_metadata(
379
+ place_id, request, fields
380
+ )
381
+
382
+ proto = _call_gplaces_sync(
383
+ "Google Places Get Place",
384
+ transport_call,
385
+ request_params,
386
+ metadata,
387
+ not_found_ok=True,
388
+ )
389
+ return None if proto is None else _normalize_place_from_proto(proto)
390
+
391
+
392
+ async def handle_get_place_impl_async(
393
+ transport_call: Callable[
394
+ [dict[str, Any], tuple[tuple[str, str], ...]], Awaitable[Any]
395
+ ],
396
+ place_id: Optional[str] = None,
397
+ request: Optional[models.GetPlaceRequest] = None,
398
+ fields: FieldMaskInput = DEFAULT_PLACE_FIELDS,
399
+ ) -> Optional[models.Place]:
400
+ """Shared implementation for async get place method."""
401
+ request_params, metadata = _prepare_get_params_and_metadata(
402
+ place_id, request, fields
403
+ )
404
+
405
+ proto = await _call_gplaces_async(
406
+ "Google Places Get Place",
407
+ transport_call,
408
+ request_params,
409
+ metadata,
410
+ not_found_ok=True,
411
+ )
412
+ return None if proto is None else _normalize_place_from_proto(proto)
413
+
414
+
415
+ def _prepare_search_params_and_metadata(
416
+ query: Optional[str],
417
+ request: Optional[models.SearchTextRequest],
418
+ fields: FieldMaskInput,
419
+ **kwargs: Any,
420
+ ) -> tuple[dict[str, Any], tuple[tuple[str, str], ...]]:
421
+ """Prepare search parameters and metadata for both sync/async."""
422
+ request_params = _build_search_request_params(query, request, **kwargs)
423
+ metadata = _mask_header(fields, prefix="places")
424
+ return request_params, metadata
425
+
426
+
427
+ def _prepare_get_params_and_metadata(
428
+ place_id: Optional[str],
429
+ request: Optional[models.GetPlaceRequest],
430
+ fields: FieldMaskInput,
431
+ ) -> tuple[dict[str, Any], tuple[tuple[str, str], ...]]:
432
+ """Prepare get place parameters and metadata for both sync/async."""
433
+ request_params = _build_get_place_request(place_id, request)
434
+ metadata = _mask_header(fields)
435
+ return request_params, metadata
436
+
437
+
438
+ def create_search_text_request(
439
+ query: str,
440
+ region_code: Optional[str],
441
+ ) -> models.SearchTextRequest:
442
+ return models.SearchTextRequest(
443
+ text_query=query,
444
+ region_code=region_code,
445
+ max_result_count=5,
446
+ rank_preference=RankPreference.RELEVANCE,
447
+ # included_type="airport",
448
+ strict_type_filtering=False,
449
+ )
450
+
451
+
452
+ def handle_resolve_airport_preprocessing(
453
+ query: Optional[str],
454
+ place_id: Optional[str],
455
+ places: Optional[list[models.Place]],
456
+ max_distance_km: Optional[float],
457
+ ) -> tuple[Optional[str], list[models.Place], bool]:
458
+ """
459
+ Handle the preprocessing logic for resolve_airport that doesn't depend on sync/async.
460
+ Returns (effective_query_or_none, places_to_resolve, needs_search_call).
461
+ If needs_search_call is True, the caller should perform a search using the query.
462
+ """
463
+ # Validate inputs
464
+ _validate_resolve_airport_inputs(place_id, places, max_distance_km)
465
+
466
+ # Handle different input scenarios
467
+ if place_id is not None:
468
+ # Caller needs to fetch the place using get()
469
+ return query, [], True # Signal that caller needs to call get()
470
+
471
+ elif places is not None:
472
+ if len(places) == 0 and (query is None or not str(query).strip()):
473
+ raise ValueError(
474
+ "Empty 'places' and no 'query' provided; nothing to resolve."
475
+ )
476
+ return query, places, False
477
+
478
+ else:
479
+ # Neither place_id nor places: fall back to query-driven search
480
+ if query is None or not str(query).strip():
481
+ raise ValueError("Either place_id, places, or query must be provided.")
482
+ return query, [], True # Signal that caller needs to call search()
483
+
484
+
485
+ def handle_resolve_airport_postprocessing(
486
+ query: Optional[str],
487
+ effective_places: list[models.Place],
488
+ max_distance_km: Optional[float],
489
+ max_results: Optional[int],
490
+ confidence_threshold: Optional[float],
491
+ text_weight: float,
492
+ ) -> list[models.ResolvedAirport]:
493
+ """Handle the final processing logic for resolve_airport."""
494
+
495
+ # Derive effective query
496
+ effective_query = _derive_effective_query(query, effective_places)
497
+
498
+ google_places = [
499
+ p.google_place for p in effective_places if p.google_place is not None
500
+ ]
501
+
502
+ return resolve_airport(
503
+ effective_query,
504
+ google_places,
505
+ max_distance_km,
506
+ max_results,
507
+ confidence_threshold,
508
+ text_weight,
509
+ )