bookalimo 0.1.2__tar.gz → 0.1.4__tar.gz
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-0.1.2/src/bookalimo.egg-info → bookalimo-0.1.4}/PKG-INFO +99 -27
- {bookalimo-0.1.2 → bookalimo-0.1.4}/README.md +98 -26
- {bookalimo-0.1.2 → bookalimo-0.1.4}/pyproject.toml +1 -1
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo/__init__.py +3 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo/_client.py +126 -48
- bookalimo-0.1.4/src/bookalimo/_logging.py +244 -0
- bookalimo-0.1.4/src/bookalimo/exceptions.py +15 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo/wrapper.py +51 -4
- {bookalimo-0.1.2 → bookalimo-0.1.4/src/bookalimo.egg-info}/PKG-INFO +99 -27
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo.egg-info/SOURCES.txt +2 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/tests/test_client.py +2 -1
- {bookalimo-0.1.2 → bookalimo-0.1.4}/LICENSE +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/setup.cfg +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo/models.py +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo/py.typed +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo.egg-info/dependency_links.txt +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo.egg-info/requires.txt +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/src/bookalimo.egg-info/top_level.txt +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/tests/test_models.py +0 -0
- {bookalimo-0.1.2 → bookalimo-0.1.4}/tests/test_wrapper.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bookalimo
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: Python wrapper for the Book-A-Limo API
|
5
5
|
Author-email: Jonathan Oren <jonathan@bookalimo.com>
|
6
6
|
Maintainer-email: Jonathan Oren <jonathan@bookalimo.com>
|
@@ -63,13 +63,43 @@ Dynamic: license-file
|
|
63
63
|
[](https://opensource.org/licenses/MIT)
|
64
64
|
[](https://github.com/astral-sh/ruff)
|
65
65
|
|
66
|
-
A modern, async Python wrapper for the Book-A-Limo API with full type support.
|
66
|
+
A modern, async Python wrapper for the Book-A-Limo API with full type support. Built on top of `httpx` and `pydantic`.
|
67
|
+
|
68
|
+
## Table of Contents
|
69
|
+
|
70
|
+
- [Book-A-Limo Python SDK](#book-a-limo-python-sdk)
|
71
|
+
- [Table of Contents](#table-of-contents)
|
72
|
+
- [Features](#features)
|
73
|
+
- [Requirements](#requirements)
|
74
|
+
- [Installation](#installation)
|
75
|
+
- [Quick Start](#quick-start)
|
76
|
+
- [Authentication](#authentication)
|
77
|
+
- [Core Operations](#core-operations)
|
78
|
+
- [Get Pricing](#get-pricing)
|
79
|
+
- [Book a Reservation](#book-a-reservation)
|
80
|
+
- [Location Builders](#location-builders)
|
81
|
+
- [Airport Locations](#airport-locations)
|
82
|
+
- [Address Locations](#address-locations)
|
83
|
+
- [Stops](#stops)
|
84
|
+
- [Advanced](#advanced)
|
85
|
+
- [Using Account Info (Travel Agents)](#using-account-info-travel-agents)
|
86
|
+
- [Edit / Cancel a Reservation](#edit--cancel-a-reservation)
|
87
|
+
- [Error Handling](#error-handling)
|
88
|
+
- [Logging](#logging)
|
89
|
+
- [Enable Debug Logging](#enable-debug-logging)
|
90
|
+
- [Custom Logging](#custom-logging)
|
91
|
+
- [Security](#security)
|
92
|
+
- [Disable Logging](#disable-logging)
|
93
|
+
- [Development](#development)
|
94
|
+
- [Security Notes](#security-notes)
|
95
|
+
- [License](#license)
|
96
|
+
- [Changelog](#changelog)
|
67
97
|
|
68
98
|
## Features
|
69
99
|
|
70
|
-
* **
|
71
|
-
* **Typed
|
72
|
-
* **Input validation**
|
100
|
+
* **Asynchronous**
|
101
|
+
* **Fully Typed** for requests & responses
|
102
|
+
* **Input validation** including airports and addresses.
|
73
103
|
* **Clean, minimal interface** for each API operation
|
74
104
|
* **Custom exceptions & error handling**
|
75
105
|
* **Tests and examples**
|
@@ -90,7 +120,6 @@ pip install bookalimo
|
|
90
120
|
|
91
121
|
```python
|
92
122
|
import asyncio
|
93
|
-
from httpx import AsyncClient
|
94
123
|
|
95
124
|
from bookalimo import (
|
96
125
|
BookALimo,
|
@@ -98,30 +127,29 @@ from bookalimo import (
|
|
98
127
|
create_airport_location,
|
99
128
|
create_address_location,
|
100
129
|
)
|
101
|
-
from bookalimo.models import RateType
|
130
|
+
from bookalimo.models import RateType
|
102
131
|
|
103
132
|
async def main():
|
104
133
|
# For Travel Agents (customers: pass is_customer=True)
|
105
134
|
credentials = create_credentials("TA10007", "your_password")
|
106
135
|
|
107
|
-
async with
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
print(f"- {price.car_description}: ${price.price}")
|
136
|
+
async with BookALimo(credentials) as client:
|
137
|
+
# Build locations
|
138
|
+
pickup = create_airport_location("JFK", "New York")
|
139
|
+
dropoff = create_address_location("53 East 34th Street, Manhattan")
|
140
|
+
|
141
|
+
prices = await client.get_prices(
|
142
|
+
rate_type=RateType.P2P,
|
143
|
+
date_time="09/05/2025 12:44 AM",
|
144
|
+
pickup=pickup,
|
145
|
+
dropoff=dropoff,
|
146
|
+
passengers=2,
|
147
|
+
luggage=3,
|
148
|
+
)
|
149
|
+
|
150
|
+
print(f"Available cars: {len(prices.prices)}")
|
151
|
+
for price in prices.prices:
|
152
|
+
print(f"- {price.car_description}: ${price.price}")
|
125
153
|
|
126
154
|
if __name__ == "__main__":
|
127
155
|
asyncio.run(main())
|
@@ -241,7 +269,7 @@ stops = [
|
|
241
269
|
### Using Account Info (Travel Agents)
|
242
270
|
|
243
271
|
```python
|
244
|
-
from bookalimo.models import Account
|
272
|
+
from bookalimo.models import Account
|
245
273
|
|
246
274
|
account = Account(
|
247
275
|
id="TA10007",
|
@@ -279,7 +307,7 @@ cancel_result = await client.edit_reservation(
|
|
279
307
|
## Error Handling
|
280
308
|
|
281
309
|
```python
|
282
|
-
from bookalimo.
|
310
|
+
from bookalimo.exceptions import BookALimoError
|
283
311
|
|
284
312
|
try:
|
285
313
|
reservations = await client.list_reservations()
|
@@ -289,6 +317,50 @@ except BookALimoError as e:
|
|
289
317
|
print(f"Response Data: {e.response_data}")
|
290
318
|
```
|
291
319
|
|
320
|
+
## Logging
|
321
|
+
|
322
|
+
By default, no log messages appear. Enable logging for debugging or monitoring.
|
323
|
+
|
324
|
+
### Enable Debug Logging
|
325
|
+
|
326
|
+
```python
|
327
|
+
import bookalimo
|
328
|
+
|
329
|
+
bookalimo.enable_debug_logging()
|
330
|
+
|
331
|
+
async with bookalimo.BookALimo(credentials) as client:
|
332
|
+
reservations = await client.list_reservations() # Shows API calls, timing, etc.
|
333
|
+
```
|
334
|
+
|
335
|
+
Or use the environment variable:
|
336
|
+
```bash
|
337
|
+
export BOOKALIMO_LOG_LEVEL=DEBUG
|
338
|
+
```
|
339
|
+
|
340
|
+
### Custom Logging
|
341
|
+
|
342
|
+
```python
|
343
|
+
import logging
|
344
|
+
import bookalimo
|
345
|
+
|
346
|
+
logging.basicConfig(level=logging.INFO)
|
347
|
+
bookalimo.get_logger().setLevel(logging.WARNING) # Production setting
|
348
|
+
```
|
349
|
+
|
350
|
+
### Security
|
351
|
+
|
352
|
+
Sensitive data is automatically redacted in logs:
|
353
|
+
- Passwords, tokens, CVV codes: `******`
|
354
|
+
- API keys: `abc123…89` (first 6, last 2 chars)
|
355
|
+
- Emails: `j***@example.com`
|
356
|
+
- Credit cards: `**** **** **** 1234`
|
357
|
+
|
358
|
+
### Disable Logging
|
359
|
+
|
360
|
+
```python
|
361
|
+
bookalimo.disable_debug_logging()
|
362
|
+
```
|
363
|
+
|
292
364
|
## Development
|
293
365
|
|
294
366
|
```bash
|
@@ -5,13 +5,43 @@
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
6
6
|
[](https://github.com/astral-sh/ruff)
|
7
7
|
|
8
|
-
A modern, async Python wrapper for the Book-A-Limo API with full type support.
|
8
|
+
A modern, async Python wrapper for the Book-A-Limo API with full type support. Built on top of `httpx` and `pydantic`.
|
9
|
+
|
10
|
+
## Table of Contents
|
11
|
+
|
12
|
+
- [Book-A-Limo Python SDK](#book-a-limo-python-sdk)
|
13
|
+
- [Table of Contents](#table-of-contents)
|
14
|
+
- [Features](#features)
|
15
|
+
- [Requirements](#requirements)
|
16
|
+
- [Installation](#installation)
|
17
|
+
- [Quick Start](#quick-start)
|
18
|
+
- [Authentication](#authentication)
|
19
|
+
- [Core Operations](#core-operations)
|
20
|
+
- [Get Pricing](#get-pricing)
|
21
|
+
- [Book a Reservation](#book-a-reservation)
|
22
|
+
- [Location Builders](#location-builders)
|
23
|
+
- [Airport Locations](#airport-locations)
|
24
|
+
- [Address Locations](#address-locations)
|
25
|
+
- [Stops](#stops)
|
26
|
+
- [Advanced](#advanced)
|
27
|
+
- [Using Account Info (Travel Agents)](#using-account-info-travel-agents)
|
28
|
+
- [Edit / Cancel a Reservation](#edit--cancel-a-reservation)
|
29
|
+
- [Error Handling](#error-handling)
|
30
|
+
- [Logging](#logging)
|
31
|
+
- [Enable Debug Logging](#enable-debug-logging)
|
32
|
+
- [Custom Logging](#custom-logging)
|
33
|
+
- [Security](#security)
|
34
|
+
- [Disable Logging](#disable-logging)
|
35
|
+
- [Development](#development)
|
36
|
+
- [Security Notes](#security-notes)
|
37
|
+
- [License](#license)
|
38
|
+
- [Changelog](#changelog)
|
9
39
|
|
10
40
|
## Features
|
11
41
|
|
12
|
-
* **
|
13
|
-
* **Typed
|
14
|
-
* **Input validation**
|
42
|
+
* **Asynchronous**
|
43
|
+
* **Fully Typed** for requests & responses
|
44
|
+
* **Input validation** including airports and addresses.
|
15
45
|
* **Clean, minimal interface** for each API operation
|
16
46
|
* **Custom exceptions & error handling**
|
17
47
|
* **Tests and examples**
|
@@ -32,7 +62,6 @@ pip install bookalimo
|
|
32
62
|
|
33
63
|
```python
|
34
64
|
import asyncio
|
35
|
-
from httpx import AsyncClient
|
36
65
|
|
37
66
|
from bookalimo import (
|
38
67
|
BookALimo,
|
@@ -40,30 +69,29 @@ from bookalimo import (
|
|
40
69
|
create_airport_location,
|
41
70
|
create_address_location,
|
42
71
|
)
|
43
|
-
from bookalimo.models import RateType
|
72
|
+
from bookalimo.models import RateType
|
44
73
|
|
45
74
|
async def main():
|
46
75
|
# For Travel Agents (customers: pass is_customer=True)
|
47
76
|
credentials = create_credentials("TA10007", "your_password")
|
48
77
|
|
49
|
-
async with
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
print(f"- {price.car_description}: ${price.price}")
|
78
|
+
async with BookALimo(credentials) as client:
|
79
|
+
# Build locations
|
80
|
+
pickup = create_airport_location("JFK", "New York")
|
81
|
+
dropoff = create_address_location("53 East 34th Street, Manhattan")
|
82
|
+
|
83
|
+
prices = await client.get_prices(
|
84
|
+
rate_type=RateType.P2P,
|
85
|
+
date_time="09/05/2025 12:44 AM",
|
86
|
+
pickup=pickup,
|
87
|
+
dropoff=dropoff,
|
88
|
+
passengers=2,
|
89
|
+
luggage=3,
|
90
|
+
)
|
91
|
+
|
92
|
+
print(f"Available cars: {len(prices.prices)}")
|
93
|
+
for price in prices.prices:
|
94
|
+
print(f"- {price.car_description}: ${price.price}")
|
67
95
|
|
68
96
|
if __name__ == "__main__":
|
69
97
|
asyncio.run(main())
|
@@ -183,7 +211,7 @@ stops = [
|
|
183
211
|
### Using Account Info (Travel Agents)
|
184
212
|
|
185
213
|
```python
|
186
|
-
from bookalimo.models import Account
|
214
|
+
from bookalimo.models import Account
|
187
215
|
|
188
216
|
account = Account(
|
189
217
|
id="TA10007",
|
@@ -221,7 +249,7 @@ cancel_result = await client.edit_reservation(
|
|
221
249
|
## Error Handling
|
222
250
|
|
223
251
|
```python
|
224
|
-
from bookalimo.
|
252
|
+
from bookalimo.exceptions import BookALimoError
|
225
253
|
|
226
254
|
try:
|
227
255
|
reservations = await client.list_reservations()
|
@@ -231,6 +259,50 @@ except BookALimoError as e:
|
|
231
259
|
print(f"Response Data: {e.response_data}")
|
232
260
|
```
|
233
261
|
|
262
|
+
## Logging
|
263
|
+
|
264
|
+
By default, no log messages appear. Enable logging for debugging or monitoring.
|
265
|
+
|
266
|
+
### Enable Debug Logging
|
267
|
+
|
268
|
+
```python
|
269
|
+
import bookalimo
|
270
|
+
|
271
|
+
bookalimo.enable_debug_logging()
|
272
|
+
|
273
|
+
async with bookalimo.BookALimo(credentials) as client:
|
274
|
+
reservations = await client.list_reservations() # Shows API calls, timing, etc.
|
275
|
+
```
|
276
|
+
|
277
|
+
Or use the environment variable:
|
278
|
+
```bash
|
279
|
+
export BOOKALIMO_LOG_LEVEL=DEBUG
|
280
|
+
```
|
281
|
+
|
282
|
+
### Custom Logging
|
283
|
+
|
284
|
+
```python
|
285
|
+
import logging
|
286
|
+
import bookalimo
|
287
|
+
|
288
|
+
logging.basicConfig(level=logging.INFO)
|
289
|
+
bookalimo.get_logger().setLevel(logging.WARNING) # Production setting
|
290
|
+
```
|
291
|
+
|
292
|
+
### Security
|
293
|
+
|
294
|
+
Sensitive data is automatically redacted in logs:
|
295
|
+
- Passwords, tokens, CVV codes: `******`
|
296
|
+
- API keys: `abc123…89` (first 6, last 2 chars)
|
297
|
+
- Emails: `j***@example.com`
|
298
|
+
- Credit cards: `**** **** **** 1234`
|
299
|
+
|
300
|
+
### Disable Logging
|
301
|
+
|
302
|
+
```python
|
303
|
+
bookalimo.disable_debug_logging()
|
304
|
+
```
|
305
|
+
|
234
306
|
## Development
|
235
307
|
|
236
308
|
```bash
|
@@ -5,6 +5,7 @@ Provides a clean, typed interface to the Book-A-Limo API.
|
|
5
5
|
|
6
6
|
import importlib.metadata
|
7
7
|
|
8
|
+
from ._logging import disable_debug_logging, enable_debug_logging
|
8
9
|
from .wrapper import (
|
9
10
|
BookALimo,
|
10
11
|
create_address_location,
|
@@ -23,6 +24,8 @@ __all__ = [
|
|
23
24
|
"create_stop",
|
24
25
|
"create_passenger",
|
25
26
|
"create_credit_card",
|
27
|
+
"enable_debug_logging",
|
28
|
+
"disable_debug_logging",
|
26
29
|
]
|
27
30
|
|
28
31
|
__version__ = importlib.metadata.version(__package__ or __name__)
|
@@ -3,13 +3,18 @@ Base HTTP client for Book-A-Limo API.
|
|
3
3
|
Handles authentication, headers, and common request/response patterns.
|
4
4
|
"""
|
5
5
|
|
6
|
+
import logging
|
6
7
|
from enum import Enum
|
7
8
|
from functools import lru_cache
|
8
|
-
from
|
9
|
+
from time import perf_counter
|
10
|
+
from typing import Any, Optional, TypeVar, cast, overload
|
11
|
+
from uuid import uuid4
|
9
12
|
|
10
13
|
import httpx
|
11
14
|
from pydantic import BaseModel
|
12
15
|
|
16
|
+
from ._logging import get_logger
|
17
|
+
from .exceptions import BookALimoError
|
13
18
|
from .models import (
|
14
19
|
BookRequest,
|
15
20
|
BookResponse,
|
@@ -28,6 +33,10 @@ from .models import (
|
|
28
33
|
PriceResponse,
|
29
34
|
)
|
30
35
|
|
36
|
+
logger = get_logger("client")
|
37
|
+
|
38
|
+
T = TypeVar("T")
|
39
|
+
|
31
40
|
|
32
41
|
@lru_cache(maxsize=1)
|
33
42
|
def get_version() -> str:
|
@@ -37,20 +46,6 @@ def get_version() -> str:
|
|
37
46
|
return __version__
|
38
47
|
|
39
48
|
|
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
49
|
class BookALimoClient:
|
55
50
|
"""
|
56
51
|
Base HTTP client for Book-A-Limo API.
|
@@ -76,6 +71,13 @@ class BookALimoClient:
|
|
76
71
|
}
|
77
72
|
self.base_url = base_url
|
78
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
|
+
)
|
79
81
|
|
80
82
|
def _convert_model_to_api_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
81
83
|
"""
|
@@ -141,6 +143,19 @@ class BookALimoClient:
|
|
141
143
|
|
142
144
|
return converted
|
143
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
|
+
|
144
159
|
def handle_enums(self, obj: Any) -> Any:
|
145
160
|
"""
|
146
161
|
Simple utility to convert enums to their values for JSON serialization.
|
@@ -171,6 +186,21 @@ class BookALimoClient:
|
|
171
186
|
result.append(char.lower())
|
172
187
|
return "".join(result)
|
173
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
|
+
|
174
204
|
async def _make_request(
|
175
205
|
self,
|
176
206
|
endpoint: str,
|
@@ -178,29 +208,24 @@ class BookALimoClient:
|
|
178
208
|
model: type[BaseModel],
|
179
209
|
timeout: Optional[float] = None,
|
180
210
|
) -> BaseModel:
|
181
|
-
"""
|
182
|
-
Make a POST request to the API with proper error handling.
|
183
|
-
|
184
|
-
Args:
|
185
|
-
endpoint: API endpoint (e.g., "/booking/reservation/list/")
|
186
|
-
data: Request payload as dict or Pydantic model
|
187
|
-
model: Pydantic model to parse the response into
|
188
|
-
timeout: Request timeout in seconds
|
189
|
-
|
190
|
-
Returns:
|
191
|
-
Parsed JSON response as pydantic model
|
192
|
-
|
193
|
-
Raises:
|
194
|
-
BookALimoError: On API errors or HTTP errors
|
195
|
-
"""
|
196
211
|
url = f"{self.base_url}{endpoint}"
|
197
212
|
|
198
213
|
# Convert model data to API format
|
199
|
-
api_data = self.
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
+
)
|
204
229
|
|
205
230
|
try:
|
206
231
|
response = await self.client.post(
|
@@ -211,11 +236,13 @@ class BookALimoClient:
|
|
211
236
|
)
|
212
237
|
response.raise_for_status()
|
213
238
|
|
214
|
-
#
|
239
|
+
# HTTP 4xx/5xx already raise in httpx, but keep defensive check:
|
215
240
|
if response.status_code >= 400:
|
216
|
-
error_msg = f"HTTP {response.status_code}
|
241
|
+
error_msg = f"HTTP {response.status_code}"
|
242
|
+
if debug_on:
|
243
|
+
logger.warning("× [%s] %s %s", req_id or "-", endpoint, error_msg)
|
217
244
|
raise BookALimoError(
|
218
|
-
error_msg,
|
245
|
+
f"{error_msg}: {response.text}",
|
219
246
|
status_code=response.status_code,
|
220
247
|
response_data={"raw_response": response.text},
|
221
248
|
)
|
@@ -223,41 +250,92 @@ class BookALimoClient:
|
|
223
250
|
try:
|
224
251
|
json_data = response.json()
|
225
252
|
except ValueError as e:
|
253
|
+
if debug_on:
|
254
|
+
logger.warning("× [%s] %s invalid JSON", req_id or "-", endpoint)
|
226
255
|
raise BookALimoError(f"Invalid JSON response: {str(e)}") from e
|
227
256
|
|
228
|
-
#
|
257
|
+
# API-level errors
|
229
258
|
if isinstance(json_data, dict):
|
230
|
-
if
|
259
|
+
if json_data.get("error"):
|
260
|
+
if debug_on:
|
261
|
+
logger.warning("× [%s] %s API error", req_id or "-", endpoint)
|
231
262
|
raise BookALimoError(
|
232
263
|
f"API Error: {json_data['error']}", response_data=json_data
|
233
264
|
)
|
234
|
-
|
235
|
-
# Check success flag if present
|
236
265
|
if "success" in json_data and not json_data["success"]:
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
)
|
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
|
+
)
|
241
290
|
|
242
|
-
# Convert response back to model format
|
243
291
|
return model.model_validate(self._convert_api_to_model_dict(json_data))
|
244
292
|
|
245
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
|
+
)
|
246
301
|
raise BookALimoError(
|
247
302
|
f"Request timeout after {timeout or self.http_timeout}s"
|
248
303
|
) from None
|
249
304
|
except httpx.ConnectError:
|
305
|
+
if debug_on:
|
306
|
+
logger.warning("× [%s] %s connection error", req_id or "-", endpoint)
|
250
307
|
raise BookALimoError(
|
251
308
|
"Connection error - unable to reach Book-A-Limo API"
|
252
309
|
) from None
|
253
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
|
+
)
|
254
318
|
raise BookALimoError(f"HTTP Error: {str(e)}") from e
|
255
319
|
except BookALimoError:
|
256
|
-
#
|
320
|
+
# already logged above where relevant
|
257
321
|
raise
|
258
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
|
+
)
|
259
330
|
raise BookALimoError(f"Unexpected error: {str(e)}") from e
|
260
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
|
+
|
261
339
|
def _remove_none_values(self, data: Any) -> Any:
|
262
340
|
"""Recursively remove None values from data structure."""
|
263
341
|
if isinstance(data, dict):
|
@@ -0,0 +1,244 @@
|
|
1
|
+
"""
|
2
|
+
Logging configuration for bookalimo package.
|
3
|
+
Public SDK-style logging with built-in redaction helpers.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import re
|
11
|
+
from collections.abc import Iterable, Mapping
|
12
|
+
from time import perf_counter
|
13
|
+
from typing import Any, Callable
|
14
|
+
|
15
|
+
logger = logging.getLogger("bookalimo")
|
16
|
+
logger.addHandler(logging.NullHandler())
|
17
|
+
logger.setLevel(logging.WARNING)
|
18
|
+
|
19
|
+
REDACTED = "******"
|
20
|
+
|
21
|
+
|
22
|
+
def mask_token(s: Any, *, show_prefix: int = 6, show_suffix: int = 2) -> str:
|
23
|
+
if not isinstance(s, str) or not s:
|
24
|
+
return REDACTED
|
25
|
+
if len(s) <= show_prefix + show_suffix:
|
26
|
+
return REDACTED
|
27
|
+
return f"{s[:show_prefix]}…{s[-show_suffix:]}"
|
28
|
+
|
29
|
+
|
30
|
+
def mask_email(s: Any) -> str:
|
31
|
+
if not isinstance(s, str) or "@" not in s:
|
32
|
+
return REDACTED
|
33
|
+
name, domain = s.split("@", 1)
|
34
|
+
return f"{name[:1]}***@{domain}"
|
35
|
+
|
36
|
+
|
37
|
+
def mask_phone(s: Any) -> str:
|
38
|
+
if not isinstance(s, str):
|
39
|
+
return REDACTED
|
40
|
+
digits = re.sub(r"\D", "", s)
|
41
|
+
tail = digits[-4:] if digits else ""
|
42
|
+
return f"***-***-{tail}" if tail else REDACTED
|
43
|
+
|
44
|
+
|
45
|
+
def mask_card_number(s: Any) -> str:
|
46
|
+
if not isinstance(s, str) or len(s) < 4:
|
47
|
+
return REDACTED
|
48
|
+
return f"**** **** **** {s[-4:]}"
|
49
|
+
|
50
|
+
|
51
|
+
def _safe_str(x: Any) -> str:
|
52
|
+
# Avoid large/complex reprs when logging
|
53
|
+
try:
|
54
|
+
s = str(x)
|
55
|
+
except Exception:
|
56
|
+
s = object.__repr__(x)
|
57
|
+
# hard scrub for obvious long tokens
|
58
|
+
if len(s) > 256:
|
59
|
+
return s[:256] + "…"
|
60
|
+
return s
|
61
|
+
|
62
|
+
|
63
|
+
def summarize_card(card: Any) -> dict[str, Any]:
|
64
|
+
"""
|
65
|
+
Produce a tiny, safe card summary from either a mapping or an object with attributes.
|
66
|
+
"""
|
67
|
+
|
68
|
+
def get(obj: Any, key: str) -> Any:
|
69
|
+
if isinstance(obj, Mapping):
|
70
|
+
return obj.get(key)
|
71
|
+
return getattr(obj, key, None)
|
72
|
+
|
73
|
+
number = get(card, "number")
|
74
|
+
exp = get(card, "expiration")
|
75
|
+
holder_type = get(card, "holder_type")
|
76
|
+
zip_code = get(card, "zip") or get(card, "zip_code")
|
77
|
+
|
78
|
+
return {
|
79
|
+
"last4": number[-4:] if isinstance(number, str) and len(number) >= 4 else None,
|
80
|
+
"expiration": REDACTED if exp else None,
|
81
|
+
"holder_type": str(holder_type) if holder_type is not None else None,
|
82
|
+
"zip_present": bool(zip_code),
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
def summarize_mapping(
|
87
|
+
data: Mapping[str, Any], *, whitelist: Iterable[str] | None = None
|
88
|
+
) -> dict[str, Any]:
|
89
|
+
"""
|
90
|
+
Keep only whitelisted keys; for everything else just show presence (True/False).
|
91
|
+
Avoids logging raw contents of complex payloads.
|
92
|
+
"""
|
93
|
+
out: dict[str, Any] = {}
|
94
|
+
allowed = set(whitelist or [])
|
95
|
+
for k, v in data.items():
|
96
|
+
if k in allowed:
|
97
|
+
out[k] = v
|
98
|
+
else:
|
99
|
+
out[k] = bool(v) # presence only
|
100
|
+
return out
|
101
|
+
|
102
|
+
|
103
|
+
def redact_param(name: str, value: Any) -> Any:
|
104
|
+
key = name.lower()
|
105
|
+
if key in {"password", "password_hash"}:
|
106
|
+
return REDACTED
|
107
|
+
if key in {"token", "authorization", "authorization_bearer", "api_key", "secret"}:
|
108
|
+
return mask_token(value)
|
109
|
+
if key in {"email"}:
|
110
|
+
return mask_email(value)
|
111
|
+
if key in {"phone"}:
|
112
|
+
return mask_phone(value)
|
113
|
+
if key in {"cvv", "cvc", "promo"}:
|
114
|
+
return REDACTED
|
115
|
+
if key in {"number", "card_number"}:
|
116
|
+
return mask_card_number(value)
|
117
|
+
if key in {"credit_card", "card"}:
|
118
|
+
return summarize_card(value)
|
119
|
+
if key in {"zip", "zipcode", "postal_code"}:
|
120
|
+
return REDACTED
|
121
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
122
|
+
return value
|
123
|
+
return _safe_str(value)
|
124
|
+
|
125
|
+
|
126
|
+
# ---- public API --------------------------------------------------------------
|
127
|
+
|
128
|
+
|
129
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
130
|
+
if name:
|
131
|
+
return logging.getLogger(f"bookalimo.{name}")
|
132
|
+
return logger
|
133
|
+
|
134
|
+
|
135
|
+
def enable_debug_logging(level: int | None = None) -> None:
|
136
|
+
level = level or _level_from_env() or logging.DEBUG
|
137
|
+
logger.setLevel(level)
|
138
|
+
|
139
|
+
has_real_handler = any(
|
140
|
+
not isinstance(h, logging.NullHandler) for h in logger.handlers
|
141
|
+
)
|
142
|
+
if not has_real_handler:
|
143
|
+
handler = logging.StreamHandler()
|
144
|
+
formatter = logging.Formatter(
|
145
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
146
|
+
)
|
147
|
+
handler.setFormatter(formatter)
|
148
|
+
logger.addHandler(handler)
|
149
|
+
|
150
|
+
logger.info("bookalimo logging enabled at %s", logging.getLevelName(logger.level))
|
151
|
+
|
152
|
+
|
153
|
+
def disable_debug_logging() -> None:
|
154
|
+
logger.setLevel(logging.WARNING)
|
155
|
+
for handler in logger.handlers[:]:
|
156
|
+
if not isinstance(handler, logging.NullHandler):
|
157
|
+
logger.removeHandler(handler)
|
158
|
+
|
159
|
+
|
160
|
+
def _level_from_env() -> int | None:
|
161
|
+
lvl = os.getenv("BOOKALIMO_LOG_LEVEL")
|
162
|
+
if not lvl:
|
163
|
+
return None
|
164
|
+
try:
|
165
|
+
return int(lvl)
|
166
|
+
except ValueError:
|
167
|
+
try:
|
168
|
+
return logging._nameToLevel.get(lvl.upper(), None)
|
169
|
+
except Exception:
|
170
|
+
return None
|
171
|
+
|
172
|
+
|
173
|
+
# ---- decorator for async methods --------------------------------------------
|
174
|
+
|
175
|
+
|
176
|
+
def log_call(
|
177
|
+
*,
|
178
|
+
include_params: Iterable[str] | None = None,
|
179
|
+
transforms: Mapping[str, Callable[[Any], Any]] | None = None,
|
180
|
+
operation: str | None = None,
|
181
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
182
|
+
"""
|
183
|
+
Decorator for async SDK methods.
|
184
|
+
- DEBUG: logs start/end with sanitized params + duration
|
185
|
+
- WARNING: logs errors (sanitized). No overhead when DEBUG is off.
|
186
|
+
"""
|
187
|
+
include = set(include_params or [])
|
188
|
+
transforms = transforms or {}
|
189
|
+
|
190
|
+
def _decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
|
191
|
+
async def _async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
192
|
+
log = get_logger("wrapper")
|
193
|
+
op = operation or fn.__name__
|
194
|
+
|
195
|
+
# Fast path: if debug disabled, skip param binding/redaction entirely
|
196
|
+
debug_on = log.isEnabledFor(logging.DEBUG)
|
197
|
+
if debug_on:
|
198
|
+
# Build a minimal, sanitized args snapshot
|
199
|
+
snapshot: dict[str, Any] = {}
|
200
|
+
# Map positional args to param names without inspect overhead by relying on kwargs only:
|
201
|
+
# we assume call sites are using kwargs in the wrapper (they do).
|
202
|
+
for k in include:
|
203
|
+
val = kwargs.get(k, None)
|
204
|
+
if k in transforms:
|
205
|
+
try:
|
206
|
+
val = transforms[k](val)
|
207
|
+
except Exception:
|
208
|
+
val = REDACTED
|
209
|
+
else:
|
210
|
+
val = redact_param(k, val)
|
211
|
+
snapshot[k] = val
|
212
|
+
|
213
|
+
start = perf_counter()
|
214
|
+
log.debug(
|
215
|
+
"→ %s(%s)",
|
216
|
+
op,
|
217
|
+
", ".join(f"{k}={snapshot[k]}" for k in snapshot),
|
218
|
+
extra={"operation": op},
|
219
|
+
)
|
220
|
+
|
221
|
+
try:
|
222
|
+
result = await fn(self, *args, **kwargs)
|
223
|
+
if debug_on:
|
224
|
+
dur_ms = (perf_counter() - start) * 1000.0
|
225
|
+
# Keep result logging ultra-light
|
226
|
+
result_type = type(result).__name__
|
227
|
+
log.debug(
|
228
|
+
"← %s ok in %.1f ms (%s)",
|
229
|
+
op,
|
230
|
+
dur_ms,
|
231
|
+
result_type,
|
232
|
+
extra={"operation": op},
|
233
|
+
)
|
234
|
+
return result
|
235
|
+
except Exception as e:
|
236
|
+
# WARNING with sanitized error; no param dump on failures
|
237
|
+
log.warning(
|
238
|
+
"%s failed: %s", op, e.__class__.__name__, extra={"operation": op}
|
239
|
+
)
|
240
|
+
raise
|
241
|
+
|
242
|
+
return _async_wrapper
|
243
|
+
|
244
|
+
return _decorate
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from typing import Any, Optional
|
2
|
+
|
3
|
+
|
4
|
+
class BookALimoError(Exception):
|
5
|
+
"""Base exception for Book-A-Limo API errors."""
|
6
|
+
|
7
|
+
def __init__(
|
8
|
+
self,
|
9
|
+
message: str,
|
10
|
+
status_code: Optional[int] = None,
|
11
|
+
response_data: Optional[dict[str, Any]] = None,
|
12
|
+
):
|
13
|
+
super().__init__(message)
|
14
|
+
self.status_code = status_code
|
15
|
+
self.response_data = response_data or {}
|
@@ -3,12 +3,15 @@ High-level API wrapper for Book-A-Limo operations.
|
|
3
3
|
Provides clean, LLM-friendly functions that abstract API complexities.
|
4
4
|
"""
|
5
5
|
|
6
|
+
import logging
|
6
7
|
from types import TracebackType
|
7
8
|
from typing import Any, Optional
|
8
9
|
|
9
10
|
from httpx import AsyncClient
|
10
11
|
|
11
|
-
from ._client import BookALimoClient
|
12
|
+
from ._client import BookALimoClient
|
13
|
+
from ._logging import get_logger, log_call
|
14
|
+
from .exceptions import BookALimoError
|
12
15
|
from .models import (
|
13
16
|
Address,
|
14
17
|
Airport,
|
@@ -33,6 +36,8 @@ from .models import (
|
|
33
36
|
Stop,
|
34
37
|
)
|
35
38
|
|
39
|
+
logger = get_logger("wrapper")
|
40
|
+
|
36
41
|
|
37
42
|
class BookALimo:
|
38
43
|
"""
|
@@ -44,7 +49,7 @@ class BookALimo:
|
|
44
49
|
self,
|
45
50
|
credentials: Credentials,
|
46
51
|
http_client: Optional[AsyncClient] = None,
|
47
|
-
base_url: str = "https://
|
52
|
+
base_url: str = "https://www.bookalimo.com/web/api",
|
48
53
|
http_timeout: float = 5.0,
|
49
54
|
**kwargs: Any,
|
50
55
|
):
|
@@ -65,11 +70,20 @@ class BookALimo:
|
|
65
70
|
http_timeout=http_timeout,
|
66
71
|
**kwargs,
|
67
72
|
)
|
73
|
+
if logger.isEnabledFor(logging.DEBUG): # NEW: tiny, safe init log
|
74
|
+
logger.debug(
|
75
|
+
"BookALimo initialized (base_url=%s, timeout=%s, owns_http_client=%s)",
|
76
|
+
base_url,
|
77
|
+
http_timeout,
|
78
|
+
self._owns_http_client,
|
79
|
+
)
|
68
80
|
|
69
81
|
async def aclose(self) -> None:
|
70
82
|
"""Close the HTTP client if we own it."""
|
71
83
|
if self._owns_http_client and not self.http_client.is_closed:
|
72
84
|
await self.http_client.aclose()
|
85
|
+
if logger.isEnabledFor(logging.DEBUG):
|
86
|
+
logger.debug("HTTP client closed")
|
73
87
|
|
74
88
|
async def __aenter__(self) -> "BookALimo":
|
75
89
|
"""Async context manager entry."""
|
@@ -84,6 +98,7 @@ class BookALimo:
|
|
84
98
|
"""Async context manager exit."""
|
85
99
|
await self.aclose()
|
86
100
|
|
101
|
+
@log_call(include_params=["is_archive"], operation="list_reservations")
|
87
102
|
async def list_reservations(
|
88
103
|
self, is_archive: bool = False
|
89
104
|
) -> ListReservationsResponse:
|
@@ -103,6 +118,7 @@ class BookALimo:
|
|
103
118
|
except Exception as e:
|
104
119
|
raise BookALimoError(f"Failed to list reservations: {str(e)}") from e
|
105
120
|
|
121
|
+
@log_call(include_params=["confirmation"], operation="get_reservation")
|
106
122
|
async def get_reservation(self, confirmation: str) -> GetReservationResponse:
|
107
123
|
"""
|
108
124
|
Get detailed reservation information.
|
@@ -120,6 +136,15 @@ class BookALimo:
|
|
120
136
|
except Exception as e:
|
121
137
|
raise BookALimoError(f"Failed to get reservation: {str(e)}") from e
|
122
138
|
|
139
|
+
@log_call(
|
140
|
+
include_params=[
|
141
|
+
"rate_type",
|
142
|
+
"date_time",
|
143
|
+
"passengers",
|
144
|
+
"luggage",
|
145
|
+
],
|
146
|
+
operation="get_prices",
|
147
|
+
)
|
123
148
|
async def get_prices(
|
124
149
|
self,
|
125
150
|
rate_type: RateType,
|
@@ -136,8 +161,8 @@ class BookALimo:
|
|
136
161
|
Args:
|
137
162
|
rate_type: 0=P2P, 1=Hourly (or string names)
|
138
163
|
date_time: 'MM/dd/yyyy hh:mm tt' format
|
139
|
-
pickup_location: Location
|
140
|
-
dropoff_location: Location
|
164
|
+
pickup_location: Location
|
165
|
+
dropoff_location: Location
|
141
166
|
passengers: Number of passengers
|
142
167
|
luggage: Number of luggage pieces
|
143
168
|
**kwargs: Optional fields like stops, account, car_class_code, etc.
|
@@ -183,6 +208,15 @@ class BookALimo:
|
|
183
208
|
except Exception as e:
|
184
209
|
raise BookALimoError(f"Failed to get prices: {str(e)}") from e
|
185
210
|
|
211
|
+
@log_call(
|
212
|
+
include_params=["token", "details"],
|
213
|
+
transforms={
|
214
|
+
"details": lambda d: sorted(
|
215
|
+
[k for k, v in (d or {}).items() if v is not None]
|
216
|
+
),
|
217
|
+
},
|
218
|
+
operation="set_details",
|
219
|
+
)
|
186
220
|
async def set_details(self, token: str, **details: Any) -> DetailsResponse:
|
187
221
|
"""
|
188
222
|
Set reservation details and get updated pricing.
|
@@ -212,6 +246,10 @@ class BookALimo:
|
|
212
246
|
except Exception as e:
|
213
247
|
raise BookALimoError(f"Failed to set details: {str(e)}") from e
|
214
248
|
|
249
|
+
@log_call(
|
250
|
+
include_params=["token", "method", "promo", "credit_card"],
|
251
|
+
operation="book",
|
252
|
+
)
|
215
253
|
async def book(
|
216
254
|
self,
|
217
255
|
token: str,
|
@@ -254,6 +292,15 @@ class BookALimo:
|
|
254
292
|
except Exception as e:
|
255
293
|
raise BookALimoError(f"Failed to book reservation: {str(e)}") from e
|
256
294
|
|
295
|
+
@log_call(
|
296
|
+
include_params=["confirmation", "is_cancel_request", "changes"],
|
297
|
+
transforms={
|
298
|
+
"changes": lambda d: sorted(
|
299
|
+
[k for k, v in (d or {}).items() if v is not None]
|
300
|
+
),
|
301
|
+
},
|
302
|
+
operation="edit_reservation",
|
303
|
+
)
|
257
304
|
async def edit_reservation(
|
258
305
|
self, confirmation: str, is_cancel_request: bool = False, **changes: Any
|
259
306
|
) -> EditReservationResponse:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bookalimo
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: Python wrapper for the Book-A-Limo API
|
5
5
|
Author-email: Jonathan Oren <jonathan@bookalimo.com>
|
6
6
|
Maintainer-email: Jonathan Oren <jonathan@bookalimo.com>
|
@@ -63,13 +63,43 @@ Dynamic: license-file
|
|
63
63
|
[](https://opensource.org/licenses/MIT)
|
64
64
|
[](https://github.com/astral-sh/ruff)
|
65
65
|
|
66
|
-
A modern, async Python wrapper for the Book-A-Limo API with full type support.
|
66
|
+
A modern, async Python wrapper for the Book-A-Limo API with full type support. Built on top of `httpx` and `pydantic`.
|
67
|
+
|
68
|
+
## Table of Contents
|
69
|
+
|
70
|
+
- [Book-A-Limo Python SDK](#book-a-limo-python-sdk)
|
71
|
+
- [Table of Contents](#table-of-contents)
|
72
|
+
- [Features](#features)
|
73
|
+
- [Requirements](#requirements)
|
74
|
+
- [Installation](#installation)
|
75
|
+
- [Quick Start](#quick-start)
|
76
|
+
- [Authentication](#authentication)
|
77
|
+
- [Core Operations](#core-operations)
|
78
|
+
- [Get Pricing](#get-pricing)
|
79
|
+
- [Book a Reservation](#book-a-reservation)
|
80
|
+
- [Location Builders](#location-builders)
|
81
|
+
- [Airport Locations](#airport-locations)
|
82
|
+
- [Address Locations](#address-locations)
|
83
|
+
- [Stops](#stops)
|
84
|
+
- [Advanced](#advanced)
|
85
|
+
- [Using Account Info (Travel Agents)](#using-account-info-travel-agents)
|
86
|
+
- [Edit / Cancel a Reservation](#edit--cancel-a-reservation)
|
87
|
+
- [Error Handling](#error-handling)
|
88
|
+
- [Logging](#logging)
|
89
|
+
- [Enable Debug Logging](#enable-debug-logging)
|
90
|
+
- [Custom Logging](#custom-logging)
|
91
|
+
- [Security](#security)
|
92
|
+
- [Disable Logging](#disable-logging)
|
93
|
+
- [Development](#development)
|
94
|
+
- [Security Notes](#security-notes)
|
95
|
+
- [License](#license)
|
96
|
+
- [Changelog](#changelog)
|
67
97
|
|
68
98
|
## Features
|
69
99
|
|
70
|
-
* **
|
71
|
-
* **Typed
|
72
|
-
* **Input validation**
|
100
|
+
* **Asynchronous**
|
101
|
+
* **Fully Typed** for requests & responses
|
102
|
+
* **Input validation** including airports and addresses.
|
73
103
|
* **Clean, minimal interface** for each API operation
|
74
104
|
* **Custom exceptions & error handling**
|
75
105
|
* **Tests and examples**
|
@@ -90,7 +120,6 @@ pip install bookalimo
|
|
90
120
|
|
91
121
|
```python
|
92
122
|
import asyncio
|
93
|
-
from httpx import AsyncClient
|
94
123
|
|
95
124
|
from bookalimo import (
|
96
125
|
BookALimo,
|
@@ -98,30 +127,29 @@ from bookalimo import (
|
|
98
127
|
create_airport_location,
|
99
128
|
create_address_location,
|
100
129
|
)
|
101
|
-
from bookalimo.models import RateType
|
130
|
+
from bookalimo.models import RateType
|
102
131
|
|
103
132
|
async def main():
|
104
133
|
# For Travel Agents (customers: pass is_customer=True)
|
105
134
|
credentials = create_credentials("TA10007", "your_password")
|
106
135
|
|
107
|
-
async with
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
print(f"- {price.car_description}: ${price.price}")
|
136
|
+
async with BookALimo(credentials) as client:
|
137
|
+
# Build locations
|
138
|
+
pickup = create_airport_location("JFK", "New York")
|
139
|
+
dropoff = create_address_location("53 East 34th Street, Manhattan")
|
140
|
+
|
141
|
+
prices = await client.get_prices(
|
142
|
+
rate_type=RateType.P2P,
|
143
|
+
date_time="09/05/2025 12:44 AM",
|
144
|
+
pickup=pickup,
|
145
|
+
dropoff=dropoff,
|
146
|
+
passengers=2,
|
147
|
+
luggage=3,
|
148
|
+
)
|
149
|
+
|
150
|
+
print(f"Available cars: {len(prices.prices)}")
|
151
|
+
for price in prices.prices:
|
152
|
+
print(f"- {price.car_description}: ${price.price}")
|
125
153
|
|
126
154
|
if __name__ == "__main__":
|
127
155
|
asyncio.run(main())
|
@@ -241,7 +269,7 @@ stops = [
|
|
241
269
|
### Using Account Info (Travel Agents)
|
242
270
|
|
243
271
|
```python
|
244
|
-
from bookalimo.models import Account
|
272
|
+
from bookalimo.models import Account
|
245
273
|
|
246
274
|
account = Account(
|
247
275
|
id="TA10007",
|
@@ -279,7 +307,7 @@ cancel_result = await client.edit_reservation(
|
|
279
307
|
## Error Handling
|
280
308
|
|
281
309
|
```python
|
282
|
-
from bookalimo.
|
310
|
+
from bookalimo.exceptions import BookALimoError
|
283
311
|
|
284
312
|
try:
|
285
313
|
reservations = await client.list_reservations()
|
@@ -289,6 +317,50 @@ except BookALimoError as e:
|
|
289
317
|
print(f"Response Data: {e.response_data}")
|
290
318
|
```
|
291
319
|
|
320
|
+
## Logging
|
321
|
+
|
322
|
+
By default, no log messages appear. Enable logging for debugging or monitoring.
|
323
|
+
|
324
|
+
### Enable Debug Logging
|
325
|
+
|
326
|
+
```python
|
327
|
+
import bookalimo
|
328
|
+
|
329
|
+
bookalimo.enable_debug_logging()
|
330
|
+
|
331
|
+
async with bookalimo.BookALimo(credentials) as client:
|
332
|
+
reservations = await client.list_reservations() # Shows API calls, timing, etc.
|
333
|
+
```
|
334
|
+
|
335
|
+
Or use the environment variable:
|
336
|
+
```bash
|
337
|
+
export BOOKALIMO_LOG_LEVEL=DEBUG
|
338
|
+
```
|
339
|
+
|
340
|
+
### Custom Logging
|
341
|
+
|
342
|
+
```python
|
343
|
+
import logging
|
344
|
+
import bookalimo
|
345
|
+
|
346
|
+
logging.basicConfig(level=logging.INFO)
|
347
|
+
bookalimo.get_logger().setLevel(logging.WARNING) # Production setting
|
348
|
+
```
|
349
|
+
|
350
|
+
### Security
|
351
|
+
|
352
|
+
Sensitive data is automatically redacted in logs:
|
353
|
+
- Passwords, tokens, CVV codes: `******`
|
354
|
+
- API keys: `abc123…89` (first 6, last 2 chars)
|
355
|
+
- Emails: `j***@example.com`
|
356
|
+
- Credit cards: `**** **** **** 1234`
|
357
|
+
|
358
|
+
### Disable Logging
|
359
|
+
|
360
|
+
```python
|
361
|
+
bookalimo.disable_debug_logging()
|
362
|
+
```
|
363
|
+
|
292
364
|
## Development
|
293
365
|
|
294
366
|
```bash
|
@@ -4,7 +4,8 @@ import httpx
|
|
4
4
|
import pytest
|
5
5
|
import respx
|
6
6
|
|
7
|
-
from bookalimo._client import BookALimoClient
|
7
|
+
from bookalimo._client import BookALimoClient
|
8
|
+
from bookalimo.exceptions import BookALimoError
|
8
9
|
from bookalimo.models import ListReservationsResponse
|
9
10
|
|
10
11
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|