Skip to content

Commit b5669eb

Browse files
authored
fix: Retry-After in batch path response (#136)
1 parent 3258a9e commit b5669eb

File tree

4 files changed

+83
-28
lines changed

4 files changed

+83
-28
lines changed

smartcar/exception.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ def __init__(self, **kwargs):
3939
super().__init__(self.message)
4040

4141

42-
def exception_factory(status_code: int, headers: dict, body: str):
42+
def exception_factory(
43+
status_code: int, headers: dict, body: str, check_content_type=True
44+
):
4345
# v1.0 Exception: Content type other than application/json
44-
if "application/json" not in headers["Content-Type"]:
46+
if check_content_type and "application/json" not in headers["Content-Type"]:
4547
return SmartcarException(status_code=status_code, message=body)
4648

4749
# Parse body into JSON. Throw SDK error if this fails.
@@ -103,7 +105,6 @@ def exception_factory(status_code: int, headers: dict, body: str):
103105
suggested_user_message=response.get("suggestedUserMessage"),
104106
)
105107

106-
# Weird...
107108
else:
108109
return SmartcarException(
109110
status_code=status_code,

smartcar/vehicle.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import namedtuple
22
import json
3-
from typing import List
3+
from typing import Callable, List
44
import smartcar.config as config
55
import smartcar.helpers as helpers
66
import smartcar.smartcar
@@ -397,6 +397,30 @@ def send_destination(self, latitude, longitude) -> types.Action:
397397
)
398398
return types.select_named_tuple("send_destination", response)
399399

400+
@staticmethod
401+
def _batch_path_response(
402+
path: str, path_response: dict, top_response: dict
403+
) -> Callable[[], namedtuple]:
404+
if path_response.get("code") == 200:
405+
# attach top-level sc-request-id to res_dict
406+
path_response["headers"]["sc-request-id"] = top_response.headers.get(
407+
"sc-request-id"
408+
)
409+
# use lambda default args to avoid issues with closures
410+
return lambda p=path, r=path_response: types.select_named_tuple(p, r)
411+
412+
# if individual response is erroneous, attach a lambda that returns a SmartcarException
413+
def _attribute_raise_exception(smartcar_exception):
414+
raise smartcar_exception
415+
416+
path_status_code = path_response.get("code")
417+
path_headers = path_response.get("headers", {})
418+
path_body = json.dumps(path_response.get("body"))
419+
sc_exception = sce.exception_factory(
420+
path_status_code, path_headers, path_body, False
421+
)
422+
return lambda e=sc_exception: _attribute_raise_exception(e)
423+
400424
def batch(self, paths: List[str]) -> namedtuple:
401425
"""
402426
POST Vehicle.batch
@@ -432,32 +456,13 @@ def batch(self, paths: List[str]) -> namedtuple:
432456
# success of the request.
433457
batch_dict = dict()
434458
path_responses = response.json()["responses"]
435-
for res_dict in path_responses:
459+
for path_response in path_responses:
436460
path, attribute = helpers.format_path_and_attribute_for_batch(
437-
res_dict["path"]
461+
path_response["path"]
462+
)
463+
batch_dict[attribute] = Vehicle._batch_path_response(
464+
path, path_response, response
438465
)
439-
440-
if res_dict.get("code") == 200:
441-
# attach top-level sc-request-id to res_dict
442-
res_dict["headers"]["sc-request-id"] = response.headers.get(
443-
"sc-request-id"
444-
)
445-
# use lambda default args to avoid issues with closures
446-
batch_dict[attribute] = (
447-
lambda p=path, r=res_dict: types.select_named_tuple(p, r)
448-
)
449-
else:
450-
# if individual response is erroneous, attach a lambda that returns a SmartcarException
451-
def _attribute_raise_exception(smartcar_exception):
452-
raise smartcar_exception
453-
454-
code = res_dict.get("code")
455-
headers = response.headers
456-
body = json.dumps(res_dict.get("body"))
457-
sc_exception = sce.exception_factory(code, headers, body)
458-
batch_dict[attribute] = (
459-
lambda e=sc_exception: _attribute_raise_exception(e)
460-
)
461466

462467
# STEP 3 - Attach Meta to batch_dict
463468
batch_dict["meta"] = types.build_meta(response.headers)

tests/e2e/test_exception.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import smartcar
23
import smartcar.smartcar
34
from smartcar.smartcar import get_user

tests/unit/test_vehicle.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from smartcar import Vehicle
2+
from smartcar.exception import SmartcarException
23

34

45
def test_vehicle_constructor():
@@ -57,3 +58,50 @@ def test_vehicle_constructor_options():
5758
test_url = vehicle._format_url(path)
5859
expected_url = f"https://api.smartcar.com/v{version}/vehicles/{vid}/{path}"
5960
assert test_url == expected_url
61+
62+
63+
def test_batch_path_response():
64+
path = "battery_capacity"
65+
path_response = {
66+
"code": 429,
67+
"path": "/battery/capacity",
68+
"body": {
69+
"statusCode": 429,
70+
"type": "RATE_LIMIT",
71+
"code": "VEHICLE",
72+
"description": "You have reached the throttling rate limit for this vehicle. Please see the retry-after header for when to retry the request.",
73+
"docURL": "https://smartcar.com/docs/errors/api-errors/rate-limit-errors#vehicle",
74+
"resolution": {"type": "RETRY_LATER"},
75+
"suggestedUserMessage": "Your vehicle is temporarily unable to connect to Optiwatt. Please be patient while we’re working to resolve this issue.",
76+
"requestId": "test-request-id",
77+
},
78+
"headers": {"Retry-After": 999},
79+
}
80+
81+
top_response = {
82+
"responses": [path_response],
83+
"headers": {"Content-Type": "application/json"},
84+
}
85+
resulting_lambda = Vehicle._batch_path_response(path, path_response, top_response)
86+
87+
try:
88+
resulting_lambda()
89+
except Exception as e:
90+
path_exception = e
91+
92+
assert isinstance(path_exception, SmartcarException)
93+
assert path_exception.status_code == 429
94+
assert path_exception.request_id == "test-request-id"
95+
assert path_exception.type == "RATE_LIMIT"
96+
assert (
97+
path_exception.description
98+
== "You have reached the throttling rate limit for this vehicle. Please see the retry-after header for when to retry the request."
99+
)
100+
assert path_exception.code == "VEHICLE"
101+
assert path_exception.resolution == {"type": "RETRY_LATER", "url": None}
102+
assert path_exception.detail == None
103+
assert (
104+
path_exception.suggested_user_message
105+
== "Your vehicle is temporarily unable to connect to Optiwatt. Please be patient while we’re working to resolve this issue."
106+
)
107+
assert path_exception.retry_after == 999

0 commit comments

Comments
 (0)