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.
Files changed (42) hide show
  1. bookalimo/__init__.py +17 -24
  2. bookalimo/_version.py +9 -0
  3. bookalimo/client.py +310 -0
  4. bookalimo/config.py +16 -0
  5. bookalimo/exceptions.py +115 -5
  6. bookalimo/integrations/__init__.py +1 -0
  7. bookalimo/integrations/google_places/__init__.py +31 -0
  8. bookalimo/integrations/google_places/client_async.py +289 -0
  9. bookalimo/integrations/google_places/client_sync.py +287 -0
  10. bookalimo/integrations/google_places/common.py +231 -0
  11. bookalimo/integrations/google_places/proto_adapter.py +224 -0
  12. bookalimo/integrations/google_places/resolve_airport.py +397 -0
  13. bookalimo/integrations/google_places/transports.py +98 -0
  14. bookalimo/{_logging.py → logging.py} +45 -42
  15. bookalimo/schemas/__init__.py +103 -0
  16. bookalimo/schemas/base.py +56 -0
  17. bookalimo/{models.py → schemas/booking.py} +88 -100
  18. bookalimo/schemas/places/__init__.py +62 -0
  19. bookalimo/schemas/places/common.py +351 -0
  20. bookalimo/schemas/places/field_mask.py +221 -0
  21. bookalimo/schemas/places/google.py +883 -0
  22. bookalimo/schemas/places/place.py +334 -0
  23. bookalimo/services/__init__.py +11 -0
  24. bookalimo/services/pricing.py +191 -0
  25. bookalimo/services/reservations.py +227 -0
  26. bookalimo/transport/__init__.py +7 -0
  27. bookalimo/transport/auth.py +41 -0
  28. bookalimo/transport/base.py +44 -0
  29. bookalimo/transport/httpx_async.py +230 -0
  30. bookalimo/transport/httpx_sync.py +230 -0
  31. bookalimo/transport/retry.py +102 -0
  32. bookalimo/transport/utils.py +59 -0
  33. bookalimo-1.0.1.dist-info/METADATA +370 -0
  34. bookalimo-1.0.1.dist-info/RECORD +38 -0
  35. bookalimo-1.0.1.dist-info/licenses/LICENSE +21 -0
  36. bookalimo/_client.py +0 -420
  37. bookalimo/wrapper.py +0 -444
  38. bookalimo-0.1.5.dist-info/METADATA +0 -392
  39. bookalimo-0.1.5.dist-info/RECORD +0 -12
  40. bookalimo-0.1.5.dist-info/licenses/LICENSE +0 -0
  41. {bookalimo-0.1.5.dist-info → bookalimo-1.0.1.dist-info}/WHEEL +0 -0
  42. {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
+ )