diff --git a/src/textbee/__init__.py b/src/textbee/__init__.py index 84f5490..f6d5d65 100644 --- a/src/textbee/__init__.py +++ b/src/textbee/__init__.py @@ -17,6 +17,8 @@ Direction, Message, MessageDevice, + MessageIssueEntry, + MessageIssueSource, MessageList, MessageListMeta, MessageStatus, @@ -39,6 +41,8 @@ "Direction", "Message", "MessageDevice", + "MessageIssueEntry", + "MessageIssueSource", "MessageList", "MessageListMeta", "MessageStatus", diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 013845a..23de68b 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -121,6 +121,32 @@ class MessageDevice(_MessageDeviceRequired, total=False): enabled: bool +MessageIssueSource = Literal["push", "device", "server"] +"""Where a failure was reported from.""" + + +class _MessageIssueEntryRequired(TypedDict): + code: str + at: str + """When the failure happened. ISO date.""" + + source: MessageIssueSource + + +class MessageIssueEntry(_MessageIssueEntryRequired, total=False): + """One failure a message had. Kept after the message later succeeds.""" + + message: str + backfilled: bool + """Set on entries rebuilt from old message fields.""" + + deliveryReport: bool + """True for a delivery report that did not confirm delivery. + + The message was sent; this is not a send failure. + """ + + class _MessageRequired(TypedDict): _id: str message: str @@ -142,9 +168,28 @@ class _MessageFields(_MessageRequired, total=False): sentAt: str deliveredAt: str failedAt: str + """When the message failed. Present only while status is ``failed``.""" + receivedAt: str errorCode: str + """Current failure code. Present only while status is ``failed``. + + Test accounts can keep ``FCM_SEND_SKIPPED``. + """ + errorMessage: str + """Current failure text. + + Present only while status is ``failed``, or as a timeout note on ``unknown``. + """ + + issueHistory: List[MessageIssueEntry] + """Every failure the message had, plus unconfirmed delivery reports. + + Delivery reports carry ``deliveryReport: true`` and are not failures. + Oldest first. At most 15: the first 5 and the newest 10. + """ + simSubscriptionId: int smsBatch: str createdAt: str diff --git a/src/textbee/_version.py b/src/textbee/_version.py index 485f44a..d3ec452 100644 --- a/src/textbee/_version.py +++ b/src/textbee/_version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.2.0" diff --git a/tests/test_client.py b/tests/test_client.py index 0aa7428..2f48edf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -329,6 +329,34 @@ def test_returns_get_messages_as_data_and_meta_because_it_has_no_outer_wrapper( assert query["limit"] == "50" assert "type" not in query + def test_returns_error_history_on_a_message_that_failed_and_later_succeeded( + self, http: FakeUrlopen + ) -> None: + entry = { + "code": "FCM_SEND_FAILED", + "message": "push failed", + "at": "2026-10-01T10:00:00.000Z", + "source": "push", + "backfilled": True, + } + page = { + "data": [ + { + "_id": "sms-2", + "message": "hi", + "type": "SENT", + "status": "sent", + "issueHistory": [entry], + } + ], + "meta": {}, + } + http.respond(200, page) + + result = client().get_messages() + + assert result["data"][0]["issueHistory"] == [entry] + def test_sends_no_query_at_all_for_a_bare_get_messages(self, http: FakeUrlopen) -> None: http.respond(200, {"data": [], "meta": {}})