bookalimo 0.1.3__py3-none-any.whl → 0.1.4__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 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 typing import Any, Optional, cast
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._convert_model_to_api_dict(data.model_dump())
187
-
188
- # Remove None values to avoid API issues
189
- api_data = self._remove_none_values(api_data)
190
- api_data = self.handle_enums(api_data)
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
- # Handle different HTTP status codes
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}: {response.text}"
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
- # Check for API-level errors
257
+ # API-level errors
216
258
  if isinstance(json_data, dict):
217
- if "error" in json_data and json_data["error"]:
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
- error_msg = json_data.get("error", "Unknown API error")
225
- raise BookALimoError(
226
- f"API Error: {error_msg}", response_data=json_data
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
- # Re-raise our custom errors
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,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
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://api.bookalimo.com",
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 dict
141
- dropoff_location: Location dict
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
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>
@@ -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 # enums/models come from bookalimo.models
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=frnwQtx9FTADPa0r38A1y7j-1VLBgGJ1p6dod_g0n6k,7646
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.4.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ bookalimo-0.1.4.dist-info/METADATA,sha256=f_kTmUYU_MhJJNXhienHzlSDEo4M8IVw0Tcc4WyGkfQ,10739
10
+ bookalimo-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ bookalimo-0.1.4.dist-info/top_level.txt,sha256=ZgYiDX2GfZCp4pevWn4X2qyEr-Dh7-cq5Y1j2jMhB1s,10
12
+ bookalimo-0.1.4.dist-info/RECORD,,
@@ -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,,