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.
@@ -1,4 +1,15 @@
1
1
  import re
2
+ import sys
3
+
4
+ if sys.version_info >= (3, 11):
5
+ from enum import StrEnum
6
+ else:
7
+ from enum import Enum
8
+
9
+ class StrEnum(str, Enum):
10
+ pass
11
+
12
+
2
13
  from typing import Any, Optional, cast
3
14
 
4
15
  import pycountry
@@ -24,7 +35,7 @@ from .common import (
24
35
  LatLng,
25
36
  Viewport,
26
37
  )
27
- from .place import Place as GooglePlace
38
+ from .place import GooglePlace, PriceLevel
28
39
 
29
40
  # ---------- Constants & Enums ----------
30
41
 
@@ -33,7 +44,7 @@ COUNTRY_CODES = {
33
44
  }
34
45
 
35
46
 
36
- class PlaceType:
47
+ class PlaceType(StrEnum):
37
48
  """Place type constants."""
38
49
 
39
50
  ADDRESS = "address"
@@ -41,6 +52,31 @@ class PlaceType:
41
52
  POI = "poi" # Point of Interest
42
53
 
43
54
 
55
+ class RankPreference(StrEnum):
56
+ """Ranking preference constants for SearchTextRequest."""
57
+
58
+ RANK_PREFERENCE_UNSPECIFIED = "RANK_PREFERENCE_UNSPECIFIED"
59
+ DISTANCE = "DISTANCE"
60
+ RELEVANCE = "RELEVANCE"
61
+
62
+
63
+ class EVConnectorType(StrEnum):
64
+ """EV connector type constants."""
65
+
66
+ EV_CONNECTOR_TYPE_UNSPECIFIED = "EV_CONNECTOR_TYPE_UNSPECIFIED"
67
+ EV_CONNECTOR_TYPE_OTHER = "EV_CONNECTOR_TYPE_OTHER"
68
+ EV_CONNECTOR_TYPE_J1772 = "EV_CONNECTOR_TYPE_J1772"
69
+ EV_CONNECTOR_TYPE_TYPE_2 = "EV_CONNECTOR_TYPE_TYPE_2"
70
+ EV_CONNECTOR_TYPE_CHADEMO = "EV_CONNECTOR_TYPE_CHADEMO"
71
+ EV_CONNECTOR_TYPE_CCS_COMBO_1 = "EV_CONNECTOR_TYPE_CCS_COMBO_1"
72
+ EV_CONNECTOR_TYPE_CCS_COMBO_2 = "EV_CONNECTOR_TYPE_CCS_COMBO_2"
73
+ EV_CONNECTOR_TYPE_TESLA = "EV_CONNECTOR_TYPE_TESLA"
74
+ EV_CONNECTOR_TYPE_UNSPECIFIED_GB_T = "EV_CONNECTOR_TYPE_UNSPECIFIED_GB_T"
75
+ EV_CONNECTOR_TYPE_UNSPECIFIED_WALL_OUTLET = (
76
+ "EV_CONNECTOR_TYPE_UNSPECIFIED_WALL_OUTLET"
77
+ )
78
+
79
+
44
80
  # ---------- Text Primitives ----------
45
81
  class StringRange(BaseModel):
46
82
  """Identifies a substring within a given text."""
@@ -148,6 +184,82 @@ class LocationRestriction(BaseModel):
148
184
  return self
149
185
 
150
186
 
187
+ # ---------- Search Text Supporting Models ----------
188
+
189
+
190
+ class Polyline(BaseModel):
191
+ """Route polyline representation."""
192
+
193
+ model_config = ConfigDict(extra="forbid")
194
+
195
+ encoded_polyline: Optional[str] = Field(None, description="Encoded polyline string")
196
+
197
+
198
+ class RoutingParameters(BaseModel):
199
+ """Parameters to configure routing calculations."""
200
+
201
+ model_config = ConfigDict(extra="forbid")
202
+
203
+ origin: Optional[LatLng] = Field(None, description="Explicit routing origin")
204
+ travel_mode: Optional[str] = Field(
205
+ None, description="Travel mode (DRIVE, WALK, BICYCLE, TRANSIT)"
206
+ )
207
+ routing_preference: Optional[str] = Field(
208
+ None, description="Routing preference (TRAFFIC_AWARE, etc.)"
209
+ )
210
+
211
+
212
+ class EVOptions(BaseModel):
213
+ """Searchable EV options for place search."""
214
+
215
+ model_config = ConfigDict(extra="forbid")
216
+
217
+ minimum_charging_rate_kw: Optional[float] = Field(
218
+ None, gt=0, description="Minimum required charging rate in kilowatts"
219
+ )
220
+ connector_types: list[EVConnectorType] = Field(
221
+ default_factory=list, description="Preferred EV connector types"
222
+ )
223
+
224
+ @field_validator("connector_types")
225
+ @classmethod
226
+ def _validate_connector_types(
227
+ cls, v: list[EVConnectorType]
228
+ ) -> list[EVConnectorType]:
229
+ # Remove duplicates while preserving order
230
+ seen = set()
231
+ cleaned = []
232
+ for connector_type in v:
233
+ if connector_type not in seen:
234
+ cleaned.append(connector_type)
235
+ seen.add(connector_type)
236
+ return cleaned
237
+
238
+
239
+ class SearchAlongRouteParameters(BaseModel):
240
+ """Parameters for searching along a route."""
241
+
242
+ model_config = ConfigDict(extra="forbid")
243
+
244
+ polyline: Polyline = Field(..., description="Route polyline")
245
+
246
+
247
+ class SearchTextLocationRestriction(BaseModel):
248
+ """
249
+ Location restriction for SearchTextRequest - allows only rectangle.
250
+ """
251
+
252
+ model_config = ConfigDict(extra="forbid")
253
+
254
+ rectangle: Optional[Viewport] = None
255
+
256
+ @model_validator(mode="after")
257
+ def _validate_rectangle_required(self) -> Self:
258
+ if self.rectangle is None:
259
+ raise ValueError("rectangle is required for LocationRestriction")
260
+ return self
261
+
262
+
151
263
  # ---------- Responses ----------
152
264
 
153
265
 
@@ -157,10 +269,7 @@ class Place(BaseModel):
157
269
  formatted_address: str = Field(..., description="Full formatted address")
158
270
  lat: float = Field(..., description="Latitude")
159
271
  lng: float = Field(..., description="Longitude")
160
- place_type: str = Field(..., description="Type: address, airport, or poi")
161
- iata_code: Optional[str] = Field(
162
- None, description="IATA airport code if applicable"
163
- )
272
+ place_type: PlaceType = Field(..., description="Type: address, airport, or poi")
164
273
  google_place: Optional[GooglePlace] = Field(
165
274
  None, description="Raw Google Places API response"
166
275
  )
@@ -594,3 +703,181 @@ class GeocodingRequest(BaseModel):
594
703
  params = params.add("region", self.region)
595
704
 
596
705
  return params
706
+
707
+
708
+ class SearchTextRequest(BaseModel):
709
+ """
710
+ Pydantic model for SearchTextRequest with rich validations.
711
+ """
712
+
713
+ model_config = ConfigDict(
714
+ extra="forbid", str_strip_whitespace=True, populate_by_name=True
715
+ )
716
+
717
+ # Required field
718
+ text_query: str = Field(
719
+ ..., min_length=1, description="Required. The text query for textual search."
720
+ )
721
+
722
+ # Localization
723
+ language_code: Optional[str] = Field(
724
+ default=None,
725
+ description="Place details will be displayed with the preferred language if available.",
726
+ )
727
+ region_code: Optional[str] = Field(
728
+ default=None,
729
+ description="The Unicode country/region code (CLDR) of the location.",
730
+ )
731
+
732
+ # Ranking and type filtering
733
+ rank_preference: Optional[RankPreference] = Field(
734
+ default=None, description="How results will be ranked in the response."
735
+ )
736
+ included_type: Optional[str] = Field(
737
+ default=None,
738
+ description="The requested place type. Only support one included type.",
739
+ )
740
+ strict_type_filtering: bool = Field(
741
+ default=False,
742
+ description="Used to set strict type filtering for included_type.",
743
+ )
744
+
745
+ # Filters
746
+ open_now: bool = Field(
747
+ default=False,
748
+ description="Used to restrict the search to places that are currently open.",
749
+ )
750
+ min_rating: Optional[float] = Field(
751
+ default=None,
752
+ ge=0,
753
+ le=5,
754
+ description="Filter out results whose average user rating is strictly less than this limit.",
755
+ )
756
+ max_result_count: Optional[int] = Field(
757
+ default=None,
758
+ ge=1,
759
+ le=20,
760
+ description="Maximum number of results to return. Must be between 1 and 20.",
761
+ )
762
+ price_levels: list[PriceLevel] = Field(
763
+ default_factory=list,
764
+ description="Used to restrict the search to places that are marked as certain price levels.",
765
+ )
766
+
767
+ # Location constraints (mutually exclusive)
768
+ location_bias: Optional[LocationBias] = Field(
769
+ default=None,
770
+ description="The region to search. This location serves as a bias.",
771
+ )
772
+ location_restriction: Optional[SearchTextLocationRestriction] = Field(
773
+ default=None,
774
+ description="The region to search. This location serves as a restriction.",
775
+ )
776
+
777
+ # Advanced options
778
+ ev_options: Optional[EVOptions] = Field(
779
+ default=None,
780
+ description="Set the searchable EV options of a place search request.",
781
+ )
782
+ routing_parameters: Optional[RoutingParameters] = Field(
783
+ default=None, description="Additional parameters for routing to results."
784
+ )
785
+ search_along_route_parameters: Optional[SearchAlongRouteParameters] = Field(
786
+ default=None, description="Additional parameters for searching along a route."
787
+ )
788
+ include_pure_service_area_businesses: bool = Field(
789
+ default=False,
790
+ description="Include pure service area businesses if the field is set to true.",
791
+ )
792
+
793
+ # Field validators
794
+ @field_validator("language_code")
795
+ @classmethod
796
+ def _validate_language_code(cls, v: Optional[str]) -> Optional[str]:
797
+ if v is None:
798
+ return v
799
+ if not BCP47.match(v):
800
+ raise ValueError(
801
+ "language_code must be a valid BCP-47 tag (e.g., 'en', 'en-US', 'zh-Hant')."
802
+ )
803
+ return v
804
+
805
+ @field_validator("region_code")
806
+ @classmethod
807
+ def _validate_region_code(cls, v: Optional[str]) -> Optional[str]:
808
+ if v is None:
809
+ return v
810
+ v2 = v.upper()
811
+ if not CLDR_REGION_2.fullmatch(v2):
812
+ raise ValueError(
813
+ "region_code must be a two-letter CLDR region code (e.g., 'US', 'GB')."
814
+ )
815
+ return v2
816
+
817
+ @field_validator("included_type")
818
+ @classmethod
819
+ def _validate_included_type(cls, v: Optional[str]) -> Optional[str]:
820
+ if v is None:
821
+ return v
822
+ if not PLACE_TYPE.match(v):
823
+ raise ValueError(
824
+ f"Invalid place type '{v}'. Use lowercase letters, digits, and underscores."
825
+ )
826
+ return v
827
+
828
+ @field_validator("min_rating")
829
+ @classmethod
830
+ def _validate_min_rating(cls, v: Optional[float]) -> Optional[float]:
831
+ if v is None:
832
+ return v
833
+ # Round up to nearest 0.5 as per Google's specification
834
+ import math
835
+
836
+ return math.ceil(v * 2) / 2
837
+
838
+ @field_validator("price_levels")
839
+ @classmethod
840
+ def _validate_price_levels(cls, v: list[PriceLevel]) -> list[PriceLevel]:
841
+ # Remove duplicates while preserving order
842
+ seen = set()
843
+ cleaned = []
844
+ for level in v:
845
+ if level not in seen:
846
+ cleaned.append(level)
847
+ seen.add(level)
848
+ return cleaned
849
+
850
+ # Cross-field validation
851
+ @model_validator(mode="after")
852
+ def _validate_cross_fields(self) -> Self:
853
+ # Mutually exclusive location constraints
854
+ if self.location_bias is not None and self.location_restriction is not None:
855
+ raise ValueError(
856
+ "Cannot set both location_bias and location_restriction. Choose one."
857
+ )
858
+ return self
859
+
860
+
861
+ class SearchTextResponse(BaseModel):
862
+ """Response proto for SearchText."""
863
+
864
+ model_config = ConfigDict(extra="forbid")
865
+
866
+ places: list[GooglePlace] = Field(
867
+ default_factory=list,
868
+ description="A list of places that meet the user's text search criteria.",
869
+ )
870
+
871
+
872
+ class ResolvedAirport(BaseModel):
873
+ """Airport result from the resolve_airport() method."""
874
+
875
+ name: str = Field(..., description="Name of the airport")
876
+ city: str = Field(..., description="City of the airport")
877
+ iata_code: Optional[str] = Field(
878
+ None, description="IATA airport code if applicable"
879
+ )
880
+ icao_code: Optional[str] = Field(
881
+ None, description="ICAO airport code if applicable"
882
+ )
883
+ confidence: float = Field(..., description="Search result confidence score")
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import List, Optional
3
+ from enum import IntEnum
4
+ from typing import Optional
4
5
 
5
6
  from pydantic import (
6
7
  AnyUrl,
@@ -24,6 +25,7 @@ from .common import (
24
25
  EVChargeOptions,
25
26
  FuelOptions,
26
27
  LatLng,
28
+ LocalizedText,
27
29
  OpeningHours,
28
30
  ParkingOptions,
29
31
  PaymentOptions,
@@ -38,7 +40,7 @@ from .common import (
38
40
  )
39
41
 
40
42
 
41
- class PriceLevel(int):
43
+ class PriceLevel(IntEnum):
42
44
  PRICE_LEVEL_UNSPECIFIED = 0
43
45
  PRICE_LEVEL_FREE = 1
44
46
  PRICE_LEVEL_INEXPENSIVE = 2
@@ -47,7 +49,7 @@ class PriceLevel(int):
47
49
  PRICE_LEVEL_VERY_EXPENSIVE = 5
48
50
 
49
51
 
50
- class BusinessStatus(int):
52
+ class BusinessStatus(IntEnum):
51
53
  BUSINESS_STATUS_UNSPECIFIED = 0
52
54
  OPERATIONAL = 1
53
55
  CLOSED_TEMPORARILY = 2
@@ -59,12 +61,14 @@ class AddressComponent(BaseModel):
59
61
 
60
62
  long_text: str
61
63
  short_text: Optional[str] = None
62
- types: List[str] = Field(default_factory=list)
64
+ types: list[str] = Field(
65
+ default_factory=list
66
+ ) # limited to https://developers.google.com/maps/documentation/places/web-service/place-types
63
67
  language_code: Optional[str] = None
64
68
 
65
69
  @field_validator("types")
66
70
  @classmethod
67
- def _types(cls, v: List[str]) -> List[str]:
71
+ def _types(cls, v: list[str]) -> list[str]:
68
72
  out, seen = [], set()
69
73
  for raw in v:
70
74
  t = raw.strip()
@@ -114,13 +118,6 @@ class NeighborhoodSummary(BaseModel):
114
118
  disclosure_text: Optional[LocalizedText] = None
115
119
 
116
120
 
117
- class LocalizedText(BaseModel):
118
- model_config = ConfigDict(extra="allow")
119
-
120
- text: str
121
- language_code: str
122
-
123
-
124
121
  class ContainingPlace(BaseModel):
125
122
  model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
126
123
 
@@ -148,7 +145,7 @@ class ContainingPlace(BaseModel):
148
145
  return self
149
146
 
150
147
 
151
- class Place(BaseModel):
148
+ class GooglePlace(BaseModel):
152
149
  model_config = ConfigDict(extra="allow", str_strip_whitespace=True)
153
150
 
154
151
  # Identity
@@ -157,7 +154,7 @@ class Place(BaseModel):
157
154
 
158
155
  # Labels & typing
159
156
  display_name: Optional[LocalizedText] = None
160
- types: List[str] = Field(default_factory=list)
157
+ types: list[str] = Field(default_factory=list)
161
158
  primary_type: Optional[str] = None
162
159
  primary_type_display_name: Optional[LocalizedText] = None
163
160
 
@@ -165,9 +162,10 @@ class Place(BaseModel):
165
162
  national_phone_number: Optional[str] = None
166
163
  international_phone_number: Optional[str] = None
167
164
  formatted_address: Optional[str] = None
165
+ address_descriptor: Optional[AddressDescriptor] = None
168
166
  short_formatted_address: Optional[str] = None
169
167
  postal_address: Optional[PostalAddress] = None
170
- address_components: List[AddressComponent] = Field(default_factory=list)
168
+ address_components: list[AddressComponent] = Field(default_factory=list)
171
169
  plus_code: Optional[PlusCode] = None
172
170
 
173
171
  # Location & map
@@ -178,22 +176,22 @@ class Place(BaseModel):
178
176
  rating: Optional[float] = None
179
177
  google_maps_uri: Optional[AnyUrl] = None
180
178
  website_uri: Optional[AnyUrl] = None
181
- reviews: List[Review] = Field(default_factory=list)
182
- photos: List[Photo] = Field(default_factory=list)
179
+ reviews: list[Review] = Field(default_factory=list)
180
+ photos: list[Photo] = Field(default_factory=list)
183
181
 
184
182
  # Hours
185
183
  regular_opening_hours: Optional[OpeningHours] = None
186
184
  current_opening_hours: Optional[OpeningHours] = None
187
- current_secondary_opening_hours: List[OpeningHours] = Field(default_factory=list)
188
- regular_secondary_opening_hours: List[OpeningHours] = Field(default_factory=list)
185
+ current_secondary_opening_hours: list[OpeningHours] = Field(default_factory=list)
186
+ regular_secondary_opening_hours: list[OpeningHours] = Field(default_factory=list)
189
187
  utc_offset_minutes: Optional[int] = None
190
188
  time_zone: Optional[TimeZone] = None
191
189
 
192
190
  # Misc attributes
193
191
  adr_format_address: Optional[str] = None
194
- business_status: Optional[int] = None
195
- price_level: Optional[int] = None
196
- attributions: List[Attribution] = Field(default_factory=list)
192
+ business_status: Optional[BusinessStatus] = None
193
+ price_level: Optional[PriceLevel] = None
194
+ attributions: list[Attribution] = Field(default_factory=list)
197
195
  user_rating_count: Optional[int] = None
198
196
  icon_mask_base_uri: Optional[AnyUrl] = None
199
197
  icon_background_color: Optional[str] = None
@@ -227,7 +225,7 @@ class Place(BaseModel):
227
225
  # Options & related places
228
226
  payment_options: Optional[PaymentOptions] = None
229
227
  parking_options: Optional[ParkingOptions] = None
230
- sub_destinations: List[SubDestination] = Field(default_factory=list)
228
+ sub_destinations: list[SubDestination] = Field(default_factory=list)
231
229
  accessibility_options: Optional[AccessibilityOptions] = None
232
230
 
233
231
  # Fuel/EV & AI summaries
@@ -239,9 +237,8 @@ class Place(BaseModel):
239
237
  neighborhood_summary: Optional[NeighborhoodSummary] = None
240
238
 
241
239
  # Context
242
- containing_places: List[ContainingPlace] = Field(default_factory=list)
240
+ containing_places: list[ContainingPlace] = Field(default_factory=list)
243
241
  pure_service_area_business: Optional[bool] = None
244
- address_descriptor: Optional[AddressDescriptor] = None
245
242
  price_range: Optional[PriceRange] = None
246
243
 
247
244
  # ---------- Validators ----------
@@ -273,7 +270,7 @@ class Place(BaseModel):
273
270
 
274
271
  @field_validator("types")
275
272
  @classmethod
276
- def _types(cls, v: List[str]) -> List[str]:
273
+ def _types(cls, v: list[str]) -> list[str]:
277
274
  out, seen = [], set()
278
275
  for raw in v:
279
276
  t = raw.strip()
@@ -324,14 +321,14 @@ class Place(BaseModel):
324
321
 
325
322
  @field_validator("reviews")
326
323
  @classmethod
327
- def _max_reviews(cls, v: List[Review]) -> List[Review]:
324
+ def _max_reviews(cls, v: list[Review]) -> list[Review]:
328
325
  if len(v) > 5:
329
326
  raise ValueError("reviews can contain at most 5 items")
330
327
  return v
331
328
 
332
329
  @field_validator("photos")
333
330
  @classmethod
334
- def _max_photos(cls, v: List[Photo]) -> List[Photo]:
331
+ def _max_photos(cls, v: list[Photo]) -> list[Photo]:
335
332
  if len(v) > 10:
336
333
  raise ValueError("photos can contain at most 10 items")
337
334
  return v