Skip to content

Commit

Permalink
Merge pull request #134 from smartcar/add-service-history-endpoint-2
Browse files Browse the repository at this point in the history
Implement Service History API Endpoint and Associated Types
  • Loading branch information
aytekin-smartcar authored May 10, 2024
2 parents b5669eb + 5d975d9 commit 89b9847
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 3 deletions.
25 changes: 25 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,31 @@ the [exceptions section](https://github.com/smartcar/python-sdk#handling-excepti

---

### `service_history(self, start_date: Optional[str] = None, end_date: Optional[str] = None)`

Returns a list of all the service records performed on the vehicle, filtered by the optional date range. If no dates are specified, records from the last year are returned.

#### Args

| Argument | Type | Description |
| :----------- | :------------ | :------------------------------------------------------------------------------------------ |
| `start_date` | Optional[str] | The start date for the record filter, in 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS.SSSZ' format. |
| `end_date` | Optional[str] | The end date for the record filter, similar format to start_date. |

#### Return

| Value | Type | Description |
| :--------------------- | :--------------------- | :------------------------------------------------------------------------- |
| `ServiceHistory` | typing.NamedTuple | The returned object with a list of service entries. |
| `ServiceHistory.items` | List[ServiceRecord] | List of service records describing maintenance activities. |
| `ServiceHistory.meta` | collections.namedtuple | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) |

#### Raises

`SmartcarException` - See the [exceptions section](https://github.com/smartcar/python-sdk#handling-exceptions) for all possible exceptions.

---

### `attributes(self)`

Returns a single vehicle object, containing identifying information.
Expand Down
34 changes: 33 additions & 1 deletion smartcar/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from collections import namedtuple
from typing import List, NamedTuple, Union
from typing import List, Optional, NamedTuple, Union
import re
import requests.structures as rs
import enum
Expand Down Expand Up @@ -186,6 +186,35 @@ def format_capabilities(capabilities_list: List[dict]) -> List[Capability]:

ChargeLimit = NamedTuple("ChargeLimit", [("limit", float), ("meta", namedtuple)])


class ServiceCost:
total_cost: Optional[float] = None
currency: Optional[str] = None


class ServiceDetail:
type: str
value: Union[None, str, float] = None


class ServiceTask:
task_id: Optional[str] = None
task_description: Optional[str] = None


class ServiceRecord:
odometer_distance: float
service_date: datetime
service_id: Optional[str] = None
service_tasks: List[ServiceTask]
service_details: List[ServiceDetail]
service_cost: ServiceCost


ServiceHistory = NamedTuple(
"ServiceHistory", [("items", List[ServiceRecord]), ("meta", namedtuple)]
)

Battery = NamedTuple(
"Battery",
[("percent_remaining", float), ("range", float), ("meta", namedtuple)],
Expand Down Expand Up @@ -399,6 +428,9 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple:
elif path == "charge/limit":
return ChargeLimit(data["limit"], headers)

elif path == "service/history":
return ServiceHistory(data, headers)

elif path == "permissions":
return Permissions(
data["permissions"],
Expand Down
37 changes: 36 additions & 1 deletion smartcar/vehicle.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections import namedtuple
import json
from typing import Callable, List
from typing import Callable, List, Optional
import smartcar.config as config
import smartcar.helpers as helpers
import smartcar.smartcar
Expand Down Expand Up @@ -184,6 +184,41 @@ def odometer(self) -> types.Odometer:
response = helpers.requester("GET", url, headers=headers)
return types.select_named_tuple(path, response)

def service_history(
self, start_date: Optional[str] = None, end_date: Optional[str] = None
) -> types.ServiceHistory:
"""
Returns a list of all the service records performed on the vehicle,
filtered by the optional date range. If no dates are specified, records from the
last year are returned.
Args:
start_date (Optional[str]): The start date for the record filter, either in 'YYYY-MM-DD' or
'YYYY-MM-DDTHH:MM:SS.SSSZ' format.
end_date (Optional[str]): The end date for the record filter, similar format to start_date.
Returns:
ServiceHistory: NamedTuple("ServiceHistory", [("items", List[ServiceRecord]), ("meta", namedtuple)])
Raises:
SmartcarException: If an error occurs during the API call.
See Also:
Smartcar API Documentation for Vehicle Service History:
https://smartcar.com/docs/api#get-vehicle-service-history
"""
path = "service/history"
url = self._format_url(path)
headers = self._get_headers()
params = {}
if start_date:
params["startDate"] = start_date
if end_date:
params["endDate"] = end_date

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

def location(self) -> types.Location:
"""
GET Vehicle.location
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ def access_ford(client):
client = sc.AuthClient(*ah.get_auth_client_params())
code = ah.run_auth_flow(
client.get_auth_url(
["required:read_charge", "required:control_charge", "control_navigation"]
[
"required:read_charge",
"required:control_charge",
"control_navigation",
"read_service_history",
]
),
"FORD",
)
Expand Down
23 changes: 23 additions & 0 deletions tests/e2e/test_vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ def test_send_destination(ford_car):
assert response._fields == ("status", "message", "meta")


def test_service_history(ford_car):
response = ford_car.service_history("2023-05-20", "2024-02-10")
assert isinstance(
response, types.ServiceHistory
), "Response should be an instance of ServiceHistory"
assert hasattr(response, "_fields"), "Response should have '_fields' attribute"
assert "items" in response._fields, "'items' should be a key in the response fields"

# Check the 'items' array.
assert isinstance(response.items, list), "Items should be a list"

# Iterate over each item in the 'items' list to perform further validations.
for item in response.items:
assert isinstance(
item["odometerDistance"], (float, int)
), "Odometer distance should be a numeric type (float or int)"
assert (
item["odometerDistance"] > 0
), "Odometer distance should be greater than zero"

assert response._fields == ("items", "meta")


def test_batch_success(chevy_volt):
batch = chevy_volt.batch(
[
Expand Down

0 comments on commit 89b9847

Please sign in to comment.