From 086df1df972298ff3eb6860e19ef503b65166df0 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 01:58:42 -0700 Subject: [PATCH 1/4] fix: coerce null eventType to empty string in _parse_card_note (#350) Signed-off-by: SAY-5 --- pytr/event.py | 6 +++++- tests/test_events.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pytr/event.py b/pytr/event.py index a923403..51b8e7c 100644 --- a/pytr/event.py +++ b/pytr/event.py @@ -743,7 +743,11 @@ def _parse_card_note(event_dict: Dict[Any, Any]) -> Optional[str]: Returns: Optional[str]: note """ - eventTypeStr = event_dict.get("eventType", "") + # `dict.get(k, default)` only returns the default when the key is + # absent; if the API explicitly sets eventType=None (observed on + # "Aktien erhalten" / shares-received events) the default is bypassed + # and `.startswith` then nil-derefs (#350). + eventTypeStr = event_dict.get("eventType") or "" if eventTypeStr.startswith("card_"): return eventTypeStr diff --git a/tests/test_events.py b/tests/test_events.py index 1bd47a8..965a728 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -2053,3 +2053,19 @@ def test_events(): entry.setdefault("ISIN2", None) entry.setdefault("Stück2", None) assert transactions == rowtransactions + + +def test_parse_card_note_returns_none_when_event_type_is_null(): + """Regression test for #350: an event_dict with explicit ``eventType: None`` + (e.g. the "Aktien erhalten" / shares-received timeline entry) used to + crash ``_parse_card_note`` with + ``AttributeError: 'NoneType' object has no attribute 'startswith'``.""" + # eventType key is explicitly null + assert Event._parse_card_note({"eventType": None}) is None + + # eventType key absent — already covered by `.get(..., "")` default but + # double-check we do not regress this safe path + assert Event._parse_card_note({}) is None + + # Existing happy path: card_ prefix remains recognised + assert Event._parse_card_note({"eventType": "card_refund"}) == "card_refund" From c30214b4b13fc9d4d111b0df63080760043abbf0 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 12:59:50 -0700 Subject: [PATCH 2/4] chore: remove em-dashes from comments --- tests/test_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_events.py b/tests/test_events.py index 965a728..6d3d79d 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -2063,7 +2063,7 @@ def test_parse_card_note_returns_none_when_event_type_is_null(): # eventType key is explicitly null assert Event._parse_card_note({"eventType": None}) is None - # eventType key absent — already covered by `.get(..., "")` default but + # eventType key absent, already covered by `.get(..., "")` default but # double-check we do not regress this safe path assert Event._parse_card_note({}) is None From 7ba092712f02d0639190d18ac1b1270509d28f29 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 17:53:12 -0700 Subject: [PATCH 3/4] trim verbose comments --- pytr/event.py | 4 ---- tests/test_events.py | 11 +---------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/pytr/event.py b/pytr/event.py index 51b8e7c..7d2390b 100644 --- a/pytr/event.py +++ b/pytr/event.py @@ -743,10 +743,6 @@ def _parse_card_note(event_dict: Dict[Any, Any]) -> Optional[str]: Returns: Optional[str]: note """ - # `dict.get(k, default)` only returns the default when the key is - # absent; if the API explicitly sets eventType=None (observed on - # "Aktien erhalten" / shares-received events) the default is bypassed - # and `.startswith` then nil-derefs (#350). eventTypeStr = event_dict.get("eventType") or "" if eventTypeStr.startswith("card_"): return eventTypeStr diff --git a/tests/test_events.py b/tests/test_events.py index 6d3d79d..c7273d0 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -2056,16 +2056,7 @@ def test_events(): def test_parse_card_note_returns_none_when_event_type_is_null(): - """Regression test for #350: an event_dict with explicit ``eventType: None`` - (e.g. the "Aktien erhalten" / shares-received timeline entry) used to - crash ``_parse_card_note`` with - ``AttributeError: 'NoneType' object has no attribute 'startswith'``.""" - # eventType key is explicitly null + """Regression for #350.""" assert Event._parse_card_note({"eventType": None}) is None - - # eventType key absent, already covered by `.get(..., "")` default but - # double-check we do not regress this safe path assert Event._parse_card_note({}) is None - - # Existing happy path: card_ prefix remains recognised assert Event._parse_card_note({"eventType": "card_refund"}) == "card_refund" From 8c898eab2a326da4743ef2dacda48aaae0a0e866 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 26 May 2026 19:23:30 -0700 Subject: [PATCH 4/4] test: add aktien_erhalten fixture with null eventType for #350 Signed-off-by: Sai Asish Y --- tests/aktien_erhalten_null_eventType.json | 27 +++++++++++++++++++++++ tests/test_events.py | 3 +++ 2 files changed, 30 insertions(+) create mode 100644 tests/aktien_erhalten_null_eventType.json diff --git a/tests/aktien_erhalten_null_eventType.json b/tests/aktien_erhalten_null_eventType.json new file mode 100644 index 0000000..62a98c1 --- /dev/null +++ b/tests/aktien_erhalten_null_eventType.json @@ -0,0 +1,27 @@ +{ + "id": "9e81acd0-3fe1-409b-abfd-96f26088eeb0", + "timestamp": "2025-03-15T10:00:00.000+0000", + "title": "AI & Big Data USD (Acc)", + "icon": "logos/IE000716YHJ7/v2", + "subtitle": "Aktien erhalten", + "eventType": null, + "action": { + "type": "timelineDetail", + "payload": "9e81acd0-3fe1-409b-abfd-96f26088eeb0" + }, + "source": "timelineActivity", + "details": { + "id": "9e81acd0-3fe1-409b-abfd-96f26088eeb0", + "sections": [ + { + "title": "Du hast Aktien erhalten", + "data": { + "icon": "logos/IE000716YHJ7/v2", + "timestamp": "2025-03-15T10:00:00.000Z", + "status": "executed" + }, + "type": "header" + } + ] + } +} diff --git a/tests/test_events.py b/tests/test_events.py index c7273d0..d7de399 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -2005,6 +2005,9 @@ def test_events(): } ], }, + { + "filename": "aktien_erhalten_null_eventType.json", + }, ] # Create an instance of EventCsvFormatter