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.
- bookalimo/config.py +1 -1
- bookalimo/integrations/google_places/client_async.py +139 -108
- bookalimo/integrations/google_places/client_sync.py +139 -109
- bookalimo/integrations/google_places/common.py +186 -200
- bookalimo/integrations/google_places/resolve_airport.py +397 -0
- bookalimo/integrations/google_places/transports.py +98 -0
- bookalimo/schemas/__init__.py +6 -0
- bookalimo/schemas/places/__init__.py +25 -0
- bookalimo/schemas/places/common.py +155 -2
- bookalimo/schemas/places/field_mask.py +221 -0
- bookalimo/schemas/places/google.py +293 -6
- bookalimo/schemas/places/place.py +25 -28
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.1.dist-info}/METADATA +132 -69
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.1.dist-info}/RECORD +17 -14
- bookalimo-1.0.1.dist-info/licenses/LICENSE +21 -0
- bookalimo-1.0.0.dist-info/licenses/LICENSE +0 -0
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.1.dist-info}/WHEEL +0 -0
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.1.dist-info}/top_level.txt +0 -0
@@ -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
|
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:
|
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
|
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(
|
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(
|
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:
|
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:
|
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
|
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:
|
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:
|
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:
|
182
|
-
photos:
|
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:
|
188
|
-
regular_secondary_opening_hours:
|
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[
|
195
|
-
price_level: Optional[
|
196
|
-
attributions:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|