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/__init__.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
"""
|
2
|
+
Book-A-Limo API Wrapper Package.
|
3
|
+
Provides a clean, typed interface to the Book-A-Limo API.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import importlib.metadata
|
7
|
+
|
8
|
+
from .wrapper import (
|
9
|
+
BookALimo,
|
10
|
+
create_address_location,
|
11
|
+
create_airport_location,
|
12
|
+
create_credentials,
|
13
|
+
create_credit_card,
|
14
|
+
create_passenger,
|
15
|
+
create_stop,
|
16
|
+
)
|
17
|
+
|
18
|
+
__all__ = [
|
19
|
+
"BookALimo",
|
20
|
+
"create_credentials",
|
21
|
+
"create_address_location",
|
22
|
+
"create_airport_location",
|
23
|
+
"create_stop",
|
24
|
+
"create_passenger",
|
25
|
+
"create_credit_card",
|
26
|
+
]
|
27
|
+
|
28
|
+
__version__ = importlib.metadata.version(__package__ or __name__)
|
bookalimo/_client.py
ADDED
@@ -0,0 +1,350 @@
|
|
1
|
+
"""
|
2
|
+
Base HTTP client for Book-A-Limo API.
|
3
|
+
Handles authentication, headers, and common request/response patterns.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from enum import Enum
|
7
|
+
from functools import lru_cache
|
8
|
+
from typing import Any, Optional, cast
|
9
|
+
|
10
|
+
import httpx
|
11
|
+
from pydantic import BaseModel
|
12
|
+
|
13
|
+
from .models import (
|
14
|
+
BookRequest,
|
15
|
+
BookResponse,
|
16
|
+
Credentials,
|
17
|
+
DetailsRequest,
|
18
|
+
DetailsResponse,
|
19
|
+
EditableReservationRequest,
|
20
|
+
EditableReservationRequestAuthenticated,
|
21
|
+
EditReservationResponse,
|
22
|
+
GetReservationRequest,
|
23
|
+
GetReservationResponse,
|
24
|
+
ListReservationsRequest,
|
25
|
+
ListReservationsResponse,
|
26
|
+
PriceRequest,
|
27
|
+
PriceRequestAuthenticated,
|
28
|
+
PriceResponse,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
@lru_cache(maxsize=1)
|
33
|
+
def get_version() -> str:
|
34
|
+
"""Get the version of the BookALimo client."""
|
35
|
+
from bookalimo import __version__
|
36
|
+
|
37
|
+
return __version__
|
38
|
+
|
39
|
+
|
40
|
+
class BookALimoError(Exception):
|
41
|
+
"""Base exception for Book-A-Limo API errors."""
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
message: str,
|
46
|
+
status_code: Optional[int] = None,
|
47
|
+
response_data: Optional[dict[str, Any]] = None,
|
48
|
+
):
|
49
|
+
super().__init__(message)
|
50
|
+
self.status_code = status_code
|
51
|
+
self.response_data = response_data or {}
|
52
|
+
|
53
|
+
|
54
|
+
class BookALimoClient:
|
55
|
+
"""
|
56
|
+
Base HTTP client for Book-A-Limo API.
|
57
|
+
Centralizes headers, auth, request helpers, and error handling.
|
58
|
+
"""
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self,
|
62
|
+
client: httpx.AsyncClient,
|
63
|
+
credentials: Credentials,
|
64
|
+
user_agent: str = "bookalimo-python",
|
65
|
+
version: Optional[str] = None,
|
66
|
+
sandbox: bool = False,
|
67
|
+
base_url: str = "https://api.bookalimo.com",
|
68
|
+
base_url_sandbox: str = "https://sandbox.bookalimo.com",
|
69
|
+
http_timeout: float = 5.0,
|
70
|
+
):
|
71
|
+
"""Initialize the client with an HTTP client."""
|
72
|
+
self.client = client
|
73
|
+
version = version or get_version()
|
74
|
+
self.credentials = credentials
|
75
|
+
self.headers = {
|
76
|
+
"content-type": "application/json",
|
77
|
+
"user-agent": f"{user_agent}/{version}",
|
78
|
+
}
|
79
|
+
self.sandbox = sandbox
|
80
|
+
self.base_url = base_url
|
81
|
+
self.base_url_sandbox = base_url_sandbox
|
82
|
+
self.http_timeout = http_timeout
|
83
|
+
|
84
|
+
def _convert_model_to_api_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
85
|
+
"""
|
86
|
+
Convert Python model field names to API field names.
|
87
|
+
Handles snake_case to camelCase and other API-specific naming.
|
88
|
+
"""
|
89
|
+
converted: dict[str, Any] = {}
|
90
|
+
|
91
|
+
for key, value in data.items():
|
92
|
+
# Convert snake_case to camelCase for API
|
93
|
+
api_key = self._to_camel_case(key)
|
94
|
+
|
95
|
+
# Handle nested objects
|
96
|
+
if isinstance(value, dict):
|
97
|
+
converted[api_key] = self._convert_model_to_api_dict(value)
|
98
|
+
elif isinstance(value, list):
|
99
|
+
converted[api_key] = [
|
100
|
+
(
|
101
|
+
self._convert_model_to_api_dict(item)
|
102
|
+
if isinstance(item, dict)
|
103
|
+
else item
|
104
|
+
)
|
105
|
+
for item in value
|
106
|
+
]
|
107
|
+
else:
|
108
|
+
converted[api_key] = value
|
109
|
+
|
110
|
+
return converted
|
111
|
+
|
112
|
+
def _to_camel_case(self, snake_str: str) -> str:
|
113
|
+
"""Convert snake_case to camelCase."""
|
114
|
+
if "_" not in snake_str:
|
115
|
+
return snake_str
|
116
|
+
|
117
|
+
components = snake_str.split("_")
|
118
|
+
return components[0] + "".join(word.capitalize() for word in components[1:])
|
119
|
+
|
120
|
+
def _convert_api_to_model_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
121
|
+
"""
|
122
|
+
Convert API response field names to Python model field names.
|
123
|
+
Handles camelCase to snake_case conversion.
|
124
|
+
"""
|
125
|
+
converted: dict[str, Any] = {}
|
126
|
+
|
127
|
+
for key, value in data.items():
|
128
|
+
# Convert camelCase to snake_case
|
129
|
+
model_key = self._to_snake_case(key)
|
130
|
+
|
131
|
+
# Handle nested objects
|
132
|
+
if isinstance(value, dict):
|
133
|
+
converted[model_key] = self._convert_api_to_model_dict(value)
|
134
|
+
elif isinstance(value, list):
|
135
|
+
converted[model_key] = [
|
136
|
+
(
|
137
|
+
self._convert_api_to_model_dict(item)
|
138
|
+
if isinstance(item, dict)
|
139
|
+
else item
|
140
|
+
)
|
141
|
+
for item in value
|
142
|
+
]
|
143
|
+
else:
|
144
|
+
converted[model_key] = value
|
145
|
+
|
146
|
+
return converted
|
147
|
+
|
148
|
+
def handle_enums(self, obj: Any) -> Any:
|
149
|
+
"""
|
150
|
+
Simple utility to convert enums to their values for JSON serialization.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
obj: Any object that might contain enums
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
Object with enums converted to their values
|
157
|
+
"""
|
158
|
+
if isinstance(obj, Enum):
|
159
|
+
return obj.value
|
160
|
+
elif isinstance(obj, dict):
|
161
|
+
return {k: self.handle_enums(v) for k, v in obj.items()}
|
162
|
+
elif isinstance(obj, (list, tuple)):
|
163
|
+
return type(obj)(self.handle_enums(item) for item in obj)
|
164
|
+
elif isinstance(obj, set):
|
165
|
+
return {self.handle_enums(item) for item in obj}
|
166
|
+
else:
|
167
|
+
return obj
|
168
|
+
|
169
|
+
def _to_snake_case(self, camel_str: str) -> str:
|
170
|
+
"""Convert camelCase to snake_case."""
|
171
|
+
result = []
|
172
|
+
for i, char in enumerate(camel_str):
|
173
|
+
if char.isupper() and i > 0:
|
174
|
+
result.append("_")
|
175
|
+
result.append(char.lower())
|
176
|
+
return "".join(result)
|
177
|
+
|
178
|
+
def get_base_url(self) -> str:
|
179
|
+
"""Get the base URL for the API."""
|
180
|
+
return self.base_url_sandbox if self.sandbox else self.base_url
|
181
|
+
|
182
|
+
async def _make_request(
|
183
|
+
self,
|
184
|
+
endpoint: str,
|
185
|
+
data: BaseModel,
|
186
|
+
model: type[BaseModel],
|
187
|
+
timeout: Optional[float] = None,
|
188
|
+
) -> BaseModel:
|
189
|
+
"""
|
190
|
+
Make a POST request to the API with proper error handling.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
endpoint: API endpoint (e.g., "/booking/reservation/list/")
|
194
|
+
data: Request payload as dict or Pydantic model
|
195
|
+
model: Pydantic model to parse the response into
|
196
|
+
timeout: Request timeout in seconds
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
Parsed JSON response as pydantic model
|
200
|
+
|
201
|
+
Raises:
|
202
|
+
BookALimoError: On API errors or HTTP errors
|
203
|
+
"""
|
204
|
+
url = f"{self.get_base_url()}{endpoint}"
|
205
|
+
|
206
|
+
# Convert model data to API format
|
207
|
+
api_data = self._convert_model_to_api_dict(data.model_dump())
|
208
|
+
|
209
|
+
# Remove None values to avoid API issues
|
210
|
+
api_data = self._remove_none_values(api_data)
|
211
|
+
api_data = self.handle_enums(api_data)
|
212
|
+
|
213
|
+
try:
|
214
|
+
response = await self.client.post(
|
215
|
+
url,
|
216
|
+
json=api_data,
|
217
|
+
headers=self.headers,
|
218
|
+
timeout=timeout or self.http_timeout,
|
219
|
+
)
|
220
|
+
response.raise_for_status()
|
221
|
+
|
222
|
+
# Handle different HTTP status codes
|
223
|
+
if response.status_code >= 400:
|
224
|
+
error_msg = f"HTTP {response.status_code}: {response.text}"
|
225
|
+
raise BookALimoError(
|
226
|
+
error_msg,
|
227
|
+
status_code=response.status_code,
|
228
|
+
response_data={"raw_response": response.text},
|
229
|
+
)
|
230
|
+
|
231
|
+
try:
|
232
|
+
json_data = response.json()
|
233
|
+
except ValueError as e:
|
234
|
+
raise BookALimoError(f"Invalid JSON response: {str(e)}") from e
|
235
|
+
|
236
|
+
# Check for API-level errors
|
237
|
+
if isinstance(json_data, dict):
|
238
|
+
if "error" in json_data and json_data["error"]:
|
239
|
+
raise BookALimoError(
|
240
|
+
f"API Error: {json_data['error']}", response_data=json_data
|
241
|
+
)
|
242
|
+
|
243
|
+
# Check success flag if present
|
244
|
+
if "success" in json_data and not json_data["success"]:
|
245
|
+
error_msg = json_data.get("error", "Unknown API error")
|
246
|
+
raise BookALimoError(
|
247
|
+
f"API Error: {error_msg}", response_data=json_data
|
248
|
+
)
|
249
|
+
|
250
|
+
# Convert response back to model format
|
251
|
+
return model.model_validate(self._convert_api_to_model_dict(json_data))
|
252
|
+
|
253
|
+
except httpx.TimeoutException:
|
254
|
+
raise BookALimoError(
|
255
|
+
f"Request timeout after {timeout or self.http_timeout}s"
|
256
|
+
) from None
|
257
|
+
except httpx.ConnectError:
|
258
|
+
raise BookALimoError(
|
259
|
+
"Connection error - unable to reach Book-A-Limo API"
|
260
|
+
) from None
|
261
|
+
except httpx.HTTPError as e:
|
262
|
+
raise BookALimoError(f"HTTP Error: {str(e)}") from e
|
263
|
+
except BookALimoError:
|
264
|
+
# Re-raise our custom errors
|
265
|
+
raise
|
266
|
+
except Exception as e:
|
267
|
+
raise BookALimoError(f"Unexpected error: {str(e)}") from e
|
268
|
+
|
269
|
+
def _remove_none_values(self, data: Any) -> Any:
|
270
|
+
"""Recursively remove None values from data structure."""
|
271
|
+
if isinstance(data, dict):
|
272
|
+
return {
|
273
|
+
k: self._remove_none_values(v) for k, v in data.items() if v is not None
|
274
|
+
}
|
275
|
+
elif isinstance(data, list):
|
276
|
+
return [self._remove_none_values(item) for item in data]
|
277
|
+
else:
|
278
|
+
return data
|
279
|
+
|
280
|
+
async def list_reservations(
|
281
|
+
self, is_archive: bool = False
|
282
|
+
) -> ListReservationsResponse:
|
283
|
+
"""List reservations for the given credentials."""
|
284
|
+
data = ListReservationsRequest(
|
285
|
+
credentials=self.credentials, is_archive=is_archive
|
286
|
+
)
|
287
|
+
return cast(
|
288
|
+
ListReservationsResponse,
|
289
|
+
await self._make_request(
|
290
|
+
"/booking/reservation/list/", data, model=ListReservationsResponse
|
291
|
+
),
|
292
|
+
)
|
293
|
+
|
294
|
+
async def get_reservation(self, confirmation: str) -> GetReservationResponse:
|
295
|
+
"""Get detailed reservation information."""
|
296
|
+
data = GetReservationRequest(
|
297
|
+
credentials=self.credentials, confirmation=confirmation
|
298
|
+
)
|
299
|
+
return cast(
|
300
|
+
GetReservationResponse,
|
301
|
+
await self._make_request(
|
302
|
+
"/booking/reservation/get/", data, model=GetReservationResponse
|
303
|
+
),
|
304
|
+
)
|
305
|
+
|
306
|
+
async def get_prices(self, price_request: PriceRequest) -> PriceResponse:
|
307
|
+
"""Get pricing for a trip."""
|
308
|
+
price_request_authenticated = PriceRequestAuthenticated(
|
309
|
+
credentials=self.credentials, **price_request.model_dump()
|
310
|
+
)
|
311
|
+
return cast(
|
312
|
+
PriceResponse,
|
313
|
+
await self._make_request(
|
314
|
+
"/booking/price/", price_request_authenticated, model=PriceResponse
|
315
|
+
),
|
316
|
+
)
|
317
|
+
|
318
|
+
async def set_details(self, details_request: DetailsRequest) -> DetailsResponse:
|
319
|
+
"""Set reservation details and get updated pricing."""
|
320
|
+
return cast(
|
321
|
+
DetailsResponse,
|
322
|
+
await self._make_request(
|
323
|
+
"/booking/details/", details_request, model=DetailsResponse
|
324
|
+
),
|
325
|
+
)
|
326
|
+
|
327
|
+
async def book_reservation(self, book_request: BookRequest) -> BookResponse:
|
328
|
+
"""Book a reservation."""
|
329
|
+
return cast(
|
330
|
+
BookResponse,
|
331
|
+
await self._make_request(
|
332
|
+
"/booking/book/", book_request, model=BookResponse
|
333
|
+
),
|
334
|
+
)
|
335
|
+
|
336
|
+
async def edit_reservation(
|
337
|
+
self, edit_request: EditableReservationRequest
|
338
|
+
) -> EditReservationResponse:
|
339
|
+
"""Edit or cancel a reservation."""
|
340
|
+
edit_request_authenticated = EditableReservationRequestAuthenticated(
|
341
|
+
credentials=self.credentials, **edit_request.model_dump()
|
342
|
+
)
|
343
|
+
return cast(
|
344
|
+
EditReservationResponse,
|
345
|
+
await self._make_request(
|
346
|
+
"/booking/edit/",
|
347
|
+
edit_request_authenticated,
|
348
|
+
model=EditReservationResponse,
|
349
|
+
),
|
350
|
+
)
|