bookalimo 0.1.5__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} +45 -42
- 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.5.dist-info/METADATA +0 -392
- bookalimo-0.1.5.dist-info/RECORD +0 -12
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,337 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
from pydantic import (
|
6
|
+
AnyUrl,
|
7
|
+
BaseModel,
|
8
|
+
ConfigDict,
|
9
|
+
Field,
|
10
|
+
field_validator,
|
11
|
+
model_validator,
|
12
|
+
)
|
13
|
+
from typing_extensions import Self
|
14
|
+
|
15
|
+
from .common import (
|
16
|
+
HEX_COLOR,
|
17
|
+
PLACE_ID,
|
18
|
+
PLACE_RESOURCE,
|
19
|
+
PLACE_TYPE,
|
20
|
+
AccessibilityOptions,
|
21
|
+
AddressDescriptor,
|
22
|
+
Attribution,
|
23
|
+
ContentBlock,
|
24
|
+
EVChargeOptions,
|
25
|
+
FuelOptions,
|
26
|
+
LatLng,
|
27
|
+
OpeningHours,
|
28
|
+
ParkingOptions,
|
29
|
+
PaymentOptions,
|
30
|
+
Photo,
|
31
|
+
PlusCode,
|
32
|
+
PostalAddress,
|
33
|
+
PriceRange,
|
34
|
+
Review,
|
35
|
+
SubDestination,
|
36
|
+
TimeZone,
|
37
|
+
Viewport,
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
class PriceLevel(int):
|
42
|
+
PRICE_LEVEL_UNSPECIFIED = 0
|
43
|
+
PRICE_LEVEL_FREE = 1
|
44
|
+
PRICE_LEVEL_INEXPENSIVE = 2
|
45
|
+
PRICE_LEVEL_MODERATE = 3
|
46
|
+
PRICE_LEVEL_EXPENSIVE = 4
|
47
|
+
PRICE_LEVEL_VERY_EXPENSIVE = 5
|
48
|
+
|
49
|
+
|
50
|
+
class BusinessStatus(int):
|
51
|
+
BUSINESS_STATUS_UNSPECIFIED = 0
|
52
|
+
OPERATIONAL = 1
|
53
|
+
CLOSED_TEMPORARILY = 2
|
54
|
+
CLOSED_PERMANENTLY = 3
|
55
|
+
|
56
|
+
|
57
|
+
class AddressComponent(BaseModel):
|
58
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
59
|
+
|
60
|
+
long_text: str
|
61
|
+
short_text: Optional[str] = None
|
62
|
+
types: List[str] = Field(default_factory=list)
|
63
|
+
language_code: Optional[str] = None
|
64
|
+
|
65
|
+
@field_validator("types")
|
66
|
+
@classmethod
|
67
|
+
def _types(cls, v: List[str]) -> List[str]:
|
68
|
+
out, seen = [], set()
|
69
|
+
for raw in v:
|
70
|
+
t = raw.strip()
|
71
|
+
if not t:
|
72
|
+
raise ValueError("types cannot contain empty strings")
|
73
|
+
if not PLACE_TYPE.fullmatch(t):
|
74
|
+
raise ValueError(f"invalid place type '{t}'")
|
75
|
+
if t not in seen:
|
76
|
+
out.append(t)
|
77
|
+
seen.add(t)
|
78
|
+
return out
|
79
|
+
|
80
|
+
|
81
|
+
class GenerativeSummary(BaseModel):
|
82
|
+
model_config = ConfigDict(extra="forbid")
|
83
|
+
|
84
|
+
overview: Optional[LocalizedText] = None
|
85
|
+
overview_flag_content_uri: Optional[AnyUrl] = None
|
86
|
+
disclosure_text: Optional[LocalizedText] = None
|
87
|
+
|
88
|
+
|
89
|
+
class ReviewSummary(BaseModel):
|
90
|
+
model_config = ConfigDict(extra="forbid")
|
91
|
+
|
92
|
+
text: Optional[LocalizedText] = None
|
93
|
+
flag_content_uri: Optional[AnyUrl] = None
|
94
|
+
disclosure_text: Optional[LocalizedText] = None
|
95
|
+
|
96
|
+
|
97
|
+
class EvChargeAmenitySummary(BaseModel):
|
98
|
+
model_config = ConfigDict(extra="forbid")
|
99
|
+
|
100
|
+
overview: ContentBlock
|
101
|
+
coffee: Optional[ContentBlock] = None
|
102
|
+
restaurant: Optional[ContentBlock] = None
|
103
|
+
store: Optional[ContentBlock] = None
|
104
|
+
flag_content_uri: Optional[AnyUrl] = None
|
105
|
+
disclosure_text: Optional[LocalizedText] = None
|
106
|
+
|
107
|
+
|
108
|
+
class NeighborhoodSummary(BaseModel):
|
109
|
+
model_config = ConfigDict(extra="forbid")
|
110
|
+
|
111
|
+
overview: Optional[ContentBlock] = None
|
112
|
+
description: Optional[ContentBlock] = None
|
113
|
+
flag_content_uri: Optional[AnyUrl] = None
|
114
|
+
disclosure_text: Optional[LocalizedText] = None
|
115
|
+
|
116
|
+
|
117
|
+
class LocalizedText(BaseModel):
|
118
|
+
model_config = ConfigDict(extra="allow")
|
119
|
+
|
120
|
+
text: str
|
121
|
+
language_code: str
|
122
|
+
|
123
|
+
|
124
|
+
class ContainingPlace(BaseModel):
|
125
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
126
|
+
|
127
|
+
name: str
|
128
|
+
id: str
|
129
|
+
|
130
|
+
@field_validator("name")
|
131
|
+
@classmethod
|
132
|
+
def _name(cls, v: str) -> str:
|
133
|
+
if not PLACE_RESOURCE.fullmatch(v):
|
134
|
+
raise ValueError("name must be 'places/{place_id}'")
|
135
|
+
return v
|
136
|
+
|
137
|
+
@field_validator("id")
|
138
|
+
@classmethod
|
139
|
+
def _id(cls, v: str) -> str:
|
140
|
+
if not PLACE_ID.fullmatch(v):
|
141
|
+
raise ValueError("id must look like a Place ID")
|
142
|
+
return v
|
143
|
+
|
144
|
+
@model_validator(mode="after")
|
145
|
+
def _match(self) -> Self:
|
146
|
+
if self.name.split("/", 1)[1] != self.id:
|
147
|
+
raise ValueError("id must match the trailing component of name")
|
148
|
+
return self
|
149
|
+
|
150
|
+
|
151
|
+
class Place(BaseModel):
|
152
|
+
model_config = ConfigDict(extra="allow", str_strip_whitespace=True)
|
153
|
+
|
154
|
+
# Identity
|
155
|
+
name: Optional[str] = Field(default=None, description="places/{place_id}")
|
156
|
+
id: Optional[str] = Field(default=None, description="Place ID")
|
157
|
+
|
158
|
+
# Labels & typing
|
159
|
+
display_name: Optional[LocalizedText] = None
|
160
|
+
types: List[str] = Field(default_factory=list)
|
161
|
+
primary_type: Optional[str] = None
|
162
|
+
primary_type_display_name: Optional[LocalizedText] = None
|
163
|
+
|
164
|
+
# Phones & addresses
|
165
|
+
national_phone_number: Optional[str] = None
|
166
|
+
international_phone_number: Optional[str] = None
|
167
|
+
formatted_address: Optional[str] = None
|
168
|
+
short_formatted_address: Optional[str] = None
|
169
|
+
postal_address: Optional[PostalAddress] = None
|
170
|
+
address_components: List[AddressComponent] = Field(default_factory=list)
|
171
|
+
plus_code: Optional[PlusCode] = None
|
172
|
+
|
173
|
+
# Location & map
|
174
|
+
location: Optional[LatLng] = None
|
175
|
+
viewport: Optional[Viewport] = None
|
176
|
+
|
177
|
+
# Scores, links, media
|
178
|
+
rating: Optional[float] = None
|
179
|
+
google_maps_uri: Optional[AnyUrl] = None
|
180
|
+
website_uri: Optional[AnyUrl] = None
|
181
|
+
reviews: List[Review] = Field(default_factory=list)
|
182
|
+
photos: List[Photo] = Field(default_factory=list)
|
183
|
+
|
184
|
+
# Hours
|
185
|
+
regular_opening_hours: Optional[OpeningHours] = None
|
186
|
+
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)
|
189
|
+
utc_offset_minutes: Optional[int] = None
|
190
|
+
time_zone: Optional[TimeZone] = None
|
191
|
+
|
192
|
+
# Misc attributes
|
193
|
+
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)
|
197
|
+
user_rating_count: Optional[int] = None
|
198
|
+
icon_mask_base_uri: Optional[AnyUrl] = None
|
199
|
+
icon_background_color: Optional[str] = None
|
200
|
+
|
201
|
+
# Food/venue features (optionals in proto)
|
202
|
+
takeout: Optional[bool] = None
|
203
|
+
delivery: Optional[bool] = None
|
204
|
+
dine_in: Optional[bool] = None
|
205
|
+
curbside_pickup: Optional[bool] = None
|
206
|
+
reservable: Optional[bool] = None
|
207
|
+
editorial_summary: Optional[LocalizedText] = None
|
208
|
+
serves_breakfast: Optional[bool] = None
|
209
|
+
serves_lunch: Optional[bool] = None
|
210
|
+
serves_dinner: Optional[bool] = None
|
211
|
+
serves_beer: Optional[bool] = None
|
212
|
+
serves_wine: Optional[bool] = None
|
213
|
+
serves_brunch: Optional[bool] = None
|
214
|
+
serves_vegetarian_food: Optional[bool] = None
|
215
|
+
outdoor_seating: Optional[bool] = None
|
216
|
+
live_music: Optional[bool] = None
|
217
|
+
menu_for_children: Optional[bool] = None
|
218
|
+
serves_cocktails: Optional[bool] = None
|
219
|
+
serves_dessert: Optional[bool] = None
|
220
|
+
serves_coffee: Optional[bool] = None
|
221
|
+
good_for_children: Optional[bool] = None
|
222
|
+
allows_dogs: Optional[bool] = None
|
223
|
+
restroom: Optional[bool] = None
|
224
|
+
good_for_groups: Optional[bool] = None
|
225
|
+
good_for_watching_sports: Optional[bool] = None
|
226
|
+
|
227
|
+
# Options & related places
|
228
|
+
payment_options: Optional[PaymentOptions] = None
|
229
|
+
parking_options: Optional[ParkingOptions] = None
|
230
|
+
sub_destinations: List[SubDestination] = Field(default_factory=list)
|
231
|
+
accessibility_options: Optional[AccessibilityOptions] = None
|
232
|
+
|
233
|
+
# Fuel/EV & AI summaries
|
234
|
+
fuel_options: Optional[FuelOptions] = None
|
235
|
+
ev_charge_options: Optional[EVChargeOptions] = None
|
236
|
+
generative_summary: Optional[GenerativeSummary] = None
|
237
|
+
review_summary: Optional[ReviewSummary] = None
|
238
|
+
ev_charge_amenity_summary: Optional[EvChargeAmenitySummary] = None
|
239
|
+
neighborhood_summary: Optional[NeighborhoodSummary] = None
|
240
|
+
|
241
|
+
# Context
|
242
|
+
containing_places: List[ContainingPlace] = Field(default_factory=list)
|
243
|
+
pure_service_area_business: Optional[bool] = None
|
244
|
+
address_descriptor: Optional[AddressDescriptor] = None
|
245
|
+
price_range: Optional[PriceRange] = None
|
246
|
+
|
247
|
+
# ---------- Validators ----------
|
248
|
+
@field_validator("name")
|
249
|
+
@classmethod
|
250
|
+
def _name(cls, v: Optional[str]) -> Optional[str]:
|
251
|
+
if v is None:
|
252
|
+
return v
|
253
|
+
if not PLACE_RESOURCE.fullmatch(v):
|
254
|
+
raise ValueError("name must be 'places/{place_id}'")
|
255
|
+
return v
|
256
|
+
|
257
|
+
@field_validator("id")
|
258
|
+
@classmethod
|
259
|
+
def _id(cls, v: Optional[str]) -> Optional[str]:
|
260
|
+
if v is None:
|
261
|
+
return v
|
262
|
+
if not PLACE_ID.fullmatch(v):
|
263
|
+
raise ValueError(
|
264
|
+
"id must be a plausible Place ID (base64url-like, >=10 chars)"
|
265
|
+
)
|
266
|
+
return v
|
267
|
+
|
268
|
+
@model_validator(mode="after")
|
269
|
+
def _id_consistency(self) -> Self:
|
270
|
+
if self.name and self.id and self.name.split("/", 1)[1] != self.id:
|
271
|
+
raise ValueError("id must match trailing component of name")
|
272
|
+
return self
|
273
|
+
|
274
|
+
@field_validator("types")
|
275
|
+
@classmethod
|
276
|
+
def _types(cls, v: List[str]) -> List[str]:
|
277
|
+
out, seen = [], set()
|
278
|
+
for raw in v:
|
279
|
+
t = raw.strip()
|
280
|
+
if not t:
|
281
|
+
raise ValueError("types cannot contain empty strings")
|
282
|
+
if not PLACE_TYPE.fullmatch(t):
|
283
|
+
raise ValueError(f"invalid place type '{t}'")
|
284
|
+
if t not in seen:
|
285
|
+
out.append(t)
|
286
|
+
seen.add(t)
|
287
|
+
return out
|
288
|
+
|
289
|
+
@model_validator(mode="after")
|
290
|
+
def _primary_type_is_in_types(self) -> Self:
|
291
|
+
if self.primary_type:
|
292
|
+
if not PLACE_TYPE.fullmatch(self.primary_type):
|
293
|
+
raise ValueError("primary_type must match place-type token pattern")
|
294
|
+
if self.types and self.primary_type not in self.types:
|
295
|
+
raise ValueError("primary_type must be included in types")
|
296
|
+
return self
|
297
|
+
|
298
|
+
@field_validator("rating")
|
299
|
+
@classmethod
|
300
|
+
def _rating(cls, v: Optional[float]) -> Optional[float]:
|
301
|
+
if v is None:
|
302
|
+
return v
|
303
|
+
if not (1.0 <= v <= 5.0):
|
304
|
+
raise ValueError("rating must be in [1.0, 5.0]")
|
305
|
+
return v
|
306
|
+
|
307
|
+
@field_validator("user_rating_count")
|
308
|
+
@classmethod
|
309
|
+
def _urc(cls, v: Optional[int]) -> Optional[int]:
|
310
|
+
if v is None:
|
311
|
+
return v
|
312
|
+
if v < 0:
|
313
|
+
raise ValueError("user_rating_count must be >= 0")
|
314
|
+
return v
|
315
|
+
|
316
|
+
@field_validator("icon_background_color")
|
317
|
+
@classmethod
|
318
|
+
def _hex(cls, v: Optional[str]) -> Optional[str]:
|
319
|
+
if v is None:
|
320
|
+
return v
|
321
|
+
if not HEX_COLOR.fullmatch(v):
|
322
|
+
raise ValueError("icon_background_color must be a hex color like #909CE1")
|
323
|
+
return v
|
324
|
+
|
325
|
+
@field_validator("reviews")
|
326
|
+
@classmethod
|
327
|
+
def _max_reviews(cls, v: List[Review]) -> List[Review]:
|
328
|
+
if len(v) > 5:
|
329
|
+
raise ValueError("reviews can contain at most 5 items")
|
330
|
+
return v
|
331
|
+
|
332
|
+
@field_validator("photos")
|
333
|
+
@classmethod
|
334
|
+
def _max_photos(cls, v: List[Photo]) -> List[Photo]:
|
335
|
+
if len(v) > 10:
|
336
|
+
raise ValueError("photos can contain at most 10 items")
|
337
|
+
return v
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Service layer for Bookalimo API operations."""
|
2
|
+
|
3
|
+
from .pricing import AsyncPricingService, PricingService
|
4
|
+
from .reservations import AsyncReservationsService, ReservationsService
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ReservationsService",
|
8
|
+
"AsyncReservationsService",
|
9
|
+
"PricingService",
|
10
|
+
"AsyncPricingService",
|
11
|
+
]
|
@@ -0,0 +1,191 @@
|
|
1
|
+
"""Pricing service for getting quotes and updating trip details."""
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from ..schemas.booking import (
|
6
|
+
DetailsRequest,
|
7
|
+
DetailsResponse,
|
8
|
+
Location,
|
9
|
+
PriceRequest,
|
10
|
+
PriceResponse,
|
11
|
+
RateType,
|
12
|
+
)
|
13
|
+
from ..transport.base import AsyncBaseTransport, BaseTransport
|
14
|
+
|
15
|
+
|
16
|
+
class AsyncPricingService:
|
17
|
+
"""Async pricing service."""
|
18
|
+
|
19
|
+
def __init__(self, transport: AsyncBaseTransport):
|
20
|
+
self._transport = transport
|
21
|
+
|
22
|
+
async def quote(
|
23
|
+
self,
|
24
|
+
rate_type: RateType,
|
25
|
+
date_time: str,
|
26
|
+
pickup: Location,
|
27
|
+
dropoff: Location,
|
28
|
+
passengers: int,
|
29
|
+
luggage: int,
|
30
|
+
**opts: Any,
|
31
|
+
) -> PriceResponse:
|
32
|
+
"""
|
33
|
+
Get pricing for a trip.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
rate_type: Rate type (P2P, HOURLY, etc.)
|
37
|
+
date_time: Date and time in 'MM/dd/yyyy hh:mm tt' format
|
38
|
+
pickup: Pickup location
|
39
|
+
dropoff: Dropoff location
|
40
|
+
passengers: Number of passengers
|
41
|
+
luggage: Number of luggage pieces
|
42
|
+
**opts: Optional fields like hours, stops, account, car_class_code,
|
43
|
+
passenger, rewards, pets, car_seats, boosters, infants,
|
44
|
+
customer_comment
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
PriceResponse with pricing information and session token
|
48
|
+
"""
|
49
|
+
# Build request with optional fields
|
50
|
+
request_data: dict[str, Any] = {
|
51
|
+
"rate_type": rate_type,
|
52
|
+
"date_time": date_time,
|
53
|
+
"pickup": pickup,
|
54
|
+
"dropoff": dropoff,
|
55
|
+
"passengers": passengers,
|
56
|
+
"luggage": luggage,
|
57
|
+
}
|
58
|
+
|
59
|
+
# Add optional fields if provided
|
60
|
+
optional_fields = [
|
61
|
+
"hours",
|
62
|
+
"stops",
|
63
|
+
"account",
|
64
|
+
"passenger",
|
65
|
+
"rewards",
|
66
|
+
"car_class_code",
|
67
|
+
"pets",
|
68
|
+
"car_seats",
|
69
|
+
"boosters",
|
70
|
+
"infants",
|
71
|
+
"customer_comment",
|
72
|
+
]
|
73
|
+
|
74
|
+
for field in optional_fields:
|
75
|
+
if field in opts and opts[field] is not None:
|
76
|
+
request_data[field] = opts[field]
|
77
|
+
|
78
|
+
request = PriceRequest(**request_data)
|
79
|
+
return await self._transport.post("/booking/price/", request, PriceResponse)
|
80
|
+
|
81
|
+
async def update_details(self, token: str, **details: Any) -> DetailsResponse:
|
82
|
+
"""
|
83
|
+
Update reservation details and get updated pricing.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
token: Session token from quote()
|
87
|
+
**details: Fields to update (car_class_code, pickup, dropoff,
|
88
|
+
stops, account, passenger, rewards, pets, car_seats,
|
89
|
+
boosters, infants, customer_comment, ta_fee)
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
DetailsResponse with updated pricing
|
93
|
+
"""
|
94
|
+
request_data: dict[str, Any] = {"token": token}
|
95
|
+
|
96
|
+
# Add provided details
|
97
|
+
for key, value in details.items():
|
98
|
+
if value is not None:
|
99
|
+
request_data[key] = value
|
100
|
+
|
101
|
+
request = DetailsRequest(**request_data)
|
102
|
+
return await self._transport.post("/booking/details/", request, DetailsResponse)
|
103
|
+
|
104
|
+
|
105
|
+
class PricingService:
|
106
|
+
"""Sync pricing service."""
|
107
|
+
|
108
|
+
def __init__(self, transport: BaseTransport):
|
109
|
+
self._transport = transport
|
110
|
+
|
111
|
+
def quote(
|
112
|
+
self,
|
113
|
+
rate_type: RateType,
|
114
|
+
date_time: str,
|
115
|
+
pickup: Location,
|
116
|
+
dropoff: Location,
|
117
|
+
passengers: int,
|
118
|
+
luggage: int,
|
119
|
+
**opts: Any,
|
120
|
+
) -> PriceResponse:
|
121
|
+
"""
|
122
|
+
Get pricing for a trip.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
rate_type: Rate type (P2P, HOURLY, etc.)
|
126
|
+
date_time: Date and time in 'MM/dd/yyyy hh:mm tt' format
|
127
|
+
pickup: Pickup location
|
128
|
+
dropoff: Location
|
129
|
+
passengers: Number of passengers
|
130
|
+
luggage: Number of luggage pieces
|
131
|
+
**opts: Optional fields like hours, stops, account, car_class_code,
|
132
|
+
passenger, rewards, pets, car_seats, boosters, infants,
|
133
|
+
customer_comment
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
PriceResponse with pricing information and session token
|
137
|
+
"""
|
138
|
+
# Build request with optional fields
|
139
|
+
request_data: dict[str, Any] = {
|
140
|
+
"rate_type": rate_type,
|
141
|
+
"date_time": date_time,
|
142
|
+
"pickup": pickup,
|
143
|
+
"dropoff": dropoff,
|
144
|
+
"passengers": passengers,
|
145
|
+
"luggage": luggage,
|
146
|
+
}
|
147
|
+
|
148
|
+
# Add optional fields if provided
|
149
|
+
optional_fields = [
|
150
|
+
"hours",
|
151
|
+
"stops",
|
152
|
+
"account",
|
153
|
+
"passenger",
|
154
|
+
"rewards",
|
155
|
+
"car_class_code",
|
156
|
+
"pets",
|
157
|
+
"car_seats",
|
158
|
+
"boosters",
|
159
|
+
"infants",
|
160
|
+
"customer_comment",
|
161
|
+
]
|
162
|
+
|
163
|
+
for field in optional_fields:
|
164
|
+
if field in opts and opts[field] is not None:
|
165
|
+
request_data[field] = opts[field]
|
166
|
+
|
167
|
+
request = PriceRequest(**request_data)
|
168
|
+
return self._transport.post("/booking/price/", request, PriceResponse)
|
169
|
+
|
170
|
+
def update_details(self, token: str, **details: Any) -> DetailsResponse:
|
171
|
+
"""
|
172
|
+
Update reservation details and get updated pricing.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
token: Session token from quote()
|
176
|
+
**details: Fields to update (car_class_code, pickup, dropoff,
|
177
|
+
stops, account, passenger, rewards, pets, car_seats,
|
178
|
+
boosters, infants, customer_comment, ta_fee)
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
DetailsResponse with updated pricing
|
182
|
+
"""
|
183
|
+
request_data: dict[str, Any] = {"token": token}
|
184
|
+
|
185
|
+
# Add provided details
|
186
|
+
for key, value in details.items():
|
187
|
+
if value is not None:
|
188
|
+
request_data[key] = value
|
189
|
+
|
190
|
+
request = DetailsRequest(**request_data)
|
191
|
+
return self._transport.post("/booking/details/", request, DetailsResponse)
|