bookalimo 0.1.4__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.
Files changed (38) hide show
  1. bookalimo/__init__.py +17 -24
  2. bookalimo/_version.py +9 -0
  3. bookalimo/client.py +310 -0
  4. bookalimo/config.py +16 -0
  5. bookalimo/exceptions.py +115 -5
  6. bookalimo/integrations/__init__.py +1 -0
  7. bookalimo/integrations/google_places/__init__.py +31 -0
  8. bookalimo/integrations/google_places/client_async.py +258 -0
  9. bookalimo/integrations/google_places/client_sync.py +257 -0
  10. bookalimo/integrations/google_places/common.py +245 -0
  11. bookalimo/integrations/google_places/proto_adapter.py +224 -0
  12. bookalimo/{_logging.py → logging.py} +59 -62
  13. bookalimo/schemas/__init__.py +97 -0
  14. bookalimo/schemas/base.py +56 -0
  15. bookalimo/{models.py → schemas/booking.py} +88 -100
  16. bookalimo/schemas/places/__init__.py +37 -0
  17. bookalimo/schemas/places/common.py +198 -0
  18. bookalimo/schemas/places/google.py +596 -0
  19. bookalimo/schemas/places/place.py +337 -0
  20. bookalimo/services/__init__.py +11 -0
  21. bookalimo/services/pricing.py +191 -0
  22. bookalimo/services/reservations.py +227 -0
  23. bookalimo/transport/__init__.py +7 -0
  24. bookalimo/transport/auth.py +41 -0
  25. bookalimo/transport/base.py +44 -0
  26. bookalimo/transport/httpx_async.py +230 -0
  27. bookalimo/transport/httpx_sync.py +230 -0
  28. bookalimo/transport/retry.py +102 -0
  29. bookalimo/transport/utils.py +59 -0
  30. bookalimo-1.0.0.dist-info/METADATA +307 -0
  31. bookalimo-1.0.0.dist-info/RECORD +35 -0
  32. bookalimo/_client.py +0 -420
  33. bookalimo/wrapper.py +0 -444
  34. bookalimo-0.1.4.dist-info/METADATA +0 -392
  35. bookalimo-0.1.4.dist-info/RECORD +0 -12
  36. {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
  37. {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
  38. {bookalimo-0.1.4.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)