bookalimo 0.1.4__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bookalimo/__init__.py +17 -24
- bookalimo/_version.py +9 -0
- bookalimo/client.py +310 -0
- bookalimo/config.py +16 -0
- bookalimo/exceptions.py +115 -5
- bookalimo/integrations/__init__.py +1 -0
- bookalimo/integrations/google_places/__init__.py +31 -0
- bookalimo/integrations/google_places/client_async.py +258 -0
- bookalimo/integrations/google_places/client_sync.py +257 -0
- bookalimo/integrations/google_places/common.py +245 -0
- bookalimo/integrations/google_places/proto_adapter.py +224 -0
- bookalimo/{_logging.py → logging.py} +59 -62
- bookalimo/schemas/__init__.py +97 -0
- bookalimo/schemas/base.py +56 -0
- bookalimo/{models.py → schemas/booking.py} +88 -100
- bookalimo/schemas/places/__init__.py +37 -0
- bookalimo/schemas/places/common.py +198 -0
- bookalimo/schemas/places/google.py +596 -0
- bookalimo/schemas/places/place.py +337 -0
- bookalimo/services/__init__.py +11 -0
- bookalimo/services/pricing.py +191 -0
- bookalimo/services/reservations.py +227 -0
- bookalimo/transport/__init__.py +7 -0
- bookalimo/transport/auth.py +41 -0
- bookalimo/transport/base.py +44 -0
- bookalimo/transport/httpx_async.py +230 -0
- bookalimo/transport/httpx_sync.py +230 -0
- bookalimo/transport/retry.py +102 -0
- bookalimo/transport/utils.py +59 -0
- bookalimo-1.0.0.dist-info/METADATA +307 -0
- bookalimo-1.0.0.dist-info/RECORD +35 -0
- bookalimo/_client.py +0 -420
- bookalimo/wrapper.py +0 -444
- bookalimo-0.1.4.dist-info/METADATA +0 -392
- bookalimo-0.1.4.dist-info/RECORD +0 -12
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
"""Sync HTTP transport using httpx."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from time import perf_counter
|
5
|
+
from typing import Any, Optional, TypeVar, Union, overload
|
6
|
+
from uuid import uuid4
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
from pydantic import BaseModel
|
10
|
+
|
11
|
+
from ..config import (
|
12
|
+
DEFAULT_BACKOFF,
|
13
|
+
DEFAULT_BASE_URL,
|
14
|
+
DEFAULT_RETRIES,
|
15
|
+
DEFAULT_TIMEOUTS,
|
16
|
+
DEFAULT_USER_AGENT,
|
17
|
+
)
|
18
|
+
from ..exceptions import (
|
19
|
+
BookalimoConnectionError,
|
20
|
+
BookalimoError,
|
21
|
+
BookalimoHTTPError,
|
22
|
+
BookalimoRequestError,
|
23
|
+
BookalimoTimeout,
|
24
|
+
)
|
25
|
+
from .auth import Credentials, inject_credentials
|
26
|
+
from .base import BaseTransport
|
27
|
+
from .retry import should_retry_exception, should_retry_status, sync_retry
|
28
|
+
from .utils import handle_api_errors, handle_http_error
|
29
|
+
|
30
|
+
logger = logging.getLogger("bookalimo.transport")
|
31
|
+
|
32
|
+
T = TypeVar("T", bound=BaseModel)
|
33
|
+
|
34
|
+
|
35
|
+
class SyncTransport(BaseTransport):
|
36
|
+
"""Sync HTTP transport using httpx."""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
base_url: str = DEFAULT_BASE_URL,
|
41
|
+
timeouts: Any = DEFAULT_TIMEOUTS,
|
42
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
43
|
+
credentials: Optional[Credentials] = None,
|
44
|
+
client: Optional[httpx.Client] = None,
|
45
|
+
retries: int = DEFAULT_RETRIES,
|
46
|
+
backoff: float = DEFAULT_BACKOFF,
|
47
|
+
):
|
48
|
+
self.base_url = base_url.rstrip("/")
|
49
|
+
self.credentials = credentials
|
50
|
+
self.retries = retries
|
51
|
+
self.backoff = backoff
|
52
|
+
self.headers = {
|
53
|
+
"content-type": "application/json",
|
54
|
+
"user-agent": user_agent,
|
55
|
+
}
|
56
|
+
|
57
|
+
# Create client if not provided
|
58
|
+
self._owns_client = client is None
|
59
|
+
self.client = client or httpx.Client(timeout=timeouts)
|
60
|
+
|
61
|
+
if logger.isEnabledFor(logging.DEBUG):
|
62
|
+
logger.debug(
|
63
|
+
"SyncTransport initialized (base_url=%s, timeout=%s, user_agent=%s)",
|
64
|
+
self.base_url,
|
65
|
+
timeouts,
|
66
|
+
user_agent,
|
67
|
+
)
|
68
|
+
|
69
|
+
@overload
|
70
|
+
def post(self, path: str, model: BaseModel) -> Any: ...
|
71
|
+
@overload
|
72
|
+
def post(self, path: str, model: BaseModel, response_model: type[T]) -> T: ...
|
73
|
+
|
74
|
+
def post(
|
75
|
+
self, path: str, model: BaseModel, response_model: Optional[type[T]] = None
|
76
|
+
) -> Union[T, Any]:
|
77
|
+
"""Make a POST request and return parsed response."""
|
78
|
+
# Prepare URL
|
79
|
+
path = path if path.startswith("/") else f"/{path}"
|
80
|
+
url = f"{self.base_url}{path}"
|
81
|
+
|
82
|
+
# Prepare data and inject credentials
|
83
|
+
data = self.prepare_data(model)
|
84
|
+
data = inject_credentials(data, self.credentials)
|
85
|
+
|
86
|
+
# Debug logging
|
87
|
+
req_id = None
|
88
|
+
start = 0.0
|
89
|
+
if logger.isEnabledFor(logging.DEBUG):
|
90
|
+
req_id = uuid4().hex[:8]
|
91
|
+
start = perf_counter()
|
92
|
+
body_keys = sorted(k for k in data.keys() if k != "credentials")
|
93
|
+
logger.debug(
|
94
|
+
"→ [%s] POST %s body_keys=%s",
|
95
|
+
req_id,
|
96
|
+
path,
|
97
|
+
body_keys,
|
98
|
+
)
|
99
|
+
|
100
|
+
try:
|
101
|
+
# Make request with retry logic
|
102
|
+
response = sync_retry(
|
103
|
+
lambda: self._make_request(url, data),
|
104
|
+
retries=self.retries,
|
105
|
+
backoff=self.backoff,
|
106
|
+
should_retry=lambda e: should_retry_exception(e)
|
107
|
+
or (
|
108
|
+
isinstance(e, httpx.HTTPStatusError)
|
109
|
+
and should_retry_status(e.response.status_code)
|
110
|
+
),
|
111
|
+
)
|
112
|
+
|
113
|
+
# Handle HTTP errors
|
114
|
+
if response.status_code >= 400:
|
115
|
+
handle_http_error(response, req_id, path)
|
116
|
+
|
117
|
+
# Parse JSON
|
118
|
+
try:
|
119
|
+
json_data = response.json()
|
120
|
+
except ValueError as e:
|
121
|
+
if logger.isEnabledFor(logging.DEBUG):
|
122
|
+
logger.warning("× [%s] %s invalid JSON", req_id or "-", path)
|
123
|
+
preview = (
|
124
|
+
(response.text or "")[:256] if hasattr(response, "text") else None
|
125
|
+
)
|
126
|
+
raise BookalimoError(
|
127
|
+
f"Invalid JSON response: {preview}",
|
128
|
+
) from e
|
129
|
+
|
130
|
+
# Handle API-level errors
|
131
|
+
handle_api_errors(json_data, req_id, path)
|
132
|
+
|
133
|
+
# Debug logging for success
|
134
|
+
if logger.isEnabledFor(logging.DEBUG):
|
135
|
+
dur_ms = (perf_counter() - start) * 1000.0
|
136
|
+
reqid_hdr = response.headers.get(
|
137
|
+
"x-request-id"
|
138
|
+
) or response.headers.get("request-id")
|
139
|
+
content_len = (
|
140
|
+
len(response.content) if hasattr(response, "content") else None
|
141
|
+
)
|
142
|
+
logger.debug(
|
143
|
+
"← [%s] %s %s in %.1f ms len=%s reqid=%s",
|
144
|
+
req_id,
|
145
|
+
response.status_code,
|
146
|
+
path,
|
147
|
+
dur_ms,
|
148
|
+
content_len,
|
149
|
+
reqid_hdr,
|
150
|
+
)
|
151
|
+
|
152
|
+
# Parse and return response
|
153
|
+
return (
|
154
|
+
response_model.model_validate(json_data)
|
155
|
+
if response_model
|
156
|
+
else json_data
|
157
|
+
)
|
158
|
+
|
159
|
+
except httpx.TimeoutException:
|
160
|
+
if logger.isEnabledFor(logging.DEBUG):
|
161
|
+
logger.warning("× [%s] %s timeout", req_id or "-", path)
|
162
|
+
raise BookalimoTimeout("Request timeout") from None
|
163
|
+
|
164
|
+
except httpx.ConnectError:
|
165
|
+
if logger.isEnabledFor(logging.DEBUG):
|
166
|
+
logger.warning("× [%s] %s connection error", req_id or "-", path)
|
167
|
+
raise BookalimoConnectionError(
|
168
|
+
"Connection error - unable to reach Book-A-Limo API"
|
169
|
+
) from None
|
170
|
+
except httpx.RequestError as e:
|
171
|
+
if logger.isEnabledFor(logging.DEBUG):
|
172
|
+
logger.warning(
|
173
|
+
"× [%s] %s request error: %s",
|
174
|
+
req_id or "-",
|
175
|
+
path,
|
176
|
+
e.__class__.__name__,
|
177
|
+
)
|
178
|
+
raise BookalimoRequestError(f"Request Error: {e}") from e
|
179
|
+
|
180
|
+
except httpx.HTTPStatusError as e:
|
181
|
+
if logger.isEnabledFor(logging.DEBUG):
|
182
|
+
logger.warning(
|
183
|
+
"× [%s] %s HTTP error: %s",
|
184
|
+
req_id or "-",
|
185
|
+
path,
|
186
|
+
e.__class__.__name__,
|
187
|
+
)
|
188
|
+
status_code = getattr(getattr(e, "response", None), "status_code", None)
|
189
|
+
raise BookalimoHTTPError(f"HTTP Error: {e}", status_code=status_code) from e
|
190
|
+
|
191
|
+
except (BookalimoError, BookalimoHTTPError):
|
192
|
+
# Already handled above
|
193
|
+
raise
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
if logger.isEnabledFor(logging.DEBUG):
|
197
|
+
logger.warning(
|
198
|
+
"× [%s] %s unexpected error: %s",
|
199
|
+
req_id or "-",
|
200
|
+
path,
|
201
|
+
e.__class__.__name__,
|
202
|
+
)
|
203
|
+
raise BookalimoError(f"Unexpected error: {str(e)}") from e
|
204
|
+
|
205
|
+
def _make_request(self, url: str, data: dict[str, Any]) -> httpx.Response:
|
206
|
+
"""Make the actual HTTP request."""
|
207
|
+
resp = self.client.post(url, json=data, headers=self.headers)
|
208
|
+
if should_retry_status(resp.status_code):
|
209
|
+
# Construct an HTTPStatusError so sync_retry can catch & decide.
|
210
|
+
raise httpx.HTTPStatusError(
|
211
|
+
message=f"Retryable HTTP status: {resp.status_code}",
|
212
|
+
request=resp.request,
|
213
|
+
response=resp,
|
214
|
+
)
|
215
|
+
return resp
|
216
|
+
|
217
|
+
def close(self) -> None:
|
218
|
+
"""Close the HTTP client if we own it."""
|
219
|
+
if self._owns_client and not self.client.is_closed:
|
220
|
+
self.client.close()
|
221
|
+
if logger.isEnabledFor(logging.DEBUG):
|
222
|
+
logger.debug("SyncTransport HTTP client closed")
|
223
|
+
|
224
|
+
def __enter__(self) -> "SyncTransport":
|
225
|
+
"""Context manager entry."""
|
226
|
+
return self
|
227
|
+
|
228
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
229
|
+
"""Context manager exit."""
|
230
|
+
self.close()
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""Retry logic and backoff utilities."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import random
|
5
|
+
from collections.abc import Awaitable
|
6
|
+
from typing import Callable, TypeVar, Union
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
|
10
|
+
from ..config import DEFAULT_STATUS_FORCELIST
|
11
|
+
|
12
|
+
T = TypeVar("T")
|
13
|
+
|
14
|
+
|
15
|
+
def should_retry_status(
|
16
|
+
status_code: int, status_forcelist: tuple[int, ...] = DEFAULT_STATUS_FORCELIST
|
17
|
+
) -> bool:
|
18
|
+
"""Check if a status code should trigger a retry."""
|
19
|
+
return status_code in status_forcelist
|
20
|
+
|
21
|
+
|
22
|
+
def should_retry_exception(exc: Exception) -> bool:
|
23
|
+
"""Check if an exception should trigger a retry."""
|
24
|
+
return isinstance(
|
25
|
+
exc,
|
26
|
+
(
|
27
|
+
httpx.TimeoutException,
|
28
|
+
httpx.ConnectError,
|
29
|
+
httpx.ReadTimeout,
|
30
|
+
httpx.ConnectError,
|
31
|
+
ConnectionError,
|
32
|
+
),
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
def calculate_backoff(attempt: int, base_backoff: float) -> float:
|
37
|
+
"""Calculate exponential backoff with jitter."""
|
38
|
+
# Exponential backoff: base * (2 ^ attempt)
|
39
|
+
backoff = base_backoff * (2**attempt)
|
40
|
+
# Add jitter: ±25% randomization
|
41
|
+
jitter = backoff * 0.25 * (random.random() * 2 - 1) # nosec B311: non-crypto jitter
|
42
|
+
return float(max(0.1, backoff + jitter)) # Minimum 100ms
|
43
|
+
|
44
|
+
|
45
|
+
async def async_retry(
|
46
|
+
func: Callable[[], Awaitable[T]],
|
47
|
+
retries: int,
|
48
|
+
backoff: float,
|
49
|
+
should_retry: Callable[[Exception], bool],
|
50
|
+
) -> T:
|
51
|
+
"""Retry an async function with exponential backoff."""
|
52
|
+
attempt = 0
|
53
|
+
last_exc: Union[Exception, None] = None
|
54
|
+
|
55
|
+
while attempt <= retries:
|
56
|
+
try:
|
57
|
+
return await func()
|
58
|
+
except Exception as e:
|
59
|
+
last_exc = e
|
60
|
+
if attempt >= retries or not should_retry(e):
|
61
|
+
break
|
62
|
+
|
63
|
+
wait_time = calculate_backoff(attempt, backoff)
|
64
|
+
await asyncio.sleep(wait_time)
|
65
|
+
attempt += 1
|
66
|
+
|
67
|
+
if last_exc is not None:
|
68
|
+
raise last_exc
|
69
|
+
else:
|
70
|
+
raise RuntimeError("Last exception is None")
|
71
|
+
|
72
|
+
|
73
|
+
def sync_retry(
|
74
|
+
func: Callable[[], T],
|
75
|
+
retries: int,
|
76
|
+
backoff: float,
|
77
|
+
should_retry: Callable[[Exception], bool],
|
78
|
+
) -> T:
|
79
|
+
"""Retry a sync function with exponential backoff."""
|
80
|
+
import time
|
81
|
+
|
82
|
+
attempt = 0
|
83
|
+
last_exc: Union[Exception, None] = None
|
84
|
+
|
85
|
+
while attempt <= retries:
|
86
|
+
try:
|
87
|
+
return func()
|
88
|
+
except Exception as e:
|
89
|
+
last_exc = e
|
90
|
+
if attempt >= retries or not should_retry(e):
|
91
|
+
break
|
92
|
+
|
93
|
+
wait_time = calculate_backoff(attempt, backoff)
|
94
|
+
time.sleep(wait_time)
|
95
|
+
attempt += 1
|
96
|
+
|
97
|
+
# Re-raise the last exception
|
98
|
+
if last_exc is not None:
|
99
|
+
raise last_exc
|
100
|
+
else:
|
101
|
+
raise RuntimeError("Last exception is None")
|
102
|
+
raise last_exc
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any, Optional
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
|
6
|
+
from ..exceptions import BookalimoError, BookalimoHTTPError, BookalimoTimeout
|
7
|
+
|
8
|
+
logger = logging.getLogger("bookalimo.transport")
|
9
|
+
|
10
|
+
|
11
|
+
def handle_api_errors(
|
12
|
+
json_data: Any,
|
13
|
+
req_id: Optional[str],
|
14
|
+
path: str,
|
15
|
+
) -> None:
|
16
|
+
"""Handle API-level errors in the response."""
|
17
|
+
if not isinstance(json_data, dict):
|
18
|
+
return
|
19
|
+
|
20
|
+
if json_data.get("error"):
|
21
|
+
if logger.isEnabledFor(logging.DEBUG):
|
22
|
+
logger.warning("× [%s] %s API error", req_id or "-", path)
|
23
|
+
raise BookalimoError(f"API Error: {json_data['error']}")
|
24
|
+
|
25
|
+
if "success" in json_data and not json_data["success"]:
|
26
|
+
msg = json_data.get("error", "Unknown API error")
|
27
|
+
if logger.isEnabledFor(logging.DEBUG):
|
28
|
+
logger.warning("× [%s] %s API error", req_id or "-", path)
|
29
|
+
raise BookalimoError(f"API Error: {msg}")
|
30
|
+
|
31
|
+
|
32
|
+
def handle_http_error(
|
33
|
+
response: httpx.Response, req_id: Optional[str], path: str
|
34
|
+
) -> None:
|
35
|
+
"""Handle HTTP status errors."""
|
36
|
+
status = response.status_code
|
37
|
+
|
38
|
+
if status == 408:
|
39
|
+
raise BookalimoTimeout(f"HTTP {status}: Request timeout")
|
40
|
+
elif status in (502, 503, 504):
|
41
|
+
raise BookalimoHTTPError(
|
42
|
+
f"HTTP {status}: Service unavailable", status_code=status
|
43
|
+
)
|
44
|
+
|
45
|
+
# Try to parse error payload
|
46
|
+
try:
|
47
|
+
payload = response.json()
|
48
|
+
except Exception as e:
|
49
|
+
text_preview = (response.text or "")[:256] if hasattr(response, "text") else ""
|
50
|
+
raise BookalimoHTTPError(
|
51
|
+
f"HTTP {status}: {text_preview}",
|
52
|
+
status_code=status,
|
53
|
+
) from e
|
54
|
+
|
55
|
+
raise BookalimoHTTPError(
|
56
|
+
f"HTTP {status}",
|
57
|
+
status_code=status,
|
58
|
+
payload=payload if isinstance(payload, dict) else {"raw": payload},
|
59
|
+
)
|
@@ -0,0 +1,307 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: bookalimo
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Python wrapper for the Book-A-Limo API
|
5
|
+
Author-email: Jonathan Oren <jonathan@bookalimo.com>
|
6
|
+
Maintainer-email: Jonathan Oren <jonathan@bookalimo.com>
|
7
|
+
License: MIT
|
8
|
+
Project-URL: Homepage, https://github.com/asparagusbeef/bookalimo-python
|
9
|
+
Project-URL: Documentation, https://asparagusbeef.github.io/bookalimo-python
|
10
|
+
Project-URL: Repository, https://github.com/asparagusbeef/bookalimo-python
|
11
|
+
Project-URL: Issues, https://github.com/asparagusbeef/bookalimo-python/issues
|
12
|
+
Project-URL: Changelog, https://github.com/asparagusbeef/bookalimo-python/blob/main/CHANGELOG.md
|
13
|
+
Keywords: bookalimo,api,transportation,booking
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
15
|
+
Classifier: Intended Audience :: Developers
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
17
|
+
Classifier: Operating System :: OS Independent
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
25
|
+
Requires-Python: >=3.9
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
License-File: LICENSE
|
28
|
+
Requires-Dist: httpx>=0.25.0
|
29
|
+
Requires-Dist: pydantic>=2.11.0
|
30
|
+
Requires-Dist: pycountry>=22.0.0
|
31
|
+
Requires-Dist: us>=3.0.0
|
32
|
+
Requires-Dist: airportsdata>=20230101
|
33
|
+
Provides-Extra: places
|
34
|
+
Requires-Dist: google-maps-places>=0.1.0; extra == "places"
|
35
|
+
Requires-Dist: google-api-core>=2.0.0; extra == "places"
|
36
|
+
Provides-Extra: dev
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
38
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
39
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
40
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
41
|
+
Requires-Dist: httpx[mock]>=0.25.0; extra == "dev"
|
42
|
+
Requires-Dist: respx>=0.20.0; extra == "dev"
|
43
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
44
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
45
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
46
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
47
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
48
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
49
|
+
Requires-Dist: types-protobuf>=6.0.0; extra == "dev"
|
50
|
+
Provides-Extra: test
|
51
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
52
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
53
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
54
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
|
55
|
+
Requires-Dist: httpx[mock]>=0.25.0; extra == "test"
|
56
|
+
Requires-Dist: respx>=0.20.0; extra == "test"
|
57
|
+
Dynamic: license-file
|
58
|
+
|
59
|
+
# Bookalimo Python SDK
|
60
|
+
|
61
|
+
[](https://codecov.io/gh/asparagusbeef/bookalimo-python)
|
62
|
+
[](https://bookalimo-python.readthedocs.io/en/latest/?badge=latest)
|
63
|
+
[](https://badge.fury.io/py/bookalimo)
|
64
|
+
[](https://pypi.org/project/bookalimo/)
|
65
|
+
[](https://opensource.org/licenses/MIT)
|
66
|
+
[](https://github.com/astral-sh/ruff)
|
67
|
+
|
68
|
+
Python client library for the Book-A-Limo transportation booking API with async/sync support, type safety, and Google Places integration.
|
69
|
+
|
70
|
+
## Features
|
71
|
+
|
72
|
+
- **Async & Sync Support** - Choose the right client for your use case
|
73
|
+
- **Type Safety** - Full Pydantic models with validation
|
74
|
+
- **Google Places Integration** - Location search and geocoding
|
75
|
+
- **Automatic Retry** - Built-in exponential backoff for reliability
|
76
|
+
- **Comprehensive Error Handling** - Detailed exceptions with context
|
77
|
+
|
78
|
+
## Installation
|
79
|
+
|
80
|
+
```bash
|
81
|
+
pip install bookalimo
|
82
|
+
|
83
|
+
# With Google Places integration
|
84
|
+
pip install bookalimo[places]
|
85
|
+
```
|
86
|
+
|
87
|
+
## Core API
|
88
|
+
|
89
|
+
### Clients
|
90
|
+
- `AsyncBookalimo` - Async client for high-concurrency applications
|
91
|
+
- `Bookalimo` - Sync client for simple scripts and legacy code
|
92
|
+
|
93
|
+
### Services
|
94
|
+
- `client.pricing` - Get quotes and update booking details
|
95
|
+
- `client.reservations` - Book, list, modify, and cancel reservations
|
96
|
+
- `client.places` - Google Places search and geocoding (optional)
|
97
|
+
|
98
|
+
### Authentication
|
99
|
+
SHA256-based credential system with automatic password hashing:
|
100
|
+
```python
|
101
|
+
from bookalimo.transport.auth import Credentials
|
102
|
+
|
103
|
+
# Agency account
|
104
|
+
credentials = Credentials.create("AGENCY123", "password", is_customer=False)
|
105
|
+
|
106
|
+
# Customer account
|
107
|
+
credentials = Credentials.create("user@email.com", "password", is_customer=True)
|
108
|
+
```
|
109
|
+
|
110
|
+
### Booking Flow
|
111
|
+
1. **Get Pricing** - `client.pricing.quote()` returns session token + vehicle options
|
112
|
+
2. **Update Details** - `client.pricing.update_details()` modifies booking (optional)
|
113
|
+
3. **Book Reservation** - `client.reservations.book()` confirms with payment
|
114
|
+
|
115
|
+
## Quick Example
|
116
|
+
|
117
|
+
```python
|
118
|
+
import asyncio
|
119
|
+
from bookalimo import AsyncBookalimo
|
120
|
+
from bookalimo.transport.auth import Credentials
|
121
|
+
from bookalimo.schemas.booking import RateType, Location, LocationType, Address, City
|
122
|
+
|
123
|
+
async def book_ride():
|
124
|
+
credentials = Credentials.create("your_id", "your_password")
|
125
|
+
|
126
|
+
# Define locations
|
127
|
+
pickup = Location(
|
128
|
+
type=LocationType.ADDRESS,
|
129
|
+
address=Address(
|
130
|
+
place_name="Empire State Building",
|
131
|
+
city=City(city_name="New York", country_code="US", state_code="NY")
|
132
|
+
)
|
133
|
+
)
|
134
|
+
|
135
|
+
dropoff = Location(
|
136
|
+
type=LocationType.ADDRESS,
|
137
|
+
address=Address(
|
138
|
+
place_name="JFK Airport",
|
139
|
+
city=City(city_name="New York", country_code="US", state_code="NY")
|
140
|
+
)
|
141
|
+
)
|
142
|
+
|
143
|
+
async with AsyncBookalimo(credentials=credentials) as client:
|
144
|
+
# 1. Get pricing
|
145
|
+
quote = await client.pricing.quote(
|
146
|
+
rate_type=RateType.P2P,
|
147
|
+
date_time="12/25/2024 03:00 PM",
|
148
|
+
pickup=pickup,
|
149
|
+
dropoff=dropoff,
|
150
|
+
passengers=2,
|
151
|
+
luggage=2
|
152
|
+
)
|
153
|
+
|
154
|
+
# 2. Book reservation
|
155
|
+
booking = await client.reservations.book(
|
156
|
+
token=quote.token,
|
157
|
+
method="charge" # or credit_card=CreditCard(...)
|
158
|
+
)
|
159
|
+
|
160
|
+
return booking.reservation_id
|
161
|
+
|
162
|
+
confirmation = asyncio.run(book_ride())
|
163
|
+
```
|
164
|
+
|
165
|
+
## Rate Types & Options
|
166
|
+
|
167
|
+
```python
|
168
|
+
from bookalimo.schemas.booking import RateType
|
169
|
+
|
170
|
+
# Point-to-point transfer
|
171
|
+
quote = await client.pricing.quote(
|
172
|
+
rate_type=RateType.P2P,
|
173
|
+
pickup=pickup_location,
|
174
|
+
dropoff=dropoff_location,
|
175
|
+
# ...
|
176
|
+
)
|
177
|
+
|
178
|
+
# Hourly service (minimum 2 hours)
|
179
|
+
quote = await client.pricing.quote(
|
180
|
+
rate_type=RateType.HOURLY,
|
181
|
+
hours=4,
|
182
|
+
pickup=pickup_location,
|
183
|
+
dropoff=pickup_location, # Same for hourly
|
184
|
+
# ...
|
185
|
+
)
|
186
|
+
|
187
|
+
# Daily service
|
188
|
+
quote = await client.pricing.quote(
|
189
|
+
rate_type=RateType.DAILY,
|
190
|
+
pickup=hotel_location,
|
191
|
+
dropoff=hotel_location,
|
192
|
+
# ...
|
193
|
+
)
|
194
|
+
```
|
195
|
+
|
196
|
+
## Location Types
|
197
|
+
|
198
|
+
```python
|
199
|
+
from bookalimo.schemas.booking import Location, LocationType, Address, Airport, City
|
200
|
+
|
201
|
+
# Street address
|
202
|
+
address_location = Location(
|
203
|
+
type=LocationType.ADDRESS,
|
204
|
+
address=Address(
|
205
|
+
place_name="Empire State Building",
|
206
|
+
street_name="350 5th Ave",
|
207
|
+
city=City(city_name="New York", country_code="US", state_code="NY")
|
208
|
+
)
|
209
|
+
)
|
210
|
+
|
211
|
+
# Airport with flight details
|
212
|
+
airport_location = Location(
|
213
|
+
type=LocationType.AIRPORT,
|
214
|
+
airport=Airport(
|
215
|
+
iata_code="JFK",
|
216
|
+
flight_number="UA123",
|
217
|
+
terminal="4"
|
218
|
+
)
|
219
|
+
)
|
220
|
+
```
|
221
|
+
|
222
|
+
## Google Places Integration
|
223
|
+
|
224
|
+
```python
|
225
|
+
async with AsyncBookalimo(
|
226
|
+
credentials=credentials,
|
227
|
+
google_places_api_key="your-google-places-key"
|
228
|
+
) as client:
|
229
|
+
# Search locations
|
230
|
+
results = await client.places.search("JFK Airport Terminal 4")
|
231
|
+
|
232
|
+
# Convert to booking location
|
233
|
+
location = Location(
|
234
|
+
type=LocationType.ADDRESS,
|
235
|
+
address=Address(
|
236
|
+
google_geocode=results[0].google_place.model_dump(),
|
237
|
+
place_name=results[0].formatted_address
|
238
|
+
)
|
239
|
+
)
|
240
|
+
```
|
241
|
+
|
242
|
+
## Reservation Management
|
243
|
+
|
244
|
+
```python
|
245
|
+
# List reservations
|
246
|
+
reservations = await client.reservations.list(is_archive=False)
|
247
|
+
|
248
|
+
# Get details
|
249
|
+
details = await client.reservations.get("ABC123")
|
250
|
+
|
251
|
+
# Modify reservation
|
252
|
+
edit_result = await client.reservations.edit(
|
253
|
+
confirmation="ABC123",
|
254
|
+
passengers=3,
|
255
|
+
pickup_date="12/26/2024"
|
256
|
+
)
|
257
|
+
|
258
|
+
# Cancel reservation
|
259
|
+
cancel_result = await client.reservations.edit(
|
260
|
+
confirmation="ABC123",
|
261
|
+
is_cancel=True
|
262
|
+
)
|
263
|
+
```
|
264
|
+
|
265
|
+
## Error Handling
|
266
|
+
|
267
|
+
```python
|
268
|
+
from bookalimo.exceptions import BookalimoHTTPError, BookalimoValidationError
|
269
|
+
|
270
|
+
try:
|
271
|
+
booking = await client.reservations.book(...)
|
272
|
+
except BookalimoValidationError as e:
|
273
|
+
print(f"Invalid input: {e.message}")
|
274
|
+
for error in e.errors():
|
275
|
+
print(f" {error['loc']}: {error['msg']}")
|
276
|
+
except BookalimoHTTPError as e:
|
277
|
+
if e.status_code == 401:
|
278
|
+
print("Authentication failed")
|
279
|
+
elif e.status_code == 400:
|
280
|
+
print(f"Bad request: {e.payload}")
|
281
|
+
```
|
282
|
+
|
283
|
+
## Documentation
|
284
|
+
|
285
|
+
**📖 [Complete Documentation](https://asparagusbeef.github.io/bookalimo-python)**
|
286
|
+
|
287
|
+
- [Quick Start Guide](https://asparagusbeef.github.io/bookalimo-python/guide/quickstart/)
|
288
|
+
- [API Reference](https://asparagusbeef.github.io/bookalimo-python/api/)
|
289
|
+
- [Examples](https://asparagusbeef.github.io/bookalimo-python/examples/basic/)
|
290
|
+
|
291
|
+
## Environment Options
|
292
|
+
|
293
|
+
```bash
|
294
|
+
export GOOGLE_PLACES_API_KEY="your_google_places_key"
|
295
|
+
export BOOKALIMO_LOG_LEVEL="DEBUG"
|
296
|
+
```
|
297
|
+
|
298
|
+
## Requirements
|
299
|
+
|
300
|
+
- Python 3.9+
|
301
|
+
- Book-A-Limo API credentials
|
302
|
+
- Dependencies: httpx, pydantic, pycountry, us, airportsdata
|
303
|
+
- Optional: google-maps-places (for Places integration)
|
304
|
+
|
305
|
+
## License
|
306
|
+
|
307
|
+
MIT License - see [LICENSE](LICENSE) for details.
|