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.
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} +45 -42
  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.5.dist-info/METADATA +0 -392
  35. bookalimo-0.1.5.dist-info/RECORD +0 -12
  36. {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
  37. {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
  38. {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,35 @@
1
+ bookalimo/__init__.py,sha256=ZFDpnSOo5VQEgusbVAxyF-neFxBptPZANZ-eE1PeIsU,516
2
+ bookalimo/_version.py,sha256=KcHRbOssHorjmpk3Xs1RTqGNSA-sw9tjYqwV19Zfa9c,259
3
+ bookalimo/client.py,sha256=za9o_D1jkaq7atWpM5d3bguWO-rtMYr5StXrBZInsMg,10599
4
+ bookalimo/config.py,sha256=LSbWIeTZYc4b3XSCmPSEU-jEcYmo7GWAyxdOmA69X6A,509
5
+ bookalimo/exceptions.py,sha256=pShnuvfMZEP_cChczWhNBLIqa7EOgr4ZNnWj6kphARU,4001
6
+ bookalimo/logging.py,sha256=IF6RQRJvVHgmpXK1qMEGcd0BjU6GxvzbNUBrBKHe-v8,7409
7
+ bookalimo/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
8
+ bookalimo/integrations/__init__.py,sha256=edmQP2Uo8JiNIVaeB4rxM8vagz7BQBhyGEJo70OVS0M,51
9
+ bookalimo/integrations/google_places/__init__.py,sha256=A6jBfGk8ZLYgFiTCGBytBOsyzKVWHI_kjnuZOgdCONc,866
10
+ bookalimo/integrations/google_places/client_async.py,sha256=6hfl3am2qtJLZHogPk57a-jFNqRzTJt3dyj58Mpt290,8956
11
+ bookalimo/integrations/google_places/client_sync.py,sha256=uscvNJuUb6iF8q29f_ZgftGmPWv2Zog72t0E8PCBmFY,8840
12
+ bookalimo/integrations/google_places/common.py,sha256=MphkEGarmM8AapjBrdsWRAg0yXyzMFZad_S8YB-esbI,7219
13
+ bookalimo/integrations/google_places/proto_adapter.py,sha256=XuVDr-PH9fopykRwNZxQTS_kIBCdwPwcNYBIHCZjmRo,6809
14
+ bookalimo/schemas/__init__.py,sha256=vS9skyuk5WV472TKgBqAg5Qzwvb0Bh0n4ff9BDSl8aE,1919
15
+ bookalimo/schemas/base.py,sha256=XEe0Qg7p-LzTmUOJ8CLVDsrry2lKn8sWNin7zwBiw-U,2022
16
+ bookalimo/schemas/booking.py,sha256=ll1usMa6Rfuz63LAWimWlz1UswWn26ZOPBpclN-8yGY,15861
17
+ bookalimo/schemas/places/__init__.py,sha256=SkUR6EIvnetNWqhLOiuq0k5Zy5XA4yZp8F2U92qCPOI,713
18
+ bookalimo/schemas/places/common.py,sha256=v39FpI3i7aDWRWsnb2zzSDddNE09XyQ5WVACBww3uC8,5425
19
+ bookalimo/schemas/places/google.py,sha256=HbQHKTj9bk6q-KaFWNMkKTgQIvp3kZF8dSnPqguy4Ng,20050
20
+ bookalimo/schemas/places/place.py,sha256=Y55QFhm7zjsqwjbPxHwNTl_02u30Yv2TqrhtBzgb6iM,10620
21
+ bookalimo/services/__init__.py,sha256=4wom7MpoeqKy5y99LYFKkl-CAJ7faDN9ZlXUhKVqlAg,303
22
+ bookalimo/services/pricing.py,sha256=MIUVzdnZsmxEEyw9qUdjj71gt3MsckiVLgfXqnPHmNE,5873
23
+ bookalimo/services/reservations.py,sha256=OsbVrVJRggjnifh1eER9PHGqt4PwxZ-Jq1-Dpo4wGKk,7147
24
+ bookalimo/transport/__init__.py,sha256=yD5TDnoqg1xtex41jqJ7Hes849efiYgvW3nkaYr6ZjE,221
25
+ bookalimo/transport/auth.py,sha256=n1XWtU3ddodsDuTuXnDw_pE0qaTe1zrOv7fqHGyOiCg,1249
26
+ bookalimo/transport/base.py,sha256=qeX2HfLaE6gUT5evjOdPiBh8x7z7A84UuWVsgBK3Pyo,1394
27
+ bookalimo/transport/httpx_async.py,sha256=XbXbxcAdUc_CQh_XEYsL0WH6vvECP4fcZWfFCeW5wEU,8100
28
+ bookalimo/transport/httpx_sync.py,sha256=SIlDfiIYKQgAvWsOVeEgxNxlqTKviN_LJX4r8i4Cl5M,7988
29
+ bookalimo/transport/retry.py,sha256=YiJtcdNZUbkrLa1CtKsRtcFO273XJX8naKd8d6ht0LU,2688
30
+ bookalimo/transport/utils.py,sha256=Yoj4P5AzuHbf6pzNPtiiswKab4nHZTCOpIff30-sgbw,1797
31
+ bookalimo-1.0.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ bookalimo-1.0.0.dist-info/METADATA,sha256=J_p8zz2sgot7msNBGa8dxLSDG-7nYXoLsGQrZcmggnw,9754
33
+ bookalimo-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ bookalimo-1.0.0.dist-info/top_level.txt,sha256=ZgYiDX2GfZCp4pevWn4X2qyEr-Dh7-cq5Y1j2jMhB1s,10
35
+ bookalimo-1.0.0.dist-info/RECORD,,
bookalimo/_client.py DELETED
@@ -1,420 +0,0 @@
1
- """
2
- Base HTTP client for Book-A-Limo API.
3
- Handles authentication, headers, and common request/response patterns.
4
- """
5
-
6
- import logging
7
- from enum import Enum
8
- from functools import lru_cache
9
- from time import perf_counter
10
- from typing import Any, Optional, TypeVar, cast, overload
11
- from uuid import uuid4
12
-
13
- import httpx
14
- from pydantic import BaseModel
15
-
16
- from ._logging import get_logger
17
- from .exceptions import BookALimoError
18
- from .models import (
19
- BookRequest,
20
- BookResponse,
21
- Credentials,
22
- DetailsRequest,
23
- DetailsResponse,
24
- EditableReservationRequest,
25
- EditableReservationRequestAuthenticated,
26
- EditReservationResponse,
27
- GetReservationRequest,
28
- GetReservationResponse,
29
- ListReservationsRequest,
30
- ListReservationsResponse,
31
- PriceRequest,
32
- PriceRequestAuthenticated,
33
- PriceResponse,
34
- )
35
-
36
- logger = get_logger("client")
37
-
38
- T = TypeVar("T")
39
-
40
-
41
- @lru_cache(maxsize=1)
42
- def get_version() -> str:
43
- """Get the version of the BookALimo client."""
44
- from bookalimo import __version__
45
-
46
- return __version__
47
-
48
-
49
- class BookALimoClient:
50
- """
51
- Base HTTP client for Book-A-Limo API.
52
- Centralizes headers, auth, request helpers, and error handling.
53
- """
54
-
55
- def __init__(
56
- self,
57
- client: httpx.AsyncClient,
58
- credentials: Credentials,
59
- user_agent: str = "bookalimo-python",
60
- version: Optional[str] = None,
61
- base_url: str = "https://api.bookalimo.com",
62
- http_timeout: float = 5.0,
63
- ):
64
- """Initialize the client with an HTTP client."""
65
- self.client = client
66
- version = version or get_version()
67
- self.credentials = credentials
68
- self.headers = {
69
- "content-type": "application/json",
70
- "user-agent": f"{user_agent}/{version}",
71
- }
72
- self.base_url = base_url
73
- self.http_timeout = http_timeout
74
- if logger.isEnabledFor(logging.DEBUG):
75
- logger.debug(
76
- "Client initialized (base_url=%s, timeout=%s, user_agent=%s)",
77
- self.base_url,
78
- self.http_timeout,
79
- self.headers.get("user-agent"),
80
- )
81
-
82
- def _convert_model_to_api_dict(self, data: dict[str, Any]) -> dict[str, Any]:
83
- """
84
- Convert Python model field names to API field names.
85
- Handles snake_case to camelCase and other API-specific naming.
86
- """
87
- converted: dict[str, Any] = {}
88
-
89
- for key, value in data.items():
90
- # Convert snake_case to camelCase for API
91
- api_key = self._to_camel_case(key)
92
-
93
- # Handle nested objects
94
- if isinstance(value, dict):
95
- converted[api_key] = self._convert_model_to_api_dict(value)
96
- elif isinstance(value, list):
97
- converted[api_key] = [
98
- (
99
- self._convert_model_to_api_dict(item)
100
- if isinstance(item, dict)
101
- else item
102
- )
103
- for item in value
104
- ]
105
- else:
106
- converted[api_key] = value
107
-
108
- return converted
109
-
110
- def _to_camel_case(self, snake_str: str) -> str:
111
- """Convert snake_case to camelCase."""
112
- if "_" not in snake_str:
113
- return snake_str
114
-
115
- components = snake_str.split("_")
116
- return components[0] + "".join(word.capitalize() for word in components[1:])
117
-
118
- def _convert_api_to_model_dict(self, data: dict[str, Any]) -> dict[str, Any]:
119
- """
120
- Convert API response field names to Python model field names.
121
- Handles camelCase to snake_case conversion.
122
- """
123
- converted: dict[str, Any] = {}
124
-
125
- for key, value in data.items():
126
- # Convert camelCase to snake_case
127
- model_key = self._to_snake_case(key)
128
-
129
- # Handle nested objects
130
- if isinstance(value, dict):
131
- converted[model_key] = self._convert_api_to_model_dict(value)
132
- elif isinstance(value, list):
133
- converted[model_key] = [
134
- (
135
- self._convert_api_to_model_dict(item)
136
- if isinstance(item, dict)
137
- else item
138
- )
139
- for item in value
140
- ]
141
- else:
142
- converted[model_key] = value
143
-
144
- return converted
145
-
146
- @overload
147
- def handle_enums(self, obj: Enum) -> Any: ...
148
- @overload
149
- def handle_enums(self, obj: dict[str, Any]) -> dict[str, Any]: ...
150
- @overload
151
- def handle_enums(self, obj: list[Any]) -> list[Any]: ...
152
- @overload
153
- def handle_enums(self, obj: tuple[Any, ...]) -> tuple[Any, ...]: ...
154
- @overload
155
- def handle_enums(self, obj: set[Any]) -> set[Any]: ...
156
- @overload
157
- def handle_enums(self, obj: Any) -> Any: ...
158
-
159
- def handle_enums(self, obj: Any) -> Any:
160
- """
161
- Simple utility to convert enums to their values for JSON serialization.
162
-
163
- Args:
164
- obj: Any object that might contain enums
165
-
166
- Returns:
167
- Object with enums converted to their values
168
- """
169
- if isinstance(obj, Enum):
170
- return obj.value
171
- elif isinstance(obj, dict):
172
- return {k: self.handle_enums(v) for k, v in obj.items()}
173
- elif isinstance(obj, (list, tuple)):
174
- return type(obj)(self.handle_enums(item) for item in obj)
175
- elif isinstance(obj, set):
176
- return {self.handle_enums(item) for item in obj}
177
- else:
178
- return obj
179
-
180
- def _to_snake_case(self, camel_str: str) -> str:
181
- """Convert camelCase to snake_case."""
182
- result = []
183
- for i, char in enumerate(camel_str):
184
- if char.isupper() and i > 0:
185
- result.append("_")
186
- result.append(char.lower())
187
- return "".join(result)
188
-
189
- def prepare_data(self, data: BaseModel) -> dict[str, Any]:
190
- """
191
- Prepare data for API requests by converting it to the appropriate format.
192
-
193
- Args:
194
- data: The data to prepare, as a Pydantic model instance.
195
-
196
- Returns:
197
- A dictionary representation of the data, ready for API consumption.
198
- """
199
- api_data = self._convert_model_to_api_dict(data.model_dump())
200
- api_data = self._remove_none_values(api_data)
201
- api_data = self.handle_enums(api_data)
202
- return cast(dict[str, Any], api_data)
203
-
204
- async def _make_request(
205
- self,
206
- endpoint: str,
207
- data: BaseModel,
208
- model: type[BaseModel],
209
- timeout: Optional[float] = None,
210
- ) -> BaseModel:
211
- url = f"{self.base_url}{endpoint}"
212
-
213
- # Convert model data to API format
214
- api_data = self.prepare_data(data)
215
-
216
- debug_on = logger.isEnabledFor(logging.DEBUG)
217
- req_id = None
218
- if debug_on:
219
- req_id = uuid4().hex[:8]
220
- start = perf_counter()
221
- body_keys = sorted(k for k in api_data.keys() if k != "credentials")
222
- logger.debug(
223
- "→ [%s] POST %s timeout=%s body_keys=%s",
224
- req_id,
225
- endpoint,
226
- timeout or self.http_timeout,
227
- body_keys,
228
- )
229
-
230
- try:
231
- response = await self.client.post(
232
- url,
233
- json=api_data,
234
- headers=self.headers,
235
- timeout=timeout or self.http_timeout,
236
- )
237
- response.raise_for_status()
238
-
239
- # HTTP 4xx/5xx already raise in httpx, but keep defensive check:
240
- if response.status_code >= 400:
241
- error_msg = f"HTTP {response.status_code}"
242
- if debug_on:
243
- logger.warning("× [%s] %s %s", req_id or "-", endpoint, error_msg)
244
- raise BookALimoError(
245
- f"{error_msg}: {response.text}",
246
- status_code=response.status_code,
247
- response_data={"raw_response": response.text},
248
- )
249
-
250
- try:
251
- json_data = response.json()
252
- except ValueError as e:
253
- if debug_on:
254
- logger.warning("× [%s] %s invalid JSON", req_id or "-", endpoint)
255
- raise BookALimoError(f"Invalid JSON response: {str(e)}") from e
256
-
257
- # API-level errors
258
- if isinstance(json_data, dict):
259
- if json_data.get("error"):
260
- if debug_on:
261
- logger.warning("× [%s] %s API error", req_id or "-", endpoint)
262
- raise BookALimoError(
263
- f"API Error: {json_data['error']}", response_data=json_data
264
- )
265
- if "success" in json_data and not json_data["success"]:
266
- msg = json_data.get("error", "Unknown API error")
267
- if debug_on:
268
- logger.warning("× [%s] %s API error", req_id or "-", endpoint)
269
- raise BookALimoError(f"API Error: {msg}", response_data=json_data)
270
-
271
- if debug_on:
272
- dur_ms = (perf_counter() - start) * 1000.0
273
- reqid_hdr = response.headers.get(
274
- "x-request-id"
275
- ) or response.headers.get("request-id")
276
- content_len = None
277
- try:
278
- content_len = len(response.content)
279
- except Exception:
280
- pass
281
- logger.debug(
282
- "← [%s] %s %s in %.1f ms len=%s reqid=%s",
283
- req_id,
284
- response.status_code,
285
- endpoint,
286
- dur_ms,
287
- content_len,
288
- reqid_hdr,
289
- )
290
-
291
- return model.model_validate(self._convert_api_to_model_dict(json_data))
292
-
293
- except httpx.TimeoutException:
294
- if debug_on:
295
- logger.warning(
296
- "× [%s] %s timeout after %ss",
297
- req_id or "-",
298
- endpoint,
299
- timeout or self.http_timeout,
300
- )
301
- raise BookALimoError(
302
- f"Request timeout after {timeout or self.http_timeout}s"
303
- ) from None
304
- except httpx.ConnectError:
305
- if debug_on:
306
- logger.warning("× [%s] %s connection error", req_id or "-", endpoint)
307
- raise BookALimoError(
308
- "Connection error - unable to reach Book-A-Limo API"
309
- ) from None
310
- except httpx.HTTPError as e:
311
- if debug_on:
312
- logger.warning(
313
- "× [%s] %s HTTP error: %s",
314
- req_id or "-",
315
- endpoint,
316
- e.__class__.__name__,
317
- )
318
- raise BookALimoError(f"HTTP Error: {str(e)}") from e
319
- except BookALimoError:
320
- # already logged above where relevant
321
- raise
322
- except Exception as e:
323
- if debug_on:
324
- logger.warning(
325
- "× [%s] %s unexpected error: %s",
326
- req_id or "-",
327
- endpoint,
328
- e.__class__.__name__,
329
- )
330
- raise BookALimoError(f"Unexpected error: {str(e)}") from e
331
-
332
- @overload
333
- def _remove_none_values(self, data: dict[str, Any]) -> dict[str, Any]: ...
334
- @overload
335
- def _remove_none_values(self, data: list[Any]) -> list[Any]: ...
336
- @overload
337
- def _remove_none_values(self, data: Any) -> Any: ...
338
-
339
- def _remove_none_values(self, data: Any) -> Any:
340
- """Recursively remove None values from data structure."""
341
- if isinstance(data, dict):
342
- return {
343
- k: self._remove_none_values(v) for k, v in data.items() if v is not None
344
- }
345
- elif isinstance(data, list):
346
- return [self._remove_none_values(item) for item in data]
347
- else:
348
- return data
349
-
350
- async def list_reservations(
351
- self, is_archive: bool = False
352
- ) -> ListReservationsResponse:
353
- """List reservations for the given credentials."""
354
- data = ListReservationsRequest(
355
- credentials=self.credentials, is_archive=is_archive
356
- )
357
- return cast(
358
- ListReservationsResponse,
359
- await self._make_request(
360
- "/booking/reservation/list/", data, model=ListReservationsResponse
361
- ),
362
- )
363
-
364
- async def get_reservation(self, confirmation: str) -> GetReservationResponse:
365
- """Get detailed reservation information."""
366
- data = GetReservationRequest(
367
- credentials=self.credentials, confirmation=confirmation
368
- )
369
- return cast(
370
- GetReservationResponse,
371
- await self._make_request(
372
- "/booking/reservation/get/", data, model=GetReservationResponse
373
- ),
374
- )
375
-
376
- async def get_prices(self, price_request: PriceRequest) -> PriceResponse:
377
- """Get pricing for a trip."""
378
- price_request_authenticated = PriceRequestAuthenticated(
379
- credentials=self.credentials, **price_request.model_dump()
380
- )
381
- return cast(
382
- PriceResponse,
383
- await self._make_request(
384
- "/booking/price/", price_request_authenticated, model=PriceResponse
385
- ),
386
- )
387
-
388
- async def set_details(self, details_request: DetailsRequest) -> DetailsResponse:
389
- """Set reservation details and get updated pricing."""
390
- return cast(
391
- DetailsResponse,
392
- await self._make_request(
393
- "/booking/details/", details_request, model=DetailsResponse
394
- ),
395
- )
396
-
397
- async def book_reservation(self, book_request: BookRequest) -> BookResponse:
398
- """Book a reservation."""
399
- return cast(
400
- BookResponse,
401
- await self._make_request(
402
- "/booking/book/", book_request, model=BookResponse
403
- ),
404
- )
405
-
406
- async def edit_reservation(
407
- self, edit_request: EditableReservationRequest
408
- ) -> EditReservationResponse:
409
- """Edit or cancel a reservation."""
410
- edit_request_authenticated = EditableReservationRequestAuthenticated(
411
- credentials=self.credentials, **edit_request.model_dump()
412
- )
413
- return cast(
414
- EditReservationResponse,
415
- await self._make_request(
416
- "/booking/edit/",
417
- edit_request_authenticated,
418
- model=EditReservationResponse,
419
- ),
420
- )