bookalimo 0.1.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/__init__.py +28 -0
- bookalimo/_client.py +350 -0
- bookalimo/models.py +532 -0
- bookalimo/py.typed +1 -0
- bookalimo/wrapper.py +396 -0
- bookalimo-0.1.1.dist-info/METADATA +331 -0
- bookalimo-0.1.1.dist-info/RECORD +10 -0
- bookalimo-0.1.1.dist-info/WHEEL +5 -0
- bookalimo-0.1.1.dist-info/licenses/LICENSE +0 -0
- bookalimo-0.1.1.dist-info/top_level.txt +1 -0
bookalimo/models.py
ADDED
@@ -0,0 +1,532 @@
|
|
1
|
+
"""
|
2
|
+
Pydantic models for Book-A-Limo API data structures.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import hashlib
|
6
|
+
import warnings
|
7
|
+
from enum import Enum
|
8
|
+
from typing import Any, Optional
|
9
|
+
|
10
|
+
import airportsdata
|
11
|
+
import pycountry
|
12
|
+
import us
|
13
|
+
from pydantic import BaseModel, Field, model_validator
|
14
|
+
from typing_extensions import Self
|
15
|
+
|
16
|
+
icao_data = airportsdata.load("ICAO")
|
17
|
+
iata_data = airportsdata.load("IATA")
|
18
|
+
|
19
|
+
|
20
|
+
class RateType(Enum):
|
21
|
+
"""Rate types for reservations."""
|
22
|
+
|
23
|
+
P2P = 0 # Point-to-Point (best guess from context)
|
24
|
+
HOURLY = 1 # Hourly (best guess from context)
|
25
|
+
DAILY = 2 # Daily
|
26
|
+
TOUR = 3 # Tour
|
27
|
+
ROUND_TRIP = 4 # Round Trip
|
28
|
+
RT_HALF = 5 # RT Half
|
29
|
+
|
30
|
+
|
31
|
+
class LocationType(Enum):
|
32
|
+
"""Location types."""
|
33
|
+
|
34
|
+
ADDRESS = 0
|
35
|
+
AIRPORT = 1
|
36
|
+
TRAIN_STATION = 2
|
37
|
+
CRUISE = 3
|
38
|
+
|
39
|
+
|
40
|
+
class MeetGreetType(Enum):
|
41
|
+
"""Meet & Greet options."""
|
42
|
+
|
43
|
+
OTHER = 0
|
44
|
+
FBO = 1
|
45
|
+
BAGGAGE_CLAIM = 2
|
46
|
+
CURB_SIDE = 3
|
47
|
+
GATE = 4
|
48
|
+
INTERNATIONAL = 5
|
49
|
+
GREETER_SERVICE = 6
|
50
|
+
|
51
|
+
|
52
|
+
class RewardType(Enum):
|
53
|
+
"""Reward account types."""
|
54
|
+
|
55
|
+
UNITED_MILEAGEPLUS = 0
|
56
|
+
|
57
|
+
|
58
|
+
class ReservationStatus(Enum):
|
59
|
+
"""Reservation status."""
|
60
|
+
|
61
|
+
ACTIVE = None
|
62
|
+
NO_SHOW = 0
|
63
|
+
CANCELED = 1
|
64
|
+
LATE_CANCELED = 2
|
65
|
+
|
66
|
+
|
67
|
+
class CardHolderType(Enum):
|
68
|
+
"""
|
69
|
+
Credit card holder types (API Documentation Unclear).
|
70
|
+
TODO: Update when API documentation is clarified by Ivan.
|
71
|
+
Best guess based on typical credit card processing:
|
72
|
+
"""
|
73
|
+
|
74
|
+
PERSONAL = 0 # Personal/Individual account (best guess)
|
75
|
+
BUSINESS = 1 # Business/Corporate account (best guess)
|
76
|
+
# Note: Add UNKNOWN = 3 if you see this value in responses
|
77
|
+
UNKNOWN = 3 # From example in API doc
|
78
|
+
|
79
|
+
|
80
|
+
class Credentials(BaseModel):
|
81
|
+
"""User credentials for API authentication."""
|
82
|
+
|
83
|
+
id: str
|
84
|
+
is_customer: bool = False
|
85
|
+
password_hash: str
|
86
|
+
|
87
|
+
@classmethod
|
88
|
+
def create_hash(cls, password: str, user_id: str) -> str:
|
89
|
+
"""Create password hash as required by API: Sha256(Sha256(Password) + LowerCase(Id))"""
|
90
|
+
inner_hash = hashlib.sha256(password.encode()).hexdigest()
|
91
|
+
full_string = inner_hash + user_id.lower()
|
92
|
+
return hashlib.sha256(full_string.encode()).hexdigest()
|
93
|
+
|
94
|
+
|
95
|
+
class City(BaseModel):
|
96
|
+
"""City information."""
|
97
|
+
|
98
|
+
city_name: str
|
99
|
+
country_code: str = Field(..., description="ISO 3166-1 alpha-2 country code")
|
100
|
+
state_code: Optional[str] = Field(None, description="US state code")
|
101
|
+
state_name: Optional[str] = Field(None, description="US state name")
|
102
|
+
|
103
|
+
@model_validator(mode="after")
|
104
|
+
def validate_country_code(self) -> Self:
|
105
|
+
"""Validate that country_code is a valid ISO 3166-1 alpha-2 country code."""
|
106
|
+
if not pycountry.countries.get(alpha_2=self.country_code):
|
107
|
+
raise ValueError(f"Invalid country code: {self.country_code}")
|
108
|
+
|
109
|
+
return self
|
110
|
+
|
111
|
+
@model_validator(mode="after")
|
112
|
+
def validate_us(self) -> Self:
|
113
|
+
"""Validate that state_code is a valid US state code and state_name is a valid US state name."""
|
114
|
+
code_match = us.states.lookup(str(self.state_code))
|
115
|
+
name_match = us.states.lookup(str(self.state_name))
|
116
|
+
if not code_match and not name_match:
|
117
|
+
raise ValueError(
|
118
|
+
f"Invalid state code or name: {self.state_code} or {self.state_name}"
|
119
|
+
)
|
120
|
+
if code_match and name_match and code_match != name_match:
|
121
|
+
raise ValueError(
|
122
|
+
f"State code and name do not match: {self.state_code} and {self.state_name}"
|
123
|
+
)
|
124
|
+
match = code_match or name_match
|
125
|
+
if match:
|
126
|
+
self.state_code = match.abbr
|
127
|
+
self.state_name = match.name
|
128
|
+
return self
|
129
|
+
raise ValueError(
|
130
|
+
f"Invalid state code or name: {self.state_code} or {self.state_name}"
|
131
|
+
)
|
132
|
+
|
133
|
+
|
134
|
+
class Address(BaseModel):
|
135
|
+
"""
|
136
|
+
Address information.
|
137
|
+
|
138
|
+
Note: API documentation inconsistency - mentions 'googlePlaceId' in text
|
139
|
+
but examples use 'googleGeocode'. Following examples and using google_geocode.
|
140
|
+
TODO: Confirm with API author if this should be googlePlaceId instead.
|
141
|
+
"""
|
142
|
+
|
143
|
+
google_geocode: Optional[dict[str, Any]] = Field(
|
144
|
+
None, description="Raw Google Geocoding API response (recommended)"
|
145
|
+
)
|
146
|
+
city: Optional[City] = Field(
|
147
|
+
None, description="Use only if google_geocode not available"
|
148
|
+
)
|
149
|
+
district: Optional[str] = Field(None, description="e.g., Manhattan")
|
150
|
+
neighbourhood: Optional[str] = Field(None, description="e.g., Lower Manhattan")
|
151
|
+
place_name: Optional[str] = Field(None, description="e.g., Empire State Building")
|
152
|
+
street_name: Optional[str] = Field(None, description="e.g., East 34th St")
|
153
|
+
building: Optional[str] = Field(None, description="e.g., 53")
|
154
|
+
suite: Optional[str] = Field(None, description="e.g., 5P")
|
155
|
+
zip: Optional[str] = Field(None, description="e.g., 10016")
|
156
|
+
|
157
|
+
@model_validator(mode="after")
|
158
|
+
def validate_address(self) -> Self:
|
159
|
+
"""Validate that either place_name or street_name is provided."""
|
160
|
+
if not self.place_name and not self.street_name:
|
161
|
+
raise ValueError("Either place_name or street_name must be provided")
|
162
|
+
|
163
|
+
return self
|
164
|
+
|
165
|
+
@model_validator(mode="after")
|
166
|
+
def validate_city_or_google_geocode(self) -> Self:
|
167
|
+
"""Validate that exactly one of city or google_geocode is provided, with preference for google_geocode."""
|
168
|
+
if not self.city and not self.google_geocode:
|
169
|
+
raise ValueError("Either city or google_geocode must be provided")
|
170
|
+
|
171
|
+
if self.city and self.google_geocode:
|
172
|
+
raise ValueError("Only one of city or google_geocode must be provided")
|
173
|
+
|
174
|
+
if self.city:
|
175
|
+
warnings.warn(
|
176
|
+
"Not recommended. Use only if you can't provide googleGeocode.",
|
177
|
+
stacklevel=3,
|
178
|
+
)
|
179
|
+
|
180
|
+
return self
|
181
|
+
|
182
|
+
|
183
|
+
class Airport(BaseModel):
|
184
|
+
"""Airport information."""
|
185
|
+
|
186
|
+
iata_code: str = Field(..., description="3-letter IATA code, e.g., JFK")
|
187
|
+
country_code: Optional[str] = Field(None, description="ISO 3166-1 alpha-2")
|
188
|
+
state_code: Optional[str] = Field(None, description="US state code, e.g., NY")
|
189
|
+
airline_iata_code: Optional[str] = Field(
|
190
|
+
None, description="2-letter IATA airline code"
|
191
|
+
)
|
192
|
+
airline_icao_code: Optional[str] = Field(
|
193
|
+
None, description="3-letter ICAO airline code"
|
194
|
+
)
|
195
|
+
flight_number: Optional[str] = Field(None, description="e.g., UA1234")
|
196
|
+
terminal: Optional[str] = Field(None, description="e.g., 7")
|
197
|
+
arriving_from_city: Optional[City] = None
|
198
|
+
meet_greet: Optional[int] = Field(
|
199
|
+
None,
|
200
|
+
description="Meet & greet option ID. Leave empty on price request to see options.",
|
201
|
+
)
|
202
|
+
|
203
|
+
@model_validator(mode="after")
|
204
|
+
def validate_country_code(self) -> Self:
|
205
|
+
"""Validate that country_code is a valid ISO 3166-1 alpha-2 country code."""
|
206
|
+
if not pycountry.countries.get(alpha_2=self.country_code):
|
207
|
+
raise ValueError(f"Invalid country code: {self.country_code}")
|
208
|
+
|
209
|
+
return self
|
210
|
+
|
211
|
+
@model_validator(mode="after")
|
212
|
+
def validate_state_code(self) -> Self:
|
213
|
+
"""Validate that state_code is a valid US state code."""
|
214
|
+
if self.state_code and not us.states.lookup(str(self.state_code)):
|
215
|
+
raise ValueError(f"Invalid state code: {self.state_code}")
|
216
|
+
|
217
|
+
return self
|
218
|
+
|
219
|
+
@model_validator(mode="after")
|
220
|
+
def validate_airport(self) -> Self:
|
221
|
+
"""Validate that iata_code or airline_icao_code is a valid IATA or ICAO code."""
|
222
|
+
if self.iata_code and self.iata_code not in iata_data:
|
223
|
+
raise ValueError(f"Invalid IATA code: {self.iata_code}")
|
224
|
+
if self.airline_icao_code and self.airline_icao_code not in icao_data:
|
225
|
+
raise ValueError(f"Invalid ICAO code: {self.airline_icao_code}")
|
226
|
+
|
227
|
+
return self
|
228
|
+
|
229
|
+
|
230
|
+
class Location(BaseModel):
|
231
|
+
"""Location (address or airport)."""
|
232
|
+
|
233
|
+
type: LocationType
|
234
|
+
address: Optional[Address] = None
|
235
|
+
airport: Optional[Airport] = None
|
236
|
+
|
237
|
+
@model_validator(mode="after")
|
238
|
+
def validate_location(self) -> Self:
|
239
|
+
"""Validate that the correct location type is provided."""
|
240
|
+
if self.type == LocationType.ADDRESS and not self.address:
|
241
|
+
raise ValueError("Address is required when type is ADDRESS")
|
242
|
+
if self.type == LocationType.AIRPORT and not self.airport:
|
243
|
+
raise ValueError("Airport is required when type is AIRPORT")
|
244
|
+
|
245
|
+
return self
|
246
|
+
|
247
|
+
|
248
|
+
class Stop(BaseModel):
|
249
|
+
"""Stop information."""
|
250
|
+
|
251
|
+
description: str = Field(..., description="Address, place name, or comment")
|
252
|
+
is_en_route: bool = Field(..., description="True if stop is en-route")
|
253
|
+
|
254
|
+
|
255
|
+
class Account(BaseModel):
|
256
|
+
"""Travel agency or corporate account info."""
|
257
|
+
|
258
|
+
id: str = Field(..., description="TA or corporate account number")
|
259
|
+
department: Optional[str] = None
|
260
|
+
booker_first_name: Optional[str] = None
|
261
|
+
booker_last_name: Optional[str] = None
|
262
|
+
booker_email: Optional[str] = None
|
263
|
+
booker_phone: Optional[str] = Field(None, description="E164 format")
|
264
|
+
|
265
|
+
|
266
|
+
class Passenger(BaseModel):
|
267
|
+
"""Passenger information."""
|
268
|
+
|
269
|
+
first_name: str
|
270
|
+
last_name: str
|
271
|
+
email: Optional[str] = None
|
272
|
+
phone: str = Field(..., description="E164 format recommended")
|
273
|
+
|
274
|
+
|
275
|
+
class Reward(BaseModel):
|
276
|
+
"""Reward account information."""
|
277
|
+
|
278
|
+
type: RewardType
|
279
|
+
value: str = Field(..., description="Reward account number")
|
280
|
+
|
281
|
+
|
282
|
+
class CreditCard(BaseModel):
|
283
|
+
"""Credit card information."""
|
284
|
+
|
285
|
+
number: str
|
286
|
+
expiration: str = Field(..., description="MM/YY format")
|
287
|
+
cvv: str
|
288
|
+
card_holder: str
|
289
|
+
zip: Optional[str] = None
|
290
|
+
holder_type: Optional[CardHolderType] = Field(
|
291
|
+
None,
|
292
|
+
description="Card holder type - API documentation unclear, using best guess",
|
293
|
+
)
|
294
|
+
|
295
|
+
|
296
|
+
class BreakdownItem(BaseModel):
|
297
|
+
"""Price breakdown item."""
|
298
|
+
|
299
|
+
name: str
|
300
|
+
value: float
|
301
|
+
is_grand: bool = Field(
|
302
|
+
..., description="True if item should be highlighted (totals)"
|
303
|
+
)
|
304
|
+
|
305
|
+
|
306
|
+
class MeetGreetAdditional(BaseModel):
|
307
|
+
"""Additional meet & greet charges."""
|
308
|
+
|
309
|
+
name: str
|
310
|
+
price: float
|
311
|
+
|
312
|
+
|
313
|
+
class MeetGreet(BaseModel):
|
314
|
+
"""Meet & greet option."""
|
315
|
+
|
316
|
+
id: int
|
317
|
+
name: str
|
318
|
+
base_price: float
|
319
|
+
instructions: str
|
320
|
+
additional: list[MeetGreetAdditional]
|
321
|
+
total_price: float
|
322
|
+
fees: float
|
323
|
+
reservation_price: float
|
324
|
+
|
325
|
+
|
326
|
+
class Price(BaseModel):
|
327
|
+
"""Car class pricing information."""
|
328
|
+
|
329
|
+
car_class: str
|
330
|
+
car_description: str
|
331
|
+
max_passengers: int
|
332
|
+
max_luggage: int
|
333
|
+
price: float = Field(..., description="Price WITHOUT Meet&Greet")
|
334
|
+
price_default: float = Field(..., description="Price WITH default Meet&Greet")
|
335
|
+
image_128: str = Field(alias="image128")
|
336
|
+
image_256: str = Field(alias="image256")
|
337
|
+
image_512: str = Field(alias="image512")
|
338
|
+
default_meet_greet: Optional[int] = None
|
339
|
+
meet_greets: list[MeetGreet] = Field(default_factory=list)
|
340
|
+
|
341
|
+
|
342
|
+
class Reservation(BaseModel):
|
343
|
+
"""Basic reservation information."""
|
344
|
+
|
345
|
+
confirmation_number: str
|
346
|
+
is_archive: bool
|
347
|
+
local_date_time: str
|
348
|
+
eastern_date_time: Optional[str] = None
|
349
|
+
rate_type: RateType
|
350
|
+
passenger_name: Optional[str] = None
|
351
|
+
pickup_type: LocationType
|
352
|
+
pickup: str
|
353
|
+
dropoff_type: LocationType
|
354
|
+
dropoff: str
|
355
|
+
car_class: str
|
356
|
+
status: Optional[ReservationStatus] = None
|
357
|
+
|
358
|
+
|
359
|
+
class EditableReservationRequest(BaseModel):
|
360
|
+
"""
|
361
|
+
Editable reservation for modifications.
|
362
|
+
|
363
|
+
Note: API documentation inconsistency - credit_card marked as required in model
|
364
|
+
but omitted in edit examples. Making it optional as edit requests may not need it.
|
365
|
+
TODO: Clarify with API author when credit_card is actually required.
|
366
|
+
"""
|
367
|
+
|
368
|
+
confirmation: str
|
369
|
+
is_cancel_request: bool = False
|
370
|
+
rate_type: Optional[RateType] = None
|
371
|
+
pickup_date: Optional[str] = Field(None, description="MM/dd/yyyy format")
|
372
|
+
pickup_time: Optional[str] = Field(None, description="hh:mm tt format")
|
373
|
+
stops: Optional[list[Stop]] = None
|
374
|
+
credit_card: Optional[CreditCard] = Field(
|
375
|
+
None, description="Conditionally required - unclear from API docs when exactly"
|
376
|
+
)
|
377
|
+
passengers: Optional[int] = None
|
378
|
+
luggage: Optional[int] = None
|
379
|
+
pets: Optional[int] = None
|
380
|
+
car_seats: Optional[int] = None
|
381
|
+
boosters: Optional[int] = None
|
382
|
+
infants: Optional[int] = None
|
383
|
+
other: Optional[str] = Field(None, description="Other changes not listed")
|
384
|
+
|
385
|
+
|
386
|
+
class EditableReservationRequestAuthenticated(EditableReservationRequest):
|
387
|
+
"""Request for editing reservation with authenticated credentials."""
|
388
|
+
|
389
|
+
credentials: Credentials
|
390
|
+
|
391
|
+
|
392
|
+
# Request/Response Models
|
393
|
+
|
394
|
+
|
395
|
+
class PriceRequest(BaseModel):
|
396
|
+
"""Request for getting prices."""
|
397
|
+
|
398
|
+
rate_type: RateType
|
399
|
+
date_time: str = Field(..., description="MM/dd/yyyy hh:mm tt format")
|
400
|
+
pickup: Location
|
401
|
+
dropoff: Location
|
402
|
+
hours: Optional[int] = Field(None, description="For hourly rate_type only")
|
403
|
+
passengers: int
|
404
|
+
luggage: int
|
405
|
+
stops: Optional[list[Stop]] = None
|
406
|
+
account: Optional[Account] = Field(
|
407
|
+
None, description="TAs must provide for commission"
|
408
|
+
)
|
409
|
+
passenger: Optional[Passenger] = None
|
410
|
+
rewards: Optional[list[Reward]] = None
|
411
|
+
car_class_code: Optional[str] = Field(
|
412
|
+
None, description="e.g., 'SD' for specific car class"
|
413
|
+
)
|
414
|
+
pets: Optional[int] = None
|
415
|
+
car_seats: Optional[int] = None
|
416
|
+
boosters: Optional[int] = None
|
417
|
+
infants: Optional[int] = None
|
418
|
+
customer_comment: Optional[str] = None
|
419
|
+
|
420
|
+
|
421
|
+
class PriceRequestAuthenticated(PriceRequest):
|
422
|
+
"""Request for getting prices with authenticated credentials."""
|
423
|
+
|
424
|
+
credentials: Credentials
|
425
|
+
|
426
|
+
|
427
|
+
class PriceResponse(BaseModel):
|
428
|
+
"""Response from get prices."""
|
429
|
+
|
430
|
+
token: str
|
431
|
+
prices: list[Price]
|
432
|
+
|
433
|
+
|
434
|
+
class DetailsRequest(BaseModel):
|
435
|
+
"""Request for setting reservation details."""
|
436
|
+
|
437
|
+
token: str
|
438
|
+
car_class_code: Optional[str] = None
|
439
|
+
pickup: Optional[Location] = None
|
440
|
+
dropoff: Optional[Location] = None
|
441
|
+
stops: Optional[list[Stop]] = None
|
442
|
+
account: Optional[Account] = None
|
443
|
+
passenger: Optional[Passenger] = None
|
444
|
+
rewards: Optional[list[Reward]] = None
|
445
|
+
pets: Optional[int] = None
|
446
|
+
car_seats: Optional[int] = None
|
447
|
+
boosters: Optional[int] = None
|
448
|
+
infants: Optional[int] = None
|
449
|
+
customer_comment: Optional[str] = None
|
450
|
+
ta_fee: Optional[float] = Field(
|
451
|
+
None, description="For Travel Agencies - additional fee in USD"
|
452
|
+
)
|
453
|
+
|
454
|
+
|
455
|
+
class DetailsResponse(BaseModel):
|
456
|
+
"""Response from set details."""
|
457
|
+
|
458
|
+
price: float
|
459
|
+
breakdown: list[BreakdownItem]
|
460
|
+
|
461
|
+
|
462
|
+
class BookRequest(BaseModel):
|
463
|
+
"""Request for booking reservation."""
|
464
|
+
|
465
|
+
token: str
|
466
|
+
promo: Optional[str] = None
|
467
|
+
method: Optional[str] = Field(None, description="'charge' for charge accounts")
|
468
|
+
credit_card: Optional[CreditCard] = None
|
469
|
+
|
470
|
+
@model_validator(mode="after")
|
471
|
+
def validate_book_request(self) -> Self:
|
472
|
+
"""Validate that either method or credit_card is provided."""
|
473
|
+
if not self.method and not self.credit_card:
|
474
|
+
raise ValueError("Either method='charge' or credit_card must be provided")
|
475
|
+
|
476
|
+
return self
|
477
|
+
|
478
|
+
|
479
|
+
class BookResponse(BaseModel):
|
480
|
+
"""Response from book reservation."""
|
481
|
+
|
482
|
+
reservation_id: str
|
483
|
+
|
484
|
+
|
485
|
+
class ListReservationsRequest(BaseModel):
|
486
|
+
"""Request for listing reservations."""
|
487
|
+
|
488
|
+
credentials: Credentials
|
489
|
+
is_archive: bool = False
|
490
|
+
|
491
|
+
|
492
|
+
class ListReservationsResponse(BaseModel):
|
493
|
+
"""Response from list reservations."""
|
494
|
+
|
495
|
+
success: bool
|
496
|
+
reservations: list[Reservation] = Field(default_factory=list)
|
497
|
+
error: Optional[str] = None
|
498
|
+
|
499
|
+
|
500
|
+
class GetReservationRequest(BaseModel):
|
501
|
+
"""Request for getting reservation details."""
|
502
|
+
|
503
|
+
credentials: Credentials
|
504
|
+
confirmation: str
|
505
|
+
|
506
|
+
|
507
|
+
class GetReservationResponse(BaseModel):
|
508
|
+
"""Response from get reservation."""
|
509
|
+
|
510
|
+
reservation: EditableReservationRequest
|
511
|
+
is_editable: bool
|
512
|
+
status: Optional[ReservationStatus] = None
|
513
|
+
is_cancellation_pending: bool
|
514
|
+
car_description: Optional[str] = None
|
515
|
+
cancellation_policy: Optional[str] = None
|
516
|
+
pickup_type: LocationType
|
517
|
+
pickup_description: str
|
518
|
+
dropoff_type: LocationType
|
519
|
+
dropoff_description: str
|
520
|
+
additional_services: Optional[str] = None
|
521
|
+
payment_method: Optional[str] = None
|
522
|
+
breakdown: list[BreakdownItem] = Field(default_factory=list)
|
523
|
+
passenger_name: Optional[str] = None
|
524
|
+
evoucher_url: Optional[str] = None
|
525
|
+
receipt_url: Optional[str] = None
|
526
|
+
pending_changes: list[list[str]] = Field(default_factory=list)
|
527
|
+
|
528
|
+
|
529
|
+
class EditReservationResponse(BaseModel):
|
530
|
+
"""Response from edit reservation."""
|
531
|
+
|
532
|
+
success: bool
|
bookalimo/py.typed
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Marker file for PEP 561
|