Spaces:
Running
on
Zero
Running
on
Zero
File size: 3,626 Bytes
223daa0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
"""JSON serialisation utilities with Decimal precision preservation.
This module provides safe JSON serialisation for financial data,
ensuring Decimal types are converted to strings (not floats) to
preserve full precision. Converting Decimal to float can cause
monetary errors in large values (e.g., Β£1,000,000,000.47 β Β£1,000,000,000.50).
"""
from typing import Any
from decimal import Decimal
from datetime import datetime, date
import orjson
import logging
logger = logging.getLogger(__name__)
def default_handler(obj: Any) -> Any:
"""Custom default handler for orjson serialisation.
Handles types that aren't natively JSON-serialisable:
- Decimal β string (preserves precision for financial data)
- datetime/date β ISO format string
Args:
obj: Object to serialise
Returns:
JSON-serialisable representation
Raises:
TypeError: If object type is not handled
Example:
>>> import orjson
>>> from decimal import Decimal
>>> orjson.dumps({"price": Decimal("19.99")}, default=default_handler)
b'{"price":"19.99"}'
"""
if isinstance(obj, Decimal):
# CRITICAL: Convert to string, NOT float
# Float loses precision in large numbers
return str(obj)
elif isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError(f"Type {type(obj)} not serialisable")
def dumps(obj: Any) -> bytes:
"""Serialise object to JSON bytes with Decimal support.
Args:
obj: Object to serialise
Returns:
JSON bytes with Decimal values as strings
Example:
>>> from decimal import Decimal
>>> dumps({"price": Decimal("19.99"), "quantity": 5})
b'{"price":"19.99","quantity":5}'
"""
return orjson.dumps(obj, default=default_handler)
def dumps_str(obj: Any) -> str:
"""Serialise object to JSON string with Decimal support.
Args:
obj: Object to serialise
Returns:
JSON string with Decimal values as strings
Example:
>>> from decimal import Decimal
>>> dumps_str({"price": Decimal("19.99")})
'{"price":"19.99"}'
"""
return dumps(obj).decode('utf-8')
def decimal_to_json_safe(obj: Any) -> Any:
"""Recursively convert non-JSON-serialisable objects to safe types.
This function provides an alternative approach when you need
Python dict/list structures rather than JSON bytes.
Converts:
- Decimal objects β string (preserves full precision)
- datetime/date objects β ISO format strings
This preserves precision for financial calculations, which is critical
as converting Decimal to float can lose pence in large numbers.
Args:
obj: Any Python object (dict, list, Decimal, datetime, etc.)
Returns:
Same structure with non-serialisable objects converted
Example:
>>> from decimal import Decimal
>>> decimal_to_json_safe({"price": Decimal('19.99')})
{'price': '19.99'}
>>> decimal_to_json_safe([Decimal('1.23'), Decimal('4.56')])
['1.23', '4.56']
"""
if isinstance(obj, Decimal):
# CRITICAL: String preserves precision, float does not
return str(obj)
elif isinstance(obj, (datetime, date)):
return obj.isoformat()
elif isinstance(obj, dict):
return {k: decimal_to_json_safe(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [decimal_to_json_safe(item) for item in obj]
elif isinstance(obj, tuple):
return tuple(decimal_to_json_safe(item) for item in obj)
return obj
|