Add get_pump_logs() BFF events fetch with typed response

Implement TandemSourceApi.get_pump_logs(device_id, ...) for GET
api/reports/bff/pump-logs/{assignmentId}?pumperId&startDate&endDate&eventIds,
the JSON events endpoint that replaces the base64 reportsfacade/pumpevents.
startDate/endDate are sent as {ymd}T00:00:00Z / {ymd}T23:59:59Z per the
captured request. Adds typed PumpLogsResponse/PumpLogEvent TypedDicts,
verified to cover every key across all captured pump-logs responses.

Tests: URL/path/param construction (order-independent parse_qs), default
vs custom vs empty eventIds, empty query dict passthrough, return
passthrough, and None-dates-default-to-today.
This commit is contained in:
James Woglom
2026-07-01 01:38:39 +00:00
parent 27ddf0cb1d
commit d2e947ffa3
2 changed files with 141 additions and 0 deletions
+45
View File
@@ -168,6 +168,33 @@ class PumpMetadata(TypedDict, total=False):
settings: Optional[dict]
class PumpLogEvent(TypedDict):
"""One entry in a PumpLogsResponse (events[] or clockChanges[]) from
GET api/reports/bff/pump-logs/{deviceAssignmentId}. The server pre-decodes
each event, so eventProperties holds already-decoded per-event fields
(values are int/float/list/str keyed by camelCase field name).
pumpDateTime is the pump's local wall-clock time (ISO-8601, no tz);
estimatedDateTime is the same value with a 'Z' suffix. eventCode matches
the numeric event id in EVENT_IDS; sequenceNumber is the old seqNum.
"""
deviceAssignmentId: str
eventCode: int
sequenceGroup: int
sequenceNumber: int
pumpDateTime: str
eventProperties: Dict[str, Any]
estimatedDateTime: str
class PumpLogsResponse(TypedDict):
"""Response of GET api/reports/bff/pump-logs/{deviceAssignmentId}. Replaces
the old base64 reportsfacade/pumpevents payload. clockChanges (eventCodes
13/14) are returned separately and span the device's full history."""
events: List[PumpLogEvent]
clockChanges: List[PumpLogEvent]
class TandemSourceApi:
# Common URLs that are shared between regions
LOGIN_PAGE_URL = 'https://sso.tandemdiabetes.com/'
@@ -617,6 +644,24 @@ class TandemSourceApi:
# the live GET api/reports/bff/pump-logs request. Includes FSL3 ids 477/480/486.
DEFAULT_EVENT_IDS: List[int] = [229,5,28,4,26,99,279,3,16,59,21,55,20,280,64,65,66,61,33,371,171,369,460,172,370,461,372,480,399,256,213,406,477,394,212,404,214,405,486,447,313,60,14,6,90,230,140,12,11,53,13,63,203,307,191]
def get_pump_logs(self, device_id: str, min_date: Optional[str] = None, max_date: Optional[str] = None, event_ids_filter: Optional[List[int]] = DEFAULT_EVENT_IDS) -> PumpLogsResponse:
"""Fetch pre-decoded pump events for a single date window from the BFF
endpoint GET api/reports/bff/pump-logs/{device_id}. device_id is the
UUID assignmentId (PumpMetadata.deviceId). Returns {events, clockChanges}.
The server caps the window at ~4 weeks; callers needing a longer range
must page by date window (see pump_events)."""
minDate = parse_ymd_date(min_date)
maxDate = parse_ymd_date(max_date)
logger.debug(f'get_pump_logs({device_id}, {minDate}, {maxDate})')
query = urllib.parse.urlencode({
'pumperId': self.pumperId,
'startDate': '%sT00:00:00Z' % minDate,
'endDate': '%sT23:59:59Z' % maxDate,
'eventIds': ','.join(map(str, event_ids_filter)) if event_ids_filter else '',
})
return self.get('api/reports/bff/pump-logs/%s?%s' % (device_id, query), {})
"""
Returns raw unparsed string for pump events.
tconnect_device_id is the device id from pump_metadata() (deviceId).
+96
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import datetime
import unittest
import urllib.parse
from unittest.mock import patch
from tconnectsync.api.tandemsource import TandemSourceApi
@@ -153,5 +155,99 @@ class TestDefaultEventIds(unittest.TestCase):
self.assertTrue({477, 480, 486}.issubset(set(ids)))
# Trimmed real-shape pump-logs response (1 event + 1 clockChange).
PUMP_LOGS = {
"events": [
{
"deviceAssignmentId": "1b493210-9336-4901-a329-a352775738c5",
"eventCode": 16,
"sequenceGroup": 1,
"sequenceNumber": 100123,
"pumpDateTime": "2024-01-10T08:15:30",
"eventProperties": {"iob": 1.25, "bg": 112},
"estimatedDateTime": "2024-01-10T08:15:30Z",
}
],
"clockChanges": [
{
"deviceAssignmentId": "1b493210-9336-4901-a329-a352775738c5",
"eventCode": 13,
"sequenceGroup": 0,
"sequenceNumber": 5,
"pumpDateTime": "2024-01-01T00:00:00",
"eventProperties": {"timePrior": 1, "timeAfter": 2, "rawRtcTime": 3},
"estimatedDateTime": "2024-01-01T00:00:00Z",
}
],
}
class TestGetPumpLogs(unittest.TestCase):
maxDiff = None
def _api(self):
api = TandemSourceApi.__new__(TandemSourceApi)
api.pumperId = "PUMPER123"
return api
def _endpoint(self, mock_get):
mock_get.assert_called_once()
# (endpoint, query_dict) positional args
self.assertEqual(mock_get.call_args.args[1], {})
return mock_get.call_args.args[0]
def _qs(self, endpoint, keep_blank=False):
parsed = urllib.parse.urlparse(endpoint)
return parsed.path, urllib.parse.parse_qs(parsed.query, keep_blank_values=keep_blank)
def test_endpoint_path_and_params(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS) as mock_get:
api.get_pump_logs("dev-uuid", min_date="2024-01-01", max_date="2024-01-15")
endpoint = self._endpoint(mock_get)
path, qs = self._qs(endpoint)
self.assertEqual(path, "api/reports/bff/pump-logs/dev-uuid")
self.assertEqual(qs["pumperId"], ["PUMPER123"])
self.assertEqual(qs["startDate"], ["2024-01-01T00:00:00Z"])
self.assertEqual(qs["endDate"], ["2024-01-15T23:59:59Z"])
def test_default_event_ids_comma_joined(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS) as mock_get:
api.get_pump_logs("dev", min_date="2024-01-01", max_date="2024-01-02")
_, qs = self._qs(self._endpoint(mock_get))
self.assertEqual(qs["eventIds"][0].split(","),
[str(i) for i in TandemSourceApi.DEFAULT_EVENT_IDS])
def test_custom_event_ids_comma_joined(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS) as mock_get:
api.get_pump_logs("dev", "2024-01-01", "2024-01-02", event_ids_filter=[16, 5, 28])
_, qs = self._qs(self._endpoint(mock_get))
self.assertEqual(qs["eventIds"], ["16,5,28"])
def test_none_event_ids_empty(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS) as mock_get:
api.get_pump_logs("dev", "2024-01-01", "2024-01-02", event_ids_filter=None)
_, qs = self._qs(self._endpoint(mock_get), keep_blank=True)
self.assertEqual(qs["eventIds"], [""])
def test_return_value_passthrough(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS):
result = api.get_pump_logs("dev", "2024-01-01", "2024-01-15")
self.assertIs(result, PUMP_LOGS)
def test_none_dates_default_to_today(self):
api = self._api()
with patch.object(TandemSourceApi, "get", return_value=PUMP_LOGS) as mock_get:
api.get_pump_logs("dev")
_, qs = self._qs(self._endpoint(mock_get))
today = datetime.datetime.now().strftime('%Y-%m-%d')
self.assertEqual(qs["startDate"], ["%sT00:00:00Z" % today])
self.assertEqual(qs["endDate"], ["%sT23:59:59Z" % today])
if __name__ == "__main__":
unittest.main()