bookalimo 1.0.0__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.
@@ -4,194 +4,31 @@ Common utilities and shared functionality for Google Places clients.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from typing import Literal, Sequence, Union
7
+ import re
8
+ from typing import Any, Optional
8
9
 
9
10
  from ...logging import get_logger
11
+ from ...schemas.places import (
12
+ AddressComponent,
13
+ AddressDescriptor,
14
+ FieldMaskInput,
15
+ GooglePlace,
16
+ Place,
17
+ PlaceType,
18
+ compile_field_mask,
19
+ )
20
+ from ...schemas.places import (
21
+ google as models,
22
+ )
10
23
 
11
24
  logger = get_logger("places")
12
25
 
13
-
14
- Fields = Union[
15
- str,
16
- Sequence[
17
- Literal[
18
- "*",
19
- # Identity
20
- "name",
21
- # Labels & typing
22
- "display_name",
23
- "types",
24
- "primary_type",
25
- "primary_type_display_name",
26
- # Phones & addresses
27
- "national_phone_number",
28
- "international_phone_number",
29
- "formatted_address",
30
- "short_formatted_address",
31
- "postal_address",
32
- "address_components",
33
- "plus_code",
34
- # Location & map
35
- "location",
36
- "viewport",
37
- # Scores, links, media
38
- "rating",
39
- "google_maps_uri",
40
- "website_uri",
41
- "reviews",
42
- "photos",
43
- # Hours
44
- "regular_opening_hours",
45
- "current_opening_hours",
46
- "current_secondary_opening_hours",
47
- "regular_secondary_opening_hours",
48
- "utc_offset_minutes",
49
- "time_zone",
50
- # Misc attributes
51
- "adr_format_address",
52
- "business_status",
53
- "price_level",
54
- "attributions",
55
- "user_rating_count",
56
- "icon_mask_base_uri",
57
- "icon_background_color",
58
- # Food/venue features
59
- "takeout",
60
- "delivery",
61
- "dine_in",
62
- "curbside_pickup",
63
- "reservable",
64
- "serves_breakfast",
65
- "serves_lunch",
66
- "serves_dinner",
67
- "serves_beer",
68
- "serves_wine",
69
- "serves_brunch",
70
- "serves_vegetarian_food",
71
- "outdoor_seating",
72
- "live_music",
73
- "menu_for_children",
74
- "serves_cocktails",
75
- "serves_dessert",
76
- "serves_coffee",
77
- "good_for_children",
78
- "allows_dogs",
79
- "restroom",
80
- "good_for_groups",
81
- "good_for_watching_sports",
82
- # Options & related places
83
- "payment_options",
84
- "parking_options",
85
- "sub_destinations",
86
- "accessibility_options",
87
- # Fuel/EV & AI summaries
88
- "fuel_options",
89
- "ev_charge_options",
90
- "generative_summary",
91
- "review_summary",
92
- "ev_charge_amenity_summary",
93
- "neighborhood_summary",
94
- # Context
95
- "containing_places",
96
- "pure_service_area_business",
97
- "address_descriptor",
98
- "price_range",
99
- # Missing in your model but present in proto
100
- "editorial_summary",
101
- ]
102
- ],
103
- ]
104
-
105
- PlaceListFields = Union[
106
- str,
107
- Sequence[
108
- Literal[
109
- "*",
110
- # Identity
111
- "places.name",
112
- # Labels & typing
113
- "places.display_name",
114
- "places.types",
115
- "places.primary_type",
116
- "places.primary_type_display_name",
117
- # Phones & addresses
118
- "places.national_phone_number",
119
- "places.international_phone_number",
120
- "places.formatted_address",
121
- "places.short_formatted_address",
122
- "places.postal_address",
123
- "places.address_components",
124
- "places.plus_code",
125
- # Location & map
126
- "places.location",
127
- "places.viewport",
128
- # Scores, links, media
129
- "places.rating",
130
- "places.google_maps_uri",
131
- "places.website_uri",
132
- "places.reviews",
133
- "places.photos",
134
- # Hours
135
- "places.regular_opening_hours",
136
- "places.current_opening_hours",
137
- "places.current_secondary_opening_hours",
138
- "places.regular_secondary_opening_hours",
139
- "places.utc_offset_minutes",
140
- "places.time_zone",
141
- # Misc attributes
142
- "places.adr_format_address",
143
- "places.business_status",
144
- "places.price_level",
145
- "places.attributions",
146
- "places.user_rating_count",
147
- "places.icon_mask_base_uri",
148
- "places.icon_background_color",
149
- # Food/venue features
150
- "places.takeout",
151
- "places.delivery",
152
- "places.dine_in",
153
- "places.curbside_pickup",
154
- "places.reservable",
155
- "places.serves_breakfast",
156
- "places.serves_lunch",
157
- "places.serves_dinner",
158
- "places.serves_beer",
159
- "places.serves_wine",
160
- "places.serves_brunch",
161
- "places.serves_vegetarian_food",
162
- "places.outdoor_seating",
163
- "places.live_music",
164
- "places.menu_for_children",
165
- "places.serves_cocktails",
166
- "places.serves_dessert",
167
- "places.serves_coffee",
168
- "places.good_for_children",
169
- "places.allows_dogs",
170
- "places.restroom",
171
- "places.good_for_groups",
172
- "places.good_for_watching_sports",
173
- # Options & related places
174
- "places.payment_options",
175
- "places.parking_options",
176
- "places.sub_destinations",
177
- "places.accessibility_options",
178
- # Fuel/EV & AI summaries
179
- "places.fuel_options",
180
- "places.ev_charge_options",
181
- "places.generative_summary",
182
- "places.review_summary",
183
- "places.ev_charge_amenity_summary",
184
- "places.neighborhood_summary",
185
- # Context
186
- "places.containing_places",
187
- "places.pure_service_area_business",
188
- "places.address_descriptor",
189
- "places.price_range",
190
- # Missing in your model but present in proto
191
- "places.editorial_summary",
192
- ]
193
- ],
194
- ]
26
+ # Default field mask for places queries
27
+ DEFAULT_PLACE_FIELDS = (
28
+ "display_name",
29
+ "formatted_address",
30
+ "location",
31
+ )
195
32
 
196
33
  ADDRESS_TYPES = {
197
34
  "street_address",
@@ -217,29 +54,178 @@ def fmt_exc(e: BaseException) -> str:
217
54
  return f"{type(e).__name__}: {e}"
218
55
 
219
56
 
220
- def mask_header(fields: Sequence[str] | str | None) -> tuple[tuple[str, str], ...]:
57
+ def mask_header(
58
+ fields: FieldMaskInput, prefix: str = ""
59
+ ) -> tuple[tuple[str, str], ...]:
221
60
  """
222
61
  Build the X-Goog-FieldMask header. Pass a comma-separated string or a sequence.
223
62
  If None, no header is added (e.g., autocomplete, get_photo_media).
224
63
  """
225
64
  if fields is None:
226
- return ()
227
- if isinstance(fields, str):
228
- value = fields
65
+ return () # type: ignore[unreachable]
66
+ value = compile_field_mask(fields, prefix=prefix)
67
+ return (("x-goog-fieldmask", ",".join(value)),)
68
+
69
+
70
+ def strip_html(s: str) -> str:
71
+ # Simple fallback for adr_format_address (which is HTML)
72
+ return re.sub(r"<[^>]+>", "", s) if s else s
73
+
74
+
75
+ def infer_place_type(m: GooglePlace) -> PlaceType:
76
+ # 1) Airport wins outright
77
+ tset = set(m.types or [])
78
+ ptype = (m.primary_type or "").lower() if getattr(m, "primary_type", None) else ""
79
+ if "airport" in tset or ptype == "airport":
80
+ return PlaceType.AIRPORT
81
+ # 2) Anything that looks like a geocoded address
82
+ if tset & ADDRESS_TYPES:
83
+ return PlaceType.ADDRESS
84
+ # 3) Otherwise treat as a point of interest
85
+ return PlaceType.POI
86
+
87
+
88
+ def get_lat_lng(model: GooglePlace) -> tuple[float, float]:
89
+ if model.location:
90
+ lat = model.location.latitude
91
+ lng = model.location.longitude
92
+ elif model.viewport:
93
+ lat = model.viewport.low.latitude
94
+ lng = model.viewport.low.longitude
229
95
  else:
230
- value = ",".join(fields)
231
- return (("x-goog-fieldmask", value),)
96
+ lat = 0
97
+ lng = 0
98
+ return lat, lng
232
99
 
233
100
 
234
- # Default field mask for places queries
235
- DEFAULT_PLACE_FIELDS: Fields = (
236
- "display_name",
237
- "formatted_address",
238
- "location",
239
- )
101
+ def name_from_place(p: Place) -> Optional[str]:
102
+ # Try common fields; support Google Places v1 structure where display_name may have `.text`
103
+ gp = getattr(p, "google_place", None)
104
+ candidates = [
105
+ getattr(gp, "display_name", None),
106
+ getattr(gp, "name", None),
107
+ getattr(p, "name", None),
108
+ getattr(p, "address_descriptor", None),
109
+ getattr(p, "address_components", None),
110
+ getattr(p, "formatted_address", None),
111
+ ]
112
+ for c in candidates:
113
+ if not c:
114
+ continue
240
115
 
241
- DEFAULT_PLACE_LIST_FIELDS: PlaceListFields = (
242
- "places.display_name",
243
- "places.formatted_address",
244
- "places.location",
245
- )
116
+ maybe_text = getattr(c, "text", c)
117
+ if isinstance(maybe_text, str) and maybe_text.strip():
118
+ return maybe_text.strip()
119
+ elif isinstance(maybe_text, AddressDescriptor):
120
+ return maybe_text.landmarks[0].display_name.text
121
+ elif isinstance(maybe_text, list):
122
+ for item in maybe_text:
123
+ if isinstance(item, AddressComponent):
124
+ return item.long_text
125
+ else:
126
+ break
127
+ return None
128
+
129
+
130
+ def build_search_request_params(
131
+ query: Optional[str],
132
+ request: Optional[models.SearchTextRequest],
133
+ **kwargs: Any,
134
+ ) -> dict[str, Any]:
135
+ """Build request parameters for search_text API call."""
136
+ if query is None and request is None:
137
+ raise ValueError("Either 'query' or 'request' must be provided")
138
+ if query is not None and request is not None:
139
+ raise ValueError(
140
+ "Only one of 'query' or 'request' should be provided, not both"
141
+ )
142
+
143
+ if request is not None:
144
+ request_params = request.model_dump(exclude_none=True)
145
+ request_params.update(kwargs)
146
+ return request_params
147
+ else:
148
+ return {"text_query": query, **kwargs}
149
+
150
+
151
+ def normalize_place_from_proto(proto: Any) -> models.Place:
152
+ """Convert a proto Place to our normalized Place model."""
153
+ from .proto_adapter import validate_proto_to_model
154
+
155
+ model = validate_proto_to_model(proto, GooglePlace)
156
+ adr_format = getattr(model, "adr_format_address", None)
157
+ addr = (
158
+ getattr(model, "formatted_address", None)
159
+ or getattr(model, "short_formatted_address", None)
160
+ or strip_html(adr_format or "")
161
+ or ""
162
+ )
163
+ lat, lng = get_lat_lng(model)
164
+ return models.Place(
165
+ formatted_address=addr,
166
+ lat=lat,
167
+ lng=lng,
168
+ place_type=infer_place_type(model),
169
+ google_place=model,
170
+ )
171
+
172
+
173
+ def normalize_search_results(proto_response: Any) -> list[models.Place]:
174
+ """Convert search_text response to list of normalized Place models."""
175
+ return [normalize_place_from_proto(proto) for proto in proto_response.places]
176
+
177
+
178
+ def build_get_place_request(
179
+ place_id: Optional[str], request: Optional[models.GetPlaceRequest]
180
+ ) -> dict[str, Any]:
181
+ """Build request parameters for get_place API call."""
182
+ if place_id is None and request is None:
183
+ raise ValueError("Either 'place_id' or 'request' must be provided")
184
+
185
+ if request:
186
+ return request.model_dump(exclude_none=True)
187
+ else:
188
+ return {"name": f"places/{place_id}"}
189
+
190
+
191
+ def derive_effective_query(query: Optional[str], places: list[models.Place]) -> str:
192
+ """Derive an effective query string from query and/or places."""
193
+ effective_query = (query or "").strip()
194
+
195
+ if not effective_query:
196
+ if len(places) == 1:
197
+ derived = name_from_place(places[0])
198
+ if not derived:
199
+ raise ValueError("Could not derive a query from the provided place.")
200
+ effective_query = derived
201
+ else:
202
+ raise ValueError(
203
+ "Multiple places provided but no 'query' to disambiguate them."
204
+ )
205
+
206
+ return effective_query
207
+
208
+
209
+ def validate_resolve_airport_inputs(
210
+ place_id: Optional[str],
211
+ places: Optional[list[models.Place]],
212
+ max_distance_km: Optional[float],
213
+ ) -> None:
214
+ """Validate inputs for resolve_airport method."""
215
+ if place_id is not None and places is not None:
216
+ raise ValueError("Provide only one of place_id or places, not both.")
217
+ if max_distance_km is not None and max_distance_km <= 0:
218
+ raise ValueError("max_distance_km, if provided, must be > 0.")
219
+
220
+
221
+ def validate_autocomplete_inputs(
222
+ input: Optional[str],
223
+ request: Optional[models.AutocompletePlacesRequest],
224
+ ) -> models.AutocompletePlacesRequest:
225
+ """Validate inputs for autocomplete method."""
226
+ if request is None:
227
+ if input is None:
228
+ raise ValueError("Either input or request must be provided.")
229
+ else:
230
+ request = models.AutocompletePlacesRequest(input=input)
231
+ return request