Spaces:
Running
on
Zero
Running
on
Zero
| """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 | |