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.
Files changed (38) hide show
  1. bookalimo/__init__.py +17 -24
  2. bookalimo/_version.py +9 -0
  3. bookalimo/client.py +310 -0
  4. bookalimo/config.py +16 -0
  5. bookalimo/exceptions.py +115 -5
  6. bookalimo/integrations/__init__.py +1 -0
  7. bookalimo/integrations/google_places/__init__.py +31 -0
  8. bookalimo/integrations/google_places/client_async.py +258 -0
  9. bookalimo/integrations/google_places/client_sync.py +257 -0
  10. bookalimo/integrations/google_places/common.py +245 -0
  11. bookalimo/integrations/google_places/proto_adapter.py +224 -0
  12. bookalimo/{_logging.py → logging.py} +59 -62
  13. bookalimo/schemas/__init__.py +97 -0
  14. bookalimo/schemas/base.py +56 -0
  15. bookalimo/{models.py → schemas/booking.py} +88 -100
  16. bookalimo/schemas/places/__init__.py +37 -0
  17. bookalimo/schemas/places/common.py +198 -0
  18. bookalimo/schemas/places/google.py +596 -0
  19. bookalimo/schemas/places/place.py +337 -0
  20. bookalimo/services/__init__.py +11 -0
  21. bookalimo/services/pricing.py +191 -0
  22. bookalimo/services/reservations.py +227 -0
  23. bookalimo/transport/__init__.py +7 -0
  24. bookalimo/transport/auth.py +41 -0
  25. bookalimo/transport/base.py +44 -0
  26. bookalimo/transport/httpx_async.py +230 -0
  27. bookalimo/transport/httpx_sync.py +230 -0
  28. bookalimo/transport/retry.py +102 -0
  29. bookalimo/transport/utils.py +59 -0
  30. bookalimo-1.0.0.dist-info/METADATA +307 -0
  31. bookalimo-1.0.0.dist-info/RECORD +35 -0
  32. bookalimo/_client.py +0 -420
  33. bookalimo/wrapper.py +0 -444
  34. bookalimo-0.1.4.dist-info/METADATA +0 -392
  35. bookalimo-0.1.4.dist-info/RECORD +0 -12
  36. {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
  37. {bookalimo-0.1.4.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
  38. {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
+ [![codecov](https://codecov.io/gh/asparagusbeef/bookalimo-python/branch/main/graph/badge.svg?token=H588J8Q1M8)](https://codecov.io/gh/asparagusbeef/bookalimo-python)
62
+ [![Documentation Status](https://readthedocs.org/projects/bookalimo-python/badge/?version=latest)](https://bookalimo-python.readthedocs.io/en/latest/?badge=latest)
63
+ [![PyPI version](https://badge.fury.io/py/bookalimo.svg)](https://badge.fury.io/py/bookalimo)
64
+ [![Python Support](https://img.shields.io/pypi/pyversions/bookalimo.svg)](https://pypi.org/project/bookalimo/)
65
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66
+ [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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.