bookalimo 1.0.0__py3-none-any.whl → 1.0.2__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/client.py +45 -22
- bookalimo/config.py +1 -1
- bookalimo/integrations/google_places/client_async.py +141 -156
- bookalimo/integrations/google_places/client_sync.py +142 -156
- bookalimo/integrations/google_places/common.py +464 -200
- bookalimo/integrations/google_places/resolve_airport.py +426 -0
- bookalimo/integrations/google_places/transports.py +105 -0
- bookalimo/logging.py +103 -0
- bookalimo/schemas/__init__.py +126 -34
- bookalimo/schemas/base.py +74 -14
- bookalimo/schemas/places/__init__.py +27 -0
- bookalimo/schemas/places/common.py +155 -2
- bookalimo/schemas/places/field_mask.py +212 -0
- bookalimo/schemas/places/google.py +458 -16
- bookalimo/schemas/places/place.py +25 -28
- bookalimo/schemas/requests.py +214 -0
- bookalimo/schemas/responses.py +196 -0
- bookalimo/schemas/{booking.py → shared.py} +55 -218
- bookalimo/services/pricing.py +9 -129
- bookalimo/services/reservations.py +10 -100
- bookalimo/transport/auth.py +2 -2
- bookalimo/transport/httpx_async.py +41 -125
- bookalimo/transport/httpx_sync.py +30 -109
- bookalimo/transport/utils.py +204 -3
- bookalimo-1.0.2.dist-info/METADATA +245 -0
- bookalimo-1.0.2.dist-info/RECORD +40 -0
- bookalimo-1.0.2.dist-info/licenses/LICENSE +21 -0
- bookalimo-1.0.0.dist-info/METADATA +0 -307
- bookalimo-1.0.0.dist-info/RECORD +0 -35
- bookalimo-1.0.0.dist-info/licenses/LICENSE +0 -0
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.2.dist-info}/WHEEL +0 -0
- {bookalimo-1.0.0.dist-info → bookalimo-1.0.2.dist-info}/top_level.txt +0 -0
bookalimo/schemas/__init__.py
CHANGED
@@ -1,84 +1,170 @@
|
|
1
1
|
"""Pydantic schemas for the Bookalimo SDK."""
|
2
2
|
|
3
3
|
from ..transport.auth import Credentials
|
4
|
-
from .
|
4
|
+
from .places import (
|
5
|
+
AutocompletePlacesRequest,
|
6
|
+
AutocompletePlacesResponse,
|
7
|
+
BusinessStatus,
|
8
|
+
Circle,
|
9
|
+
EVConnectorType,
|
10
|
+
FieldMaskInput,
|
11
|
+
FormattableText,
|
12
|
+
GeocodingRequest,
|
13
|
+
GetPlaceRequest,
|
14
|
+
GooglePlace,
|
15
|
+
LocationBias,
|
16
|
+
LocationRestriction,
|
17
|
+
Place,
|
18
|
+
PlacePrediction,
|
19
|
+
PlaceType,
|
20
|
+
PriceLevel,
|
21
|
+
QueryPrediction,
|
22
|
+
RankPreference,
|
23
|
+
SearchTextRequest,
|
24
|
+
StringRange,
|
25
|
+
StructuredFormat,
|
26
|
+
Suggestion,
|
27
|
+
)
|
28
|
+
|
29
|
+
# Import request models (for API calls - serialize to camelCase)
|
30
|
+
from .requests import (
|
31
|
+
Account,
|
5
32
|
Address,
|
6
33
|
Airport,
|
7
34
|
BookRequest,
|
8
|
-
BookResponse,
|
9
|
-
CardHolderType,
|
10
35
|
City,
|
11
36
|
CreditCard,
|
12
37
|
DetailsRequest,
|
13
|
-
|
14
|
-
EditableReservationRequest,
|
15
|
-
EditReservationResponse,
|
38
|
+
EditReservationRequest,
|
16
39
|
GetReservationRequest,
|
17
40
|
ListReservationsRequest,
|
18
|
-
ListReservationsResponse,
|
19
41
|
Location,
|
20
|
-
LocationType,
|
21
42
|
MeetGreetAdditional,
|
22
|
-
MeetGreetType,
|
23
43
|
Passenger,
|
24
44
|
Price,
|
25
45
|
PriceRequest,
|
26
|
-
PriceResponse,
|
27
|
-
RateType,
|
28
46
|
Reservation,
|
29
|
-
ReservationStatus,
|
30
47
|
Reward,
|
31
|
-
RewardType,
|
32
48
|
Stop,
|
33
49
|
)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
|
51
|
+
# Import response versions with explicit naming for clarity
|
52
|
+
from .responses import (
|
53
|
+
Account as AccountResponse,
|
54
|
+
)
|
55
|
+
from .responses import (
|
56
|
+
Address as AddressResponse,
|
57
|
+
)
|
58
|
+
from .responses import (
|
59
|
+
Airport as AirportResponse,
|
60
|
+
)
|
61
|
+
|
62
|
+
# Import response models (from API responses - serialize to snake_case)
|
63
|
+
from .responses import (
|
64
|
+
BookResponse,
|
65
|
+
CarClassPrice,
|
66
|
+
DetailsResponse,
|
67
|
+
EditReservationResponse,
|
68
|
+
GetReservationResponse,
|
69
|
+
ListReservationsResponse,
|
70
|
+
PriceResponse,
|
71
|
+
ReservationData,
|
72
|
+
)
|
73
|
+
from .responses import (
|
74
|
+
BreakdownItem as BreakdownItemResponse,
|
75
|
+
)
|
76
|
+
from .responses import (
|
77
|
+
City as CityResponse,
|
78
|
+
)
|
79
|
+
from .responses import (
|
80
|
+
CreditCard as CreditCardResponse,
|
81
|
+
)
|
82
|
+
from .responses import (
|
83
|
+
Location as LocationResponse,
|
84
|
+
)
|
85
|
+
from .responses import (
|
86
|
+
MeetGreet as MeetGreetResponse,
|
87
|
+
)
|
88
|
+
from .responses import (
|
89
|
+
MeetGreetAdditional as MeetGreetAdditionalResponse,
|
90
|
+
)
|
91
|
+
from .responses import (
|
92
|
+
Passenger as PassengerResponse,
|
93
|
+
)
|
94
|
+
from .responses import (
|
95
|
+
Reservation as ReservationResponse,
|
96
|
+
)
|
97
|
+
from .responses import (
|
98
|
+
Reward as RewardResponse,
|
99
|
+
)
|
100
|
+
from .responses import (
|
101
|
+
Stop as StopResponse,
|
102
|
+
)
|
103
|
+
|
104
|
+
# Import enums and shared types from shared module
|
105
|
+
from .shared import (
|
106
|
+
CardHolderType,
|
107
|
+
LocationType,
|
108
|
+
MeetGreetType,
|
109
|
+
RateType,
|
110
|
+
ReservationStatus,
|
111
|
+
RewardType,
|
50
112
|
)
|
51
113
|
|
52
114
|
__all__ = [
|
115
|
+
# Enums and shared types
|
53
116
|
"RateType",
|
54
117
|
"LocationType",
|
55
118
|
"MeetGreetType",
|
56
119
|
"RewardType",
|
57
120
|
"ReservationStatus",
|
58
121
|
"CardHolderType",
|
122
|
+
# Request models (default exports - serialize to camelCase for API)
|
59
123
|
"City",
|
60
124
|
"Address",
|
61
125
|
"Airport",
|
62
126
|
"Location",
|
63
127
|
"Stop",
|
128
|
+
"Account",
|
64
129
|
"Passenger",
|
65
130
|
"Reward",
|
66
131
|
"CreditCard",
|
67
132
|
"MeetGreetAdditional",
|
68
133
|
"Price",
|
69
134
|
"Reservation",
|
70
|
-
"
|
135
|
+
"EditReservationRequest",
|
71
136
|
"PriceRequest",
|
72
|
-
"PriceResponse",
|
73
137
|
"DetailsRequest",
|
74
|
-
"DetailsResponse",
|
75
138
|
"BookRequest",
|
76
|
-
"BookResponse",
|
77
139
|
"ListReservationsRequest",
|
78
|
-
"ListReservationsResponse",
|
79
140
|
"GetReservationRequest",
|
141
|
+
# Response models (serialize to snake_case for Python DX)
|
142
|
+
"BookResponse",
|
143
|
+
"CarClassPrice",
|
144
|
+
"DetailsResponse",
|
80
145
|
"EditReservationResponse",
|
146
|
+
"GetReservationResponse",
|
147
|
+
"ListReservationsResponse",
|
148
|
+
"PriceResponse",
|
149
|
+
# Explicit response model variants (for handling API responses)
|
150
|
+
"AccountResponse",
|
151
|
+
"AddressResponse",
|
152
|
+
"AirportResponse",
|
153
|
+
"BreakdownItemResponse",
|
154
|
+
"CityResponse",
|
155
|
+
"CreditCardResponse",
|
156
|
+
"ReservationData",
|
157
|
+
"LocationResponse",
|
158
|
+
"MeetGreetResponse",
|
159
|
+
"MeetGreetAdditionalResponse",
|
160
|
+
"PassengerResponse",
|
161
|
+
"ReservationResponse",
|
162
|
+
"RewardResponse",
|
163
|
+
"StopResponse",
|
164
|
+
# Places API schemas
|
81
165
|
"PlaceType",
|
166
|
+
"RankPreference",
|
167
|
+
"EVConnectorType",
|
82
168
|
"StringRange",
|
83
169
|
"FormattableText",
|
84
170
|
"StructuredFormat",
|
@@ -93,5 +179,11 @@ __all__ = [
|
|
93
179
|
"GetPlaceRequest",
|
94
180
|
"AutocompletePlacesRequest",
|
95
181
|
"GeocodingRequest",
|
182
|
+
"SearchTextRequest",
|
183
|
+
"GooglePlace",
|
184
|
+
"PriceLevel",
|
185
|
+
"FieldMaskInput",
|
186
|
+
"BusinessStatus",
|
187
|
+
# Auth
|
96
188
|
"Credentials",
|
97
189
|
]
|
bookalimo/schemas/base.py
CHANGED
@@ -7,32 +7,78 @@ from enum import Enum
|
|
7
7
|
from typing import Any, cast
|
8
8
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, model_serializer
|
10
|
-
from pydantic.alias_generators import to_camel
|
10
|
+
from pydantic.alias_generators import to_camel, to_snake
|
11
11
|
from pydantic_core.core_schema import SerializationInfo, SerializerFunctionWrapHandler
|
12
12
|
|
13
13
|
|
14
|
-
|
14
|
+
def _deep_to_snake(obj: Any) -> Any:
|
15
|
+
if isinstance(obj, dict):
|
16
|
+
return {
|
17
|
+
(to_snake(k) if isinstance(k, str) else k): _deep_to_snake(v)
|
18
|
+
for k, v in obj.items()
|
19
|
+
}
|
20
|
+
if isinstance(obj, list):
|
21
|
+
return [_deep_to_snake(v) for v in obj]
|
22
|
+
if isinstance(obj, tuple):
|
23
|
+
return type(obj)(_deep_to_snake(v) for v in obj)
|
24
|
+
return obj
|
25
|
+
|
26
|
+
|
27
|
+
class SharedModel(BaseModel):
|
15
28
|
"""
|
16
|
-
Base model for
|
29
|
+
Base model for shared data structures used in both API requests and responses.
|
17
30
|
|
18
|
-
Provides
|
19
|
-
|
31
|
+
Provides field name conversion but no serialization opinions - subclasses decide
|
32
|
+
whether to serialize to camelCase (for requests) or snake_case (for responses).
|
20
33
|
"""
|
21
34
|
|
22
35
|
model_config = ConfigDict(
|
23
36
|
# Auto-generate camelCase aliases from snake_case field names
|
24
37
|
alias_generator=to_camel,
|
25
|
-
# Accept both alias (camel) and name (snake) on input
|
38
|
+
# Accept both alias (camel) and name (snake) on input
|
26
39
|
validate_by_alias=True,
|
27
|
-
validate_by_name=True,
|
28
|
-
#
|
29
|
-
serialize_by_alias=True,
|
40
|
+
validate_by_name=True,
|
41
|
+
# Don't set serialize_by_alias - let subclasses decide
|
30
42
|
# Enums dumping handled in model_serializer
|
31
43
|
use_enum_values=False,
|
32
44
|
# Ignore unknown keys from the API
|
33
45
|
extra="ignore",
|
34
46
|
)
|
35
47
|
|
48
|
+
|
49
|
+
class RequestModel(SharedModel):
|
50
|
+
"""
|
51
|
+
Base model for API request models.
|
52
|
+
|
53
|
+
Serializes to camelCase by default (what the Book-A-Limo API expects).
|
54
|
+
"""
|
55
|
+
|
56
|
+
model_config = ConfigDict(
|
57
|
+
alias_generator=to_camel,
|
58
|
+
validate_by_alias=True,
|
59
|
+
validate_by_name=True,
|
60
|
+
serialize_by_alias=True, # Serialize to camelCase for API requests
|
61
|
+
use_enum_values=False,
|
62
|
+
extra="ignore",
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
class ResponseModel(SharedModel):
|
67
|
+
"""
|
68
|
+
Base model for API response models.
|
69
|
+
|
70
|
+
Serializes to snake_case by default (better DX for Python developers).
|
71
|
+
"""
|
72
|
+
|
73
|
+
model_config = ConfigDict(
|
74
|
+
alias_generator=to_camel,
|
75
|
+
validate_by_alias=True,
|
76
|
+
validate_by_name=True,
|
77
|
+
serialize_by_alias=False, # Serialize to snake_case for Python developers
|
78
|
+
use_enum_values=False,
|
79
|
+
extra="ignore",
|
80
|
+
)
|
81
|
+
|
36
82
|
@model_serializer(mode="wrap")
|
37
83
|
def _serialize(
|
38
84
|
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
@@ -41,16 +87,30 @@ class ApiModel(BaseModel):
|
|
41
87
|
data = handler(self)
|
42
88
|
|
43
89
|
# Decide how to emit enums based on context (default to 'value')
|
44
|
-
|
90
|
+
ctx = info.context or {}
|
91
|
+
enum_out = ctx.get("enum_out", "value")
|
45
92
|
|
46
|
-
def
|
93
|
+
def convert_values(obj: Any) -> Any:
|
47
94
|
if isinstance(obj, Enum):
|
48
95
|
return obj.name if enum_out == "name" else obj.value
|
49
96
|
if isinstance(obj, dict):
|
50
|
-
return {k:
|
97
|
+
return {k: convert_values(v) for k, v in obj.items()}
|
51
98
|
if isinstance(obj, (list, tuple)):
|
52
99
|
t = type(obj)
|
53
|
-
return t(
|
100
|
+
return t(convert_values(v) for v in obj)
|
54
101
|
return obj
|
55
102
|
|
56
|
-
|
103
|
+
out = cast(dict[str, Any], convert_values(data))
|
104
|
+
|
105
|
+
# Key case control: default "camel" (uses aliases); allow "snake" via context.
|
106
|
+
case = ctx.get("case")
|
107
|
+
# Support boolean alias for convenience: snake_case=True -> case="snake"
|
108
|
+
if ctx.get("snake_case") is True:
|
109
|
+
case = "snake"
|
110
|
+
elif ctx.get("snake_case") is False:
|
111
|
+
case = "camel"
|
112
|
+
|
113
|
+
if case == "snake":
|
114
|
+
out = cast(dict[str, Any], _deep_to_snake(out))
|
115
|
+
|
116
|
+
return out
|
@@ -1,9 +1,13 @@
|
|
1
1
|
"""Google Places API schemas."""
|
2
2
|
|
3
|
+
from .common import AddressDescriptor
|
4
|
+
from .field_mask import FieldMaskInput, FieldPath, compile_field_mask
|
3
5
|
from .google import (
|
4
6
|
AutocompletePlacesRequest,
|
5
7
|
AutocompletePlacesResponse,
|
6
8
|
Circle,
|
9
|
+
EVConnectorType,
|
10
|
+
EVOptions,
|
7
11
|
FormattableText,
|
8
12
|
GeocodingRequest,
|
9
13
|
GetPlaceRequest,
|
@@ -13,20 +17,31 @@ from .google import (
|
|
13
17
|
PlacePrediction,
|
14
18
|
PlaceType,
|
15
19
|
QueryPrediction,
|
20
|
+
RankPreference,
|
21
|
+
ResolvedAirport,
|
22
|
+
RoutingParameters,
|
23
|
+
SearchTextLocationRestriction,
|
24
|
+
SearchTextRequest,
|
25
|
+
SearchTextResponse,
|
16
26
|
StringRange,
|
17
27
|
StructuredFormat,
|
18
28
|
Suggestion,
|
19
29
|
)
|
30
|
+
from .place import AddressComponent, BusinessStatus, GooglePlace, PriceLevel
|
20
31
|
|
21
32
|
__all__ = [
|
22
33
|
"PlaceType",
|
34
|
+
"RankPreference",
|
35
|
+
"EVConnectorType",
|
23
36
|
"StringRange",
|
24
37
|
"FormattableText",
|
25
38
|
"StructuredFormat",
|
26
39
|
"Circle",
|
27
40
|
"LocationBias",
|
28
41
|
"LocationRestriction",
|
42
|
+
"SearchTextLocationRestriction",
|
29
43
|
"Place",
|
44
|
+
"GooglePlace",
|
30
45
|
"AutocompletePlacesResponse",
|
31
46
|
"PlacePrediction",
|
32
47
|
"QueryPrediction",
|
@@ -34,4 +49,16 @@ __all__ = [
|
|
34
49
|
"GetPlaceRequest",
|
35
50
|
"AutocompletePlacesRequest",
|
36
51
|
"GeocodingRequest",
|
52
|
+
"SearchTextRequest",
|
53
|
+
"ResolvedAirport",
|
54
|
+
"AddressDescriptor",
|
55
|
+
"AddressComponent",
|
56
|
+
"FieldMaskInput",
|
57
|
+
"FieldPath",
|
58
|
+
"compile_field_mask",
|
59
|
+
"EVOptions",
|
60
|
+
"RoutingParameters",
|
61
|
+
"SearchTextResponse",
|
62
|
+
"PriceLevel",
|
63
|
+
"BusinessStatus",
|
37
64
|
]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
+
from enum import IntEnum
|
4
5
|
from typing import Optional
|
5
6
|
|
6
7
|
from pydantic import (
|
@@ -40,7 +41,24 @@ BASE64URL_36 = re.compile(
|
|
40
41
|
) # up to 36 chars, URL-safe base64-ish
|
41
42
|
|
42
43
|
|
43
|
-
# ----------
|
44
|
+
# ---------- Fundamental types ----------
|
45
|
+
class LocalizedText(BaseModel):
|
46
|
+
"""Localized text with language code."""
|
47
|
+
|
48
|
+
model_config = ConfigDict(extra="allow")
|
49
|
+
|
50
|
+
text: str
|
51
|
+
language_code: Optional[str] = None
|
52
|
+
|
53
|
+
@field_validator("language_code")
|
54
|
+
@classmethod
|
55
|
+
def _language_code(cls, v: str) -> str:
|
56
|
+
if not BCP47.match(v):
|
57
|
+
raise ValueError("language_code must be a valid BCP-47 language tag")
|
58
|
+
return v
|
59
|
+
|
60
|
+
|
61
|
+
# ---------- "External" Google message wrappers ----------
|
44
62
|
class ExternalModel(BaseModel):
|
45
63
|
"""Permissive wrapper for Google messages we don't model in detail."""
|
46
64
|
|
@@ -77,7 +95,142 @@ class FuelOptions(ExternalModel): ...
|
|
77
95
|
class EVChargeOptions(ExternalModel): ...
|
78
96
|
|
79
97
|
|
80
|
-
class AddressDescriptor(
|
98
|
+
class AddressDescriptor(BaseModel):
|
99
|
+
"""A relational description of a location with nearby landmarks and containing areas."""
|
100
|
+
|
101
|
+
model_config = ConfigDict(extra="forbid")
|
102
|
+
|
103
|
+
class Landmark(BaseModel):
|
104
|
+
"""Basic landmark information and relationship with target location."""
|
105
|
+
|
106
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
107
|
+
|
108
|
+
class SpatialRelationship(IntEnum):
|
109
|
+
"""Spatial relationship between target location and landmark."""
|
110
|
+
|
111
|
+
NEAR = 0
|
112
|
+
WITHIN = 1
|
113
|
+
BESIDE = 2
|
114
|
+
ACROSS_THE_ROAD = 3
|
115
|
+
DOWN_THE_ROAD = 4
|
116
|
+
AROUND_THE_CORNER = 5
|
117
|
+
BEHIND = 6
|
118
|
+
|
119
|
+
name: str = Field(..., description="Landmark's resource name")
|
120
|
+
place_id: str = Field(..., description="Landmark's place ID")
|
121
|
+
display_name: LocalizedText = Field(..., description="Landmark's display name")
|
122
|
+
types: list[str] = Field(
|
123
|
+
default_factory=list, description="Type tags for landmark"
|
124
|
+
)
|
125
|
+
spatial_relationship: SpatialRelationship = Field(
|
126
|
+
default=SpatialRelationship.NEAR,
|
127
|
+
description="Spatial relationship to target",
|
128
|
+
)
|
129
|
+
straight_line_distance_meters: float = Field(
|
130
|
+
..., ge=0.0, description="Straight line distance in meters"
|
131
|
+
)
|
132
|
+
travel_distance_meters: Optional[float] = Field(
|
133
|
+
default=None,
|
134
|
+
ge=0.0,
|
135
|
+
description="Travel distance in meters along road network",
|
136
|
+
)
|
137
|
+
|
138
|
+
@field_validator("name")
|
139
|
+
@classmethod
|
140
|
+
def _name(cls, v: str) -> str:
|
141
|
+
if not PLACE_RESOURCE.fullmatch(v):
|
142
|
+
raise ValueError("name must be in the form 'places/{place_id}'")
|
143
|
+
return v
|
144
|
+
|
145
|
+
@field_validator("place_id")
|
146
|
+
@classmethod
|
147
|
+
def _place_id(cls, v: str) -> str:
|
148
|
+
if not PLACE_ID.fullmatch(v):
|
149
|
+
raise ValueError("place_id must be a valid Place ID")
|
150
|
+
return v
|
151
|
+
|
152
|
+
@field_validator("types")
|
153
|
+
@classmethod
|
154
|
+
def _types(cls, v: list[str]) -> list[str]:
|
155
|
+
out, seen = [], set()
|
156
|
+
for raw in v:
|
157
|
+
t = raw.strip()
|
158
|
+
if not t:
|
159
|
+
raise ValueError("types cannot contain empty strings")
|
160
|
+
if not PLACE_TYPE.fullmatch(t):
|
161
|
+
raise ValueError(f"invalid place type '{t}'")
|
162
|
+
if t not in seen:
|
163
|
+
out.append(t)
|
164
|
+
seen.add(t)
|
165
|
+
return out
|
166
|
+
|
167
|
+
@model_validator(mode="after")
|
168
|
+
def _name_id_consistency(self) -> Self:
|
169
|
+
if self.name.split("/", 1)[1] != self.place_id:
|
170
|
+
raise ValueError("place_id must match the trailing component of name")
|
171
|
+
return self
|
172
|
+
|
173
|
+
class Area(BaseModel):
|
174
|
+
"""Area information and relationship with target location."""
|
175
|
+
|
176
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
177
|
+
|
178
|
+
class Containment(IntEnum):
|
179
|
+
"""Spatial relationship between target location and area."""
|
180
|
+
|
181
|
+
CONTAINMENT_UNSPECIFIED = 0
|
182
|
+
WITHIN = 1
|
183
|
+
OUTSKIRTS = 2
|
184
|
+
NEAR = 3
|
185
|
+
|
186
|
+
name: str = Field(..., description="Area's resource name")
|
187
|
+
place_id: str = Field(..., description="Area's place ID")
|
188
|
+
display_name: LocalizedText = Field(..., description="Area's display name")
|
189
|
+
containment: Containment = Field(
|
190
|
+
default=Containment.CONTAINMENT_UNSPECIFIED,
|
191
|
+
description="Spatial relationship to target",
|
192
|
+
)
|
193
|
+
|
194
|
+
@field_validator("name")
|
195
|
+
@classmethod
|
196
|
+
def _name(cls, v: str) -> str:
|
197
|
+
if not PLACE_RESOURCE.fullmatch(v):
|
198
|
+
raise ValueError("name must be in the form 'places/{place_id}'")
|
199
|
+
return v
|
200
|
+
|
201
|
+
@field_validator("place_id")
|
202
|
+
@classmethod
|
203
|
+
def _place_id(cls, v: str) -> str:
|
204
|
+
if not PLACE_ID.fullmatch(v):
|
205
|
+
raise ValueError("place_id must be a valid Place ID")
|
206
|
+
return v
|
207
|
+
|
208
|
+
@model_validator(mode="after")
|
209
|
+
def _name_id_consistency(self) -> Self:
|
210
|
+
if self.name.split("/", 1)[1] != self.place_id:
|
211
|
+
raise ValueError("place_id must match the trailing component of name")
|
212
|
+
return self
|
213
|
+
|
214
|
+
landmarks: list[Landmark] = Field(
|
215
|
+
default_factory=list, description="Ranked list of nearby landmarks"
|
216
|
+
)
|
217
|
+
areas: list[Area] = Field(
|
218
|
+
default_factory=list, description="Ranked list of containing or adjacent areas"
|
219
|
+
)
|
220
|
+
|
221
|
+
@field_validator("landmarks")
|
222
|
+
@classmethod
|
223
|
+
def _max_landmarks(cls, v: list[Landmark]) -> list[Landmark]:
|
224
|
+
if len(v) > 10: # Reasonable limit for API responses
|
225
|
+
raise ValueError("landmarks can contain at most 10 items")
|
226
|
+
return v
|
227
|
+
|
228
|
+
@field_validator("areas")
|
229
|
+
@classmethod
|
230
|
+
def _max_areas(cls, v: list[Area]) -> list[Area]:
|
231
|
+
if len(v) > 10: # Reasonable limit for API responses
|
232
|
+
raise ValueError("areas can contain at most 10 items")
|
233
|
+
return v
|
81
234
|
|
82
235
|
|
83
236
|
class PriceRange(ExternalModel): ...
|