Skip to content

Commit 1df8758

Browse files
amofakharCopilot
andauthored
[AP-2998] Handle Decimal objects in format_messages (#167)
* added handler for Decimal * added handler for Decimal * fix raising typeerror * Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * unit tests were missed to push --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 868b132 commit 1df8758

5 files changed

Lines changed: 59 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
# Changelog
2+
## 3.0.2 (2026-04-22)
3+
* Handle Decimal objects in messages
4+
5+
## 3.0.1 (2026-04-21)
6+
* Fixed PyPI publisher
7+
8+
## 3.0.0 (2026-04-20)
9+
* Support for Python 3.12
210

311
## 2.0.1 (2021-11-23)
412
* Fixed an issue when `format_message` returned newline character

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
long_description = fh.read()
77

88
setup(name="pipelinewise-singer-python",
9-
version='3.0.1',
9+
version='3.0.2',
1010
description="Singer.io utility library - PipelineWise compatible",
1111
python_requires=">=3.12, <3.13",
1212
long_description=long_description,

singer/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
write_schema,
3636
write_state,
3737
write_version,
38-
write_batch
38+
write_batch,
39+
handler_for_decimal_object
3940
)
4041

4142
from singer.transform import (

singer/messages.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytz
44
import orjson
55
import ciso8601
6+
import decimal
67

78
import singer.utils as u
89
from .logger import get_logger
@@ -291,9 +292,13 @@ def parse_message(msg):
291292

292293
return None
293294

295+
def handler_for_decimal_object(obj):
296+
if isinstance(obj, decimal.Decimal):
297+
return float(obj)
298+
raise TypeError
294299

295300
def format_message(message, option=0):
296-
return orjson.dumps(message.asdict(), option=option)
301+
return orjson.dumps(message.asdict(), option=option, default=handler_for_decimal_object)
297302

298303

299304
def write_message(message):

tests/test_singer.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import orjson
33
import unittest
44
import dateutil
5+
import decimal
6+
7+
from unittest.mock import MagicMock
58

69

710
class TestSinger(unittest.TestCase):
@@ -206,6 +209,45 @@ def test_format_message(self):
206209
self.assertEqual(b'{"type":"RECORD","stream":"users","record":{"name":"foo"}}\n',
207210
singer.format_message(record_message, option=orjson.OPT_APPEND_NEWLINE))
208211

212+
def test_default_handler_decimal(self):
213+
"""Test that decimal.Decimal is converted to a string."""
214+
d = decimal.Decimal("10.50")
215+
result = singer.handler_for_decimal_object(d)
216+
self.assertEqual(result, 10.50)
217+
self.assertIsInstance(result, float)
218+
219+
def test_default_handler_error(self):
220+
"""Test that unsupported types still raise a TypeError."""
221+
with self.assertRaises(TypeError):
222+
singer.handler_for_decimal_object(range(5))
223+
224+
def test_format_message_with_decimals(self):
225+
"""Test that format_message correctly serializes a message containing decimals."""
226+
# Mocking the message object
227+
mock_message = MagicMock()
228+
mock_message.asdict.return_value = {
229+
"id": 1,
230+
"price": decimal.Decimal("99.99"),
231+
"status": "active"
232+
}
233+
234+
# We expect the Decimal to become a string in the resulting bytes
235+
expected_output = b'{"id":1,"price":99.99,"status":"active"}'
236+
237+
result = singer.format_message(mock_message)
238+
self.assertEqual(result, expected_output)
239+
240+
def test_format_message_options(self):
241+
"""Test that orjson options (like appending a newline) work."""
242+
mock_message = MagicMock()
243+
mock_message.asdict.return_value = {"key": "val"}
244+
245+
# orjson.OPT_APPEND_NEWLINE is an integer bitmask
246+
result = singer.format_message(mock_message, option=orjson.OPT_APPEND_NEWLINE)
247+
248+
# Should end with a newline byte (10)
249+
self.assertTrue(result.endswith(b'\n'))
250+
209251

210252
if __name__ == '__main__':
211253
unittest.main()

0 commit comments

Comments
 (0)