Skip to content

Commit

Permalink
Merge pull request #108 from smartcar/compat-v2
Browse files Browse the repository at this point in the history
feat(smartcar): adds API v2.0 compatibility functionality
  • Loading branch information
naomiperez authored Mar 24, 2022
2 parents a81059f + 827c56f commit 4997691
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 28 deletions.
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dist: xenial
dist: focal

services:
- xvfb
Expand All @@ -13,6 +13,7 @@ python:
- '3.7'
- '3.8'
- '3.9'
- '3.10'

if: tag IS blank # do not build tags

Expand All @@ -35,14 +36,14 @@ script:
jobs:
include:
- stage: publish
python: '3.8'
python: '3.9'
services: []
addons:
firefox: 'skip'
apt: []
install:
- nvm install 14
- npm install semantic-release@15.x.x @google/[email protected]
- nvm install 16
- npm install semantic-release@19.x.x @google/[email protected]
script:
- npx semantic-release
deploy:
Expand Down
16 changes: 11 additions & 5 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,11 +709,17 @@ A compatible vehicle is a vehicle that:

#### Return

| Value | Type | Description |
|:---------------------------|:-----------------------|:--------------------------------------------------------------------------------------|
| `Compatibility` | typing.NamedTuple | The returned object with vehicle's compatibility with the permissions (scope) checked |
| `Compatibility.compatible` | Boolean | Whether the vehicle is compatible with the permissions |
| `Compatibility.meta` | collections.namedtuple | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) |
| Value | Type | Availability | Description |
|:------------------------------------------|:------------------------|:----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
| `Compatibility` | typing.NamedTuple | **API v1.0 and v2.0** |The returned object with vehicle's compatibility with the permissions (scope) checked |
| `Compatibility.compatible` | Boolean | **API v1.0 and v2.0** | Whether the vehicle is compatible with the permissions |
| `Compatibility.reason` | String or None | **API v2.0 only** | One of the following string values if compatible is false, null otherwise: "VEHICLE_NOT_COMPATIBLE", "MAKE_NOT_COMPATIBLE" |
| `Compatibility.capabilities` | List | **API v2.0 only** | A list containing the set of endpoints that the provided scope value can provide authorization for. This list will be empty if compatible is false. |
| `Compatibility.capabilities[].permission` | String | **API v2.0 only** | One of the permissions provided in the scope parameter. |
| `Compatibility.capabilities[].endpoint` | String | **API v2.0 only** | One of the endpoints that the permission authorizes access to. |
| `Compatibility.capabilities[].capable` | Boolean | **API v2.0 only** | True if the vehicle is likely capable of this feature, False otherwise. |
| `Compatibility.capabilities[].reason` | String or None | **API v2.0 only** | One of the following string values if compatible is false, null otherwise: "VEHICLE_NOT_COMPATIBLE", "SMARTCAR_NOT_CAPABLE" |
| `Compatibility.meta` | collections.namedtuple | **API v1.0 and v2.0** | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) |

#### Raises

Expand Down
47 changes: 33 additions & 14 deletions smartcar/smartcar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import re
from datetime import datetime
from typing import List
from typing import List, Union

import smartcar.config as config
import smartcar.helpers as helpers
Expand All @@ -25,7 +25,7 @@ def set_api_version(version: str) -> None:
API_VERSION = version
else:
raise ValueError(
fr"Version '{version}' must match regex '\d+\.\d+' . e.g. '2.0', '1.0'"
rf"Version '{version}' must match regex '\d+\.\d+' . e.g. '2.0', '1.0'"
)


Expand Down Expand Up @@ -91,7 +91,7 @@ def get_vehicles(access_token: str, paging: dict = None) -> types.Vehicles:

def get_compatibility(
vin: str, scope: List[str], country: str = "US", options: dict = None
) -> types.Compatibility:
) -> Union[types.CompatibilityV1, types.CompatibilityV2]:
"""
Verify if a vehicle (vin) is eligible to use Smartcar. Use to confirm whether
specific vehicle is compatible with the permissions provided.
Expand All @@ -102,29 +102,40 @@ def get_compatibility(
3. Is compatible with the required permissions (scope) that your app is requesting
access to
Note: The `test_mode` and `test_mode_compatibility_level` options arguments are only valid for Smartcar API v1.0.
Args:
vin (str)
scope (List[str]): List of scopes (permissions) -> to check if vehicle is compatible
country (str, optional)
options (dictionary): Can contain client_id, client_secret, and flags.
options (dict): Can contain client_id, client_secret, and flags.
client_id (str, optional)
client_secret (str, optional)
version (str): Version of API you want to use
flags (dictionary - {str: bool}): An optional list of feature flags
flags (dict - {str: bool}): An optional list of feature flags
test_mode (bool): Indicates whether the API should be invoked in test mode (as opposed to live mode)
test_mode_compatibility_level (str): This parameter is required when the API is invoked in test mode.
Possible values with details are documented in our Integration Guide.
Returns:
Compatibility: NamedTuple("Compatibility", [("compatible", bool), ("meta", namedtuple)])
CompatibilityV1: NamedTuple("Compatibility", [("compatible", bool), ("meta", namedtuple)])
OR
CompatibilityV2: NamedTuple("Compatibility",
[
("compatible", bool),
("reason", Union[str, None]),
("capabilities", List[dict]),
("meta", namedtuple),
],
)
"""
client_id = os.environ.get("SMARTCAR_CLIENT_ID")
client_secret = os.environ.get("SMARTCAR_CLIENT_SECRET")
Expand All @@ -148,13 +159,15 @@ def get_compatibility(
if options.get("version"):
api_version = options["version"]

if options.get("test_mode_compatibility_level"):
params["mode"] = "test"
params["test_mode_compatibility_level"] = options[
"test_mode_compatibility_level"
]
elif options.get("test_mode"):
params["mode"] = "test"
if api_version == "1.0":

if options.get("test_mode_compatibility_level"):
params["mode"] = "test"
params["test_mode_compatibility_level"] = options[
"test_mode_compatibility_level"
]
elif options.get("test_mode"):
params["mode"] = "test"

# Ensuring client_id and client_secret are present
if client_id is None or client_secret is None:
Expand All @@ -176,7 +189,13 @@ def get_compatibility(
headers = {"Authorization": f"Basic {base64_id_secret}"}

response = helpers.requester("GET", url, headers=headers, params=params)
return types.select_named_tuple("compatibility", response)

if api_version == "1.0":
return types.select_named_tuple("compatibility_v1", response)
elif api_version == "2.0":
return types.select_named_tuple("compatibility_v2", response)
else:
raise Exception("Please use a valid API version (e.g. '1.0' or '2.0')")


def is_expired(expiration: datetime) -> bool:
Expand Down
58 changes: 53 additions & 5 deletions smartcar/types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import datetime
from collections import namedtuple
from typing import List, NamedTuple
from typing import List, NamedTuple, Union
import re
import requests.structures as rs

import enum

# Return types for Smartcar API.
#
Expand Down Expand Up @@ -126,10 +126,51 @@ def make_access_object(access: dict) -> Access:
[("vehicles", List[str]), ("paging", Paging), ("meta", namedtuple)],
)

Compatibility = NamedTuple(

class Reason(enum.Enum):
VEHICLE_NOT_COMPATIBLE = "VEHICLE_NOT_COMPATIBLE"
MAKE_NOT_COMPATIBLE = "MAKE_NOT_COMPATIBLE"


Capability = NamedTuple(
"Capability",
[
("permission", str),
("endpoint", str),
("capable", bool),
("reason", Union[Reason, None]),
],
)


def format_capabilities(capabilities_list: List[dict]) -> List[Capability]:
typed_capabilities_map = map(
lambda capability: Capability(
capability["permission"],
capability["endpoint"],
capability["capable"],
capability["reason"],
),
capabilities_list,
) # Returns iterable map
typed_capabilities_list = list(typed_capabilities_map)
return typed_capabilities_list


CompatibilityV1 = NamedTuple(
"Compatibility", [("compatible", bool), ("meta", namedtuple)]
)

CompatibilityV2 = NamedTuple(
"Compatibility",
[
("compatible", bool),
("reason", Union[Reason, None]),
("capabilities", List[Capability]),
("meta", namedtuple),
],
)

# ===========================================
# vehicle.py
# ===========================================
Expand Down Expand Up @@ -241,6 +282,7 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple:
NamedTuple: Appropriate to the path.
"""

if type(response_or_dict) == dict:
headers_dict = rs.CaseInsensitiveDict(response_or_dict["headers"])
headers = build_meta(headers_dict)
Expand All @@ -260,8 +302,14 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple:
headers,
)

elif path == "compatibility":
return Compatibility(data["compatible"], headers)
elif path == "compatibility_v1":
return CompatibilityV1(data["compatible"], headers)

elif path == "compatibility_v2":
typed_capabilities = format_capabilities(data["capabilities"])
return CompatibilityV2(
data["compatible"], data["reason"], typed_capabilities, headers
)

# vehicle.py
elif path == "vin":
Expand Down
25 changes: 25 additions & 0 deletions tests/e2e/test_smartcar.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ def test_get_compatibility_in_test_mode_but_no_level():
)


def test_get_compatibility_v2():

compatibility = smartcar.get_compatibility(
"0SCGMCT0386A85356",
scope=["read_odometer", "read_fuel"],
country="US",
options={
"client_id": ah.CLIENT_ID,
"client_secret": ah.CLIENT_SECRET,
"version": "2.0",
},
)

assert compatibility.compatible == True
assert compatibility.reason == None
assert compatibility.capabilities[0].permission == "read_odometer"
assert compatibility.capabilities[0].endpoint == "/odometer"
assert compatibility.capabilities[0].capable == True
assert compatibility.capabilities[0].reason == None
assert compatibility.capabilities[1].permission == "read_fuel"
assert compatibility.capabilities[1].endpoint == "/fuel"
assert compatibility.capabilities[1].capable == False
assert compatibility.capabilities[1].reason == "VEHICLE_NOT_CAPABLE"


def test_get_compatibility_with_non_test_mode_vin():
res = get_compatibility(
"WAUAFAFL1GN014882",
Expand Down

0 comments on commit 4997691

Please sign in to comment.