From dcf34d0003b18828c15ffaf9883df25c05512bac Mon Sep 17 00:00:00 2001 From: Amir Mofakhar Date: Wed, 22 Apr 2026 16:40:01 +0100 Subject: [PATCH 1/5] added handler for Decimal --- singer/messages.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/singer/messages.py b/singer/messages.py index e6c8247..1fa20d9 100644 --- a/singer/messages.py +++ b/singer/messages.py @@ -3,6 +3,7 @@ import pytz import orjson import ciso8601 +import decimal import singer.utils as u from .logger import get_logger @@ -291,9 +292,12 @@ def parse_message(msg): return None +def handler_for_decimal_object(obj): + if isinstance(obj, decimal.Decimal): + return float(obj) def format_message(message, option=0): - return orjson.dumps(message.asdict(), option=option) + return orjson.dumps(message.asdict(), option=option, default=handler_for_decimal_object) def write_message(message): From 12b3dd157390a1693e2e64d2c02feff44f0327ad Mon Sep 17 00:00:00 2001 From: Amir Mofakhar Date: Wed, 22 Apr 2026 16:40:09 +0100 Subject: [PATCH 2/5] added handler for Decimal --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58260f6..482a28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +## 3.0.2 (2026-04-22) + * Handler for Decimal object in message + +## 3.0.1 (2026-04-21) + * Fixed PyPI publisher + +## 3.0.0 (2026-04-20) + * Support for Python 3.12 ## 2.0.1 (2021-11-23) * Fixed an issue when `format_message` returned newline character diff --git a/setup.py b/setup.py index 479ee50..4019fed 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = fh.read() setup(name="pipelinewise-singer-python", - version='3.0.1', + version='3.0.2', description="Singer.io utility library - PipelineWise compatible", python_requires=">=3.12, <3.13", long_description=long_description, From 4f20bb93cd7a39070b9468edb06d21d96194ab68 Mon Sep 17 00:00:00 2001 From: Amir Mofakhar Date: Wed, 22 Apr 2026 16:46:14 +0100 Subject: [PATCH 3/5] fix raising typeerror --- singer/messages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/singer/messages.py b/singer/messages.py index 1fa20d9..80ac204 100644 --- a/singer/messages.py +++ b/singer/messages.py @@ -295,6 +295,7 @@ def parse_message(msg): def handler_for_decimal_object(obj): if isinstance(obj, decimal.Decimal): return float(obj) + raise TypeError def format_message(message, option=0): return orjson.dumps(message.asdict(), option=option, default=handler_for_decimal_object) From a86f18091481f0e78b741b84a3211e689f94d673 Mon Sep 17 00:00:00 2001 From: Amir Mofakhar Date: Wed, 22 Apr 2026 16:46:50 +0100 Subject: [PATCH 4/5] Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482a28c..98d9f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog ## 3.0.2 (2026-04-22) - * Handler for Decimal object in message + * Handle Decimal objects in messages ## 3.0.1 (2026-04-21) * Fixed PyPI publisher From 52a3c45990c32626a3fdaa5f22701e45d4eb29db Mon Sep 17 00:00:00 2001 From: Amir Mofakhar Date: Wed, 22 Apr 2026 16:59:43 +0100 Subject: [PATCH 5/5] unit tests were missed to push --- singer/__init__.py | 3 ++- tests/test_singer.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/singer/__init__.py b/singer/__init__.py index 5b72276..4f4e8e1 100644 --- a/singer/__init__.py +++ b/singer/__init__.py @@ -35,7 +35,8 @@ write_schema, write_state, write_version, - write_batch + write_batch, + handler_for_decimal_object ) from singer.transform import ( diff --git a/tests/test_singer.py b/tests/test_singer.py index dd72d4a..093f475 100644 --- a/tests/test_singer.py +++ b/tests/test_singer.py @@ -2,6 +2,9 @@ import orjson import unittest import dateutil +import decimal + +from unittest.mock import MagicMock class TestSinger(unittest.TestCase): @@ -206,6 +209,45 @@ def test_format_message(self): self.assertEqual(b'{"type":"RECORD","stream":"users","record":{"name":"foo"}}\n', singer.format_message(record_message, option=orjson.OPT_APPEND_NEWLINE)) + def test_default_handler_decimal(self): + """Test that decimal.Decimal is converted to a string.""" + d = decimal.Decimal("10.50") + result = singer.handler_for_decimal_object(d) + self.assertEqual(result, 10.50) + self.assertIsInstance(result, float) + + def test_default_handler_error(self): + """Test that unsupported types still raise a TypeError.""" + with self.assertRaises(TypeError): + singer.handler_for_decimal_object(range(5)) + + def test_format_message_with_decimals(self): + """Test that format_message correctly serializes a message containing decimals.""" + # Mocking the message object + mock_message = MagicMock() + mock_message.asdict.return_value = { + "id": 1, + "price": decimal.Decimal("99.99"), + "status": "active" + } + + # We expect the Decimal to become a string in the resulting bytes + expected_output = b'{"id":1,"price":99.99,"status":"active"}' + + result = singer.format_message(mock_message) + self.assertEqual(result, expected_output) + + def test_format_message_options(self): + """Test that orjson options (like appending a newline) work.""" + mock_message = MagicMock() + mock_message.asdict.return_value = {"key": "val"} + + # orjson.OPT_APPEND_NEWLINE is an integer bitmask + result = singer.format_message(mock_message, option=orjson.OPT_APPEND_NEWLINE) + + # Should end with a newline byte (10) + self.assertTrue(result.endswith(b'\n')) + if __name__ == '__main__': unittest.main()