Skip to content

Commit

Permalink
feat: vehicle management functions (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbry authored Aug 17, 2023
1 parent 595770c commit 6cd99d6
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .releaserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
plugins: [
'@semantic-release/commit-analyzer',
[
'@google/semantic-release-replace-plugin',
'semantic-release-replace-plugin',
{
replacements: [
{
Expand Down
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ jobs:
firefox: 'skip'
apt: []
install:
- nvm install 16
- npm install semantic-release@19.x.x @google/semantic-release-replace-plugin@1.x.x
- nvm install 18
- npm install semantic-release@20.x.x semantic-release-replace-plugin@1.x.x
script:
- npx semantic-release
deploy:
Expand Down
95 changes: 75 additions & 20 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ A client for accessing the Smartcar API

#### Arguments:

| Parameter | Type | Required | Description |
|:----------------|:--------|:---------------|:----------------------------------------------------------------------------------------------------------------------------------|
| `client_id` | String | **Optional**\* | Application clientId obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `client_secret` | String | **Optional**\* | Application clientSecret obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `redirect_uri` | String | **Optional**\* | RedirectURI set in [application settings](https://dashboard.smartcar.com/apps). Given URL must match URL in application settings. |
| `mode` | String | **Optional** | Determine what mode Smartcar Connect should be launched in. Should be one of test, live or simulated. |
| Parameter | Type | Required | Description |
|:----------------|:-------|:---------------|:----------------------------------------------------------------------------------------------------------------------------------|
| `client_id` | String | **Optional**\* | Application clientId obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `client_secret` | String | **Optional**\* | Application clientSecret obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `redirect_uri` | String | **Optional**\* | RedirectURI set in [application settings](https://dashboard.smartcar.com/apps). Given URL must match URL in application settings. |
| `mode` | String | **Optional** | Determine what mode Smartcar Connect should be launched in. Should be one of test, live or simulated. |

##### \***Environment Variables VS Passing Arguments:**

Expand Down Expand Up @@ -362,8 +362,8 @@ Returns a single vehicle object, containing identifying information.

#### Return

| Value | Type | Description |
|:-------------|:-----------------------|:---------------------------------------------------------------------------|
| Value | Type | Description |
|:-------------------|:-----------------------|:---------------------------------------------------------------------------|
| `Attributes` | typing.NamedTuple | The returned object with vehicle's info |
| `Attributes.id` | String | A vehicle ID (UUID v4). |
| `Attributes.make` | String | The manufacturer of the vehicle. |
Expand Down Expand Up @@ -609,6 +609,7 @@ Sets the version of Smartcar API to use
| None |

---

### `smartcar.get_vehicles(access_token, limit=10, offset=0)`

Get a list of the user's vehicle ids
Expand Down Expand Up @@ -690,17 +691,17 @@ A compatible vehicle is a vehicle that:

#### Return

| 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`) |
| 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 All @@ -713,7 +714,8 @@ the [exceptions section](https://github.com/smartcar/python-sdk#handling-excepti

### `hash_challenge(amt, challenge)`

Take the random string received in the challenge request and use your Application Management Token (amt) to create an SHA-256 based HMAC. Return the hex-encoding of the resulting hash
Take the random string received in the challenge request and use your Application Management Token (amt) to create an
SHA-256 based HMAC. Return the hex-encoding of the resulting hash

#### Arguments

Expand Down Expand Up @@ -747,3 +749,56 @@ Verify webhook payload against AMT and signature.
| Type | Description |
|:--------|:---------------------------------------|
| Boolean | Matching signature and response header |

# Vehicle Management Static Methods

### `get_connections(amt, filter, paging)`

Get a paged list of all the vehicles that are connected to the application associated with the management API token used
sorted in descending order by connection date.

#### Arguments

| Parameter | Type | Required | Description |
|:--------------------|:-----------|:-------------|:-------------------------------------------------------------------------|
| `amt` | String | **Required** | Application Management Token (found in Smartcar dashboard). |
| `filter` | Dictionary | **Optional** | The randomly generated string received after sending a challenge request |
| `filter.user_id` | String | **Optional** | The randomly generated string received after sending a challenge request |
| `filter.vehicle_id` | String | **Optional** | The randomly generated string received after sending a challenge request |
| `paging` | String | **Optional** | The randomly generated string received after sending a challenge request |
| `paging.cursor` | Integer | **Optional** | The randomly generated string received after sending a challenge request |
| `paging.limit` | String | **Optional** | The randomly generated string received after sending a challenge request |

#### Return

| Value | Type | Availability | Description |
|:--------------------------------------------|:------------------|:----------------------|:------------|
| `GetConnections` | typing.NamedTuple | **API v1.0 and v2.0** | |
| `GetConnections.connections` | Boolean | **API v1.0 and v2.0** | |
| `GetConnections.connections[].user_id` | Boolean | **API v1.0 and v2.0** | |
| `GetConnections.connections[].vehicle_id` | Boolean | **API v1.0 and v2.0** | |
| `GetConnections.connections[].connected_at` | Boolean | **API v1.0 and v2.0** | |
| `Compatibility.paging` | String or None | **API v1.0 and v2.0** | |
| `Compatibility.paging.cursor` | List | **API v1.0 and v2.0** | |

### `delete_connections(amt, filter)`

Delete all the connections by vehicle or user ID and returns a list of all connections that were deleted.

#### Arguments

| Parameter | Type | Required | Description |
|:--------------------|:-----------|:-------------|:-------------------------------------------------------------------------|
| `amt` | String | **Required** | Application Management Token (found in Smartcar dashboard). |
| `filter` | Dictionary | **Optional** | The randomly generated string received after sending a challenge request |
| `filter.user_id` | String | **Optional** | The randomly generated string received after sending a challenge request |
| `filter.vehicle_id` | String | **Optional** | The randomly generated string received after sending a challenge request |

#### Return

| Value | Type | Availability | Description |
|:------------------------------------------|:------------------|:----------------------|:------------|
| `GetConnections` | typing.NamedTuple | **API v1.0 and v2.0** | |
| `GetConnections.connections` | Boolean | **API v1.0 and v2.0** | |
| `GetConnections.connections[].user_id` | Boolean | **API v1.0 and v2.0** | |
| `GetConnections.connections[].vehicle_id` | Boolean | **API v1.0 and v2.0** | |
2 changes: 2 additions & 0 deletions smartcar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
get_api_version,
set_api_version,
verify_payload,
get_connections,
delete_connections,
)

from smartcar.vehicle import Vehicle
3 changes: 3 additions & 0 deletions smartcar/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
"SMARTCAR_AUTH_ORIGIN", "https://auth.smartcar.com/oauth/token"
)
CONNECT_URL = os.environ.get("SMARTCAR_CONNECT_ORIGIN", "https://connect.smartcar.com")
MANAGEMENT_API_URL = os.environ.get(
"SMARTCAR_MANAGEMENT_API_ORIGIN", "https://management.smartcar.com"
)
112 changes: 111 additions & 1 deletion smartcar/smartcar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import hashlib
import os
import re
from datetime import datetime
from typing import List, Union
from warnings import warn

Expand Down Expand Up @@ -253,3 +252,114 @@ def verify_payload(amt: str, signature: str, body: str) -> bool:
Boolean
"""
return hash_challenge(amt, body) == signature


# ===========================================
# Management
# ===========================================
def get_management_token(amt: str, username: str = "default") -> str:
secret = f"{username}:{amt}"
encoded_secret = secret.encode("ascii")
base64_bytes = base64.b64encode(encoded_secret)
return base64_bytes.decode("ascii")


def get_connections(
amt: str, filter: dict = {}, paging: dict = {}
) -> types.GetConnections:
"""
Returns a paged list of all the vehicles that are connected to the application
associated with the management API token used sorted in descending order by connection date.
Args:
amt (str): Application Management Token from Smartcar Dashboard
filter (dict, optional)
vehicle_id (str, optional)
user_id (str, optional)
paging (dict, optional)
limit (int, optional)
cursor_id (str, optional)
Returns:
GetConnections = NamedTuple("GetConnections", [
("connections", List[Connection]),
("paging", PagingCursor),
("meta", namedtuple)
],
)
"""
params = {}
if filter.get("user_id"):
params["user_id"] = filter.get("user_id")
if filter.get("vehicle_id"):
params["vehicle_id"] = filter.get("vehicle_id")
if paging.get("cursor"):
params["cursor"] = filter.get("cursor")
if paging.get("limit"):
params["limit"] = filter.get("limit")

url = f"{config.MANAGEMENT_API_URL}/v{get_api_version()}/management/connections/"
headers = {"Authorization": f"Basic {get_management_token(amt)}"}
response = helpers.requester("GET", url, headers=headers, params=params)
data = response.json()
connections = [
types.Connection(c.get("vehicleId"), c.get("userId"), c.get("connectedAt"))
for c in data["connections"]
]

response_paging = data.get("paging", {})
response_paging = types.PagingCursor(response_paging.get("cursor"))

return types.GetConnections(
connections,
response_paging,
types.build_meta(response.headers),
)


def delete_connections(amt: str, filter: dict = {}) -> types.DeleteConnections:
"""
Deletes all the connections by vehicle or user ID and returns a list
of all connections that were deleted.
Args:
amt (str): Application Management Token from Smartcar Dashboard
filter (dict, optional): Can contain EITHER vehicle_id OR user_id
vehicle_id (str, optional)
user_id (str, optional)
Returns:
DeleteConnections = NamedTuple("DeleteConnections", [
("connections", List[Connection]),
("meta", namedtuple)
],
)
"""
user_id = filter.get("user_id")
vehicle_id = filter.get("vehicle_id")
if user_id and vehicle_id:
raise Exception("Filter can contain EITHER user_id OR vehicle_id, not both")

params = {}
if user_id:
params["user_id"] = user_id
elif vehicle_id:
params["vehicle_id"] = vehicle_id

url = f"{config.MANAGEMENT_API_URL}/v{get_api_version()}/management/connections/"
headers = {"Authorization": f"Basic {get_management_token(amt)}"}
response = helpers.requester("DELETE", url, headers=headers, params=params)
data = response.json()
connections = [
types.Connection(c.get("vehicleId"), c.get("userId"), c.get("connectedAt"))
for c in data["connections"]
]

return types.DeleteConnections(
connections,
types.build_meta(response.headers),
)
17 changes: 17 additions & 0 deletions smartcar/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def make_access_object(access: dict) -> Access:

Paging = NamedTuple("Paging", [("count", int), ("offset", int)])

PagingCursor = NamedTuple("PagingCursor", [("cursor", Union[int, None])])

User = NamedTuple("User", [("id", str), ("meta", namedtuple)])

Vehicles = NamedTuple(
Expand Down Expand Up @@ -247,6 +249,21 @@ def format_capabilities(capabilities_list: List[dict]) -> List[Capability]:
[("webhook_id", str), ("vehicle_id", str), ("meta", namedtuple)],
)

Connection = NamedTuple(
"Connection",
[("vehicle_id", str), ("user_id", str), ("connected_at", Union[str, None])],
)

GetConnections = NamedTuple(
"GetConnections",
[("connections", List[Connection]), ("paging", PagingCursor), ("meta", namedtuple)],
)

DeleteConnections = NamedTuple(
"DeleteConnections",
[("connections", List[Connection]), ("meta", namedtuple)],
)

Response = NamedTuple("Response", [("body", dict), ("meta", namedtuple)])

# ===========================================
Expand Down
Loading

0 comments on commit 6cd99d6

Please sign in to comment.