bookalimo 0.1.3__py3-none-any.whl → 0.1.5__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 +3 -0
- bookalimo/_client.py +125 -34
- bookalimo/_logging.py +238 -0
- bookalimo/wrapper.py +49 -3
- {bookalimo-0.1.3.dist-info → bookalimo-0.1.5.dist-info}/METADATA +76 -3
- bookalimo-0.1.5.dist-info/RECORD +12 -0
- bookalimo-0.1.3.dist-info/RECORD +0 -11
- {bookalimo-0.1.3.dist-info → bookalimo-0.1.5.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.3.dist-info → bookalimo-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-0.1.3.dist-info → bookalimo-0.1.5.dist-info}/top_level.txt +0 -0
bookalimo/__init__.py
CHANGED
@@ -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__)
|
bookalimo/_client.py
CHANGED
@@ -3,13 +3,17 @@ 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
|
13
17
|
from .exceptions import BookALimoError
|
14
18
|
from .models import (
|
15
19
|
BookRequest,
|
@@ -29,6 +33,10 @@ from .models import (
|
|
29
33
|
PriceResponse,
|
30
34
|
)
|
31
35
|
|
36
|
+
logger = get_logger("client")
|
37
|
+
|
38
|
+
T = TypeVar("T")
|
39
|
+
|
32
40
|
|
33
41
|
@lru_cache(maxsize=1)
|
34
42
|
def get_version() -> str:
|
@@ -63,6 +71,13 @@ class BookALimoClient:
|
|
63
71
|
}
|
64
72
|
self.base_url = base_url
|
65
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
|
+
)
|
66
81
|
|
67
82
|
def _convert_model_to_api_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
68
83
|
"""
|
@@ -128,6 +143,19 @@ class BookALimoClient:
|
|
128
143
|
|
129
144
|
return converted
|
130
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
|
+
|
131
159
|
def handle_enums(self, obj: Any) -> Any:
|
132
160
|
"""
|
133
161
|
Simple utility to convert enums to their values for JSON serialization.
|
@@ -158,6 +186,21 @@ class BookALimoClient:
|
|
158
186
|
result.append(char.lower())
|
159
187
|
return "".join(result)
|
160
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
|
+
|
161
204
|
async def _make_request(
|
162
205
|
self,
|
163
206
|
endpoint: str,
|
@@ -165,29 +208,24 @@ class BookALimoClient:
|
|
165
208
|
model: type[BaseModel],
|
166
209
|
timeout: Optional[float] = None,
|
167
210
|
) -> BaseModel:
|
168
|
-
"""
|
169
|
-
Make a POST request to the API with proper error handling.
|
170
|
-
|
171
|
-
Args:
|
172
|
-
endpoint: API endpoint (e.g., "/booking/reservation/list/")
|
173
|
-
data: Request payload as dict or Pydantic model
|
174
|
-
model: Pydantic model to parse the response into
|
175
|
-
timeout: Request timeout in seconds
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
Parsed JSON response as pydantic model
|
179
|
-
|
180
|
-
Raises:
|
181
|
-
BookALimoError: On API errors or HTTP errors
|
182
|
-
"""
|
183
211
|
url = f"{self.base_url}{endpoint}"
|
184
212
|
|
185
213
|
# Convert model data to API format
|
186
|
-
api_data = self.
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
+
)
|
191
229
|
|
192
230
|
try:
|
193
231
|
response = await self.client.post(
|
@@ -198,11 +236,13 @@ class BookALimoClient:
|
|
198
236
|
)
|
199
237
|
response.raise_for_status()
|
200
238
|
|
201
|
-
#
|
239
|
+
# HTTP 4xx/5xx already raise in httpx, but keep defensive check:
|
202
240
|
if response.status_code >= 400:
|
203
|
-
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)
|
204
244
|
raise BookALimoError(
|
205
|
-
error_msg,
|
245
|
+
f"{error_msg}: {response.text}",
|
206
246
|
status_code=response.status_code,
|
207
247
|
response_data={"raw_response": response.text},
|
208
248
|
)
|
@@ -210,41 +250,92 @@ class BookALimoClient:
|
|
210
250
|
try:
|
211
251
|
json_data = response.json()
|
212
252
|
except ValueError as e:
|
253
|
+
if debug_on:
|
254
|
+
logger.warning("× [%s] %s invalid JSON", req_id or "-", endpoint)
|
213
255
|
raise BookALimoError(f"Invalid JSON response: {str(e)}") from e
|
214
256
|
|
215
|
-
#
|
257
|
+
# API-level errors
|
216
258
|
if isinstance(json_data, dict):
|
217
|
-
if
|
259
|
+
if json_data.get("error"):
|
260
|
+
if debug_on:
|
261
|
+
logger.warning("× [%s] %s API error", req_id or "-", endpoint)
|
218
262
|
raise BookALimoError(
|
219
263
|
f"API Error: {json_data['error']}", response_data=json_data
|
220
264
|
)
|
221
|
-
|
222
|
-
# Check success flag if present
|
223
265
|
if "success" in json_data and not json_data["success"]:
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
)
|
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
|
+
)
|
228
290
|
|
229
|
-
# Convert response back to model format
|
230
291
|
return model.model_validate(self._convert_api_to_model_dict(json_data))
|
231
292
|
|
232
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
|
+
)
|
233
301
|
raise BookALimoError(
|
234
302
|
f"Request timeout after {timeout or self.http_timeout}s"
|
235
303
|
) from None
|
236
304
|
except httpx.ConnectError:
|
305
|
+
if debug_on:
|
306
|
+
logger.warning("× [%s] %s connection error", req_id or "-", endpoint)
|
237
307
|
raise BookALimoError(
|
238
308
|
"Connection error - unable to reach Book-A-Limo API"
|
239
309
|
) from None
|
240
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
|
+
)
|
241
318
|
raise BookALimoError(f"HTTP Error: {str(e)}") from e
|
242
319
|
except BookALimoError:
|
243
|
-
#
|
320
|
+
# already logged above where relevant
|
244
321
|
raise
|
245
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
|
+
)
|
246
330
|
raise BookALimoError(f"Unexpected error: {str(e)}") from e
|
247
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
|
+
|
248
339
|
def _remove_none_values(self, data: Any) -> Any:
|
249
340
|
"""Recursively remove None values from data structure."""
|
250
341
|
if isinstance(data, dict):
|
bookalimo/_logging.py
ADDED
@@ -0,0 +1,238 @@
|
|
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 Awaitable, Iterable, Mapping
|
12
|
+
from functools import wraps
|
13
|
+
from time import perf_counter
|
14
|
+
from typing import Any, Callable, TypeVar
|
15
|
+
|
16
|
+
from typing_extensions import ParamSpec
|
17
|
+
|
18
|
+
P = ParamSpec("P")
|
19
|
+
R = TypeVar("R")
|
20
|
+
|
21
|
+
logger = logging.getLogger("bookalimo")
|
22
|
+
logger.addHandler(logging.NullHandler())
|
23
|
+
logger.setLevel(logging.WARNING)
|
24
|
+
|
25
|
+
REDACTED = "******"
|
26
|
+
|
27
|
+
|
28
|
+
def mask_token(s: Any, *, show_prefix: int = 6, show_suffix: int = 2) -> str:
|
29
|
+
if not isinstance(s, str) or not s:
|
30
|
+
return REDACTED
|
31
|
+
if len(s) <= show_prefix + show_suffix:
|
32
|
+
return REDACTED
|
33
|
+
return f"{s[:show_prefix]}…{s[-show_suffix:]}"
|
34
|
+
|
35
|
+
|
36
|
+
def mask_email(s: Any) -> str:
|
37
|
+
if not isinstance(s, str) or "@" not in s:
|
38
|
+
return REDACTED
|
39
|
+
name, domain = s.split("@", 1)
|
40
|
+
return f"{name[:1]}***@{domain}"
|
41
|
+
|
42
|
+
|
43
|
+
def mask_phone(s: Any) -> str:
|
44
|
+
if not isinstance(s, str):
|
45
|
+
return REDACTED
|
46
|
+
digits = re.sub(r"\D", "", s)
|
47
|
+
tail = digits[-4:] if digits else ""
|
48
|
+
return f"***-***-{tail}" if tail else REDACTED
|
49
|
+
|
50
|
+
|
51
|
+
def mask_card_number(s: Any) -> str:
|
52
|
+
if not isinstance(s, str) or len(s) < 4:
|
53
|
+
return REDACTED
|
54
|
+
return f"**** **** **** {s[-4:]}"
|
55
|
+
|
56
|
+
|
57
|
+
def _safe_str(x: Any) -> str:
|
58
|
+
# Avoid large/complex reprs when logging
|
59
|
+
try:
|
60
|
+
s = str(x)
|
61
|
+
except Exception:
|
62
|
+
s = object.__repr__(x)
|
63
|
+
# hard scrub for obvious long tokens
|
64
|
+
if len(s) > 256:
|
65
|
+
return s[:256] + "…"
|
66
|
+
return s
|
67
|
+
|
68
|
+
|
69
|
+
def summarize_card(card: Any) -> dict[str, Any]:
|
70
|
+
"""
|
71
|
+
Produce a tiny, safe card summary from either a mapping or an object with attributes.
|
72
|
+
"""
|
73
|
+
|
74
|
+
def get(obj: Any, key: str) -> Any:
|
75
|
+
if isinstance(obj, Mapping):
|
76
|
+
return obj.get(key)
|
77
|
+
return getattr(obj, key, None)
|
78
|
+
|
79
|
+
number = get(card, "number")
|
80
|
+
exp = get(card, "expiration")
|
81
|
+
holder_type = get(card, "holder_type")
|
82
|
+
zip_code = get(card, "zip") or get(card, "zip_code")
|
83
|
+
|
84
|
+
return {
|
85
|
+
"last4": number[-4:] if isinstance(number, str) and len(number) >= 4 else None,
|
86
|
+
"expiration": REDACTED if exp else None,
|
87
|
+
"holder_type": str(holder_type) if holder_type is not None else None,
|
88
|
+
"zip_present": bool(zip_code),
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
def summarize_mapping(
|
93
|
+
data: Mapping[str, Any], *, whitelist: Iterable[str] | None = None
|
94
|
+
) -> dict[str, Any]:
|
95
|
+
"""
|
96
|
+
Keep only whitelisted keys; for everything else just show presence (True/False).
|
97
|
+
Avoids logging raw contents of complex payloads.
|
98
|
+
"""
|
99
|
+
out: dict[str, Any] = {}
|
100
|
+
allowed = set(whitelist or [])
|
101
|
+
for k, v in data.items():
|
102
|
+
if k in allowed:
|
103
|
+
out[k] = v
|
104
|
+
else:
|
105
|
+
out[k] = bool(v) # presence only
|
106
|
+
return out
|
107
|
+
|
108
|
+
|
109
|
+
def redact_param(name: str, value: Any) -> Any:
|
110
|
+
key = name.lower()
|
111
|
+
if key in {"password", "password_hash"}:
|
112
|
+
return REDACTED
|
113
|
+
if key in {"token", "authorization", "authorization_bearer", "api_key", "secret"}:
|
114
|
+
return mask_token(value)
|
115
|
+
if key in {"email"}:
|
116
|
+
return mask_email(value)
|
117
|
+
if key in {"phone"}:
|
118
|
+
return mask_phone(value)
|
119
|
+
if key in {"cvv", "cvc", "promo"}:
|
120
|
+
return REDACTED
|
121
|
+
if key in {"number", "card_number"}:
|
122
|
+
return mask_card_number(value)
|
123
|
+
if key in {"credit_card", "card"}:
|
124
|
+
return summarize_card(value)
|
125
|
+
if key in {"zip", "zipcode", "postal_code"}:
|
126
|
+
return REDACTED
|
127
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
128
|
+
return value
|
129
|
+
return _safe_str(value)
|
130
|
+
|
131
|
+
|
132
|
+
# ---- public API --------------------------------------------------------------
|
133
|
+
|
134
|
+
|
135
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
136
|
+
if name:
|
137
|
+
return logging.getLogger(f"bookalimo.{name}")
|
138
|
+
return logger
|
139
|
+
|
140
|
+
|
141
|
+
def enable_debug_logging(level: int | None = None) -> None:
|
142
|
+
level = level or _level_from_env() or logging.DEBUG
|
143
|
+
logger.setLevel(level)
|
144
|
+
|
145
|
+
has_real_handler = any(
|
146
|
+
not isinstance(h, logging.NullHandler) for h in logger.handlers
|
147
|
+
)
|
148
|
+
if not has_real_handler:
|
149
|
+
handler = logging.StreamHandler()
|
150
|
+
formatter = logging.Formatter(
|
151
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
152
|
+
)
|
153
|
+
handler.setFormatter(formatter)
|
154
|
+
logger.addHandler(handler)
|
155
|
+
|
156
|
+
logger.info("bookalimo logging enabled at %s", logging.getLevelName(logger.level))
|
157
|
+
|
158
|
+
|
159
|
+
def disable_debug_logging() -> None:
|
160
|
+
logger.setLevel(logging.WARNING)
|
161
|
+
for handler in logger.handlers[:]:
|
162
|
+
if not isinstance(handler, logging.NullHandler):
|
163
|
+
logger.removeHandler(handler)
|
164
|
+
|
165
|
+
|
166
|
+
def _level_from_env() -> int | None:
|
167
|
+
lvl = os.getenv("BOOKALIMO_LOG_LEVEL")
|
168
|
+
if not lvl:
|
169
|
+
return None
|
170
|
+
try:
|
171
|
+
return int(lvl)
|
172
|
+
except ValueError:
|
173
|
+
try:
|
174
|
+
return logging._nameToLevel.get(lvl.upper(), None)
|
175
|
+
except Exception:
|
176
|
+
return None
|
177
|
+
|
178
|
+
|
179
|
+
# ---- decorator for async methods --------------------------------------------
|
180
|
+
|
181
|
+
|
182
|
+
def log_call(
|
183
|
+
*,
|
184
|
+
include_params: Iterable[str] | None = None,
|
185
|
+
transforms: Mapping[str, Callable[[Any], Any]] | None = None,
|
186
|
+
operation: str | None = None,
|
187
|
+
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
188
|
+
include = set(include_params or [])
|
189
|
+
transforms = transforms or {}
|
190
|
+
|
191
|
+
def _decorate(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
192
|
+
@wraps(fn)
|
193
|
+
async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
194
|
+
log = get_logger("wrapper")
|
195
|
+
op = operation or fn.__name__
|
196
|
+
|
197
|
+
debug_on = log.isEnabledFor(logging.DEBUG)
|
198
|
+
if debug_on:
|
199
|
+
snapshot: dict[str, Any] = {}
|
200
|
+
for k in include:
|
201
|
+
val = kwargs.get(k, None)
|
202
|
+
if k in transforms:
|
203
|
+
try:
|
204
|
+
val = transforms[k](val)
|
205
|
+
except Exception:
|
206
|
+
val = REDACTED
|
207
|
+
else:
|
208
|
+
val = redact_param(k, val)
|
209
|
+
snapshot[k] = val
|
210
|
+
start = perf_counter()
|
211
|
+
log.debug(
|
212
|
+
"→ %s(%s)",
|
213
|
+
op,
|
214
|
+
", ".join(f"{k}={snapshot[k]}" for k in snapshot),
|
215
|
+
extra={"operation": op},
|
216
|
+
)
|
217
|
+
|
218
|
+
try:
|
219
|
+
result = await fn(*args, **kwargs)
|
220
|
+
if debug_on:
|
221
|
+
dur_ms = (perf_counter() - start) * 1000.0
|
222
|
+
log.debug(
|
223
|
+
"← %s ok in %.1f ms (%s)",
|
224
|
+
op,
|
225
|
+
dur_ms,
|
226
|
+
type(result).__name__,
|
227
|
+
extra={"operation": op},
|
228
|
+
)
|
229
|
+
return result
|
230
|
+
except Exception as e:
|
231
|
+
log.warning(
|
232
|
+
"%s failed: %s", op, e.__class__.__name__, extra={"operation": op}
|
233
|
+
)
|
234
|
+
raise
|
235
|
+
|
236
|
+
return _async_wrapper
|
237
|
+
|
238
|
+
return _decorate
|
bookalimo/wrapper.py
CHANGED
@@ -3,12 +3,14 @@ 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
12
|
from ._client import BookALimoClient
|
13
|
+
from ._logging import get_logger, log_call
|
12
14
|
from .exceptions import BookALimoError
|
13
15
|
from .models import (
|
14
16
|
Address,
|
@@ -34,6 +36,8 @@ from .models import (
|
|
34
36
|
Stop,
|
35
37
|
)
|
36
38
|
|
39
|
+
logger = get_logger("wrapper")
|
40
|
+
|
37
41
|
|
38
42
|
class BookALimo:
|
39
43
|
"""
|
@@ -45,7 +49,7 @@ class BookALimo:
|
|
45
49
|
self,
|
46
50
|
credentials: Credentials,
|
47
51
|
http_client: Optional[AsyncClient] = None,
|
48
|
-
base_url: str = "https://
|
52
|
+
base_url: str = "https://www.bookalimo.com/web/api",
|
49
53
|
http_timeout: float = 5.0,
|
50
54
|
**kwargs: Any,
|
51
55
|
):
|
@@ -66,11 +70,20 @@ class BookALimo:
|
|
66
70
|
http_timeout=http_timeout,
|
67
71
|
**kwargs,
|
68
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
|
+
)
|
69
80
|
|
70
81
|
async def aclose(self) -> None:
|
71
82
|
"""Close the HTTP client if we own it."""
|
72
83
|
if self._owns_http_client and not self.http_client.is_closed:
|
73
84
|
await self.http_client.aclose()
|
85
|
+
if logger.isEnabledFor(logging.DEBUG):
|
86
|
+
logger.debug("HTTP client closed")
|
74
87
|
|
75
88
|
async def __aenter__(self) -> "BookALimo":
|
76
89
|
"""Async context manager entry."""
|
@@ -85,6 +98,7 @@ class BookALimo:
|
|
85
98
|
"""Async context manager exit."""
|
86
99
|
await self.aclose()
|
87
100
|
|
101
|
+
@log_call(include_params=["is_archive"], operation="list_reservations")
|
88
102
|
async def list_reservations(
|
89
103
|
self, is_archive: bool = False
|
90
104
|
) -> ListReservationsResponse:
|
@@ -104,6 +118,7 @@ class BookALimo:
|
|
104
118
|
except Exception as e:
|
105
119
|
raise BookALimoError(f"Failed to list reservations: {str(e)}") from e
|
106
120
|
|
121
|
+
@log_call(include_params=["confirmation"], operation="get_reservation")
|
107
122
|
async def get_reservation(self, confirmation: str) -> GetReservationResponse:
|
108
123
|
"""
|
109
124
|
Get detailed reservation information.
|
@@ -121,6 +136,15 @@ class BookALimo:
|
|
121
136
|
except Exception as e:
|
122
137
|
raise BookALimoError(f"Failed to get reservation: {str(e)}") from e
|
123
138
|
|
139
|
+
@log_call(
|
140
|
+
include_params=[
|
141
|
+
"rate_type",
|
142
|
+
"date_time",
|
143
|
+
"passengers",
|
144
|
+
"luggage",
|
145
|
+
],
|
146
|
+
operation="get_prices",
|
147
|
+
)
|
124
148
|
async def get_prices(
|
125
149
|
self,
|
126
150
|
rate_type: RateType,
|
@@ -137,8 +161,8 @@ class BookALimo:
|
|
137
161
|
Args:
|
138
162
|
rate_type: 0=P2P, 1=Hourly (or string names)
|
139
163
|
date_time: 'MM/dd/yyyy hh:mm tt' format
|
140
|
-
pickup_location: Location
|
141
|
-
dropoff_location: Location
|
164
|
+
pickup_location: Location
|
165
|
+
dropoff_location: Location
|
142
166
|
passengers: Number of passengers
|
143
167
|
luggage: Number of luggage pieces
|
144
168
|
**kwargs: Optional fields like stops, account, car_class_code, etc.
|
@@ -184,6 +208,15 @@ class BookALimo:
|
|
184
208
|
except Exception as e:
|
185
209
|
raise BookALimoError(f"Failed to get prices: {str(e)}") from e
|
186
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
|
+
)
|
187
220
|
async def set_details(self, token: str, **details: Any) -> DetailsResponse:
|
188
221
|
"""
|
189
222
|
Set reservation details and get updated pricing.
|
@@ -213,6 +246,10 @@ class BookALimo:
|
|
213
246
|
except Exception as e:
|
214
247
|
raise BookALimoError(f"Failed to set details: {str(e)}") from e
|
215
248
|
|
249
|
+
@log_call(
|
250
|
+
include_params=["token", "method", "promo", "credit_card"],
|
251
|
+
operation="book",
|
252
|
+
)
|
216
253
|
async def book(
|
217
254
|
self,
|
218
255
|
token: str,
|
@@ -255,6 +292,15 @@ class BookALimo:
|
|
255
292
|
except Exception as e:
|
256
293
|
raise BookALimoError(f"Failed to book reservation: {str(e)}") from e
|
257
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
|
+
)
|
258
304
|
async def edit_reservation(
|
259
305
|
self, confirmation: str, is_cancel_request: bool = False, **changes: Any
|
260
306
|
) -> EditReservationResponse:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bookalimo
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
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>
|
@@ -65,6 +65,36 @@ Dynamic: license-file
|
|
65
65
|
|
66
66
|
A modern, async Python wrapper for the Book-A-Limo API with full type support. Built on top of `httpx` and `pydantic`.
|
67
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)
|
97
|
+
|
68
98
|
## Features
|
69
99
|
|
70
100
|
* **Asynchronous**
|
@@ -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,7 +127,7 @@ 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)
|
@@ -288,6 +317,50 @@ except BookALimoError as e:
|
|
288
317
|
print(f"Response Data: {e.response_data}")
|
289
318
|
```
|
290
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
|
+
|
291
364
|
## Development
|
292
365
|
|
293
366
|
```bash
|
@@ -0,0 +1,12 @@
|
|
1
|
+
bookalimo/__init__.py,sha256=rzbMBdkp0hIIQJ8AVLHeA6y4YrNIezfzUxP2iNcRBaw,691
|
2
|
+
bookalimo/_client.py,sha256=-kpaWBeEwFcXL28he0zFpx557jcZTUcx_NTjraSHSPQ,14537
|
3
|
+
bookalimo/_logging.py,sha256=xXpdUZ33vdIrbIReLTwWOIrGtxhvYcI1thV9rJ29Lsk,7120
|
4
|
+
bookalimo/exceptions.py,sha256=ubOUZiXGEOWZvXyz2D5f5zwxBlQZVDBv1uOPmokAQ6Q,404
|
5
|
+
bookalimo/models.py,sha256=-gvxrT7llrYLxSHWn3vpmvkQqnk2ejSd97Sw8BxgBLw,16420
|
6
|
+
bookalimo/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
7
|
+
bookalimo/wrapper.py,sha256=tusu2D4CkQqI7VKxSh62bK9hvleNHjuGD_6EpsWi2xg,13381
|
8
|
+
bookalimo-0.1.5.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
bookalimo-0.1.5.dist-info/METADATA,sha256=I13Y5Ac15h6uXcDi4KvVEncHHEej4Dkyt390EXuwooY,10739
|
10
|
+
bookalimo-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
bookalimo-0.1.5.dist-info/top_level.txt,sha256=ZgYiDX2GfZCp4pevWn4X2qyEr-Dh7-cq5Y1j2jMhB1s,10
|
12
|
+
bookalimo-0.1.5.dist-info/RECORD,,
|
bookalimo-0.1.3.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
bookalimo/__init__.py,sha256=ch1t7uvw_F3zNpqb-IfH0n2sDwFLnAQDgm2lpwtvpj4,568
|
2
|
-
bookalimo/_client.py,sha256=vju39534nm-6TVBZ7H74VTZIKdYOAnQiN1nFUqifRTM,11166
|
3
|
-
bookalimo/exceptions.py,sha256=ubOUZiXGEOWZvXyz2D5f5zwxBlQZVDBv1uOPmokAQ6Q,404
|
4
|
-
bookalimo/models.py,sha256=-gvxrT7llrYLxSHWn3vpmvkQqnk2ejSd97Sw8BxgBLw,16420
|
5
|
-
bookalimo/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
6
|
-
bookalimo/wrapper.py,sha256=tlhfGR6jQqnx33GO6d3fQZ8a5294UkftC1ORcvbhDww,11896
|
7
|
-
bookalimo-0.1.3.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
bookalimo-0.1.3.dist-info/METADATA,sha256=9CXiMDcKrhLe_2pHpVlYA7UTo8twYGmMOsl933V4vXo,8857
|
9
|
-
bookalimo-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
bookalimo-0.1.3.dist-info/top_level.txt,sha256=ZgYiDX2GfZCp4pevWn4X2qyEr-Dh7-cq5Y1j2jMhB1s,10
|
11
|
-
bookalimo-0.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|