From d2e947ffa3e7dd42a314798214a045870e02870c Mon Sep 17 00:00:00 2001 From: James Woglom Date: Wed, 1 Jul 2026 01:38:39 +0000 Subject: [PATCH] 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. --- tconnectsync/api/tandemsource.py | 45 +++++++++++++++ tests/api/test_tandemsource.py | 96 ++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/tconnectsync/api/tandemsource.py b/tconnectsync/api/tandemsource.py index 18c3b83..1ea050f 100644 --- a/tconnectsync/api/tandemsource.py +++ b/tconnectsync/api/tandemsource.py @@ -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). diff --git a/tests/api/test_tandemsource.py b/tests/api/test_tandemsource.py index 646260d..c93b4b2 100644 --- a/tests/api/test_tandemsource.py +++ b/tests/api/test_tandemsource.py @@ -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()