bookalimo 0.1.4__py3-none-any.whl → 1.0.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.
- 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 +258 -0
- bookalimo/integrations/google_places/client_sync.py +257 -0
- bookalimo/integrations/google_places/common.py +245 -0
- bookalimo/integrations/google_places/proto_adapter.py +224 -0
- bookalimo/{_logging.py → logging.py} +59 -62
- bookalimo/schemas/__init__.py +97 -0
- bookalimo/schemas/base.py +56 -0
- bookalimo/{models.py → schemas/booking.py} +88 -100
- bookalimo/schemas/places/__init__.py +37 -0
- bookalimo/schemas/places/common.py +198 -0
- bookalimo/schemas/places/google.py +596 -0
- bookalimo/schemas/places/place.py +337 -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.0.dist-info/METADATA +307 -0
- bookalimo-1.0.0.dist-info/RECORD +35 -0
- bookalimo/_client.py +0 -420
- bookalimo/wrapper.py +0 -444
- bookalimo-0.1.4.dist-info/METADATA +0 -392
- bookalimo-0.1.4.dist-info/RECORD +0 -12
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,258 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from os import getenv
|
5
|
+
from types import TracebackType
|
6
|
+
from typing import Any, Optional, TypeVar, cast
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
from google.api_core import exceptions as gexc
|
10
|
+
from google.api_core.client_options import ClientOptions
|
11
|
+
from google.maps.places_v1 import PlacesAsyncClient
|
12
|
+
from typing_extensions import ParamSpec
|
13
|
+
|
14
|
+
from ...exceptions import BookalimoError
|
15
|
+
from ...logging import get_logger
|
16
|
+
from ...schemas.places import google as models
|
17
|
+
from ...schemas.places.place import Place as GooglePlace
|
18
|
+
from .common import (
|
19
|
+
ADDRESS_TYPES,
|
20
|
+
DEFAULT_PLACE_FIELDS,
|
21
|
+
DEFAULT_PLACE_LIST_FIELDS,
|
22
|
+
Fields,
|
23
|
+
PlaceListFields,
|
24
|
+
fmt_exc,
|
25
|
+
mask_header,
|
26
|
+
)
|
27
|
+
from .proto_adapter import validate_proto_to_model
|
28
|
+
|
29
|
+
logger = get_logger("places")
|
30
|
+
|
31
|
+
P = ParamSpec("P")
|
32
|
+
R = TypeVar("R")
|
33
|
+
|
34
|
+
|
35
|
+
def _strip_html(s: str) -> str:
|
36
|
+
# Simple fallback for adr_format_address (which is HTML)
|
37
|
+
return re.sub(r"<[^>]+>", "", s) if s else s
|
38
|
+
|
39
|
+
|
40
|
+
def _infer_place_type(m: GooglePlace) -> str:
|
41
|
+
# 1) Airport wins outright
|
42
|
+
tset = set(m.types or [])
|
43
|
+
ptype = (m.primary_type or "").lower() if getattr(m, "primary_type", None) else ""
|
44
|
+
if "airport" in tset or ptype == "airport":
|
45
|
+
return "airport"
|
46
|
+
# 2) Anything that looks like a geocoded address
|
47
|
+
if tset & ADDRESS_TYPES:
|
48
|
+
return "address"
|
49
|
+
# 3) Otherwise treat as a point of interest
|
50
|
+
return "poi"
|
51
|
+
|
52
|
+
|
53
|
+
def _get_lat_lng(model: GooglePlace) -> tuple[float, float]:
|
54
|
+
if model.location:
|
55
|
+
lat = model.location.latitude
|
56
|
+
lng = model.location.longitude
|
57
|
+
elif model.viewport:
|
58
|
+
lat = model.viewport.low.latitude
|
59
|
+
lng = model.viewport.low.longitude
|
60
|
+
else:
|
61
|
+
lat = 0
|
62
|
+
lng = 0
|
63
|
+
return lat, lng
|
64
|
+
|
65
|
+
|
66
|
+
class AsyncGooglePlaces:
|
67
|
+
"""
|
68
|
+
Google Places API client for address validation, geocoding, and autocomplete.
|
69
|
+
Provides location resolution services that integrate seamlessly with
|
70
|
+
Book-A-Limo location factory functions.
|
71
|
+
"""
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
api_key: Optional[str] = None,
|
76
|
+
client: Optional[PlacesAsyncClient] = None,
|
77
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
78
|
+
):
|
79
|
+
"""
|
80
|
+
Initialize Google Places client.
|
81
|
+
Args:
|
82
|
+
api_key: Google Places API key. If not provided, it will be read from the GOOGLE_PLACES_API_KEY environment variable.
|
83
|
+
client: Optional `PlacesAsyncClient` instance.
|
84
|
+
http_client: Optional `httpx.AsyncClient` instance.
|
85
|
+
"""
|
86
|
+
self.http_client = http_client or httpx.AsyncClient()
|
87
|
+
if client:
|
88
|
+
self.client = client
|
89
|
+
else:
|
90
|
+
api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
|
91
|
+
if not api_key:
|
92
|
+
raise ValueError("Google Places API key is required.")
|
93
|
+
self.client = PlacesAsyncClient(
|
94
|
+
client_options=ClientOptions(api_key=api_key),
|
95
|
+
)
|
96
|
+
|
97
|
+
async def __aenter__(self) -> AsyncGooglePlaces:
|
98
|
+
return self
|
99
|
+
|
100
|
+
async def __aexit__(
|
101
|
+
self,
|
102
|
+
exc_type: Optional[type[BaseException]],
|
103
|
+
exc_val: Optional[BaseException],
|
104
|
+
exc_tb: Optional[TracebackType],
|
105
|
+
) -> None:
|
106
|
+
await self.aclose()
|
107
|
+
|
108
|
+
async def aclose(self) -> None:
|
109
|
+
"""Close underlying transports safely."""
|
110
|
+
try:
|
111
|
+
await self.client.transport.close()
|
112
|
+
finally:
|
113
|
+
await self.http_client.aclose()
|
114
|
+
|
115
|
+
async def autocomplete(
|
116
|
+
self, request: models.AutocompletePlacesRequest, **kwargs: Any
|
117
|
+
) -> models.AutocompletePlacesResponse:
|
118
|
+
"""
|
119
|
+
Get autocomplete suggestions for a location query.
|
120
|
+
Args:
|
121
|
+
request: AutocompletePlacesRequest object.
|
122
|
+
**kwargs: Additional parameters for the Google Places Autocomplete API.
|
123
|
+
Returns:
|
124
|
+
`AutocompletePlacesResponse` object.
|
125
|
+
Raises:
|
126
|
+
BookalimoError: If the API request fails.
|
127
|
+
"""
|
128
|
+
try:
|
129
|
+
proto = await self.client.autocomplete_places(
|
130
|
+
request=request.model_dump(), **kwargs
|
131
|
+
)
|
132
|
+
return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
|
133
|
+
except gexc.GoogleAPICallError as e:
|
134
|
+
msg = f"Google Places Autocomplete failed: {fmt_exc(e)}"
|
135
|
+
logger.error(msg)
|
136
|
+
raise BookalimoError(msg) from e
|
137
|
+
|
138
|
+
async def geocode(self, request: models.GeocodingRequest) -> dict[str, Any]:
|
139
|
+
try:
|
140
|
+
r = await self.http_client.get(
|
141
|
+
"https://maps.googleapis.com/maps/api/geocode/json",
|
142
|
+
params=request.to_query_params(),
|
143
|
+
)
|
144
|
+
r.raise_for_status()
|
145
|
+
return cast(dict[str, Any], r.json())
|
146
|
+
except httpx.HTTPError as e:
|
147
|
+
msg = f"HTTP geocoding failed: {fmt_exc(e)}"
|
148
|
+
logger.error(msg)
|
149
|
+
raise BookalimoError(msg) from e
|
150
|
+
|
151
|
+
async def search(
|
152
|
+
self,
|
153
|
+
query: str,
|
154
|
+
*,
|
155
|
+
fields: PlaceListFields = DEFAULT_PLACE_LIST_FIELDS,
|
156
|
+
**kwargs: Any,
|
157
|
+
) -> list[models.Place]:
|
158
|
+
"""
|
159
|
+
Search for places using a text query.
|
160
|
+
Args:
|
161
|
+
query: The text query to search for.
|
162
|
+
**kwargs: Additional parameters for the Text Search API.
|
163
|
+
Returns:
|
164
|
+
list[models.Place]
|
165
|
+
Raises:
|
166
|
+
BookalimoError: If the API request fails.
|
167
|
+
"""
|
168
|
+
metadata = mask_header(fields)
|
169
|
+
try:
|
170
|
+
protos = await self.client.search_text(
|
171
|
+
request={"text_query": query, **kwargs},
|
172
|
+
metadata=metadata,
|
173
|
+
)
|
174
|
+
pydantic_models = [
|
175
|
+
validate_proto_to_model(proto, GooglePlace) for proto in protos.places
|
176
|
+
]
|
177
|
+
place_models: list[models.Place] = []
|
178
|
+
for model in pydantic_models:
|
179
|
+
adr_format = getattr(model, "adr_format_address", None)
|
180
|
+
addr = (
|
181
|
+
getattr(model, "formatted_address", None)
|
182
|
+
or getattr(model, "short_formatted_address", None)
|
183
|
+
or _strip_html(adr_format or "")
|
184
|
+
or ""
|
185
|
+
)
|
186
|
+
lat, lng = _get_lat_lng(model)
|
187
|
+
place_models.append(
|
188
|
+
models.Place(
|
189
|
+
formatted_address=addr,
|
190
|
+
lat=lat,
|
191
|
+
lng=lng,
|
192
|
+
place_type=_infer_place_type(model),
|
193
|
+
iata_code=None,
|
194
|
+
google_place=model,
|
195
|
+
)
|
196
|
+
)
|
197
|
+
return place_models
|
198
|
+
except gexc.InvalidArgument as e:
|
199
|
+
# Often caused by missing/invalid field mask
|
200
|
+
msg = f"Google Places Text Search invalid argument: {fmt_exc(e)}"
|
201
|
+
logger.error(msg)
|
202
|
+
raise BookalimoError(msg) from e
|
203
|
+
except gexc.GoogleAPICallError as e:
|
204
|
+
msg = f"Google Places Text Search failed: {fmt_exc(e)}"
|
205
|
+
logger.error(msg)
|
206
|
+
raise BookalimoError(msg) from e
|
207
|
+
|
208
|
+
async def get(
|
209
|
+
self,
|
210
|
+
place_id: models.GetPlaceRequest,
|
211
|
+
*,
|
212
|
+
fields: Fields = DEFAULT_PLACE_FIELDS,
|
213
|
+
**kwargs: Any,
|
214
|
+
) -> Optional[models.Place]:
|
215
|
+
"""
|
216
|
+
Get details for a specific place.
|
217
|
+
Args:
|
218
|
+
place_id: The ID of the place to retrieve details for.
|
219
|
+
**kwargs: Additional parameters for the Get Place API.
|
220
|
+
Returns:
|
221
|
+
A models.Place object or None if not found.
|
222
|
+
Raises:
|
223
|
+
BookalimoError: If the API request fails.
|
224
|
+
"""
|
225
|
+
metadata = mask_header(fields)
|
226
|
+
try:
|
227
|
+
proto = await self.client.get_place(
|
228
|
+
request={"name": f"places/{place_id}", **kwargs},
|
229
|
+
metadata=metadata,
|
230
|
+
)
|
231
|
+
# Convert proto to GooglePlace first, then process like search
|
232
|
+
model = validate_proto_to_model(proto, GooglePlace)
|
233
|
+
adr_format = getattr(model, "adr_format_address", None)
|
234
|
+
addr = (
|
235
|
+
getattr(model, "formatted_address", None)
|
236
|
+
or getattr(model, "short_formatted_address", None)
|
237
|
+
or _strip_html(adr_format or "")
|
238
|
+
or ""
|
239
|
+
)
|
240
|
+
lat, lng = _get_lat_lng(model)
|
241
|
+
return models.Place(
|
242
|
+
formatted_address=addr,
|
243
|
+
lat=lat,
|
244
|
+
lng=lng,
|
245
|
+
place_type=_infer_place_type(model),
|
246
|
+
iata_code=None,
|
247
|
+
google_place=model,
|
248
|
+
)
|
249
|
+
except gexc.NotFound:
|
250
|
+
return None
|
251
|
+
except gexc.InvalidArgument as e:
|
252
|
+
msg = f"Google Places Get Place invalid argument: {fmt_exc(e)}"
|
253
|
+
logger.error(msg)
|
254
|
+
raise BookalimoError(msg) from e
|
255
|
+
except gexc.GoogleAPICallError as e:
|
256
|
+
msg = f"Google Places Get Place failed: {fmt_exc(e)}"
|
257
|
+
logger.error(msg)
|
258
|
+
raise BookalimoError(msg) from e
|
@@ -0,0 +1,257 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from collections.abc import Sequence
|
5
|
+
from os import getenv
|
6
|
+
from typing import Any, Optional, TypeVar, cast
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
from google.api_core import exceptions as gexc
|
10
|
+
from google.api_core.client_options import ClientOptions
|
11
|
+
from google.maps.places_v1 import PlacesClient
|
12
|
+
from typing_extensions import ParamSpec
|
13
|
+
|
14
|
+
from ...exceptions import BookalimoError
|
15
|
+
from ...logging import get_logger
|
16
|
+
from ...schemas.places import google as models
|
17
|
+
from ...schemas.places.place import Place as GooglePlace
|
18
|
+
from .common import (
|
19
|
+
ADDRESS_TYPES,
|
20
|
+
DEFAULT_PLACE_FIELDS,
|
21
|
+
DEFAULT_PLACE_LIST_FIELDS,
|
22
|
+
PlaceListFields,
|
23
|
+
fmt_exc,
|
24
|
+
mask_header,
|
25
|
+
)
|
26
|
+
from .proto_adapter import validate_proto_to_model
|
27
|
+
|
28
|
+
logger = get_logger("places")
|
29
|
+
|
30
|
+
P = ParamSpec("P")
|
31
|
+
R = TypeVar("R")
|
32
|
+
|
33
|
+
|
34
|
+
def _strip_html(s: str) -> str:
|
35
|
+
# Simple fallback for adr_format_address (which is HTML)
|
36
|
+
return re.sub(r"<[^>]+>", "", s) if s else s
|
37
|
+
|
38
|
+
|
39
|
+
def _infer_place_type(m: GooglePlace) -> str:
|
40
|
+
# 1) Airport wins outright
|
41
|
+
tset = set(m.types or [])
|
42
|
+
ptype = (m.primary_type or "").lower() if getattr(m, "primary_type", None) else ""
|
43
|
+
if "airport" in tset or ptype == "airport":
|
44
|
+
return "airport"
|
45
|
+
# 2) Anything that looks like a geocoded address
|
46
|
+
if tset & ADDRESS_TYPES:
|
47
|
+
return "address"
|
48
|
+
# 3) Otherwise treat as a point of interest
|
49
|
+
return "poi"
|
50
|
+
|
51
|
+
|
52
|
+
def _get_lat_lng(model: GooglePlace) -> tuple[float, float]:
|
53
|
+
if model.location:
|
54
|
+
lat = model.location.latitude
|
55
|
+
lng = model.location.longitude
|
56
|
+
elif model.viewport:
|
57
|
+
lat = model.viewport.low.latitude
|
58
|
+
lng = model.viewport.low.longitude
|
59
|
+
else:
|
60
|
+
lat = 0
|
61
|
+
lng = 0
|
62
|
+
return lat, lng
|
63
|
+
|
64
|
+
|
65
|
+
class GooglePlaces:
|
66
|
+
"""
|
67
|
+
Google Places API synchronous client for address validation, geocoding, and autocomplete.
|
68
|
+
Provides location resolution services that integrate seamlessly with
|
69
|
+
Book-A-Limo location factory functions.
|
70
|
+
"""
|
71
|
+
|
72
|
+
def __init__(
|
73
|
+
self,
|
74
|
+
api_key: Optional[str] = None,
|
75
|
+
client: Optional[PlacesClient] = None,
|
76
|
+
http_client: Optional[httpx.Client] = None,
|
77
|
+
):
|
78
|
+
"""
|
79
|
+
Initialize Google Places client.
|
80
|
+
Args:
|
81
|
+
api_key: Google Places API key. If not provided, it will be read from the GOOGLE_PLACES_API_KEY environment variable.
|
82
|
+
client: Optional `PlacesClient` instance.
|
83
|
+
http_client: Optional `httpx.Client` instance.
|
84
|
+
"""
|
85
|
+
self.http_client = http_client or httpx.Client()
|
86
|
+
if client:
|
87
|
+
self.client = client
|
88
|
+
else:
|
89
|
+
api_key = api_key or getenv("GOOGLE_PLACES_API_KEY")
|
90
|
+
if not api_key:
|
91
|
+
raise ValueError("Google Places API key is required.")
|
92
|
+
self.client = PlacesClient(
|
93
|
+
client_options=ClientOptions(api_key=api_key),
|
94
|
+
)
|
95
|
+
|
96
|
+
def __enter__(self) -> GooglePlaces:
|
97
|
+
return self
|
98
|
+
|
99
|
+
def __exit__(
|
100
|
+
self,
|
101
|
+
exc_type: Optional[type[BaseException]],
|
102
|
+
exc_val: Optional[BaseException],
|
103
|
+
exc_tb: Optional[BaseException],
|
104
|
+
) -> None:
|
105
|
+
self.close()
|
106
|
+
|
107
|
+
def close(self) -> None:
|
108
|
+
"""Close underlying transports safely."""
|
109
|
+
try:
|
110
|
+
self.client.transport.close()
|
111
|
+
finally:
|
112
|
+
self.http_client.close()
|
113
|
+
|
114
|
+
def autocomplete(
|
115
|
+
self, request: models.AutocompletePlacesRequest, **kwargs: Any
|
116
|
+
) -> models.AutocompletePlacesResponse:
|
117
|
+
"""
|
118
|
+
Get autocomplete suggestions for a location query.
|
119
|
+
Args:
|
120
|
+
request: AutocompletePlacesRequest object.
|
121
|
+
**kwargs: Additional parameters for the Google Places Autocomplete API.
|
122
|
+
Returns:
|
123
|
+
`AutocompletePlacesResponse` object.
|
124
|
+
Raises:
|
125
|
+
BookalimoError: If the API request fails.
|
126
|
+
"""
|
127
|
+
try:
|
128
|
+
proto = self.client.autocomplete_places(
|
129
|
+
request=request.model_dump(), **kwargs
|
130
|
+
)
|
131
|
+
return validate_proto_to_model(proto, models.AutocompletePlacesResponse)
|
132
|
+
except gexc.GoogleAPICallError as e:
|
133
|
+
msg = f"Google Places Autocomplete failed: {fmt_exc(e)}"
|
134
|
+
logger.error(msg)
|
135
|
+
raise BookalimoError(msg) from e
|
136
|
+
|
137
|
+
def geocode(self, request: models.GeocodingRequest) -> dict[str, Any]:
|
138
|
+
try:
|
139
|
+
r = self.http_client.get(
|
140
|
+
"https://maps.googleapis.com/maps/api/geocode/json",
|
141
|
+
params=request.to_query_params(),
|
142
|
+
)
|
143
|
+
r.raise_for_status()
|
144
|
+
return cast(dict[str, Any], r.json())
|
145
|
+
except httpx.HTTPError as e:
|
146
|
+
msg = f"HTTP geocoding failed: {fmt_exc(e)}"
|
147
|
+
logger.error(msg)
|
148
|
+
raise BookalimoError(msg) from e
|
149
|
+
|
150
|
+
def search(
|
151
|
+
self,
|
152
|
+
query: str,
|
153
|
+
*,
|
154
|
+
fields: PlaceListFields = DEFAULT_PLACE_LIST_FIELDS,
|
155
|
+
**kwargs: Any,
|
156
|
+
) -> list[models.Place]:
|
157
|
+
"""
|
158
|
+
Search for places using a text query.
|
159
|
+
Args:
|
160
|
+
query: The text query to search for.
|
161
|
+
**kwargs: Additional parameters for the Text Search API.
|
162
|
+
Returns:
|
163
|
+
list[models.Place]
|
164
|
+
Raises:
|
165
|
+
BookalimoError: If the API request fails.
|
166
|
+
"""
|
167
|
+
metadata = mask_header(fields)
|
168
|
+
try:
|
169
|
+
protos = self.client.search_text(
|
170
|
+
request={"text_query": query, **kwargs},
|
171
|
+
metadata=metadata,
|
172
|
+
)
|
173
|
+
pydantic_models = [
|
174
|
+
validate_proto_to_model(proto, GooglePlace) for proto in protos.places
|
175
|
+
]
|
176
|
+
place_models: list[models.Place] = []
|
177
|
+
for model in pydantic_models:
|
178
|
+
adr_format = getattr(model, "adr_format_address", None)
|
179
|
+
addr = (
|
180
|
+
getattr(model, "formatted_address", None)
|
181
|
+
or getattr(model, "short_formatted_address", None)
|
182
|
+
or _strip_html(adr_format or "")
|
183
|
+
or ""
|
184
|
+
)
|
185
|
+
lat, lng = _get_lat_lng(model)
|
186
|
+
place_models.append(
|
187
|
+
models.Place(
|
188
|
+
formatted_address=addr,
|
189
|
+
lat=lat,
|
190
|
+
lng=lng,
|
191
|
+
place_type=_infer_place_type(model),
|
192
|
+
iata_code=None,
|
193
|
+
google_place=model,
|
194
|
+
)
|
195
|
+
)
|
196
|
+
return place_models
|
197
|
+
except gexc.InvalidArgument as e:
|
198
|
+
# Often caused by missing/invalid field mask
|
199
|
+
msg = f"Google Places Text Search invalid argument: {fmt_exc(e)}"
|
200
|
+
logger.error(msg)
|
201
|
+
raise BookalimoError(msg) from e
|
202
|
+
except gexc.GoogleAPICallError as e:
|
203
|
+
msg = f"Google Places Text Search failed: {fmt_exc(e)}"
|
204
|
+
logger.error(msg)
|
205
|
+
raise BookalimoError(msg) from e
|
206
|
+
|
207
|
+
def get(
|
208
|
+
self,
|
209
|
+
place_id: models.GetPlaceRequest,
|
210
|
+
*,
|
211
|
+
fields: Sequence[str] | str = DEFAULT_PLACE_FIELDS,
|
212
|
+
**kwargs: Any,
|
213
|
+
) -> Optional[models.Place]:
|
214
|
+
"""
|
215
|
+
Get details for a specific place.
|
216
|
+
Args:
|
217
|
+
place_id: The ID of the place to retrieve details for.
|
218
|
+
**kwargs: Additional parameters for the Get Place API.
|
219
|
+
Returns:
|
220
|
+
A models.Place object or None if not found.
|
221
|
+
Raises:
|
222
|
+
BookalimoError: If the API request fails.
|
223
|
+
"""
|
224
|
+
metadata = mask_header(fields)
|
225
|
+
try:
|
226
|
+
proto = self.client.get_place(
|
227
|
+
request={"name": f"places/{place_id}", **kwargs},
|
228
|
+
metadata=metadata,
|
229
|
+
)
|
230
|
+
# Convert proto to GooglePlace first, then process like search
|
231
|
+
model = validate_proto_to_model(proto, GooglePlace)
|
232
|
+
adr_format = getattr(model, "adr_format_address", None)
|
233
|
+
addr = (
|
234
|
+
getattr(model, "formatted_address", None)
|
235
|
+
or getattr(model, "short_formatted_address", None)
|
236
|
+
or _strip_html(adr_format or "")
|
237
|
+
or ""
|
238
|
+
)
|
239
|
+
lat, lng = _get_lat_lng(model)
|
240
|
+
return models.Place(
|
241
|
+
formatted_address=addr,
|
242
|
+
lat=lat,
|
243
|
+
lng=lng,
|
244
|
+
place_type=_infer_place_type(model),
|
245
|
+
iata_code=None,
|
246
|
+
google_place=model,
|
247
|
+
)
|
248
|
+
except gexc.NotFound:
|
249
|
+
return None
|
250
|
+
except gexc.InvalidArgument as e:
|
251
|
+
msg = f"Google Places Get Place invalid argument: {fmt_exc(e)}"
|
252
|
+
logger.error(msg)
|
253
|
+
raise BookalimoError(msg) from e
|
254
|
+
except gexc.GoogleAPICallError as e:
|
255
|
+
msg = f"Google Places Get Place failed: {fmt_exc(e)}"
|
256
|
+
logger.error(msg)
|
257
|
+
raise BookalimoError(msg) from e
|