diff --git a/singer_sdk/_singerlib/messages.py b/singer_sdk/_singerlib/messages.py index 4f45acb69..2131290b6 100644 --- a/singer_sdk/_singerlib/messages.py +++ b/singer_sdk/_singerlib/messages.py @@ -8,7 +8,7 @@ from dataclasses import asdict, dataclass, field from datetime import datetime, timezone -from singer_sdk.helpers._util import serialize_json +from singer_sdk._singerlib.serde import serialize_json if sys.version_info < (3, 11): from backports.datetime_fromisoformat import MonkeyPatch diff --git a/singer_sdk/_singerlib/serde.py b/singer_sdk/_singerlib/serde.py new file mode 100644 index 000000000..cd0cfce2e --- /dev/null +++ b/singer_sdk/_singerlib/serde.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import datetime +import decimal +import json +import logging +import typing as t + +import simplejson + +logger = logging.getLogger(__name__) + + +def _default_encoding(obj: t.Any) -> str: # noqa: ANN401 + """Default JSON encoder. + + Args: + obj: The object to encode. + + Returns: + The encoded object. + """ + return obj.isoformat(sep="T") if isinstance(obj, datetime.datetime) else str(obj) + + +def deserialize_json(json_str: str, **kwargs: t.Any) -> dict: + """Deserialize a line of json. + + Args: + json_str: A single line of json. + **kwargs: Optional key word arguments. + + Returns: + A dictionary of the deserialized json. + + Raises: + json.decoder.JSONDecodeError: raised if any lines are not valid json + """ + try: + return json.loads( # type: ignore[no-any-return] + json_str, + parse_float=decimal.Decimal, + **kwargs, + ) + except json.decoder.JSONDecodeError as exc: + logger.exception("Unable to parse:\n%s", json_str, exc_info=exc) + raise + + +def serialize_json(obj: object, **kwargs: t.Any) -> str: + """Serialize a dictionary into a line of json. + + Args: + obj: A Python object usually a dict. + **kwargs: Optional key word arguments. + + Returns: + A string of serialized json. + """ + return simplejson.dumps( + obj, + use_decimal=True, + default=_default_encoding, + separators=(",", ":"), + **kwargs, + ) diff --git a/singer_sdk/connectors/sql.py b/singer_sdk/connectors/sql.py index 652246e06..0a9a2f90a 100644 --- a/singer_sdk/connectors/sql.py +++ b/singer_sdk/connectors/sql.py @@ -12,14 +12,8 @@ import sqlalchemy as sa from singer_sdk import typing as th -from singer_sdk._singerlib import CatalogEntry, MetadataMapping, Schema +from singer_sdk._singerlib import CatalogEntry, MetadataMapping, Schema, serde from singer_sdk.exceptions import ConfigValidationError -from singer_sdk.helpers._util import ( - deserialize_json as util_deserialize_json, -) -from singer_sdk.helpers._util import ( - serialize_json as util_serialize_json, -) from singer_sdk.helpers.capabilities import TargetLoadMethods if t.TYPE_CHECKING: @@ -1170,7 +1164,7 @@ def serialize_json(self, obj: object) -> str: # noqa: PLR6301 .. versionadded:: 0.31.0 """ - return util_serialize_json(obj) + return serde.serialize_json(obj) def deserialize_json(self, json_str: str) -> object: # noqa: PLR6301 """Deserialize a JSON string to an object. @@ -1186,7 +1180,7 @@ def deserialize_json(self, json_str: str) -> object: # noqa: PLR6301 .. versionadded:: 0.31.0 """ - return util_deserialize_json(json_str) + return serde.deserialize_json(json_str) def delete_old_versions( self, diff --git a/singer_sdk/contrib/batch_encoder_jsonl.py b/singer_sdk/contrib/batch_encoder_jsonl.py index 262b840df..7c9505de4 100644 --- a/singer_sdk/contrib/batch_encoder_jsonl.py +++ b/singer_sdk/contrib/batch_encoder_jsonl.py @@ -6,8 +6,8 @@ import typing as t from uuid import uuid4 +from singer_sdk._singerlib.serde import serialize_json from singer_sdk.batch import BaseBatcher, lazy_chunked_generator -from singer_sdk.helpers._util import serialize_json __all__ = ["JSONLinesBatcher"] diff --git a/singer_sdk/helpers/_flattening.py b/singer_sdk/helpers/_flattening.py index edd262f98..c1999fcc6 100644 --- a/singer_sdk/helpers/_flattening.py +++ b/singer_sdk/helpers/_flattening.py @@ -10,7 +10,7 @@ import inflection -from singer_sdk.helpers._util import serialize_json +from singer_sdk._singerlib.serde import serialize_json DEFAULT_FLATTENING_SEPARATOR = "__" diff --git a/singer_sdk/helpers/_util.py b/singer_sdk/helpers/_util.py index 5f301fa16..0e8250c2a 100644 --- a/singer_sdk/helpers/_util.py +++ b/singer_sdk/helpers/_util.py @@ -3,78 +3,10 @@ from __future__ import annotations import datetime -import decimal import json -import logging -import sys import typing as t from pathlib import Path, PurePath -import simplejson - -if sys.version_info < (3, 11): - from backports.datetime_fromisoformat import MonkeyPatch - - MonkeyPatch.patch_fromisoformat() - - -logger = logging.getLogger(__name__) - - -def _default_encoding(obj: t.Any) -> str: # noqa: ANN401 - """Default JSON encoder. - - Args: - obj: The object to encode. - - Returns: - The encoded object. - """ - return obj.isoformat(sep="T") if isinstance(obj, datetime.datetime) else str(obj) - - -def deserialize_json(json_str: str, **kwargs: t.Any) -> dict: - """Deserialize a line of json. - - Args: - json_str: A single line of json. - **kwargs: Optional key word arguments. - - Returns: - A dictionary of the deserialized json. - - Raises: - json.decoder.JSONDecodeError: raised if any lines are not valid json - """ - try: - return json.loads( # type: ignore[no-any-return] - json_str, - parse_float=decimal.Decimal, - **kwargs, - ) - except json.decoder.JSONDecodeError as exc: - logger.exception("Unable to parse:\n%s", json_str, exc_info=exc) - raise - - -def serialize_json(obj: object, **kwargs: t.Any) -> str: - """Serialize a dictionary into a line of json. - - Args: - obj: A Python object usually a dict. - **kwargs: Optional key word arguments. - - Returns: - A string of serialized json. - """ - return simplejson.dumps( - obj, - use_decimal=True, - default=_default_encoding, - separators=(",", ":"), - **kwargs, - ) - def read_json_file(path: PurePath | str) -> dict[str, t.Any]: """Read json file, throwing an error if missing.""" @@ -89,7 +21,7 @@ def read_json_file(path: PurePath | str) -> dict[str, t.Any]: msg += f"\nFor more info, please see the sample template at: {template}" raise FileExistsError(msg) - return deserialize_json(Path(path).read_text(encoding="utf-8")) + return t.cast(dict, json.loads(Path(path).read_text(encoding="utf-8"))) def utc_now() -> datetime.datetime: diff --git a/singer_sdk/io_base.py b/singer_sdk/io_base.py index 33e045f61..f0abc568b 100644 --- a/singer_sdk/io_base.py +++ b/singer_sdk/io_base.py @@ -11,8 +11,8 @@ from singer_sdk._singerlib.messages import Message, SingerMessageType from singer_sdk._singerlib.messages import format_message as singer_format_message from singer_sdk._singerlib.messages import write_message as singer_write_message +from singer_sdk._singerlib.serde import deserialize_json from singer_sdk.exceptions import InvalidInputLine -from singer_sdk.helpers._util import deserialize_json logger = logging.getLogger(__name__) diff --git a/singer_sdk/sinks/core.py b/singer_sdk/sinks/core.py index 44dacc5cd..4fb475f8b 100644 --- a/singer_sdk/sinks/core.py +++ b/singer_sdk/sinks/core.py @@ -16,6 +16,7 @@ import jsonschema from typing_extensions import override +from singer_sdk._singerlib.serde import deserialize_json from singer_sdk.exceptions import ( InvalidJSONSchema, InvalidRecord, @@ -37,7 +38,6 @@ get_datelike_property_type, handle_invalid_timestamp_in_record, ) -from singer_sdk.helpers._util import deserialize_json if t.TYPE_CHECKING: from logging import Logger diff --git a/singer_sdk/tap_base.py b/singer_sdk/tap_base.py index 256ad90e3..89076b6f1 100644 --- a/singer_sdk/tap_base.py +++ b/singer_sdk/tap_base.py @@ -10,6 +10,7 @@ import click from singer_sdk._singerlib import Catalog, StateMessage +from singer_sdk._singerlib.serde import serialize_json from singer_sdk.configuration._dict_config import merge_missing_config_jsonschema from singer_sdk.exceptions import ( AbortedSyncFailedException, @@ -19,7 +20,7 @@ from singer_sdk.helpers import _state from singer_sdk.helpers._classproperty import classproperty from singer_sdk.helpers._state import write_stream_state -from singer_sdk.helpers._util import read_json_file, serialize_json +from singer_sdk.helpers._util import read_json_file from singer_sdk.helpers.capabilities import ( BATCH_CONFIG, CapabilitiesEnum, diff --git a/tests/core/test_io.py b/tests/core/test_io.py index ecc52fac1..9ec9532f2 100644 --- a/tests/core/test_io.py +++ b/tests/core/test_io.py @@ -10,7 +10,7 @@ import pytest from singer_sdk._singerlib import RecordMessage -from singer_sdk.helpers._util import deserialize_json +from singer_sdk._singerlib.serde import deserialize_json from singer_sdk.io_base import SingerReader, SingerWriter