smithy-json 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ # Eclipse
2
+ .classpath
3
+ .project
4
+ .settings/
5
+
6
+ # Intellij
7
+ .idea/
8
+ *.iml
9
+ *.iws
10
+
11
+ # VS Code
12
+ .vscode/
13
+
14
+ # Mac
15
+ .DS_Store
16
+
17
+ # Maven
18
+ target/
19
+ **/dependency-reduced-pom.xml
20
+
21
+ # Gradle
22
+ **/.gradle
23
+ build/
24
+ */out/
25
+ **/*/out/
26
+
27
+ # Python
28
+ __pycache__
29
+ *.swp
30
+ .mypy_cache
31
+ .coverage
32
+ coverage.xml
33
+ htmlcov
34
+ *.egg-info
35
+ dist
36
+ .venv
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ * <Add new items here>
6
+
7
+ ## v0.0.1
8
+
9
+ ### Feature
10
+ * Added support for json primitives in Smithy clients.
@@ -0,0 +1 @@
1
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: smithy-json
3
+ Version: 0.0.1
4
+ Summary: JSON serialization and deserialization support for Smithy tooling.
5
+ License-File: NOTICE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: ijson>=3.3.0
8
+ Requires-Dist: smithy-core
9
+ Description-Content-Type: text/markdown
10
+
11
+ # smithy-json
12
+
13
+ This package provides generic JSON serialization and deserialization support
14
+ for Smithy clients and servers.
@@ -0,0 +1,4 @@
1
+ # smithy-json
2
+
3
+ This package provides generic JSON serialization and deserialization support
4
+ for Smithy clients and servers.
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "smithy-json"
3
+ version = "0.0.1"
4
+ description = "JSON serialization and deserialization support for Smithy tooling."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "ijson>=3.3.0",
9
+ "smithy-core",
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build]
17
+ exclude = [
18
+ "tests",
19
+ ]
20
+
21
+ [tool.ruff]
22
+ src = ["src"]
@@ -0,0 +1,64 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import importlib.metadata
4
+ from io import BytesIO
5
+
6
+ from smithy_core.codecs import Codec
7
+ from smithy_core.deserializers import ShapeDeserializer
8
+ from smithy_core.interfaces import BytesReader, BytesWriter
9
+ from smithy_core.serializers import ShapeSerializer
10
+ from smithy_core.types import TimestampFormat
11
+
12
+ from ._private.deserializers import JSONShapeDeserializer as _JSONShapeDeserializer
13
+ from ._private.serializers import JSONShapeSerializer as _JSONShapeSerializer
14
+
15
+ __version__: str = importlib.metadata.version("smithy-json")
16
+
17
+
18
+ class JSONCodec(Codec):
19
+ """A codec for converting shapes to/from JSON."""
20
+
21
+ _use_json_name: bool
22
+ _use_timestamp_format: bool
23
+ _default_timestamp_format: TimestampFormat
24
+
25
+ def __init__(
26
+ self,
27
+ use_json_name: bool = True,
28
+ use_timestamp_format: bool = True,
29
+ default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
30
+ ) -> None:
31
+ """Initializes a JSONCodec.
32
+
33
+ :param use_json_name: Whether the codec should use `smithy.api#jsonName` trait,
34
+ if present.
35
+ :param use_timestamp_format: Whether the codec should use the
36
+ `smithy.api#timestampFormat` trait, if present.
37
+ :param default_timestamp_format: The default timestamp format to use if the
38
+ `smithy.api#timestampFormat` trait is not enabled or not present.
39
+ """
40
+ self._use_json_name = use_json_name
41
+ self._use_timestamp_format = use_timestamp_format
42
+ self._default_timestamp_format = default_timestamp_format
43
+
44
+ @property
45
+ def media_type(self) -> str:
46
+ return "application/json"
47
+
48
+ def create_serializer(self, sink: BytesWriter) -> "ShapeSerializer":
49
+ return _JSONShapeSerializer(
50
+ sink,
51
+ use_json_name=self._use_json_name,
52
+ use_timestamp_format=self._use_timestamp_format,
53
+ default_timestamp_format=self._default_timestamp_format,
54
+ )
55
+
56
+ def create_deserializer(self, source: bytes | BytesReader) -> "ShapeDeserializer":
57
+ if isinstance(source, bytes):
58
+ source = BytesIO(source)
59
+ return _JSONShapeDeserializer(
60
+ source,
61
+ use_json_name=self._use_json_name,
62
+ use_timestamp_format=self._use_timestamp_format,
63
+ default_timestamp_format=self._default_timestamp_format,
64
+ )
@@ -0,0 +1,11 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class Flushable(Protocol):
9
+ """A protocol for objects that can be flushed."""
10
+
11
+ def flush(self) -> None: ...
@@ -0,0 +1,279 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import datetime
5
+ from base64 import b64decode
6
+ from collections.abc import Callable, Iterator, Mapping, Sequence
7
+ from decimal import Decimal
8
+ from typing import Literal, NamedTuple, Protocol, cast
9
+
10
+ import ijson # type: ignore
11
+ from ijson.common import ObjectBuilder # type: ignore
12
+ from smithy_core.deserializers import ShapeDeserializer
13
+ from smithy_core.documents import Document
14
+ from smithy_core.exceptions import SmithyException
15
+ from smithy_core.interfaces import BytesReader
16
+ from smithy_core.schemas import Schema
17
+ from smithy_core.shapes import ShapeID, ShapeType
18
+ from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
19
+ from smithy_core.types import TimestampFormat
20
+
21
+ from .documents import JSONDocument
22
+
23
+ # TODO: put these type hints in a pyi somewhere. There here because ijson isn't
24
+ # typed.
25
+ type JSONParseEventType = Literal[
26
+ "null",
27
+ "string",
28
+ "number",
29
+ "boolean",
30
+ "start_array",
31
+ "end_array",
32
+ "start_map",
33
+ "map_key",
34
+ "end_map",
35
+ ]
36
+
37
+ type JSONParseEventValue = str | int | float | Decimal | bool | None
38
+
39
+ type JSON = Mapping[str, "JSON"] | Sequence["JSON"] | JSONParseEventValue
40
+
41
+
42
+ class JSONParseEvent(NamedTuple):
43
+ path: str
44
+ type: JSONParseEventType
45
+ value: JSONParseEventValue
46
+
47
+
48
+ class JSONTokenError(SmithyException):
49
+ def __init__(self, expected: str, event: JSONParseEvent) -> None:
50
+ super().__init__(
51
+ f"Error parsing JSON. Expected token of type `{expected}` at path "
52
+ f"`{event.path}`, but found: `{event.type}`: {event.value}"
53
+ )
54
+
55
+
56
+ class TypedObjectBuilder(Protocol):
57
+ value: JSON
58
+
59
+ def event(self, event: JSONParseEventType, value: JSONParseEventValue): ...
60
+
61
+
62
+ class BufferedParser:
63
+ """A wrapper around the ijson parser that allows peeking."""
64
+
65
+ def __init__(
66
+ self, stream: Iterator[tuple[str, JSONParseEventType, JSONParseEventValue]]
67
+ ) -> None:
68
+ self._stream = stream
69
+ self._pending: JSONParseEvent | None = None
70
+
71
+ def __iter__(self):
72
+ return self
73
+
74
+ def __next__(self) -> JSONParseEvent:
75
+ if self._pending is not None:
76
+ result = self._pending
77
+ self._pending = None
78
+ return result
79
+ return self._next()
80
+
81
+ def _next(self) -> JSONParseEvent:
82
+ return JSONParseEvent(*next(self._stream))
83
+
84
+ def peek(self) -> JSONParseEvent:
85
+ if self._pending is None:
86
+ self._pending = self._next()
87
+ return self._pending
88
+
89
+
90
+ class JSONShapeDeserializer(ShapeDeserializer):
91
+ def __init__(
92
+ self,
93
+ source: BytesReader,
94
+ *,
95
+ use_json_name: bool = True,
96
+ use_timestamp_format: bool = True,
97
+ default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
98
+ ) -> None:
99
+ self._stream = BufferedParser(ijson.parse(source))
100
+ self._use_json_name = use_json_name
101
+ self._use_timestamp_format = use_timestamp_format
102
+ self._default_timestamp_format = default_timestamp_format
103
+
104
+ # A mapping of json name to member name for each shape. Since the deserializer
105
+ # is shared and we don't know which shapes will be deserialized, this is
106
+ # populated on an as-needed basis.
107
+ self._json_names: dict[ShapeID, dict[str, str]] = {}
108
+
109
+ def is_null(self) -> bool:
110
+ return self._stream.peek().type == "null"
111
+
112
+ def read_null(self) -> None:
113
+ event = next(self._stream)
114
+ if event.value is not None:
115
+ raise JSONTokenError("null", event)
116
+ return None
117
+
118
+ def read_boolean(self, schema: Schema) -> bool:
119
+ event = next(self._stream)
120
+ if not isinstance(event.value, bool):
121
+ raise JSONTokenError("boolean", event)
122
+ return event.value
123
+
124
+ def read_blob(self, schema: Schema) -> bytes:
125
+ event = next(self._stream)
126
+ if event.type != "string" or not isinstance(event.value, str):
127
+ raise JSONTokenError("string", event)
128
+ return b64decode(event.value)
129
+
130
+ def read_integer(self, schema: Schema) -> int:
131
+ event = next(self._stream)
132
+ if not isinstance(event.value, int):
133
+ raise JSONTokenError("number", event)
134
+ return event.value
135
+
136
+ def read_float(self, schema: Schema) -> float:
137
+ event = next(self._stream)
138
+ match event.value:
139
+ case Decimal():
140
+ return float(event.value)
141
+ case int() | float():
142
+ return event.value
143
+ case "Infinity" | "-Infinity" | "NaN":
144
+ return float(event.value)
145
+ case _:
146
+ raise JSONTokenError("number", event)
147
+
148
+ def read_big_decimal(self, schema: Schema) -> Decimal:
149
+ event = next(self._stream)
150
+ match event.value:
151
+ case Decimal():
152
+ return event.value
153
+ case int() | float():
154
+ return Decimal.from_float(event.value)
155
+ case _:
156
+ raise JSONTokenError("number", event)
157
+
158
+ def read_string(self, schema: Schema) -> str:
159
+ event = next(self._stream)
160
+ if event.type not in ("string", "map_key") or not isinstance(event.value, str):
161
+ raise JSONTokenError("string | map_key", event)
162
+ return event.value
163
+
164
+ def read_document(self, schema: Schema) -> Document:
165
+ start = next(self._stream)
166
+ if start.type not in ("start_map", "start_array"):
167
+ return JSONDocument(
168
+ start.value,
169
+ schema=schema,
170
+ use_json_name=self._use_json_name,
171
+ default_timestamp_format=self._default_timestamp_format,
172
+ use_timestamp_format=self._use_timestamp_format,
173
+ )
174
+
175
+ end_type = "end_map" if start.type == "start_map" else "end_array"
176
+ builder = cast(TypedObjectBuilder, ObjectBuilder())
177
+ builder.event(start.type, start.value)
178
+ while (
179
+ event := next(self._stream)
180
+ ).path != start.path or event.type != end_type:
181
+ builder.event(event.type, event.value)
182
+
183
+ return JSONDocument(
184
+ builder.value,
185
+ schema=schema,
186
+ use_json_name=self._use_json_name,
187
+ default_timestamp_format=self._default_timestamp_format,
188
+ use_timestamp_format=self._use_timestamp_format,
189
+ )
190
+
191
+ def read_timestamp(self, schema: Schema) -> datetime.datetime:
192
+ format = self._default_timestamp_format
193
+ if self._use_timestamp_format:
194
+ if format_trait := schema.get_trait(TimestampFormatTrait):
195
+ format = format_trait.format
196
+
197
+ match format:
198
+ case TimestampFormat.EPOCH_SECONDS:
199
+ return format.deserialize(self.read_float(schema=schema))
200
+ case _:
201
+ return format.deserialize(self.read_string(schema=schema))
202
+
203
+ def read_struct(
204
+ self, schema: Schema, consumer: Callable[[Schema, "ShapeDeserializer"], None]
205
+ ):
206
+ event = next(self._stream)
207
+ if event.type != "start_map":
208
+ raise JSONTokenError("start_map", event)
209
+
210
+ while self._stream.peek().type != "end_map":
211
+ key = self.read_string(schema=schema)
212
+ member = self._resolve_member(schema=schema, key=key)
213
+ if not member:
214
+ self._skip()
215
+ continue
216
+ if self.is_null() and member.shape_type is not ShapeType.DOCUMENT:
217
+ self.read_null()
218
+ continue
219
+ consumer(member, self)
220
+
221
+ next(self._stream)
222
+
223
+ def _resolve_member(self, schema: Schema, key: str) -> Schema | None:
224
+ if self._use_json_name:
225
+ if schema.id not in self._json_names:
226
+ self._cache_json_names(schema=schema)
227
+ if key in self._json_names[schema.id]:
228
+ return schema.members.get(self._json_names[schema.id][key])
229
+ return None
230
+
231
+ return schema.members.get(key)
232
+
233
+ def _cache_json_names(self, schema: Schema):
234
+ result: dict[str, str] = {}
235
+ for member_name, member_schema in schema.members.items():
236
+ name: str = member_name
237
+ if json_name := member_schema.get_trait(JSONNameTrait):
238
+ name = json_name.value
239
+ result[name] = member_name
240
+ self._json_names[schema.id] = result
241
+
242
+ def read_list(
243
+ self, schema: Schema, consumer: Callable[["ShapeDeserializer"], None]
244
+ ):
245
+ event = next(self._stream)
246
+ if event.type != "start_array":
247
+ raise JSONTokenError("start_array", event)
248
+
249
+ while self._stream.peek().type != "end_array":
250
+ consumer(self)
251
+
252
+ next(self._stream)
253
+
254
+ def read_map(
255
+ self,
256
+ schema: Schema,
257
+ consumer: Callable[[str, "ShapeDeserializer"], None],
258
+ ):
259
+ event = next(self._stream)
260
+ if event.type != "start_map":
261
+ raise JSONTokenError("start_map", event)
262
+
263
+ key_schema = schema.members["key"]
264
+ while self._stream.peek().type != "end_map":
265
+ consumer(self.read_string(schema=key_schema), self)
266
+
267
+ next(self._stream)
268
+
269
+ def _skip(self) -> None:
270
+ start = next(self._stream)
271
+ if start.type not in ("start_map", "start_array"):
272
+ return
273
+
274
+ end_type = "end_map" if start.type == "start_map" else "end_array"
275
+
276
+ while (
277
+ event := next(self._stream)
278
+ ).path != start.path or event.type != end_type:
279
+ continue
@@ -0,0 +1,149 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from base64 import b64decode
5
+ from collections.abc import Mapping
6
+ from datetime import datetime
7
+ from decimal import Decimal
8
+
9
+ from smithy_core.documents import Document, DocumentValue
10
+ from smithy_core.prelude import DOCUMENT
11
+ from smithy_core.schemas import Schema
12
+ from smithy_core.shapes import ShapeType
13
+ from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
14
+ from smithy_core.types import TimestampFormat
15
+ from smithy_core.utils import expect_type
16
+
17
+
18
+ class JSONDocument(Document):
19
+ _schema: Schema
20
+ _json_names: dict[str, str]
21
+
22
+ def __init__(
23
+ self,
24
+ value: DocumentValue | dict[str, "Document"] | list["Document"],
25
+ *,
26
+ schema: Schema = DOCUMENT,
27
+ use_json_name: bool = True,
28
+ use_timestamp_format: bool = True,
29
+ default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
30
+ ) -> None:
31
+ super().__init__(value, schema=schema)
32
+ self._use_json_name = use_json_name
33
+ self._use_timestamp_format = use_timestamp_format
34
+ self._default_timestamp_format = default_timestamp_format
35
+ self._json_names = {}
36
+
37
+ if use_json_name and schema.shape_type in (
38
+ ShapeType.STRUCTURE,
39
+ ShapeType.UNION,
40
+ ):
41
+ for member_name, member_schema in schema.members.items():
42
+ if json_name := member_schema.get_trait(JSONNameTrait):
43
+ self._json_names[json_name.value] = member_name
44
+
45
+ def as_blob(self) -> bytes:
46
+ return b64decode(expect_type(str, self._value))
47
+
48
+ def as_float(self) -> float:
49
+ # Floats are returned as Decimals by default in ijson, which is the correct
50
+ # behavior, since technically JSON numbers have arbitrary precision.
51
+ return float(expect_type(Decimal, self._value))
52
+
53
+ def as_timestamp(self) -> datetime:
54
+ format = self._default_timestamp_format
55
+ if self._use_timestamp_format:
56
+ if format_trait := self._schema.get_trait(TimestampFormatTrait):
57
+ format = format_trait.format
58
+
59
+ match self._value:
60
+ case float() | int() | str():
61
+ return format.deserialize(self._value)
62
+ case Decimal():
63
+ return format.deserialize(float(self._value))
64
+ case datetime():
65
+ return self._value
66
+ case _:
67
+ raise Exception(
68
+ f"Expected number, but found {type(self._value)}: {self._value!r}"
69
+ )
70
+
71
+ def as_value(self) -> DocumentValue:
72
+ if self.is_none():
73
+ return None
74
+
75
+ match self.shape_type:
76
+ case ShapeType.STRING | ShapeType.ENUM:
77
+ return self.as_string()
78
+ case ShapeType.BOOLEAN:
79
+ return self.as_boolean()
80
+ case (
81
+ ShapeType.BYTE
82
+ | ShapeType.SHORT
83
+ | ShapeType.INTEGER
84
+ | ShapeType.INT_ENUM
85
+ | ShapeType.LONG
86
+ | ShapeType.BIG_INTEGER
87
+ ):
88
+ return self.as_integer()
89
+ case ShapeType.FLOAT | ShapeType.DOUBLE:
90
+ return self.as_float()
91
+ case ShapeType.BIG_DECIMAL:
92
+ return self.as_decimal()
93
+ case ShapeType.BLOB:
94
+ return self.as_blob()
95
+ case ShapeType.TIMESTAMP:
96
+ return self.as_timestamp()
97
+ case ShapeType.LIST:
98
+ return [e.as_value() for e in self.as_list()]
99
+ case ShapeType.MAP | ShapeType.STRUCTURE | ShapeType.UNION:
100
+ return {k: v.as_value() for k, v in self.as_map().items()}
101
+ case _:
102
+ return super().as_value()
103
+
104
+ def _new_document(
105
+ self,
106
+ value: DocumentValue | dict[str, "Document"] | list["Document"],
107
+ schema: Schema,
108
+ ) -> "Document":
109
+ return JSONDocument(
110
+ value,
111
+ schema=schema,
112
+ use_json_name=self._use_json_name,
113
+ use_timestamp_format=self._use_timestamp_format,
114
+ default_timestamp_format=self._default_timestamp_format,
115
+ )
116
+
117
+ def _wrap_map(self, value: Mapping[str, DocumentValue]) -> dict[str, "Document"]:
118
+ if self._schema.shape_type not in (ShapeType.STRUCTURE, ShapeType.UNION):
119
+ return super()._wrap_map(value)
120
+
121
+ result: dict[str, Document] = {}
122
+ for k, v in value.items():
123
+ member_name = self._json_names.get(k, k)
124
+ result[member_name] = self._new_document(
125
+ v, self._schema.members[member_name]
126
+ )
127
+ return result
128
+
129
+ def __setitem__(
130
+ self,
131
+ key: str | int,
132
+ value: "Document | list[Document] | dict[str, Document] | DocumentValue",
133
+ ) -> None:
134
+ if isinstance(key, str) and self._schema.shape_type in (
135
+ ShapeType.STRUCTURE,
136
+ ShapeType.UNION,
137
+ ):
138
+ member_name = self._json_names.get(key, key)
139
+ schema = self._schema.members[member_name]
140
+
141
+ if not isinstance(value, Document):
142
+ value = Document(value, schema=schema)
143
+ else:
144
+ value = value._with_schema(schema)
145
+
146
+ self.as_map()[member_name] = value
147
+ self._raw_value = None
148
+ else:
149
+ super().__setitem__(key, value)
@@ -0,0 +1,324 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from base64 import b64encode
5
+ from collections.abc import Callable, Mapping, Sequence
6
+ from contextlib import AbstractContextManager
7
+ from datetime import datetime
8
+ from decimal import Decimal
9
+ from io import BufferedWriter, RawIOBase
10
+ from types import TracebackType
11
+ from typing import Self
12
+
13
+ from smithy_core.documents import Document, DocumentValue
14
+ from smithy_core.interfaces import BytesWriter
15
+ from smithy_core.schemas import Schema
16
+ from smithy_core.serializers import (
17
+ InterceptingSerializer,
18
+ MapSerializer,
19
+ ShapeSerializer,
20
+ )
21
+ from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
22
+ from smithy_core.types import TimestampFormat
23
+
24
+ from . import Flushable
25
+
26
+ _INF: float = float("inf")
27
+ _NEG_INF: float = float("-inf")
28
+
29
+
30
+ class JSONShapeSerializer(ShapeSerializer):
31
+ _stream: "StreamingJSONEncoder"
32
+ _use_json_name: bool
33
+ _use_timestamp_format: bool
34
+ _default_timestamp_format: TimestampFormat
35
+
36
+ def __init__(
37
+ self,
38
+ sink: BytesWriter,
39
+ use_json_name: bool = True,
40
+ use_timestamp_format: bool = True,
41
+ default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
42
+ ) -> None:
43
+ self._stream = StreamingJSONEncoder(sink)
44
+ self._use_json_name = use_json_name
45
+ self._use_timestamp_format = use_timestamp_format
46
+ self._default_timestamp_format = default_timestamp_format
47
+
48
+ def begin_struct(
49
+ self, schema: "Schema"
50
+ ) -> AbstractContextManager["ShapeSerializer"]:
51
+ return JSONStructSerializer(self._stream, self, self._use_json_name)
52
+
53
+ def begin_list(
54
+ self, schema: "Schema", size: int
55
+ ) -> AbstractContextManager["ShapeSerializer"]:
56
+ return JSONListSerializer(self._stream, self)
57
+
58
+ def begin_map(
59
+ self, schema: "Schema", size: int
60
+ ) -> AbstractContextManager["MapSerializer"]:
61
+ return JSONMapSerializer(self._stream, self)
62
+
63
+ def write_null(self, schema: "Schema") -> None:
64
+ self._stream.write_null()
65
+
66
+ def write_boolean(self, schema: "Schema", value: bool) -> None:
67
+ self._stream.write_bool(value)
68
+
69
+ def write_integer(self, schema: "Schema", value: int) -> None:
70
+ self._stream.write_int(value)
71
+
72
+ def write_float(self, schema: "Schema", value: float) -> None:
73
+ self._stream.write_float(value)
74
+
75
+ def write_big_decimal(self, schema: "Schema", value: Decimal) -> None:
76
+ self._stream.write_float(value)
77
+
78
+ def write_string(self, schema: "Schema", value: str) -> None:
79
+ self._stream.write_string(value)
80
+
81
+ def write_blob(self, schema: "Schema", value: bytes) -> None:
82
+ self._stream.write_string(b64encode(value).decode("utf-8"))
83
+
84
+ def write_timestamp(self, schema: "Schema", value: datetime) -> None:
85
+ format = self._default_timestamp_format
86
+ if self._use_timestamp_format:
87
+ if format_trait := schema.get_trait(TimestampFormatTrait):
88
+ format = format_trait.format
89
+
90
+ self._stream.write_document_value(format.serialize(value))
91
+
92
+ def write_document(self, schema: "Schema", value: Document) -> None:
93
+ value.serialize_contents(self)
94
+
95
+ def flush(self) -> None:
96
+ self._stream.flush()
97
+
98
+
99
+ class JSONStructSerializer(InterceptingSerializer):
100
+ _stream: "StreamingJSONEncoder"
101
+ _parent: JSONShapeSerializer
102
+ _use_json_name: bool
103
+ _is_first_member = True
104
+
105
+ def __init__(
106
+ self,
107
+ stream: "StreamingJSONEncoder",
108
+ parent: JSONShapeSerializer,
109
+ use_json_name: bool,
110
+ ) -> None:
111
+ self._stream = stream
112
+ self._parent = parent
113
+ self._use_json_name = use_json_name
114
+
115
+ def __enter__(self) -> Self:
116
+ self._stream.write_object_start()
117
+ return self
118
+
119
+ def __exit__(
120
+ self,
121
+ exc_type: type[BaseException] | None,
122
+ exc_value: BaseException | None,
123
+ traceback: TracebackType | None,
124
+ ) -> None:
125
+ if not exc_value:
126
+ self._stream.write_object_end()
127
+
128
+ def before(self, schema: "Schema") -> ShapeSerializer:
129
+ if self._is_first_member:
130
+ self._is_first_member = False
131
+ else:
132
+ self._stream.write_more()
133
+
134
+ member_name = schema.expect_member_name()
135
+ if self._use_json_name and (json_name := schema.get_trait(JSONNameTrait)):
136
+ member_name = json_name.value
137
+
138
+ self._stream.write_key(member_name)
139
+ return self._parent
140
+
141
+ def after(self, schema: "Schema") -> None:
142
+ pass
143
+
144
+
145
+ class JSONListSerializer(InterceptingSerializer):
146
+ _stream: "StreamingJSONEncoder"
147
+ _parent: JSONShapeSerializer
148
+ _is_first_entry = True
149
+
150
+ def __init__(
151
+ self, stream: "StreamingJSONEncoder", parent: JSONShapeSerializer
152
+ ) -> None:
153
+ self._stream = stream
154
+ self._parent = parent
155
+
156
+ def __enter__(self) -> Self:
157
+ self._stream.write_array_start()
158
+ return self
159
+
160
+ def __exit__(
161
+ self,
162
+ exc_type: type[BaseException] | None,
163
+ exc_value: BaseException | None,
164
+ traceback: TracebackType | None,
165
+ ) -> None:
166
+ if not exc_value:
167
+ self._stream.write_array_end()
168
+
169
+ def before(self, schema: "Schema") -> ShapeSerializer:
170
+ if self._is_first_entry:
171
+ self._is_first_entry = False
172
+ else:
173
+ self._stream.write_more()
174
+
175
+ return self._parent
176
+
177
+ def after(self, schema: "Schema") -> None:
178
+ pass
179
+
180
+
181
+ class JSONMapSerializer(MapSerializer):
182
+ _stream: "StreamingJSONEncoder"
183
+ _parent: JSONShapeSerializer
184
+ _is_first_entry = True
185
+
186
+ def __init__(
187
+ self, stream: "StreamingJSONEncoder", parent: JSONShapeSerializer
188
+ ) -> None:
189
+ self._stream = stream
190
+ self._parent = parent
191
+
192
+ def __enter__(self) -> Self:
193
+ self._stream.write_object_start()
194
+ return self
195
+
196
+ def __exit__(
197
+ self,
198
+ exc_type: type[BaseException] | None,
199
+ exc_value: BaseException | None,
200
+ traceback: TracebackType | None,
201
+ ) -> None:
202
+ if not exc_value:
203
+ self._stream.write_object_end()
204
+
205
+ def entry(self, key: str, value_writer: Callable[[ShapeSerializer], None]) -> None:
206
+ if self._is_first_entry:
207
+ self._is_first_entry = False
208
+ else:
209
+ self._stream.write_more()
210
+
211
+ self._stream.write_key(key)
212
+ value_writer(self._parent)
213
+
214
+
215
+ class StreamingJSONEncoder:
216
+ def __init__(
217
+ self,
218
+ sink: BytesWriter,
219
+ default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
220
+ ) -> None:
221
+ self._sink = sink
222
+ if isinstance(sink, RawIOBase):
223
+ self._sink = BufferedWriter(sink)
224
+ self._default_timestamp_format = default_timestamp_format
225
+
226
+ def write_object_start(self) -> None:
227
+ self._sink.write(b"{")
228
+
229
+ def write_object_end(self) -> None:
230
+ self._sink.write(b"}")
231
+
232
+ def write_array_start(self) -> None:
233
+ self._sink.write(b"[")
234
+
235
+ def write_array_end(self) -> None:
236
+ self._sink.write(b"]")
237
+
238
+ def write_key(self, key: str) -> None:
239
+ self.write_string(key)
240
+ self._sink.write(b":")
241
+
242
+ def write_more(self):
243
+ self._sink.write(b",")
244
+
245
+ def write_document_value(
246
+ self, value: DocumentValue, *, timestamp_format: TimestampFormat | None = None
247
+ ) -> None:
248
+ match value:
249
+ case str():
250
+ self.write_string(value)
251
+ case bool():
252
+ self.write_bool(value)
253
+ case int():
254
+ self.write_int(value)
255
+ case float() | Decimal():
256
+ self.write_float(value)
257
+ case None:
258
+ self.write_null()
259
+ case bytes():
260
+ self.write_string(b64encode(value).decode("utf-8"))
261
+ case datetime():
262
+ format = timestamp_format or self._default_timestamp_format
263
+ self.write_document_value(value=format.serialize(value))
264
+ case Mapping():
265
+ self.write_object_start()
266
+ first = True
267
+ for k, v in value.items():
268
+ if not first:
269
+ self.write_more()
270
+ else:
271
+ first = False
272
+ self.write_key(k)
273
+ self.write_document_value(v)
274
+ self.write_object_end()
275
+ case Sequence():
276
+ self.write_array_start()
277
+ if value:
278
+ self.write_document_value(value[0])
279
+ for i in range(1, len(value)):
280
+ self.write_more()
281
+ self.write_document_value(value[i])
282
+ self.write_array_end()
283
+
284
+ def write_string(self, value: str) -> None:
285
+ self._sink.write(b'"')
286
+ self._sink.write(
287
+ value.replace("\\", "\\\\").replace('"', '\\"').encode("utf-8")
288
+ )
289
+ self._sink.write(b'"')
290
+
291
+ def write_int(self, value: int) -> None:
292
+ self._sink.write(repr(value).encode("utf-8"))
293
+
294
+ def write_float(self, value: float | Decimal) -> None:
295
+ if not self._write_non_numeric_float(value=value):
296
+ if isinstance(value, Decimal):
297
+ self._sink.write(str(value.normalize()).encode("utf-8"))
298
+ else:
299
+ self._sink.write(repr(value).encode("utf-8"))
300
+
301
+ def _write_non_numeric_float(self, value: float | Decimal) -> bool:
302
+ if value != value:
303
+ self._sink.write(b"NaN")
304
+ return True
305
+
306
+ if value == _INF:
307
+ self._sink.write(b'"Infinity"')
308
+ return True
309
+
310
+ if value == _NEG_INF:
311
+ self._sink.write(b'"-Infinity"')
312
+ return True
313
+
314
+ return False
315
+
316
+ def write_bool(self, value: bool) -> None:
317
+ self._sink.write(b"true" if value else b"false")
318
+
319
+ def write_null(self):
320
+ self._sink.write(b"null")
321
+
322
+ def flush(self) -> None:
323
+ if isinstance(self._sink, Flushable):
324
+ self._sink.flush()
@@ -0,0 +1 @@
1
+ Marker