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.
bookalimo/client.py CHANGED
@@ -59,7 +59,7 @@ class AsyncBookalimo:
59
59
  google_places_api_key="your-google-api-key"
60
60
  ) as client:
61
61
  # Find locations using Google Places
62
- places = await client.places.search_text("Empire State Building")
62
+ places = await client.places.search("Empire State Building")
63
63
 
64
64
  # Use in booking
65
65
  quote = await client.pricing.quote(
@@ -93,8 +93,7 @@ class AsyncBookalimo:
93
93
  transport: Custom transport instance (optional)
94
94
  google_places_api_key: Google Places API key for location services (optional)
95
95
  """
96
-
97
- transport_credentials = transport.credentials if transport else None
96
+ transport_credentials = transport._credentials if transport else None
98
97
 
99
98
  both_provided = all([transport_credentials, credentials])
100
99
  both_missing = not any([transport_credentials, credentials])
@@ -113,18 +112,21 @@ class AsyncBookalimo:
113
112
  stacklevel=2,
114
113
  )
115
114
 
116
- # Use whichever exists when we need to build a transport ourselves
117
115
  effective_credentials = (
118
- credentials if credentials is not None else transport_credentials
116
+ transport_credentials if transport_credentials is not None else credentials
119
117
  )
118
+
120
119
  if transport:
121
- transport.credentials = effective_credentials
122
- self._transport = transport or AsyncTransport(
123
- base_url=base_url,
124
- timeouts=timeouts,
125
- user_agent=user_agent,
126
- credentials=effective_credentials,
127
- )
120
+ if transport_credentials is None and credentials is not None:
121
+ transport.credentials = credentials
122
+ self._transport = transport
123
+ else:
124
+ self._transport = AsyncTransport(
125
+ base_url=base_url,
126
+ timeouts=timeouts,
127
+ user_agent=user_agent,
128
+ credentials=effective_credentials,
129
+ )
128
130
 
129
131
  # Initialize service instances
130
132
  self.reservations = AsyncReservationsService(self._transport)
@@ -149,7 +151,7 @@ class AsyncBookalimo:
149
151
  Auth priority is as follows:
150
152
  - provided api key in constructor
151
153
  - GOOGLE_PLACES_API_KEY environment variable
152
- - Google ADC - Except for Geocoding API.
154
+ - Google ADC - Not yet implemented.
153
155
  """
154
156
  if not _GOOGLE_PLACES_AVAILABLE:
155
157
  raise ImportError(
@@ -207,7 +209,7 @@ class Bookalimo:
207
209
  google_places_api_key="your-google-api-key"
208
210
  ) as client:
209
211
  # Find locations using Google Places
210
- places = client.places.search_text("Empire State Building")
212
+ places = client.places.search("Empire State Building")
211
213
 
212
214
  # Use in booking
213
215
  quote = client.pricing.quote(
@@ -241,20 +243,41 @@ class Bookalimo:
241
243
  transport: Custom transport instance (optional)
242
244
  google_places_api_key: Google Places API key for location services (optional)
243
245
  """
244
- if transport and transport.credentials is not None and credentials is not None:
246
+ transport_credentials = transport.credentials if transport else None
247
+
248
+ both_provided = all([transport_credentials, credentials])
249
+ both_missing = not any([transport_credentials, credentials])
250
+
251
+ if both_provided:
245
252
  warnings.warn(
246
253
  "Credentials provided in both transport and constructor. "
247
254
  "The transport credentials will be used.",
248
- UserWarning,
255
+ DuplicateCredentialsWarning,
249
256
  stacklevel=2,
250
257
  )
251
- self._transport = transport or SyncTransport(
252
- base_url=base_url,
253
- timeouts=timeouts,
254
- user_agent=user_agent,
255
- credentials=credentials,
258
+ elif both_missing:
259
+ warnings.warn(
260
+ "No credentials provided in transport or constructor; proceeding unauthenticated.",
261
+ MissingCredentialsWarning,
262
+ stacklevel=2,
263
+ )
264
+
265
+ effective_credentials = (
266
+ transport_credentials if transport_credentials is not None else credentials
256
267
  )
257
268
 
269
+ if transport:
270
+ if transport_credentials is None and credentials is not None:
271
+ transport.credentials = credentials
272
+ self._transport = transport
273
+ else:
274
+ self._transport = SyncTransport(
275
+ base_url=base_url,
276
+ timeouts=timeouts,
277
+ user_agent=user_agent,
278
+ credentials=effective_credentials,
279
+ )
280
+
258
281
  # Initialize service instances
259
282
  self.reservations = ReservationsService(self._transport)
260
283
  self.pricing = PricingService(self._transport)
@@ -278,7 +301,7 @@ class Bookalimo:
278
301
  Auth priority is as follows:
279
302
  - provided api key in constructor
280
303
  - GOOGLE_PLACES_API_KEY environment variable
281
- - Google ADC - Except for Geocoding API.
304
+ - Google ADC - Not yet implemented.
282
305
  """
283
306
  if not _GOOGLE_PLACES_AVAILABLE:
284
307
  raise ImportError(
@@ -2,31 +2,27 @@ from __future__ import annotations
2
2
 
3
3
  from os import getenv
4
4
  from types import TracebackType
5
- from typing import Any, Optional, TypeVar, cast
5
+ from typing import Any, Optional, TypeVar
6
6
 
7
7
  import httpx
8
- from google.api_core import exceptions as gexc
9
8
  from google.maps.places_v1 import PlacesAsyncClient
10
9
  from typing_extensions import ParamSpec
11
10
 
12
- from ...exceptions import BookalimoError
13
11
  from ...logging import get_logger
14
12
  from ...schemas.places import FieldMaskInput
15
13
  from ...schemas.places import google as models
16
14
  from .common import (
17
15
  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,
16
+ create_search_text_request,
17
+ handle_autocomplete_impl_async,
18
+ handle_geocode_response,
19
+ handle_get_place_impl_async,
20
+ handle_resolve_airport_postprocessing,
21
+ handle_resolve_airport_preprocessing,
22
+ handle_search_impl_async,
23
+ prepare_geocode_params,
25
24
  validate_autocomplete_inputs,
26
- validate_resolve_airport_inputs,
27
25
  )
28
- from .proto_adapter import validate_proto_to_model
29
- from .resolve_airport import resolve_airport
30
26
  from .transports import GoogleAsyncTransport
31
27
 
32
28
  logger = get_logger("places")
@@ -56,10 +52,10 @@ class AsyncGooglePlaces:
56
52
  http_client: Optional `httpx.AsyncClient` instance.
57
53
  """
58
54
  self.http_client = http_client or httpx.AsyncClient()
59
- api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
60
- if not api_key:
55
+ self.api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
56
+ if not self.api_key:
61
57
  raise ValueError("Google Places API key is required.")
62
- self.transport = GoogleAsyncTransport(api_key, client)
58
+ self.transport = GoogleAsyncTransport(self.api_key, client)
63
59
 
64
60
  async def __aenter__(self) -> AsyncGooglePlaces:
65
61
  return self
@@ -99,28 +95,19 @@ class AsyncGooglePlaces:
99
95
  BookalimoError: If the API request fails.
100
96
  """
101
97
  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
98
+ return await handle_autocomplete_impl_async(
99
+ lambda req: self.transport.autocomplete_places(request=req),
100
+ request,
101
+ )
111
102
 
112
103
  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
104
+ assert self.api_key is not None # Validated in __init__
105
+ params = prepare_geocode_params(request, self.api_key)
106
+ r = await self.http_client.get(
107
+ "https://maps.googleapis.com/maps/api/geocode/json",
108
+ params=params,
109
+ )
110
+ return handle_geocode_response(r)
124
111
 
125
112
  async def search(
126
113
  self,
@@ -143,24 +130,13 @@ class AsyncGooglePlaces:
143
130
  BookalimoError: If the API request fails.
144
131
  ValueError: If neither query nor request is provided, or if both are provided.
145
132
  """
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
133
+ return await handle_search_impl_async(
134
+ lambda req, meta: self.transport.search_text(request=req, metadata=meta),
135
+ query,
136
+ request,
137
+ fields,
138
+ **kwargs,
139
+ )
164
140
 
165
141
  async def get(
166
142
  self,
@@ -181,31 +157,19 @@ class AsyncGooglePlaces:
181
157
  ValueError: If neither place_id nor request is provided.
182
158
  BookalimoError: If the API request fails.
183
159
  """
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
160
+ return await handle_get_place_impl_async(
161
+ lambda req, meta: self.transport.get_place(request=req, metadata=meta),
162
+ place_id,
163
+ request,
164
+ fields,
165
+ )
203
166
 
204
167
  async def resolve_airport(
205
168
  self,
206
169
  query: Optional[str] = None,
207
170
  place_id: Optional[str] = None,
208
171
  places: Optional[list[models.Place]] = None,
172
+ country_code: Optional[str] = None,
209
173
  max_distance_km: Optional[float] = 100,
210
174
  max_results: Optional[int] = 5,
211
175
  confidence_threshold: Optional[float] = 0.5,
@@ -218,6 +182,7 @@ class AsyncGooglePlaces:
218
182
  query: Text query for airport search (optional)
219
183
  place_id: Google place ID for proximity matching (optional)
220
184
  places: List of existing Place objects for proximity matching (optional)
185
+ country_code: Country code for proximity matching (optional)
221
186
  max_distance_km: Maximum distance for proximity matching (default: 100km)
222
187
  max_results: Maximum number of results to return (default: 5)
223
188
  confidence_threshold: Minimum confidence threshold (default: 0.5)
@@ -242,46 +207,35 @@ class AsyncGooglePlaces:
242
207
  ValueError on invalid inputs.
243
208
  BookalimoError if underlying API requests fail.
244
209
  """
245
- # Validate inputs
246
- validate_resolve_airport_inputs(place_id, places, max_distance_km)
210
+ # Handle preprocessing that doesn't depend on sync/async
211
+ preprocessed_query, preprocessed_places, needs_call = (
212
+ handle_resolve_airport_preprocessing(
213
+ query, place_id, places, max_distance_km
214
+ )
215
+ )
247
216
 
248
- # Establish the authoritative places list
217
+ # Handle the calls that do depend on sync/async
249
218
  effective_places: list[models.Place]
250
-
251
219
  if place_id is not None:
252
220
  place = await self.get(place_id=place_id)
253
221
  if place is None:
254
222
  raise ValueError(f"Place with id {place_id!r} was not found.")
255
223
  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.")
224
+ elif needs_call and preprocessed_places == []:
225
+ # Need to perform search
268
226
  effective_places = await self.search(
269
- request=models.SearchTextRequest(
270
- text_query=str(query).strip(),
271
- max_result_count=5,
227
+ request=create_search_text_request(
228
+ query=str(preprocessed_query).strip(),
229
+ region_code=country_code,
272
230
  )
273
231
  )
232
+ else:
233
+ effective_places = preprocessed_places
274
234
 
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,
235
+ # Handle postprocessing
236
+ return handle_resolve_airport_postprocessing(
237
+ preprocessed_query,
238
+ effective_places,
285
239
  max_distance_km,
286
240
  max_results,
287
241
  confidence_threshold,
@@ -2,31 +2,27 @@ from __future__ import annotations
2
2
 
3
3
  from os import getenv
4
4
  from types import TracebackType
5
- from typing import Any, Optional, TypeVar, cast
5
+ from typing import Any, Optional, TypeVar
6
6
 
7
7
  import httpx
8
- from google.api_core import exceptions as gexc
9
8
  from google.maps.places_v1 import PlacesClient
10
9
  from typing_extensions import ParamSpec
11
10
 
12
- from ...exceptions import BookalimoError
13
11
  from ...logging import get_logger
14
12
  from ...schemas.places import FieldMaskInput
15
13
  from ...schemas.places import google as models
16
14
  from .common import (
17
15
  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,
16
+ create_search_text_request,
17
+ handle_autocomplete_impl,
18
+ handle_geocode_response,
19
+ handle_get_place_impl,
20
+ handle_resolve_airport_postprocessing,
21
+ handle_resolve_airport_preprocessing,
22
+ handle_search_impl,
23
+ prepare_geocode_params,
25
24
  validate_autocomplete_inputs,
26
- validate_resolve_airport_inputs,
27
25
  )
28
- from .proto_adapter import validate_proto_to_model
29
- from .resolve_airport import resolve_airport
30
26
  from .transports import GoogleSyncTransport
31
27
 
32
28
  logger = get_logger("places")
@@ -56,10 +52,10 @@ class GooglePlaces:
56
52
  http_client: Optional `httpx.Client` instance.
57
53
  """
58
54
  self.http_client = http_client or httpx.Client()
59
- api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
60
- if not api_key:
55
+ self.api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
56
+ if not self.api_key:
61
57
  raise ValueError("Google Places API key is required.")
62
- self.transport = GoogleSyncTransport(api_key, client)
58
+ self.transport = GoogleSyncTransport(self.api_key, client)
63
59
 
64
60
  def __enter__(self) -> GooglePlaces:
65
61
  return self
@@ -99,26 +95,19 @@ class GooglePlaces:
99
95
  BookalimoError: If the API request fails.
100
96
  """
101
97
  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
98
+ return handle_autocomplete_impl(
99
+ lambda req: self.transport.autocomplete_places(request=req),
100
+ request,
101
+ )
109
102
 
110
103
  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
104
+ assert self.api_key is not None # Validated in __init__
105
+ params = prepare_geocode_params(request, self.api_key)
106
+ r = self.http_client.get(
107
+ "https://maps.googleapis.com/maps/api/geocode/json",
108
+ params=params,
109
+ )
110
+ return handle_geocode_response(r)
122
111
 
123
112
  def search(
124
113
  self,
@@ -141,24 +130,13 @@ class GooglePlaces:
141
130
  BookalimoError: If the API request fails.
142
131
  ValueError: If neither query nor request is provided, or if both are provided.
143
132
  """
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
133
+ return handle_search_impl(
134
+ lambda req, meta: self.transport.search_text(request=req, metadata=meta),
135
+ query,
136
+ request,
137
+ fields,
138
+ **kwargs,
139
+ )
162
140
 
163
141
  def get(
164
142
  self,
@@ -179,31 +157,19 @@ class GooglePlaces:
179
157
  ValueError: If neither place_id nor request is provided.
180
158
  BookalimoError: If the API request fails.
181
159
  """
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
160
+ return handle_get_place_impl(
161
+ lambda req, meta: self.transport.get_place(request=req, metadata=meta),
162
+ place_id,
163
+ request,
164
+ fields,
165
+ )
201
166
 
202
167
  def resolve_airport(
203
168
  self,
204
169
  query: Optional[str] = None,
205
170
  place_id: Optional[str] = None,
206
171
  places: Optional[list[models.Place]] = None,
172
+ country_code: Optional[str] = None,
207
173
  max_distance_km: Optional[float] = 100,
208
174
  max_results: Optional[int] = 5,
209
175
  confidence_threshold: Optional[float] = 0.5,
@@ -216,6 +182,7 @@ class GooglePlaces:
216
182
  query: Text query for airport search (optional)
217
183
  place_id: Google place ID for proximity matching (optional)
218
184
  places: List of existing Place objects for proximity matching (optional)
185
+ country_code: Country code for proximity matching (optional)
219
186
  max_distance_km: Maximum distance for proximity matching (default: 100km)
220
187
  max_results: Maximum number of results to return (default: 5)
221
188
  confidence_threshold: Minimum confidence threshold (default: 0.5)
@@ -240,46 +207,35 @@ class GooglePlaces:
240
207
  ValueError on invalid inputs.
241
208
  BookalimoError if underlying API requests fail.
242
209
  """
243
- # Validate inputs
244
- validate_resolve_airport_inputs(place_id, places, max_distance_km)
210
+ # Handle preprocessing that doesn't depend on sync/async
211
+ preprocessed_query, preprocessed_places, needs_call = (
212
+ handle_resolve_airport_preprocessing(
213
+ query, place_id, places, max_distance_km
214
+ )
215
+ )
245
216
 
246
- # Establish the authoritative places list
217
+ # Handle the calls that do depend on sync/async
247
218
  effective_places: list[models.Place]
248
-
249
219
  if place_id is not None:
250
220
  place = self.get(place_id=place_id)
251
221
  if place is None:
252
222
  raise ValueError(f"Place with id {place_id!r} was not found.")
253
223
  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.")
224
+ elif needs_call and preprocessed_places == []:
225
+ # Need to perform search
266
226
  effective_places = self.search(
267
- request=models.SearchTextRequest(
268
- text_query=str(query).strip(),
269
- max_result_count=5,
227
+ request=create_search_text_request(
228
+ query=str(preprocessed_query).strip(),
229
+ region_code=country_code,
270
230
  )
271
231
  )
232
+ else:
233
+ effective_places = preprocessed_places
272
234
 
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,
235
+ # Handle postprocessing
236
+ return handle_resolve_airport_postprocessing(
237
+ preprocessed_query,
238
+ effective_places,
283
239
  max_distance_km,
284
240
  max_results,
285
241
  confidence_threshold,