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.
- smithy_json-0.0.1/.gitignore +36 -0
- smithy_json-0.0.1/CHANGES.md +10 -0
- smithy_json-0.0.1/NOTICE +1 -0
- smithy_json-0.0.1/PKG-INFO +14 -0
- smithy_json-0.0.1/README.md +4 -0
- smithy_json-0.0.1/pyproject.toml +22 -0
- smithy_json-0.0.1/src/smithy_json/__init__.py +64 -0
- smithy_json-0.0.1/src/smithy_json/_private/__init__.py +11 -0
- smithy_json-0.0.1/src/smithy_json/_private/deserializers.py +279 -0
- smithy_json-0.0.1/src/smithy_json/_private/documents.py +149 -0
- smithy_json-0.0.1/src/smithy_json/_private/serializers.py +324 -0
- smithy_json-0.0.1/src/smithy_json/py.typed +1 -0
|
@@ -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
|
smithy_json-0.0.1/NOTICE
ADDED
|
@@ -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,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
|