From abec89dc3c2f25ac59ec3de8459af5077edbdfd6 Mon Sep 17 00:00:00 2001 From: Naomi Date: Tue, 25 Jan 2022 10:30:04 -0800 Subject: [PATCH] feat(vehicle): add request method to Vehicle (#106) Add a new general purpose and BSE related method request(self, method: str, path: str, body: dict = {}, headers: dict = {}) -> types.Response to Vehicle that allows for making requests to the Smartcar API. Also adds new type Response to types.py. Test Plan: create a simple app with my Smartcar Developer account and make requests with this method once it's in prod. Asana: https://app.asana.com/0/1201332815308984/1201617268953139/f * feat(vehicle): add general purpose and BSE request method * fix: make Response type "meta" attribute namedtuple instead of dict * extra checks for batch request response --- smartcar/types.py | 5 ++++ smartcar/vehicle.py | 37 +++++++++++++++++++++++++++++ tests/e2e/test_vehicle.py | 49 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/smartcar/types.py b/smartcar/types.py index 53ceeeab..81251195 100644 --- a/smartcar/types.py +++ b/smartcar/types.py @@ -204,6 +204,8 @@ def make_access_object(access: dict) -> Access: [("webhook_id", str), ("vehicle_id", str), ("meta", namedtuple)], ) +Response = NamedTuple("Response", [("body", dict), ("meta", namedtuple)]) + # =========================================== # Named Tuple Selector Function # =========================================== @@ -330,6 +332,9 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple: headers, ) + elif path == "request": + return Response(data, headers) + elif type(data) == dict: return generate_named_tuple(data, "Data") diff --git a/smartcar/vehicle.py b/smartcar/vehicle.py index 2797811c..a4a2ea28 100644 --- a/smartcar/vehicle.py +++ b/smartcar/vehicle.py @@ -442,6 +442,43 @@ def unsubscribe(self, amt: str, webhook_id: str) -> types.Status: response = helpers.requester("DELETE", url, headers=headers) return types.select_named_tuple("unsubscribe", response) + # =========================================== + # General Purpose Request Method + # =========================================== + + def request( + self, method: str, path: str, body: dict = {}, headers: dict = {} + ) -> types.Response: + """ + Utility method to make a request to a Smartcar endpoint - can be used + to make requests to brand specific endpoints. + + Args: + method (str): The HTTP request method to use. + path (str): The path to make the request to. + body (dict): The request body. + headers (dict): The headers to include in the request. + + Returns: + Response = NamedTuple("Response", [("body", dict), ("meta", namedtuple)]) + + Raises: + SmartcarException + """ + url = self._format_url(path) + + # Authorization header not provided + if not "Authorization" in headers: + has_units_header = "sc-unit-system" in headers + generated_headers = self._get_headers( + need_unit_system=(not has_units_header) + ) + headers.update(generated_headers) + + response = helpers.requester(method, url, headers=headers, json=body) + + return types.select_named_tuple("request", response) + # =========================================== # Utility # =========================================== diff --git a/tests/e2e/test_vehicle.py b/tests/e2e/test_vehicle.py index c548fc34..cbd5dbcf 100644 --- a/tests/e2e/test_vehicle.py +++ b/tests/e2e/test_vehicle.py @@ -202,6 +202,55 @@ def test_webhooks(chevy_volt): assert unsubscribe._fields == ("status", "meta") +def test_request(chevy_volt): + odometer = chevy_volt.request( + "GET", "odometer", None, {"sc-unit-system": "imperial"} + ) + assert type(odometer) == types.Response + assert odometer.body is not None + assert isinstance(odometer.meta, tuple) + assert odometer._fields == ("body", "meta") + assert odometer.meta.unit_system == "imperial" + + +def test_request_override_header(chevy_volt): + try: + chevy_volt.request( + "GET", + "odometer", + None, + { + "sc-unit-system": "imperial", + "Authorization": "Bearer abc", + }, + ) + except SmartcarException as sc_e: + assert ( + sc_e.message + == "AUTHENTICATION - The authorization header is missing or malformed, or it contains invalid or expired authentication credentials. Please check for missing parameters, spelling and casing mistakes, and other syntax issues." + ) + + +def test_request_with_body(chevy_volt): + batch = chevy_volt.request( + "post", + "batch", + {"requests": [{"path": "/odometer"}, {"path": "/tires/pressure"}]}, + ) + assert type(batch) is types.Response + assert batch.body is not None + assert isinstance(batch.meta, tuple) + assert batch.body["responses"][0]["path"] == "/odometer" + assert batch.body["responses"][0]["path"] == "/odometer" + assert batch.body["responses"][0]["code"] == 200 + assert isinstance(batch.body["responses"][0]["body"]["distance"], float) + assert batch.body["responses"][1]["path"] == "/tires/pressure" + assert isinstance(batch.body["responses"][1]["body"]["frontLeft"], float) + assert isinstance(batch.body["responses"][1]["body"]["frontRight"], float) + assert isinstance(batch.body["responses"][1]["body"]["backLeft"], float) + assert isinstance(batch.body["responses"][1]["body"]["backRight"], float) + + def test_chevy_imperial(chevy_volt_imperial): response = chevy_volt_imperial.odometer() assert response.meta.unit_system == "imperial"