smithy-http 0.0.1__py3-none-any.whl
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_http/__init__.py +233 -0
- smithy_http/aio/__init__.py +53 -0
- smithy_http/aio/aiohttp.py +140 -0
- smithy_http/aio/auth/__init__.py +2 -0
- smithy_http/aio/auth/apikey.py +117 -0
- smithy_http/aio/crt.py +493 -0
- smithy_http/aio/identity/__init__.py +2 -0
- smithy_http/aio/identity/apikey.py +38 -0
- smithy_http/aio/interfaces/__init__.py +85 -0
- smithy_http/aio/interfaces/auth.py +107 -0
- smithy_http/aio/protocols.py +118 -0
- smithy_http/aio/restjson.py +60 -0
- smithy_http/deserializers.py +259 -0
- smithy_http/endpoints.py +30 -0
- smithy_http/exceptions.py +7 -0
- smithy_http/interceptors/__init__.py +2 -0
- smithy_http/interceptors/user_agent.py +153 -0
- smithy_http/interfaces/__init__.py +141 -0
- smithy_http/plugins/__init__.py +15 -0
- smithy_http/py.typed +0 -0
- smithy_http/restjson.py +29 -0
- smithy_http/serializers.py +631 -0
- smithy_http/user_agent.py +97 -0
- smithy_http/utils.py +125 -0
- smithy_http-0.0.1.dist-info/METADATA +12 -0
- smithy_http-0.0.1.dist-info/RECORD +28 -0
- smithy_http-0.0.1.dist-info/WHEEL +4 -0
- smithy_http-0.0.1.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
from asyncio import iscoroutinefunction
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from urllib.parse import quote as urlquote
|
|
9
|
+
|
|
10
|
+
from smithy_core import URI
|
|
11
|
+
from smithy_core.codecs import Codec
|
|
12
|
+
from smithy_core.schemas import Schema
|
|
13
|
+
from smithy_core.serializers import (
|
|
14
|
+
InterceptingSerializer,
|
|
15
|
+
MapSerializer,
|
|
16
|
+
ShapeSerializer,
|
|
17
|
+
SpecificShapeSerializer,
|
|
18
|
+
)
|
|
19
|
+
from smithy_core.shapes import ShapeType
|
|
20
|
+
from smithy_core.traits import (
|
|
21
|
+
EndpointTrait,
|
|
22
|
+
HostLabelTrait,
|
|
23
|
+
HTTPErrorTrait,
|
|
24
|
+
HTTPHeaderTrait,
|
|
25
|
+
HTTPLabelTrait,
|
|
26
|
+
HTTPPayloadTrait,
|
|
27
|
+
HTTPPrefixHeadersTrait,
|
|
28
|
+
HTTPQueryParamsTrait,
|
|
29
|
+
HTTPQueryTrait,
|
|
30
|
+
HTTPResponseCodeTrait,
|
|
31
|
+
HTTPTrait,
|
|
32
|
+
MediaTypeTrait,
|
|
33
|
+
StreamingTrait,
|
|
34
|
+
TimestampFormatTrait,
|
|
35
|
+
)
|
|
36
|
+
from smithy_core.types import PathPattern, TimestampFormat
|
|
37
|
+
from smithy_core.utils import serialize_float
|
|
38
|
+
|
|
39
|
+
from . import tuples_to_fields
|
|
40
|
+
from .aio import HTTPRequest as _HTTPRequest
|
|
41
|
+
from .aio import HTTPResponse as _HTTPResponse
|
|
42
|
+
from .aio.interfaces import HTTPRequest, HTTPResponse
|
|
43
|
+
from .utils import join_query_params
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from smithy_core.aio.interfaces import StreamingBlob as AsyncStreamingBlob
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["HTTPRequestSerializer", "HTTPResponseSerializer"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HTTPRequestSerializer(SpecificShapeSerializer):
|
|
53
|
+
"""Binds a serializable shape to an HTTP request.
|
|
54
|
+
|
|
55
|
+
The resultant HTTP request is not immediately sendable. In particular, the host of
|
|
56
|
+
the destination URI is incomplete and MUST be suffixed before sending.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
payload_codec: Codec,
|
|
62
|
+
http_trait: HTTPTrait,
|
|
63
|
+
endpoint_trait: EndpointTrait | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Initialize an HTTPRequestSerializer.
|
|
66
|
+
|
|
67
|
+
:param payload_codec: The codec to use to serialize the HTTP payload, if one is
|
|
68
|
+
present.
|
|
69
|
+
:param http_trait: The HTTP trait of the operation being handled.
|
|
70
|
+
:param endpoint_trait: The optional endpoint trait of the operation being
|
|
71
|
+
handled.
|
|
72
|
+
"""
|
|
73
|
+
self._http_trait = http_trait
|
|
74
|
+
self._endpoint_trait = endpoint_trait
|
|
75
|
+
self._payload_codec = payload_codec
|
|
76
|
+
self.result: HTTPRequest | None = None
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def begin_struct(self, schema: Schema) -> Iterator[ShapeSerializer]:
|
|
80
|
+
payload: Any
|
|
81
|
+
binding_serializer: HTTPRequestBindingSerializer
|
|
82
|
+
|
|
83
|
+
host_prefix = ""
|
|
84
|
+
if self._endpoint_trait is not None:
|
|
85
|
+
host_prefix = self._endpoint_trait.host_prefix
|
|
86
|
+
|
|
87
|
+
content_type = self._payload_codec.media_type
|
|
88
|
+
|
|
89
|
+
if (payload_member := self._get_payload_member(schema)) is not None:
|
|
90
|
+
if payload_member.shape_type in (ShapeType.BLOB, ShapeType.STRING):
|
|
91
|
+
content_type = (
|
|
92
|
+
"application/octet-stream"
|
|
93
|
+
if payload_member.shape_type is ShapeType.BLOB
|
|
94
|
+
else "text/plain"
|
|
95
|
+
)
|
|
96
|
+
payload_serializer = RawPayloadSerializer()
|
|
97
|
+
binding_serializer = HTTPRequestBindingSerializer(
|
|
98
|
+
payload_serializer, self._http_trait.path, host_prefix
|
|
99
|
+
)
|
|
100
|
+
yield binding_serializer
|
|
101
|
+
payload = payload_serializer.payload
|
|
102
|
+
else:
|
|
103
|
+
if (media_type := payload_member.get_trait(MediaTypeTrait)) is not None:
|
|
104
|
+
content_type = media_type.value
|
|
105
|
+
payload = BytesIO()
|
|
106
|
+
payload_serializer = self._payload_codec.create_serializer(payload)
|
|
107
|
+
binding_serializer = HTTPRequestBindingSerializer(
|
|
108
|
+
payload_serializer, self._http_trait.path, host_prefix
|
|
109
|
+
)
|
|
110
|
+
yield binding_serializer
|
|
111
|
+
else:
|
|
112
|
+
if self._get_eventstreaming_member(schema) is not None:
|
|
113
|
+
content_type = "application/vnd.amazon.eventstream"
|
|
114
|
+
payload = BytesIO()
|
|
115
|
+
payload_serializer = self._payload_codec.create_serializer(payload)
|
|
116
|
+
with payload_serializer.begin_struct(schema) as body_serializer:
|
|
117
|
+
binding_serializer = HTTPRequestBindingSerializer(
|
|
118
|
+
body_serializer, self._http_trait.path, host_prefix
|
|
119
|
+
)
|
|
120
|
+
yield binding_serializer
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
seek := getattr(payload, "seek", None)
|
|
124
|
+
) is not None and not iscoroutinefunction(seek):
|
|
125
|
+
seek(0)
|
|
126
|
+
|
|
127
|
+
# TODO: conditional on empty-ness and based on the protocol
|
|
128
|
+
headers = binding_serializer.header_serializer.headers
|
|
129
|
+
headers.append(("content-type", content_type))
|
|
130
|
+
|
|
131
|
+
self.result = _HTTPRequest(
|
|
132
|
+
method=self._http_trait.method,
|
|
133
|
+
destination=URI(
|
|
134
|
+
host=binding_serializer.host_prefix_serializer.host_prefix,
|
|
135
|
+
path=binding_serializer.path_serializer.path,
|
|
136
|
+
query=join_query_params(
|
|
137
|
+
params=binding_serializer.query_serializer.query_params,
|
|
138
|
+
prefix=self._http_trait.query or "",
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
fields=tuples_to_fields(headers),
|
|
142
|
+
body=payload,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _get_payload_member(self, schema: Schema) -> Schema | None:
|
|
146
|
+
for member in schema.members.values():
|
|
147
|
+
if HTTPPayloadTrait in member:
|
|
148
|
+
return member
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _get_eventstreaming_member(self, schema: Schema) -> Schema | None:
|
|
152
|
+
for member in schema.members.values():
|
|
153
|
+
if (
|
|
154
|
+
member.get_trait(StreamingTrait) is not None
|
|
155
|
+
and member.shape_type is ShapeType.UNION
|
|
156
|
+
):
|
|
157
|
+
return member
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class HTTPRequestBindingSerializer(InterceptingSerializer):
|
|
162
|
+
"""Delegates HTTP request bindings to binding-location-specific serializers."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
payload_serializer: ShapeSerializer,
|
|
167
|
+
path_pattern: PathPattern,
|
|
168
|
+
host_prefix_pattern: str,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Initialize an HTTPRequestBindingSerializer.
|
|
171
|
+
|
|
172
|
+
:param payload_serializer: The :py:class:`ShapeSerializer` to use to serialize
|
|
173
|
+
the payload, if necessary.
|
|
174
|
+
:param path_pattern: The pattern used to construct the path.
|
|
175
|
+
:host_prefix_pattern: The pattern used to construct the host prefix.
|
|
176
|
+
"""
|
|
177
|
+
self._payload_serializer = payload_serializer
|
|
178
|
+
self.header_serializer = HTTPHeaderSerializer()
|
|
179
|
+
self.query_serializer = HTTPQuerySerializer()
|
|
180
|
+
self.path_serializer = HTTPPathSerializer(path_pattern)
|
|
181
|
+
self.host_prefix_serializer = HostPrefixSerializer(
|
|
182
|
+
payload_serializer, host_prefix_pattern
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def before(self, schema: Schema) -> ShapeSerializer:
|
|
186
|
+
if HTTPHeaderTrait in schema or HTTPPrefixHeadersTrait in schema:
|
|
187
|
+
return self.header_serializer
|
|
188
|
+
if HTTPQueryTrait in schema or HTTPQueryParamsTrait in schema:
|
|
189
|
+
return self.query_serializer
|
|
190
|
+
if HTTPLabelTrait in schema:
|
|
191
|
+
return self.path_serializer
|
|
192
|
+
if HostLabelTrait in schema:
|
|
193
|
+
return self.host_prefix_serializer
|
|
194
|
+
|
|
195
|
+
return self._payload_serializer
|
|
196
|
+
|
|
197
|
+
def after(self, schema: Schema) -> None:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class HTTPResponseSerializer(SpecificShapeSerializer):
|
|
202
|
+
"""Binds a serializable shape to an HTTP response."""
|
|
203
|
+
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
payload_codec: Codec,
|
|
207
|
+
http_trait: HTTPTrait,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Initialize an HTTPResponseSerializer.
|
|
210
|
+
|
|
211
|
+
:param payload_codec: The codec to use to serialize the HTTP payload, if one is
|
|
212
|
+
present.
|
|
213
|
+
:param http_trait: The HTTP trait of the operation being handled.
|
|
214
|
+
"""
|
|
215
|
+
self._http_trait = http_trait
|
|
216
|
+
self._payload_codec = payload_codec
|
|
217
|
+
self.result: HTTPResponse | None = None
|
|
218
|
+
|
|
219
|
+
@contextmanager
|
|
220
|
+
def begin_struct(self, schema: Schema) -> Iterator[ShapeSerializer]:
|
|
221
|
+
payload: Any
|
|
222
|
+
binding_serializer: HTTPResponseBindingSerializer
|
|
223
|
+
|
|
224
|
+
if (payload_member := self._get_payload_member(schema)) is not None:
|
|
225
|
+
if payload_member.shape_type in (ShapeType.BLOB, ShapeType.STRING):
|
|
226
|
+
payload_serializer = RawPayloadSerializer()
|
|
227
|
+
binding_serializer = HTTPResponseBindingSerializer(payload_serializer)
|
|
228
|
+
yield binding_serializer
|
|
229
|
+
payload = payload_serializer.payload
|
|
230
|
+
else:
|
|
231
|
+
payload = BytesIO()
|
|
232
|
+
payload_serializer = self._payload_codec.create_serializer(payload)
|
|
233
|
+
binding_serializer = HTTPResponseBindingSerializer(payload_serializer)
|
|
234
|
+
yield binding_serializer
|
|
235
|
+
else:
|
|
236
|
+
payload = BytesIO()
|
|
237
|
+
payload_serializer = self._payload_codec.create_serializer(payload)
|
|
238
|
+
with payload_serializer.begin_struct(schema) as body_serializer:
|
|
239
|
+
binding_serializer = HTTPResponseBindingSerializer(body_serializer)
|
|
240
|
+
yield binding_serializer
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
seek := getattr(payload, "seek", None)
|
|
244
|
+
) is not None and not iscoroutinefunction(seek):
|
|
245
|
+
seek(0)
|
|
246
|
+
|
|
247
|
+
default_code = self._http_trait.code
|
|
248
|
+
explicit_code = binding_serializer.response_code_serializer.response_code
|
|
249
|
+
if (http_error_trait := schema.get_trait(HTTPErrorTrait)) is not None:
|
|
250
|
+
default_code = http_error_trait.code
|
|
251
|
+
|
|
252
|
+
self.result = _HTTPResponse(
|
|
253
|
+
fields=tuples_to_fields(binding_serializer.header_serializer.headers),
|
|
254
|
+
body=payload,
|
|
255
|
+
status=explicit_code or default_code,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _get_payload_member(self, schema: Schema) -> Schema | None:
|
|
259
|
+
for member in schema.members.values():
|
|
260
|
+
if HTTPPayloadTrait in member:
|
|
261
|
+
return member
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class HTTPResponseBindingSerializer(InterceptingSerializer):
|
|
266
|
+
"""Delegates HTTP response bindings to binding-location-specific serializers."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, payload_serializer: ShapeSerializer) -> None:
|
|
269
|
+
"""Initialize an HTTPResponseBindingSerializer.
|
|
270
|
+
|
|
271
|
+
:param payload_serializer: The :py:class:`ShapeSerializer` to use to serialize
|
|
272
|
+
the payload, if necessary.
|
|
273
|
+
"""
|
|
274
|
+
self._payload_serializer = payload_serializer
|
|
275
|
+
self.header_serializer = HTTPHeaderSerializer()
|
|
276
|
+
self.response_code_serializer = HTTPResponseCodeSerializer()
|
|
277
|
+
|
|
278
|
+
def before(self, schema: Schema) -> ShapeSerializer:
|
|
279
|
+
if HTTPHeaderTrait in schema or HTTPPrefixHeadersTrait in schema:
|
|
280
|
+
return self.header_serializer
|
|
281
|
+
if HTTPResponseCodeTrait in schema:
|
|
282
|
+
return self.response_code_serializer
|
|
283
|
+
|
|
284
|
+
return self._payload_serializer
|
|
285
|
+
|
|
286
|
+
def after(self, schema: Schema) -> None:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RawPayloadSerializer(SpecificShapeSerializer):
|
|
291
|
+
"""Binds properties of serializable shape to an HTTP payload."""
|
|
292
|
+
|
|
293
|
+
payload: "AsyncStreamingBlob | None"
|
|
294
|
+
"""The serialized payload.
|
|
295
|
+
|
|
296
|
+
This will only be non-null after serialization.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self) -> None:
|
|
300
|
+
"""Initialize a RawPayloadSerializer."""
|
|
301
|
+
self.payload: AsyncStreamingBlob | None = None
|
|
302
|
+
|
|
303
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
304
|
+
self.payload = value.encode("utf-8")
|
|
305
|
+
|
|
306
|
+
def write_blob(self, schema: Schema, value: bytes) -> None:
|
|
307
|
+
self.payload = value
|
|
308
|
+
|
|
309
|
+
def write_data_stream(self, schema: Schema, value: "AsyncStreamingBlob") -> None:
|
|
310
|
+
self.payload = value
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class HTTPHeaderSerializer(SpecificShapeSerializer):
|
|
314
|
+
"""Binds properties of a serializable shape to HTTP headers."""
|
|
315
|
+
|
|
316
|
+
headers: list[tuple[str, str]]
|
|
317
|
+
"""A list of serialized headers.
|
|
318
|
+
|
|
319
|
+
This should only be accessed after serialization.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self, key: str | None = None, headers: list[tuple[str, str]] | None = None
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Initialize an HTTPHeaderSerializer.
|
|
326
|
+
|
|
327
|
+
:param key: An optional key to specifically write. If not set, the
|
|
328
|
+
:py:class:`HTTPHeaderTrait` will be checked instead. Required when
|
|
329
|
+
collecting list entries.
|
|
330
|
+
:param headers: An optional list of header tuples to append to. If not
|
|
331
|
+
set, one will be created.
|
|
332
|
+
"""
|
|
333
|
+
self.headers: list[tuple[str, str]] = headers if headers is not None else []
|
|
334
|
+
self._key = key
|
|
335
|
+
|
|
336
|
+
@contextmanager
|
|
337
|
+
def begin_list(self, schema: Schema, size: int) -> Iterator[ShapeSerializer]:
|
|
338
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
339
|
+
delegate = HTTPHeaderSerializer(key=key, headers=self.headers)
|
|
340
|
+
yield delegate
|
|
341
|
+
|
|
342
|
+
@contextmanager
|
|
343
|
+
def begin_map(self, schema: Schema, size: int) -> Iterator[MapSerializer]:
|
|
344
|
+
prefix = schema.expect_trait(HTTPPrefixHeadersTrait).prefix
|
|
345
|
+
yield HTTPHeaderMapSerializer(prefix, self.headers)
|
|
346
|
+
|
|
347
|
+
def write_boolean(self, schema: Schema, value: bool) -> None:
|
|
348
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
349
|
+
self.headers.append((key, "true" if value else "false"))
|
|
350
|
+
|
|
351
|
+
def write_byte(self, schema: Schema, value: int) -> None:
|
|
352
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
353
|
+
self.headers.append((key, str(value)))
|
|
354
|
+
|
|
355
|
+
def write_short(self, schema: Schema, value: int) -> None:
|
|
356
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
357
|
+
self.headers.append((key, str(value)))
|
|
358
|
+
|
|
359
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
360
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
361
|
+
self.headers.append((key, str(value)))
|
|
362
|
+
|
|
363
|
+
def write_long(self, schema: Schema, value: int) -> None:
|
|
364
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
365
|
+
self.headers.append((key, str(value)))
|
|
366
|
+
|
|
367
|
+
def write_big_integer(self, schema: Schema, value: int) -> None:
|
|
368
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
369
|
+
self.headers.append((key, str(value)))
|
|
370
|
+
|
|
371
|
+
def write_float(self, schema: Schema, value: float) -> None:
|
|
372
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
373
|
+
self.headers.append((key, str(value)))
|
|
374
|
+
|
|
375
|
+
def write_double(self, schema: Schema, value: float) -> None:
|
|
376
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
377
|
+
self.headers.append((key, str(value)))
|
|
378
|
+
|
|
379
|
+
def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
|
|
380
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
381
|
+
self.headers.append((key, str(value.canonical())))
|
|
382
|
+
|
|
383
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
384
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
385
|
+
self.headers.append((key, value))
|
|
386
|
+
|
|
387
|
+
def write_timestamp(self, schema: Schema, value: datetime) -> None:
|
|
388
|
+
key = self._key or schema.expect_trait(HTTPHeaderTrait).key
|
|
389
|
+
format = TimestampFormat.HTTP_DATE
|
|
390
|
+
if (trait := schema.get_trait(TimestampFormatTrait)) is not None:
|
|
391
|
+
format = trait.format
|
|
392
|
+
self.headers.append((key, str(format.serialize(value))))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class HTTPHeaderMapSerializer(MapSerializer):
|
|
396
|
+
"""Binds a mapping property of a serializeable shape to multiple HTTP headers."""
|
|
397
|
+
|
|
398
|
+
def __init__(self, prefix: str, headers: list[tuple[str, str]]) -> None:
|
|
399
|
+
"""Initialize an HTTPHeaderMapSerializer.
|
|
400
|
+
|
|
401
|
+
:param prefix: The prefix to prepend to each of the map keys.
|
|
402
|
+
:param headers: The list of header tuples to append to.
|
|
403
|
+
"""
|
|
404
|
+
self._prefix = prefix
|
|
405
|
+
self._headers = headers
|
|
406
|
+
self._delegate = CapturingSerializer()
|
|
407
|
+
|
|
408
|
+
def entry(self, key: str, value_writer: Callable[[ShapeSerializer], None]):
|
|
409
|
+
value_writer(self._delegate)
|
|
410
|
+
assert self._delegate.result is not None # noqa: S101
|
|
411
|
+
self._headers.append((self._prefix + key, self._delegate.result))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class CapturingSerializer(SpecificShapeSerializer):
|
|
415
|
+
"""Directly passes along a string through a serializer."""
|
|
416
|
+
|
|
417
|
+
result: str | None
|
|
418
|
+
"""The captured string.
|
|
419
|
+
|
|
420
|
+
This will only be set after the serializer has been used.
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
def __init__(self) -> None:
|
|
424
|
+
self.result = None
|
|
425
|
+
|
|
426
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
427
|
+
self.result = value
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class HTTPQuerySerializer(SpecificShapeSerializer):
|
|
431
|
+
"""Binds properties of a serializable shape to HTTP URI query params."""
|
|
432
|
+
|
|
433
|
+
def __init__(
|
|
434
|
+
self, key: str | None = None, params: list[tuple[str, str]] | None = None
|
|
435
|
+
) -> None:
|
|
436
|
+
"""Initialize an HTTPQuerySerializer.
|
|
437
|
+
|
|
438
|
+
:param key: An optional key to specifically write. If not set, the
|
|
439
|
+
:py:class:`HTTPQueryTrait` will be checked instead. Required when
|
|
440
|
+
collecting list or map entries.
|
|
441
|
+
:param headers: An optional list of header tuples to append to. If not
|
|
442
|
+
set, one will be created.
|
|
443
|
+
"""
|
|
444
|
+
self.query_params: list[tuple[str, str]] = params if params is not None else []
|
|
445
|
+
self._key = key
|
|
446
|
+
|
|
447
|
+
@contextmanager
|
|
448
|
+
def begin_list(self, schema: Schema, size: int) -> Iterator[ShapeSerializer]:
|
|
449
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
450
|
+
yield HTTPQuerySerializer(key=key, params=self.query_params)
|
|
451
|
+
|
|
452
|
+
@contextmanager
|
|
453
|
+
def begin_map(self, schema: Schema, size: int) -> Iterator[MapSerializer]:
|
|
454
|
+
yield HTTPQueryMapSerializer(self.query_params)
|
|
455
|
+
|
|
456
|
+
def write_boolean(self, schema: Schema, value: bool) -> None:
|
|
457
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
458
|
+
self.query_params.append((key, "true" if value else "false"))
|
|
459
|
+
|
|
460
|
+
def write_byte(self, schema: Schema, value: int) -> None:
|
|
461
|
+
self.write_integer(schema, value)
|
|
462
|
+
|
|
463
|
+
def write_short(self, schema: Schema, value: int) -> None:
|
|
464
|
+
self.write_integer(schema, value)
|
|
465
|
+
|
|
466
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
467
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
468
|
+
self.query_params.append((key, str(value)))
|
|
469
|
+
|
|
470
|
+
def write_long(self, schema: Schema, value: int) -> None:
|
|
471
|
+
self.write_integer(schema, value)
|
|
472
|
+
|
|
473
|
+
def write_big_integer(self, schema: Schema, value: int) -> None:
|
|
474
|
+
self.write_integer(schema, value)
|
|
475
|
+
|
|
476
|
+
def write_float(self, schema: Schema, value: float) -> None:
|
|
477
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
478
|
+
self.query_params.append((key, serialize_float(value)))
|
|
479
|
+
|
|
480
|
+
def write_double(self, schema: Schema, value: float) -> None:
|
|
481
|
+
self.write_float(schema, value)
|
|
482
|
+
|
|
483
|
+
def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
|
|
484
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
485
|
+
self.query_params.append((key, serialize_float(value)))
|
|
486
|
+
|
|
487
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
488
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
489
|
+
self.query_params.append((key, urlquote(value, safe="")))
|
|
490
|
+
|
|
491
|
+
def write_timestamp(self, schema: Schema, value: datetime) -> None:
|
|
492
|
+
key = self._key or schema.expect_trait(HTTPQueryTrait).key
|
|
493
|
+
format = TimestampFormat.DATE_TIME
|
|
494
|
+
if (trait := schema.get_trait(TimestampFormatTrait)) is not None:
|
|
495
|
+
format = trait.format
|
|
496
|
+
self.query_params.append((key, str(format.serialize(value))))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class HTTPPathSerializer(SpecificShapeSerializer):
|
|
500
|
+
"""Binds properties of a serializable shape to the HTTP URI path."""
|
|
501
|
+
|
|
502
|
+
def __init__(self, path_pattern: PathPattern) -> None:
|
|
503
|
+
"""Initialize an HTTPPathSerializer.
|
|
504
|
+
|
|
505
|
+
:param path_pattern: The pattern to bind properties to. This is also used to
|
|
506
|
+
detect greedy labels, which have different escaping requirements.
|
|
507
|
+
"""
|
|
508
|
+
self._path_pattern = path_pattern
|
|
509
|
+
self._path_params: dict[str, str] = {}
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def path(self) -> str:
|
|
513
|
+
"""Get the formatted path.
|
|
514
|
+
|
|
515
|
+
This must not be accessed before serialization is complete, otherwise an
|
|
516
|
+
exception will be raised.
|
|
517
|
+
"""
|
|
518
|
+
return self._path_pattern.format(**self._path_params)
|
|
519
|
+
|
|
520
|
+
def write_boolean(self, schema: Schema, value: bool) -> None:
|
|
521
|
+
self._path_params[schema.expect_member_name()] = "true" if value else "false"
|
|
522
|
+
|
|
523
|
+
def write_byte(self, schema: Schema, value: int) -> None:
|
|
524
|
+
self.write_integer(schema, value)
|
|
525
|
+
|
|
526
|
+
def write_short(self, schema: Schema, value: int) -> None:
|
|
527
|
+
self.write_integer(schema, value)
|
|
528
|
+
|
|
529
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
530
|
+
self._path_params[schema.expect_member_name()] = str(value)
|
|
531
|
+
|
|
532
|
+
def write_long(self, schema: Schema, value: int) -> None:
|
|
533
|
+
self.write_integer(schema, value)
|
|
534
|
+
|
|
535
|
+
def write_big_integer(self, schema: Schema, value: int) -> None:
|
|
536
|
+
self.write_integer(schema, value)
|
|
537
|
+
|
|
538
|
+
def write_float(self, schema: Schema, value: float) -> None:
|
|
539
|
+
self._path_params[schema.expect_member_name()] = serialize_float(value)
|
|
540
|
+
|
|
541
|
+
def write_double(self, schema: Schema, value: float) -> None:
|
|
542
|
+
self.write_float(schema, value)
|
|
543
|
+
|
|
544
|
+
def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
|
|
545
|
+
self._path_params[schema.expect_member_name()] = serialize_float(value)
|
|
546
|
+
|
|
547
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
548
|
+
key = schema.expect_member_name()
|
|
549
|
+
if key in self._path_pattern.greedy_labels:
|
|
550
|
+
value = urlquote(value)
|
|
551
|
+
else:
|
|
552
|
+
value = urlquote(value, safe="")
|
|
553
|
+
self._path_params[schema.expect_member_name()] = value
|
|
554
|
+
|
|
555
|
+
def write_timestamp(self, schema: Schema, value: datetime) -> None:
|
|
556
|
+
format = TimestampFormat.DATE_TIME
|
|
557
|
+
if (trait := schema.get_trait(TimestampFormatTrait)) is not None:
|
|
558
|
+
format = trait.format
|
|
559
|
+
self._path_params[schema.expect_member_name()] = urlquote(
|
|
560
|
+
str(format.serialize(value))
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class HTTPQueryMapSerializer(MapSerializer):
|
|
565
|
+
"""Binds properties of a serializable shape to a map of HTTP query params."""
|
|
566
|
+
|
|
567
|
+
def __init__(self, query_params: list[tuple[str, str]]) -> None:
|
|
568
|
+
"""Initialize an HTTPQueryMapSerializer.
|
|
569
|
+
|
|
570
|
+
:param query_params: The list of query param tuples to append to.
|
|
571
|
+
"""
|
|
572
|
+
self._query_params = query_params
|
|
573
|
+
self._delegate = CapturingSerializer()
|
|
574
|
+
|
|
575
|
+
def entry(self, key: str, value_writer: Callable[[ShapeSerializer], None]):
|
|
576
|
+
value_writer(self._delegate)
|
|
577
|
+
assert self._delegate.result is not None # noqa: S101
|
|
578
|
+
self._query_params.append((key, urlquote(self._delegate.result, safe="")))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class HostPrefixSerializer(SpecificShapeSerializer):
|
|
582
|
+
"""Binds properites of a serializable shape to the HTTP URI host.
|
|
583
|
+
|
|
584
|
+
These properties are also bound to the payload.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
def __init__(
|
|
588
|
+
self, payload_serializer: ShapeSerializer, host_prefix_pattern: str
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Initialize a HostPrefixSerializer.
|
|
591
|
+
|
|
592
|
+
:param host_prefix_pattern: The pattern to bind properties to.
|
|
593
|
+
:param payload_serializer: The payload serializer to additionally write
|
|
594
|
+
properties to.
|
|
595
|
+
"""
|
|
596
|
+
self._prefix_params: dict[str, str] = {}
|
|
597
|
+
self._host_prefix_pattern = host_prefix_pattern
|
|
598
|
+
self._payload_serializer = payload_serializer
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def host_prefix(self) -> str:
|
|
602
|
+
"""The formatted host prefix.
|
|
603
|
+
|
|
604
|
+
This must not be accessed before serialization is complete, otherwise an
|
|
605
|
+
exception will be raised.
|
|
606
|
+
"""
|
|
607
|
+
return self._host_prefix_pattern.format(**self._prefix_params)
|
|
608
|
+
|
|
609
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
610
|
+
self._payload_serializer.write_string(schema, value)
|
|
611
|
+
self._prefix_params[schema.expect_member_name()] = urlquote(value, safe=".")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class HTTPResponseCodeSerializer(SpecificShapeSerializer):
|
|
615
|
+
"""Binds properties of a serializable shape to the HTTP response code."""
|
|
616
|
+
|
|
617
|
+
response_code: int | None
|
|
618
|
+
"""The bound response code, or None if one hasn't been bound."""
|
|
619
|
+
|
|
620
|
+
def __init__(self) -> None:
|
|
621
|
+
"""Initialize an HTTPResponseCodeSerializer."""
|
|
622
|
+
self.response_code: int | None = None
|
|
623
|
+
|
|
624
|
+
def write_byte(self, schema: Schema, value: int) -> None:
|
|
625
|
+
self.response_code = value
|
|
626
|
+
|
|
627
|
+
def write_short(self, schema: Schema, value: int) -> None:
|
|
628
|
+
self.response_code = value
|
|
629
|
+
|
|
630
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
631
|
+
self.response_code = value
|