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.
@@ -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