bookalimo 1.0.1__py3-none-any.whl → 1.0.2__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/client.py +45 -22
- bookalimo/integrations/google_places/client_async.py +56 -102
- bookalimo/integrations/google_places/client_sync.py +56 -100
- bookalimo/integrations/google_places/common.py +290 -12
- bookalimo/integrations/google_places/resolve_airport.py +148 -119
- bookalimo/integrations/google_places/transports.py +14 -7
- bookalimo/logging.py +103 -0
- bookalimo/schemas/__init__.py +121 -35
- bookalimo/schemas/base.py +74 -14
- bookalimo/schemas/places/__init__.py +3 -1
- bookalimo/schemas/places/common.py +1 -1
- bookalimo/schemas/places/field_mask.py +0 -9
- bookalimo/schemas/places/google.py +165 -10
- bookalimo/schemas/requests.py +214 -0
- bookalimo/schemas/responses.py +196 -0
- bookalimo/schemas/{booking.py → shared.py} +55 -218
- bookalimo/services/pricing.py +9 -129
- bookalimo/services/reservations.py +10 -100
- bookalimo/transport/auth.py +2 -2
- bookalimo/transport/httpx_async.py +41 -125
- bookalimo/transport/httpx_sync.py +30 -109
- bookalimo/transport/utils.py +204 -3
- bookalimo-1.0.2.dist-info/METADATA +245 -0
- bookalimo-1.0.2.dist-info/RECORD +40 -0
- bookalimo-1.0.1.dist-info/METADATA +0 -370
- bookalimo-1.0.1.dist-info/RECORD +0 -38
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/WHEEL +0 -0
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,9 @@
|
|
1
1
|
"""Reservations service for listing, getting, editing, and booking reservations."""
|
2
2
|
|
3
|
-
from
|
4
|
-
|
5
|
-
from ..exceptions import BookalimoRequestError
|
6
|
-
from ..schemas.booking import (
|
3
|
+
from ..schemas import (
|
7
4
|
BookRequest,
|
8
5
|
BookResponse,
|
9
|
-
|
10
|
-
EditableReservationRequest,
|
6
|
+
EditReservationRequest,
|
11
7
|
EditReservationResponse,
|
12
8
|
GetReservationRequest,
|
13
9
|
GetReservationResponse,
|
@@ -53,73 +49,30 @@ class AsyncReservationsService:
|
|
53
49
|
"/booking/reservation/get/", request, GetReservationResponse
|
54
50
|
)
|
55
51
|
|
56
|
-
async def edit(
|
57
|
-
self, confirmation: str, *, is_cancel: bool = False, **changes: Any
|
58
|
-
) -> EditReservationResponse:
|
52
|
+
async def edit(self, request: EditReservationRequest) -> EditReservationResponse:
|
59
53
|
"""
|
60
54
|
Edit or cancel a reservation.
|
61
55
|
|
62
56
|
Args:
|
63
|
-
|
64
|
-
is_cancel: True to cancel the reservation
|
65
|
-
**changes: Fields to change (rate_type, pickup_date, pickup_time,
|
66
|
-
stops, passengers, luggage, pets, car_seats, boosters,
|
67
|
-
infants, other)
|
57
|
+
request: Complete edit request with confirmation and fields to change
|
68
58
|
|
69
59
|
Returns:
|
70
60
|
EditReservationResponse
|
71
61
|
"""
|
72
|
-
request_data = {
|
73
|
-
"confirmation": confirmation,
|
74
|
-
"is_cancel_request": is_cancel,
|
75
|
-
}
|
76
|
-
|
77
|
-
# Add changes if not canceling
|
78
|
-
if not is_cancel:
|
79
|
-
for key, value in changes.items():
|
80
|
-
if value is not None:
|
81
|
-
request_data[key] = value
|
82
|
-
|
83
|
-
request = EditableReservationRequest.model_validate(request_data)
|
84
62
|
return await self._transport.post(
|
85
63
|
"/booking/edit/", request, EditReservationResponse
|
86
64
|
)
|
87
65
|
|
88
|
-
async def book(
|
89
|
-
self,
|
90
|
-
token: str,
|
91
|
-
*,
|
92
|
-
method: Optional[str] = None,
|
93
|
-
credit_card: Optional[CreditCard] = None,
|
94
|
-
promo: Optional[str] = None,
|
95
|
-
) -> BookResponse:
|
66
|
+
async def book(self, request: BookRequest) -> BookResponse:
|
96
67
|
"""
|
97
68
|
Book a reservation.
|
98
69
|
|
99
70
|
Args:
|
100
|
-
|
101
|
-
method: 'charge' for charge accounts, None for credit card
|
102
|
-
credit_card: Credit card information (required if method is not 'charge')
|
103
|
-
promo: Optional promo code
|
71
|
+
request: Complete booking request with token and payment details
|
104
72
|
|
105
73
|
Returns:
|
106
74
|
BookResponse with reservation_id
|
107
75
|
"""
|
108
|
-
request_data: dict[str, Any] = {"token": token}
|
109
|
-
|
110
|
-
if promo:
|
111
|
-
request_data["promo"] = promo
|
112
|
-
|
113
|
-
if method == "charge":
|
114
|
-
request_data["method"] = "charge"
|
115
|
-
elif credit_card:
|
116
|
-
request_data["credit_card"] = credit_card
|
117
|
-
else:
|
118
|
-
raise BookalimoRequestError(
|
119
|
-
"Either method='charge' or credit_card must be provided"
|
120
|
-
)
|
121
|
-
|
122
|
-
request = BookRequest(**request_data)
|
123
76
|
return await self._transport.post("/booking/book/", request, BookResponse)
|
124
77
|
|
125
78
|
|
@@ -159,69 +112,26 @@ class ReservationsService:
|
|
159
112
|
"/booking/reservation/get/", request, GetReservationResponse
|
160
113
|
)
|
161
114
|
|
162
|
-
def edit(
|
163
|
-
self, confirmation: str, *, is_cancel: bool = False, **changes: Any
|
164
|
-
) -> EditReservationResponse:
|
115
|
+
def edit(self, request: EditReservationRequest) -> EditReservationResponse:
|
165
116
|
"""
|
166
117
|
Edit or cancel a reservation.
|
167
118
|
|
168
119
|
Args:
|
169
|
-
|
170
|
-
is_cancel: True to cancel the reservation
|
171
|
-
**changes: Fields to change (rate_type, pickup_date, pickup_time,
|
172
|
-
stops, passengers, luggage, pets, car_seats, boosters,
|
173
|
-
infants, other)
|
120
|
+
request: Complete edit request with confirmation and fields to change
|
174
121
|
|
175
122
|
Returns:
|
176
123
|
EditReservationResponse
|
177
124
|
"""
|
178
|
-
request_data = {
|
179
|
-
"confirmation": confirmation,
|
180
|
-
"is_cancel_request": is_cancel,
|
181
|
-
}
|
182
|
-
|
183
|
-
# Add changes if not canceling
|
184
|
-
if not is_cancel:
|
185
|
-
for key, value in changes.items():
|
186
|
-
if value is not None:
|
187
|
-
request_data[key] = value
|
188
|
-
|
189
|
-
request = EditableReservationRequest.model_validate(request_data)
|
190
125
|
return self._transport.post("/booking/edit/", request, EditReservationResponse)
|
191
126
|
|
192
|
-
def book(
|
193
|
-
self,
|
194
|
-
token: str,
|
195
|
-
*,
|
196
|
-
method: Optional[str] = None,
|
197
|
-
credit_card: Optional[CreditCard] = None,
|
198
|
-
promo: Optional[str] = None,
|
199
|
-
) -> BookResponse:
|
127
|
+
def book(self, request: BookRequest) -> BookResponse:
|
200
128
|
"""
|
201
129
|
Book a reservation.
|
202
130
|
|
203
131
|
Args:
|
204
|
-
|
205
|
-
method: 'charge' for charge accounts, None for credit card
|
206
|
-
credit_card: Credit card information (required if method is not 'charge')
|
207
|
-
promo: Optional promo code
|
132
|
+
request: Complete booking request with token and payment details
|
208
133
|
|
209
134
|
Returns:
|
210
135
|
BookResponse with reservation_id
|
211
136
|
"""
|
212
|
-
request_data: dict[str, Any] = {"token": token}
|
213
|
-
|
214
|
-
if promo:
|
215
|
-
request_data["promo"] = promo
|
216
|
-
|
217
|
-
if method == "charge":
|
218
|
-
request_data["method"] = "charge"
|
219
|
-
elif credit_card:
|
220
|
-
request_data["credit_card"] = credit_card
|
221
|
-
else:
|
222
|
-
raise BookalimoRequestError(
|
223
|
-
"Either method='charge' or credit_card must be provided"
|
224
|
-
)
|
225
|
-
|
226
|
-
request = BookRequest.model_validate(request_data)
|
227
137
|
return self._transport.post("/booking/book/", request, BookResponse)
|
bookalimo/transport/auth.py
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
import hashlib
|
4
4
|
from typing import Any, Optional
|
5
5
|
|
6
|
-
from ..schemas.base import
|
6
|
+
from ..schemas.base import RequestModel
|
7
7
|
|
8
8
|
|
9
|
-
class Credentials(
|
9
|
+
class Credentials(RequestModel):
|
10
10
|
"""Authentication credentials for Book-A-Limo API."""
|
11
11
|
|
12
12
|
id: str
|
@@ -15,17 +15,21 @@ from ..config import (
|
|
15
15
|
DEFAULT_TIMEOUTS,
|
16
16
|
DEFAULT_USER_AGENT,
|
17
17
|
)
|
18
|
-
from ..
|
19
|
-
BookalimoConnectionError,
|
20
|
-
BookalimoError,
|
21
|
-
BookalimoHTTPError,
|
22
|
-
BookalimoRequestError,
|
23
|
-
BookalimoTimeout,
|
24
|
-
)
|
18
|
+
from ..logging import redact_url
|
25
19
|
from .auth import Credentials, inject_credentials
|
26
20
|
from .base import AsyncBaseTransport
|
27
21
|
from .retry import async_retry, should_retry_exception, should_retry_status
|
28
|
-
from .utils import
|
22
|
+
from .utils import (
|
23
|
+
build_url,
|
24
|
+
handle_api_errors,
|
25
|
+
handle_http_error,
|
26
|
+
map_httpx_exceptions,
|
27
|
+
parse_json_or_raise,
|
28
|
+
post_log,
|
29
|
+
pre_log,
|
30
|
+
raise_if_retryable_status,
|
31
|
+
setup_secure_logging_and_client,
|
32
|
+
)
|
29
33
|
|
30
34
|
logger = logging.getLogger("bookalimo.transport")
|
31
35
|
|
@@ -46,7 +50,7 @@ class AsyncTransport(AsyncBaseTransport):
|
|
46
50
|
backoff: float = DEFAULT_BACKOFF,
|
47
51
|
):
|
48
52
|
self.base_url = base_url.rstrip("/")
|
49
|
-
self.
|
53
|
+
self._credentials = credentials
|
50
54
|
self.retries = retries
|
51
55
|
self.backoff = backoff
|
52
56
|
self.headers = {
|
@@ -54,18 +58,28 @@ class AsyncTransport(AsyncBaseTransport):
|
|
54
58
|
"user-agent": user_agent,
|
55
59
|
}
|
56
60
|
|
57
|
-
#
|
58
|
-
self._owns_client =
|
59
|
-
|
61
|
+
# Setup secure logging and create client
|
62
|
+
self.client, self._owns_client = setup_secure_logging_and_client(
|
63
|
+
is_async=True, timeout=timeouts, client=client
|
64
|
+
)
|
60
65
|
|
61
66
|
if logger.isEnabledFor(logging.DEBUG):
|
62
67
|
logger.debug(
|
63
|
-
"
|
64
|
-
self.
|
68
|
+
"%s initialized (base_url=%s, timeout=%s, user_agent=%s)",
|
69
|
+
self.__class__.__name__,
|
70
|
+
redact_url(self.base_url),
|
65
71
|
timeouts,
|
66
72
|
user_agent,
|
67
73
|
)
|
68
74
|
|
75
|
+
@property
|
76
|
+
def credentials(self) -> Optional[Credentials]:
|
77
|
+
return self._credentials
|
78
|
+
|
79
|
+
@credentials.setter
|
80
|
+
def credentials(self, credentials: Optional[Credentials]) -> None:
|
81
|
+
self._credentials = credentials
|
82
|
+
|
69
83
|
@overload
|
70
84
|
async def post(self, path: str, model: BaseModel) -> Any: ...
|
71
85
|
@overload
|
@@ -75,30 +89,18 @@ class AsyncTransport(AsyncBaseTransport):
|
|
75
89
|
self, path: str, model: BaseModel, response_model: Optional[type[T]] = None
|
76
90
|
) -> Union[T, Any]:
|
77
91
|
"""Make a POST request and return parsed response."""
|
78
|
-
|
79
|
-
path = path if path.startswith("/") else f"/{path}"
|
80
|
-
url = f"{self.base_url}{path}"
|
92
|
+
url = build_url(self.base_url, path)
|
81
93
|
|
82
|
-
# Prepare data and inject credentials
|
83
94
|
data = self.prepare_data(model)
|
84
95
|
data = inject_credentials(data, self.credentials)
|
85
96
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
if logger.isEnabledFor(logging.DEBUG):
|
90
|
-
req_id = uuid4().hex[:8]
|
91
|
-
start = perf_counter()
|
97
|
+
req_id = uuid4().hex[:8] if logger.isEnabledFor(logging.DEBUG) else None
|
98
|
+
start = perf_counter() if req_id else 0.0
|
99
|
+
if req_id:
|
92
100
|
body_keys = sorted(k for k in data.keys() if k != "credentials")
|
93
|
-
|
94
|
-
"→ [%s] POST %s body_keys=%s",
|
95
|
-
req_id,
|
96
|
-
path,
|
97
|
-
body_keys,
|
98
|
-
)
|
101
|
+
pre_log(url, body_keys, req_id)
|
99
102
|
|
100
|
-
|
101
|
-
# Make request with retry logic
|
103
|
+
with map_httpx_exceptions(req_id, path):
|
102
104
|
response = await async_retry(
|
103
105
|
lambda: self._make_request(url, data),
|
104
106
|
retries=self.retries,
|
@@ -110,121 +112,35 @@ class AsyncTransport(AsyncBaseTransport):
|
|
110
112
|
),
|
111
113
|
)
|
112
114
|
|
113
|
-
# Handle HTTP errors
|
114
115
|
if response.status_code >= 400:
|
115
116
|
handle_http_error(response, req_id, path)
|
116
117
|
|
117
|
-
|
118
|
-
|
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
|
118
|
+
json_data = parse_json_or_raise(response, path, req_id)
|
119
|
+
|
131
120
|
handle_api_errors(json_data, req_id, path)
|
132
121
|
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
122
|
+
if req_id:
|
123
|
+
post_log(url, response, start, req_id)
|
124
|
+
|
153
125
|
return (
|
154
126
|
response_model.model_validate(json_data)
|
155
127
|
if response_model
|
156
128
|
else json_data
|
157
129
|
)
|
158
130
|
|
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
131
|
async def _make_request(self, url: str, data: dict[str, Any]) -> httpx.Response:
|
206
|
-
"""Make the actual HTTP request."""
|
207
132
|
resp = await self.client.post(url, json=data, headers=self.headers)
|
208
|
-
|
209
|
-
# Construct an HTTPStatusError so async_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
|
-
)
|
133
|
+
raise_if_retryable_status(resp, should_retry_status)
|
215
134
|
return resp
|
216
135
|
|
217
136
|
async def aclose(self) -> None:
|
218
|
-
"""Close the HTTP client if we own it."""
|
219
137
|
if self._owns_client and not self.client.is_closed:
|
220
138
|
await self.client.aclose()
|
221
139
|
if logger.isEnabledFor(logging.DEBUG):
|
222
|
-
logger.debug("
|
140
|
+
logger.debug("%s HTTP client closed", self.__class__.__name__)
|
223
141
|
|
224
142
|
async def __aenter__(self) -> "AsyncTransport":
|
225
|
-
"""Async context manager entry."""
|
226
143
|
return self
|
227
144
|
|
228
145
|
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
229
|
-
"""Async context manager exit."""
|
230
146
|
await self.aclose()
|
@@ -15,17 +15,21 @@ from ..config import (
|
|
15
15
|
DEFAULT_TIMEOUTS,
|
16
16
|
DEFAULT_USER_AGENT,
|
17
17
|
)
|
18
|
-
from ..
|
19
|
-
BookalimoConnectionError,
|
20
|
-
BookalimoError,
|
21
|
-
BookalimoHTTPError,
|
22
|
-
BookalimoRequestError,
|
23
|
-
BookalimoTimeout,
|
24
|
-
)
|
18
|
+
from ..logging import redact_url
|
25
19
|
from .auth import Credentials, inject_credentials
|
26
20
|
from .base import BaseTransport
|
27
21
|
from .retry import should_retry_exception, should_retry_status, sync_retry
|
28
|
-
from .utils import
|
22
|
+
from .utils import (
|
23
|
+
build_url,
|
24
|
+
handle_api_errors,
|
25
|
+
handle_http_error,
|
26
|
+
map_httpx_exceptions,
|
27
|
+
parse_json_or_raise,
|
28
|
+
post_log,
|
29
|
+
pre_log,
|
30
|
+
raise_if_retryable_status,
|
31
|
+
setup_secure_logging_and_client,
|
32
|
+
)
|
29
33
|
|
30
34
|
logger = logging.getLogger("bookalimo.transport")
|
31
35
|
|
@@ -54,14 +58,16 @@ class SyncTransport(BaseTransport):
|
|
54
58
|
"user-agent": user_agent,
|
55
59
|
}
|
56
60
|
|
57
|
-
#
|
58
|
-
self._owns_client =
|
59
|
-
|
61
|
+
# Setup secure logging and create client
|
62
|
+
self.client, self._owns_client = setup_secure_logging_and_client(
|
63
|
+
is_async=False, timeout=timeouts, client=client
|
64
|
+
)
|
60
65
|
|
61
66
|
if logger.isEnabledFor(logging.DEBUG):
|
62
67
|
logger.debug(
|
63
|
-
"
|
64
|
-
self.
|
68
|
+
"%s initialized (base_url=%s, timeout=%s, user_agent=%s)",
|
69
|
+
self.__class__.__name__,
|
70
|
+
redact_url(self.base_url),
|
65
71
|
timeouts,
|
66
72
|
user_agent,
|
67
73
|
)
|
@@ -76,28 +82,20 @@ class SyncTransport(BaseTransport):
|
|
76
82
|
) -> Union[T, Any]:
|
77
83
|
"""Make a POST request and return parsed response."""
|
78
84
|
# Prepare URL
|
79
|
-
|
80
|
-
url = f"{self.base_url}{path}"
|
85
|
+
url = build_url(self.base_url, path)
|
81
86
|
|
82
87
|
# Prepare data and inject credentials
|
83
88
|
data = self.prepare_data(model)
|
84
89
|
data = inject_credentials(data, self.credentials)
|
85
90
|
|
86
91
|
# Debug logging
|
87
|
-
req_id = None
|
88
|
-
start = 0.0
|
89
|
-
if
|
90
|
-
req_id = uuid4().hex[:8]
|
91
|
-
start = perf_counter()
|
92
|
+
req_id = uuid4().hex[:8] if logger.isEnabledFor(logging.DEBUG) else None
|
93
|
+
start = perf_counter() if req_id else 0.0
|
94
|
+
if req_id:
|
92
95
|
body_keys = sorted(k for k in data.keys() if k != "credentials")
|
93
|
-
|
94
|
-
"→ [%s] POST %s body_keys=%s",
|
95
|
-
req_id,
|
96
|
-
path,
|
97
|
-
body_keys,
|
98
|
-
)
|
96
|
+
pre_log(url, body_keys, req_id)
|
99
97
|
|
100
|
-
|
98
|
+
with map_httpx_exceptions(req_id, path):
|
101
99
|
# Make request with retry logic
|
102
100
|
response = sync_retry(
|
103
101
|
lambda: self._make_request(url, data),
|
@@ -115,39 +113,14 @@ class SyncTransport(BaseTransport):
|
|
115
113
|
handle_http_error(response, req_id, path)
|
116
114
|
|
117
115
|
# Parse JSON
|
118
|
-
|
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
|
116
|
+
json_data = parse_json_or_raise(response, path, req_id)
|
129
117
|
|
130
118
|
# Handle API-level errors
|
131
119
|
handle_api_errors(json_data, req_id, path)
|
132
120
|
|
133
121
|
# Debug logging for success
|
134
|
-
if
|
135
|
-
|
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
|
-
)
|
122
|
+
if req_id:
|
123
|
+
post_log(url, response, start, req_id)
|
151
124
|
|
152
125
|
# Parse and return response
|
153
126
|
return (
|
@@ -156,62 +129,10 @@ class SyncTransport(BaseTransport):
|
|
156
129
|
else json_data
|
157
130
|
)
|
158
131
|
|
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
132
|
def _make_request(self, url: str, data: dict[str, Any]) -> httpx.Response:
|
206
133
|
"""Make the actual HTTP request."""
|
207
134
|
resp = self.client.post(url, json=data, headers=self.headers)
|
208
|
-
|
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
|
-
)
|
135
|
+
raise_if_retryable_status(resp, should_retry_status)
|
215
136
|
return resp
|
216
137
|
|
217
138
|
def close(self) -> None:
|
@@ -219,7 +140,7 @@ class SyncTransport(BaseTransport):
|
|
219
140
|
if self._owns_client and not self.client.is_closed:
|
220
141
|
self.client.close()
|
221
142
|
if logger.isEnabledFor(logging.DEBUG):
|
222
|
-
logger.debug("
|
143
|
+
logger.debug("%s HTTP client closed", self.__class__.__name__)
|
223
144
|
|
224
145
|
def __enter__(self) -> "SyncTransport":
|
225
146
|
"""Context manager entry."""
|