From 7883c1ae30aff6de54266ad0fb038762b109fb86 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 15:47:38 +0300 Subject: [PATCH 1/9] feat: add errorHistory to the Message type Co-Authored-By: Claude Opus 5.5 --- src/textbee/__init__.py | 4 ++++ src/textbee/_types.py | 29 +++++++++++++++++++++++++++++ tests/test_client.py | 23 +++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/textbee/__init__.py b/src/textbee/__init__.py index 84f5490..9767844 100644 --- a/src/textbee/__init__.py +++ b/src/textbee/__init__.py @@ -17,6 +17,8 @@ Direction, Message, MessageDevice, + MessageErrorEntry, + MessageErrorSource, MessageList, MessageListMeta, MessageStatus, @@ -39,6 +41,8 @@ "Direction", "Message", "MessageDevice", + "MessageErrorEntry", + "MessageErrorSource", "MessageList", "MessageListMeta", "MessageStatus", diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 013845a..f48a2db 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -121,6 +121,26 @@ class MessageDevice(_MessageDeviceRequired, total=False): enabled: bool +MessageErrorSource = Literal["push", "device", "server"] +"""Where a failure was reported from.""" + + +class _MessageErrorEntryRequired(TypedDict): + code: str + at: str + """When the failure happened. ISO date.""" + + source: MessageErrorSource + + +class MessageErrorEntry(_MessageErrorEntryRequired, 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.""" + + class _MessageRequired(TypedDict): _id: str message: str @@ -142,9 +162,18 @@ 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``.""" + errorMessage: str + """Current failure text. Present only while status is ``failed``, or as a timeout note on ``unknown``.""" + + errorHistory: List[MessageErrorEntry] + """Every failure the message had, oldest first, at most 5.""" + simSubscriptionId: int smsBatch: str createdAt: str diff --git a/tests/test_client.py b/tests/test_client.py index 0aa7428..9c2ff38 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -329,6 +329,29 @@ 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", "errorHistory": [entry]} + ], + "meta": {}, + } + http.respond(200, page) + + result = client().get_messages() + + assert result["data"][0]["errorHistory"] == [entry] + assert "errorCode" not in result["data"][0] + def test_sends_no_query_at_all_for_a_bare_get_messages(self, http: FakeUrlopen) -> None: http.respond(200, {"data": [], "meta": {}}) From 6b72cf6c0ecf5b017cda8bdca554ad2961e979fa Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 15:51:26 +0300 Subject: [PATCH 2/9] docs: errorHistory keeps up to 10 entries Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index f48a2db..a924c72 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -172,7 +172,7 @@ class _MessageFields(_MessageRequired, total=False): """Current failure text. Present only while status is ``failed``, or as a timeout note on ``unknown``.""" errorHistory: List[MessageErrorEntry] - """Every failure the message had, oldest first, at most 5.""" + """Every failure the message had, oldest first, at most 10.""" simSubscriptionId: int smsBatch: str From fe218a73b28ed6e3b01d3b248855fdce6c12cf07 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:15:51 +0300 Subject: [PATCH 3/9] refactor: drop vacuous assert, note FCM_SEND_SKIPPED in errorCode doc Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 2 +- tests/test_client.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index a924c72..06d2152 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -166,7 +166,7 @@ class _MessageFields(_MessageRequired, total=False): receivedAt: str errorCode: str - """Current failure code. Present only while status is ``failed``.""" + """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``.""" diff --git a/tests/test_client.py b/tests/test_client.py index 9c2ff38..69cbcbe 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -350,7 +350,6 @@ def test_returns_error_history_on_a_message_that_failed_and_later_succeeded( result = client().get_messages() assert result["data"][0]["errorHistory"] == [entry] - assert "errorCode" not in result["data"][0] def test_sends_no_query_at_all_for_a_bare_get_messages(self, http: FakeUrlopen) -> None: http.respond(200, {"data": [], "meta": {}}) From 59d518e5ebb6b3319a2094f07cf6fc444487b863 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:32:55 +0300 Subject: [PATCH 4/9] docs: errorHistory keeps the first 5 and the newest 10 Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 06d2152..9d25752 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -172,7 +172,7 @@ class _MessageFields(_MessageRequired, total=False): """Current failure text. Present only while status is ``failed``, or as a timeout note on ``unknown``.""" errorHistory: List[MessageErrorEntry] - """Every failure the message had, oldest first, at most 10.""" + """Every failure the message had, Oldest first. At most 15: the first 5 and the newest 10.""" simSubscriptionId: int smsBatch: str From 5562739c5edcc4b30d88731332c17cac4e2fbc6c Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 16:48:23 +0300 Subject: [PATCH 5/9] feat: add deliveryReport to MessageErrorEntry Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 9d25752..729c1b5 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -140,6 +140,9 @@ class MessageErrorEntry(_MessageErrorEntryRequired, total=False): backfilled: bool """Set on entries rebuilt from old message fields.""" + deliveryReport: bool + """True when the entry is a delivery report that did not confirm delivery. The message was sent; this is not a send failure.""" + class _MessageRequired(TypedDict): _id: str From 6964f9ef4fbaa6349a59a9e7d794bc32ffcf7448 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 17:53:47 +0300 Subject: [PATCH 6/9] refactor: rename errorHistory to issueHistory Co-Authored-By: Claude Opus 5.5 --- src/textbee/__init__.py | 8 ++++---- src/textbee/_types.py | 10 +++++----- tests/test_client.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/textbee/__init__.py b/src/textbee/__init__.py index 9767844..f6d5d65 100644 --- a/src/textbee/__init__.py +++ b/src/textbee/__init__.py @@ -17,8 +17,8 @@ Direction, Message, MessageDevice, - MessageErrorEntry, - MessageErrorSource, + MessageIssueEntry, + MessageIssueSource, MessageList, MessageListMeta, MessageStatus, @@ -41,8 +41,8 @@ "Direction", "Message", "MessageDevice", - "MessageErrorEntry", - "MessageErrorSource", + "MessageIssueEntry", + "MessageIssueSource", "MessageList", "MessageListMeta", "MessageStatus", diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 729c1b5..ab5688b 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -121,19 +121,19 @@ class MessageDevice(_MessageDeviceRequired, total=False): enabled: bool -MessageErrorSource = Literal["push", "device", "server"] +MessageIssueSource = Literal["push", "device", "server"] """Where a failure was reported from.""" -class _MessageErrorEntryRequired(TypedDict): +class _MessageIssueEntryRequired(TypedDict): code: str at: str """When the failure happened. ISO date.""" - source: MessageErrorSource + source: MessageIssueSource -class MessageErrorEntry(_MessageErrorEntryRequired, total=False): +class MessageIssueEntry(_MessageIssueEntryRequired, total=False): """One failure a message had. Kept after the message later succeeds.""" message: str @@ -174,7 +174,7 @@ class _MessageFields(_MessageRequired, total=False): errorMessage: str """Current failure text. Present only while status is ``failed``, or as a timeout note on ``unknown``.""" - errorHistory: List[MessageErrorEntry] + issueHistory: List[MessageIssueEntry] """Every failure the message had, Oldest first. At most 15: the first 5 and the newest 10.""" simSubscriptionId: int diff --git a/tests/test_client.py b/tests/test_client.py index 69cbcbe..2ae4bbf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -341,7 +341,7 @@ def test_returns_error_history_on_a_message_that_failed_and_later_succeeded( } page = { "data": [ - {"_id": "sms-2", "message": "hi", "type": "SENT", "status": "sent", "errorHistory": [entry]} + {"_id": "sms-2", "message": "hi", "type": "SENT", "status": "sent", "issueHistory": [entry]} ], "meta": {}, } @@ -349,7 +349,7 @@ def test_returns_error_history_on_a_message_that_failed_and_later_succeeded( result = client().get_messages() - assert result["data"][0]["errorHistory"] == [entry] + 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": {}}) From b4983a6caf07a219a3703643d8fefd6a25ccb0d0 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 17:53:51 +0300 Subject: [PATCH 7/9] docs: issueHistory also holds unconfirmed delivery reports Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index ab5688b..83921b2 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -175,7 +175,7 @@ class _MessageFields(_MessageRequired, total=False): """Current failure text. Present only while status is ``failed``, or as a timeout note on ``unknown``.""" issueHistory: List[MessageIssueEntry] - """Every failure the message had, Oldest first. At most 15: the first 5 and the newest 10.""" + """Every failure the message had, plus delivery reports that did not confirm delivery (``deliveryReport: true``, not failures). Oldest first. At most 15: the first 5 and the newest 10.""" simSubscriptionId: int smsBatch: str From 8cbe648407bc6cf20ba8d01fccf1f21ea88ab02a Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 18:16:53 +0300 Subject: [PATCH 8/9] style: wrap long docstrings and format the issueHistory test Co-Authored-By: Claude Opus 5.5 --- src/textbee/_types.py | 21 +++++++++++++++++---- tests/test_client.py | 8 +++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 83921b2..23de68b 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -141,7 +141,10 @@ class MessageIssueEntry(_MessageIssueEntryRequired, total=False): """Set on entries rebuilt from old message fields.""" deliveryReport: bool - """True when the entry is a delivery report that did not confirm delivery. The message was sent; this is not a send failure.""" + """True for a delivery report that did not confirm delivery. + + The message was sent; this is not a send failure. + """ class _MessageRequired(TypedDict): @@ -169,13 +172,23 @@ class _MessageFields(_MessageRequired, total=False): receivedAt: str errorCode: str - """Current failure code. Present only while status is ``failed`` (test accounts can keep ``FCM_SEND_SKIPPED``).""" + """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``.""" + """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 delivery reports that did not confirm delivery (``deliveryReport: true``, not failures). Oldest first. At most 15: the first 5 and the newest 10.""" + """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 diff --git a/tests/test_client.py b/tests/test_client.py index 2ae4bbf..2f48edf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -341,7 +341,13 @@ def test_returns_error_history_on_a_message_that_failed_and_later_succeeded( } page = { "data": [ - {"_id": "sms-2", "message": "hi", "type": "SENT", "status": "sent", "issueHistory": [entry]} + { + "_id": "sms-2", + "message": "hi", + "type": "SENT", + "status": "sent", + "issueHistory": [entry], + } ], "meta": {}, } From 20a15a1db506297527109d2d506217811b151331 Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 28 Oct 2026 18:17:37 +0300 Subject: [PATCH 9/9] chore: release 0.2.0 Co-Authored-By: Claude Opus 5.5 --- src/textbee/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"