Skip to content

Commit 89b9847

Browse files
Merge pull request #134 from smartcar/add-service-history-endpoint-2
Implement Service History API Endpoint and Associated Types
2 parents b5669eb + 5d975d9 commit 89b9847

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

REFERENCE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,31 @@ the [exceptions section](https://github.com/smartcar/python-sdk#handling-excepti
358358

359359
---
360360

361+
### `service_history(self, start_date: Optional[str] = None, end_date: Optional[str] = None)`
362+
363+
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.
364+
365+
#### Args
366+
367+
| Argument | Type | Description |
368+
| :----------- | :------------ | :------------------------------------------------------------------------------------------ |
369+
| `start_date` | Optional[str] | The start date for the record filter, in 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS.SSSZ' format. |
370+
| `end_date` | Optional[str] | The end date for the record filter, similar format to start_date. |
371+
372+
#### Return
373+
374+
| Value | Type | Description |
375+
| :--------------------- | :--------------------- | :------------------------------------------------------------------------- |
376+
| `ServiceHistory` | typing.NamedTuple | The returned object with a list of service entries. |
377+
| `ServiceHistory.items` | List[ServiceRecord] | List of service records describing maintenance activities. |
378+
| `ServiceHistory.meta` | collections.namedtuple | Smartcar response headers (`request_id`, `data_age`, and/or `unit_system`) |
379+
380+
#### Raises
381+
382+
`SmartcarException` - See the [exceptions section](https://github.com/smartcar/python-sdk#handling-exceptions) for all possible exceptions.
383+
384+
---
385+
361386
### `attributes(self)`
362387

363388
Returns a single vehicle object, containing identifying information.

smartcar/types.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
22
from collections import namedtuple
3-
from typing import List, NamedTuple, Union
3+
from typing import List, Optional, NamedTuple, Union
44
import re
55
import requests.structures as rs
66
import enum
@@ -186,6 +186,35 @@ def format_capabilities(capabilities_list: List[dict]) -> List[Capability]:
186186

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

189+
190+
class ServiceCost:
191+
total_cost: Optional[float] = None
192+
currency: Optional[str] = None
193+
194+
195+
class ServiceDetail:
196+
type: str
197+
value: Union[None, str, float] = None
198+
199+
200+
class ServiceTask:
201+
task_id: Optional[str] = None
202+
task_description: Optional[str] = None
203+
204+
205+
class ServiceRecord:
206+
odometer_distance: float
207+
service_date: datetime
208+
service_id: Optional[str] = None
209+
service_tasks: List[ServiceTask]
210+
service_details: List[ServiceDetail]
211+
service_cost: ServiceCost
212+
213+
214+
ServiceHistory = NamedTuple(
215+
"ServiceHistory", [("items", List[ServiceRecord]), ("meta", namedtuple)]
216+
)
217+
189218
Battery = NamedTuple(
190219
"Battery",
191220
[("percent_remaining", float), ("range", float), ("meta", namedtuple)],
@@ -399,6 +428,9 @@ def select_named_tuple(path: str, response_or_dict) -> NamedTuple:
399428
elif path == "charge/limit":
400429
return ChargeLimit(data["limit"], headers)
401430

431+
elif path == "service/history":
432+
return ServiceHistory(data, headers)
433+
402434
elif path == "permissions":
403435
return Permissions(
404436
data["permissions"],

smartcar/vehicle.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import namedtuple
22
import json
3-
from typing import Callable, List
3+
from typing import Callable, List, Optional
44
import smartcar.config as config
55
import smartcar.helpers as helpers
66
import smartcar.smartcar
@@ -184,6 +184,41 @@ def odometer(self) -> types.Odometer:
184184
response = helpers.requester("GET", url, headers=headers)
185185
return types.select_named_tuple(path, response)
186186

187+
def service_history(
188+
self, start_date: Optional[str] = None, end_date: Optional[str] = None
189+
) -> types.ServiceHistory:
190+
"""
191+
Returns a list of all the service records performed on the vehicle,
192+
filtered by the optional date range. If no dates are specified, records from the
193+
last year are returned.
194+
195+
Args:
196+
start_date (Optional[str]): The start date for the record filter, either in 'YYYY-MM-DD' or
197+
'YYYY-MM-DDTHH:MM:SS.SSSZ' format.
198+
end_date (Optional[str]): The end date for the record filter, similar format to start_date.
199+
200+
Returns:
201+
ServiceHistory: NamedTuple("ServiceHistory", [("items", List[ServiceRecord]), ("meta", namedtuple)])
202+
203+
Raises:
204+
SmartcarException: If an error occurs during the API call.
205+
206+
See Also:
207+
Smartcar API Documentation for Vehicle Service History:
208+
https://smartcar.com/docs/api#get-vehicle-service-history
209+
"""
210+
path = "service/history"
211+
url = self._format_url(path)
212+
headers = self._get_headers()
213+
params = {}
214+
if start_date:
215+
params["startDate"] = start_date
216+
if end_date:
217+
params["endDate"] = end_date
218+
219+
response = helpers.requester("GET", url, headers=headers, params=params)
220+
return types.select_named_tuple(path, response)
221+
187222
def location(self) -> types.Location:
188223
"""
189224
GET Vehicle.location

tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ def access_ford(client):
158158
client = sc.AuthClient(*ah.get_auth_client_params())
159159
code = ah.run_auth_flow(
160160
client.get_auth_url(
161-
["required:read_charge", "required:control_charge", "control_navigation"]
161+
[
162+
"required:read_charge",
163+
"required:control_charge",
164+
"control_navigation",
165+
"read_service_history",
166+
]
162167
),
163168
"FORD",
164169
)

tests/e2e/test_vehicle.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,29 @@ def test_send_destination(ford_car):
147147
assert response._fields == ("status", "message", "meta")
148148

149149

150+
def test_service_history(ford_car):
151+
response = ford_car.service_history("2023-05-20", "2024-02-10")
152+
assert isinstance(
153+
response, types.ServiceHistory
154+
), "Response should be an instance of ServiceHistory"
155+
assert hasattr(response, "_fields"), "Response should have '_fields' attribute"
156+
assert "items" in response._fields, "'items' should be a key in the response fields"
157+
158+
# Check the 'items' array.
159+
assert isinstance(response.items, list), "Items should be a list"
160+
161+
# Iterate over each item in the 'items' list to perform further validations.
162+
for item in response.items:
163+
assert isinstance(
164+
item["odometerDistance"], (float, int)
165+
), "Odometer distance should be a numeric type (float or int)"
166+
assert (
167+
item["odometerDistance"] > 0
168+
), "Odometer distance should be greater than zero"
169+
170+
assert response._fields == ("items", "meta")
171+
172+
150173
def test_batch_success(chevy_volt):
151174
batch = chevy_volt.batch(
152175
[

0 commit comments

Comments
 (0)