Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/botocore/serialize.py: 23%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13"""Protocol input serializes.
15This module contains classes that implement input serialization
16for the various AWS protocol types.
18These classes essentially take user input, a model object that
19represents what the expected input should look like, and it returns
20a dictionary that contains the various parts of a request. A few
21high level design decisions:
24* Each protocol type maps to a separate class, all inherit from
25 ``Serializer``.
26* The return value for ``serialize_to_request`` (the main entry
27 point) returns a dictionary that represents a request. This
28 will have keys like ``url_path``, ``query_string``, etc. This
29 is done so that it's a) easy to test and b) not tied to a
30 particular HTTP library. See the ``serialize_to_request`` docstring
31 for more details.
33Unicode
34-------
36The input to the serializers should be text (str/unicode), not bytes,
37with the exception of blob types. Those are assumed to be binary,
38and if a str/unicode type is passed in, it will be encoded as utf-8.
39"""
41import base64
42import calendar
43import datetime
44import decimal
45import json
46import math
47import re
48import struct
49from xml.etree import ElementTree
51from botocore import validate
52from botocore.compat import formatdate
53from botocore.exceptions import ParamValidationError
54from botocore.useragent import register_feature_id
55from botocore.utils import (
56 has_header,
57 is_json_value_header,
58 parse_to_aware_datetime,
59 percent_encode,
60)
62# From the spec, the default timestamp format if not specified is iso8601.
63DEFAULT_TIMESTAMP_FORMAT = 'iso8601'
64ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
65# Same as ISO8601, but with microsecond precision.
66ISO8601_MICRO = '%Y-%m-%dT%H:%M:%S.%fZ'
67HOST_PREFIX_RE = re.compile(r"^[A-Za-z0-9\.\-]+$")
69TIMESTAMP_PRECISION_DEFAULT = 'default'
70TIMESTAMP_PRECISION_MILLISECOND = 'millisecond'
71TIMESTAMP_PRECISION_LEGACY = 'legacy'
72TIMESTAMP_PRECISION_OPTIONS = (
73 TIMESTAMP_PRECISION_DEFAULT,
74 TIMESTAMP_PRECISION_MILLISECOND,
75 TIMESTAMP_PRECISION_LEGACY,
76)
79def create_serializer(
80 protocol_name,
81 include_validation=True,
82 timestamp_precision=TIMESTAMP_PRECISION_DEFAULT,
83):
84 """Create a serializer for the given protocol.
85 :param protocol_name: The protocol name to create a serializer for.
86 :type protocol_name: str
87 :param include_validation: Whether to include parameter validation.
88 :type include_validation: bool
89 :param timestamp_precision: Timestamp precision level.
90 - 'default': Full precision of the provided value (up to
91 microseconds) for ISO and Unix timestamps, seconds for RFC
92 - 'millisecond': Millisecond precision (ISO/Unix), seconds for RFC
93 - 'legacy': Behavior prior to sub-second Unix timestamp support.
94 Microseconds for ISO timestamps, seconds for Unix and RFC
95 :type timestamp_precision: str
96 :return: A serializer instance for the given protocol.
97 """
98 # TODO: Unknown protocols.
99 serializer = SERIALIZERS[protocol_name](
100 timestamp_precision=timestamp_precision
101 )
102 if include_validation:
103 validator = validate.ParamValidator()
104 serializer = validate.ParamValidationDecorator(validator, serializer)
105 return serializer
108class Serializer:
109 DEFAULT_METHOD = 'POST'
110 # Clients can change this to a different MutableMapping
111 # (i.e OrderedDict) if they want. This is used in the
112 # compliance test to match the hash ordering used in the
113 # tests.
114 MAP_TYPE = dict
115 DEFAULT_ENCODING = 'utf-8'
117 def __init__(self, timestamp_precision=TIMESTAMP_PRECISION_DEFAULT):
118 if timestamp_precision not in TIMESTAMP_PRECISION_OPTIONS:
119 raise ValueError(
120 f"Invalid timestamp precision found while creating serializer: {timestamp_precision}"
121 )
122 self._timestamp_precision = timestamp_precision
124 def serialize_to_request(self, parameters, operation_model):
125 """Serialize parameters into an HTTP request.
127 This method takes user provided parameters and a shape
128 model and serializes the parameters to an HTTP request.
129 More specifically, this method returns information about
130 parts of the HTTP request, it does not enforce a particular
131 interface or standard for an HTTP request. It instead returns
132 a dictionary of:
134 * 'url_path'
135 * 'host_prefix'
136 * 'query_string'
137 * 'headers'
138 * 'body'
139 * 'method'
141 It is then up to consumers to decide how to map this to a Request
142 object of their HTTP library of choice. Below is an example
143 return value::
145 {'body': {'Action': 'OperationName',
146 'Bar': 'val2',
147 'Foo': 'val1',
148 'Version': '2014-01-01'},
149 'headers': {},
150 'method': 'POST',
151 'query_string': '',
152 'host_prefix': 'value.',
153 'url_path': '/'}
155 :param parameters: The dictionary input parameters for the
156 operation (i.e the user input).
157 :param operation_model: The OperationModel object that describes
158 the operation.
159 """
160 raise NotImplementedError("serialize_to_request")
162 def _create_default_request(self):
163 # Creates a boilerplate default request dict that subclasses
164 # can use as a starting point.
165 serialized = {
166 'url_path': '/',
167 'query_string': '',
168 'method': self.DEFAULT_METHOD,
169 'headers': {},
170 # An empty body is represented as an empty byte string.
171 'body': b'',
172 }
173 return serialized
175 # Some extra utility methods subclasses can use.
177 def _timestamp_iso8601(self, value):
178 """Return ISO8601 timestamp with precision based on timestamp_precision."""
179 # Smithy's standard is milliseconds, so we truncate the timestamp if the millisecond flag is set to true
180 if self._timestamp_precision == TIMESTAMP_PRECISION_MILLISECOND:
181 milliseconds = value.microsecond // 1000
182 return (
183 value.strftime('%Y-%m-%dT%H:%M:%S') + f'.{milliseconds:03d}Z'
184 )
185 else:
186 # Otherwise we continue supporting microseconds in iso8601 for legacy reasons
187 if value.microsecond > 0:
188 timestamp_format = ISO8601_MICRO
189 else:
190 timestamp_format = ISO8601
191 return value.strftime(timestamp_format)
193 def _timestamp_unixtimestamp(self, value):
194 """Return unix timestamp with precision based on timestamp_precision."""
195 timestamp = calendar.timegm(value.timetuple())
196 if self._timestamp_precision == TIMESTAMP_PRECISION_MILLISECOND:
197 milliseconds = (value.microsecond // 1000) / 1000.0
198 return timestamp + milliseconds
199 elif self._timestamp_precision == TIMESTAMP_PRECISION_LEGACY:
200 return timestamp
201 elif value.microsecond > 0:
202 # Add the microseconds as integers before dividing so the float is only
203 # rounded once. Dividing first and then adding rounds twice, which can
204 # produce a slightly different value, e.g. 1 + 3691 / 10**6 gives
205 # 1.0036909999999999 instead of 1.003691.
206 return (timestamp * 10**6 + value.microsecond) / 10**6
207 return timestamp
209 def _timestamp_rfc822(self, value):
210 """Return RFC822 timestamp (always second precision - RFC doesn't support sub-second)."""
211 # RFC 2822 doesn't support sub-second precision, so always use second precision format
212 if isinstance(value, datetime.datetime):
213 value = int(calendar.timegm(value.timetuple()))
214 return formatdate(value, usegmt=True)
216 def _convert_timestamp_to_str(self, value, timestamp_format=None):
217 if timestamp_format is None:
218 timestamp_format = self.TIMESTAMP_FORMAT
219 timestamp_format = timestamp_format.lower()
220 datetime_obj = parse_to_aware_datetime(value)
221 converter = getattr(self, f'_timestamp_{timestamp_format}')
222 final_value = converter(datetime_obj)
223 return final_value
225 def _get_serialized_name(self, shape, default_name):
226 # Returns the serialized name for the shape if it exists.
227 # Otherwise it will return the passed in default_name.
228 return shape.serialization.get('name', default_name)
230 def _get_base64(self, value):
231 # Returns the base64-encoded version of value, handling
232 # both strings and bytes. The returned value is a string
233 # via the default encoding.
234 if isinstance(value, str):
235 value = value.encode(self.DEFAULT_ENCODING)
236 return base64.b64encode(value).strip().decode(self.DEFAULT_ENCODING)
238 def _expand_host_prefix(self, parameters, operation_model):
239 operation_endpoint = operation_model.endpoint
240 if (
241 operation_endpoint is None
242 or 'hostPrefix' not in operation_endpoint
243 ):
244 return None
246 host_prefix_expression = operation_endpoint['hostPrefix']
247 if operation_model.input_shape is None:
248 return host_prefix_expression
249 input_members = operation_model.input_shape.members
250 host_labels = [
251 member
252 for member, shape in input_members.items()
253 if shape.serialization.get('hostLabel')
254 ]
255 format_kwargs = {}
256 bad_labels = []
257 for name in host_labels:
258 param = parameters[name]
259 if not HOST_PREFIX_RE.match(param):
260 bad_labels.append(name)
261 format_kwargs[name] = param
262 if bad_labels:
263 raise ParamValidationError(
264 report=(
265 f"Invalid value for parameter(s): {', '.join(bad_labels)}. "
266 "Must contain only alphanumeric characters, hyphen, "
267 "or period."
268 )
269 )
270 return host_prefix_expression.format(**format_kwargs)
272 def _is_shape_flattened(self, shape):
273 return shape.serialization.get('flattened')
275 def _handle_float(self, value):
276 if value == float("Infinity"):
277 value = "Infinity"
278 elif value == float("-Infinity"):
279 value = "-Infinity"
280 elif math.isnan(value):
281 value = "NaN"
282 return value
284 def _handle_query_compatible_trait(self, operation_model, serialized):
285 if operation_model.service_model.is_query_compatible:
286 serialized['headers']['x-amzn-query-mode'] = 'true'
289class QuerySerializer(Serializer):
290 TIMESTAMP_FORMAT = 'iso8601'
292 def serialize_to_request(self, parameters, operation_model):
293 shape = operation_model.input_shape
294 serialized = self._create_default_request()
295 serialized['method'] = operation_model.http.get(
296 'method', self.DEFAULT_METHOD
297 )
298 serialized['headers'] = {
299 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
300 }
301 # The query serializer only deals with body params so
302 # that's what we hand off the _serialize_* methods.
303 body_params = self.MAP_TYPE()
304 body_params['Action'] = operation_model.name
305 body_params['Version'] = operation_model.metadata['apiVersion']
306 if shape is not None:
307 self._serialize(body_params, parameters, shape)
308 serialized['body'] = body_params
310 host_prefix = self._expand_host_prefix(parameters, operation_model)
311 if host_prefix is not None:
312 serialized['host_prefix'] = host_prefix
314 return serialized
316 def _serialize(self, serialized, value, shape, prefix=''):
317 # serialized: The dict that is incrementally added to with the
318 # final serialized parameters.
319 # value: The current user input value.
320 # shape: The shape object that describes the structure of the
321 # input.
322 # prefix: The incrementally built up prefix for the serialized
323 # key (i.e Foo.bar.members.1).
324 method = getattr(
325 self,
326 f'_serialize_type_{shape.type_name}',
327 self._default_serialize,
328 )
329 method(serialized, value, shape, prefix=prefix)
331 def _serialize_type_structure(self, serialized, value, shape, prefix=''):
332 members = shape.members
333 for key, value in value.items():
334 member_shape = members[key]
335 member_prefix = self._get_serialized_name(member_shape, key)
336 if prefix:
337 member_prefix = f'{prefix}.{member_prefix}'
338 self._serialize(serialized, value, member_shape, member_prefix)
340 def _serialize_type_list(self, serialized, value, shape, prefix=''):
341 if not value:
342 # The query protocol serializes empty lists.
343 serialized[prefix] = ''
344 return
345 if self._is_shape_flattened(shape):
346 list_prefix = prefix
347 if shape.member.serialization.get('name'):
348 name = self._get_serialized_name(shape.member, default_name='')
349 # Replace '.Original' with '.{name}'.
350 list_prefix = '.'.join(prefix.split('.')[:-1] + [name])
351 else:
352 list_name = shape.member.serialization.get('name', 'member')
353 list_prefix = f'{prefix}.{list_name}'
354 for i, element in enumerate(value, 1):
355 element_prefix = f'{list_prefix}.{i}'
356 element_shape = shape.member
357 self._serialize(serialized, element, element_shape, element_prefix)
359 def _serialize_type_map(self, serialized, value, shape, prefix=''):
360 if self._is_shape_flattened(shape):
361 full_prefix = prefix
362 else:
363 full_prefix = f'{prefix}.entry'
364 template = full_prefix + '.{i}.{suffix}'
365 key_shape = shape.key
366 value_shape = shape.value
367 key_suffix = self._get_serialized_name(key_shape, default_name='key')
368 value_suffix = self._get_serialized_name(value_shape, 'value')
369 for i, key in enumerate(value, 1):
370 key_prefix = template.format(i=i, suffix=key_suffix)
371 value_prefix = template.format(i=i, suffix=value_suffix)
372 self._serialize(serialized, key, key_shape, key_prefix)
373 self._serialize(serialized, value[key], value_shape, value_prefix)
375 def _serialize_type_blob(self, serialized, value, shape, prefix=''):
376 # Blob args must be base64 encoded.
377 serialized[prefix] = self._get_base64(value)
379 def _serialize_type_timestamp(self, serialized, value, shape, prefix=''):
380 serialized[prefix] = self._convert_timestamp_to_str(
381 value, shape.serialization.get('timestampFormat')
382 )
384 def _serialize_type_boolean(self, serialized, value, shape, prefix=''):
385 if value:
386 serialized[prefix] = 'true'
387 else:
388 serialized[prefix] = 'false'
390 def _default_serialize(self, serialized, value, shape, prefix=''):
391 serialized[prefix] = value
393 def _serialize_type_float(self, serialized, value, shape, prefix=''):
394 serialized[prefix] = self._handle_float(value)
396 def _serialize_type_double(self, serialized, value, shape, prefix=''):
397 self._serialize_type_float(serialized, value, shape, prefix)
400class EC2Serializer(QuerySerializer):
401 """EC2 specific customizations to the query protocol serializers.
403 The EC2 model is almost, but not exactly, similar to the query protocol
404 serializer. This class encapsulates those differences. The model
405 will have be marked with a ``protocol`` of ``ec2``, so you don't need
406 to worry about wiring this class up correctly.
408 """
410 def _get_serialized_name(self, shape, default_name):
411 # Returns the serialized name for the shape if it exists.
412 # Otherwise it will return the passed in capitalized default_name.
413 if 'queryName' in shape.serialization:
414 return shape.serialization['queryName']
415 elif 'name' in shape.serialization:
416 # A locationName is always capitalized
417 # on input for the ec2 protocol.
418 name = shape.serialization['name']
419 return name[0].upper() + name[1:]
420 else:
421 return default_name
423 def _serialize_type_list(self, serialized, value, shape, prefix=''):
424 for i, element in enumerate(value, 1):
425 element_prefix = f'{prefix}.{i}'
426 element_shape = shape.member
427 self._serialize(serialized, element, element_shape, element_prefix)
430class JSONSerializer(Serializer):
431 TIMESTAMP_FORMAT = 'unixtimestamp'
433 def serialize_to_request(self, parameters, operation_model):
434 target = '{}.{}'.format(
435 operation_model.metadata['targetPrefix'],
436 operation_model.name,
437 )
438 json_version = operation_model.metadata['jsonVersion']
439 serialized = self._create_default_request()
440 serialized['method'] = operation_model.http.get(
441 'method', self.DEFAULT_METHOD
442 )
443 serialized['headers'] = {
444 'X-Amz-Target': target,
445 'Content-Type': f'application/x-amz-json-{json_version}',
446 }
447 self._handle_query_compatible_trait(operation_model, serialized)
449 body = self.MAP_TYPE()
450 input_shape = operation_model.input_shape
451 if input_shape is not None:
452 self._serialize(body, parameters, input_shape)
453 serialized['body'] = json.dumps(body).encode(self.DEFAULT_ENCODING)
455 host_prefix = self._expand_host_prefix(parameters, operation_model)
456 if host_prefix is not None:
457 serialized['host_prefix'] = host_prefix
459 return serialized
461 def _serialize(self, serialized, value, shape, key=None):
462 method = getattr(
463 self,
464 f'_serialize_type_{shape.type_name}',
465 self._default_serialize,
466 )
467 method(serialized, value, shape, key)
469 def _serialize_type_structure(self, serialized, value, shape, key):
470 if shape.is_document_type:
471 serialized[key] = value
472 else:
473 if key is not None:
474 # If a key is provided, this is a result of a recursive
475 # call so we need to add a new child dict as the value
476 # of the passed in serialized dict. We'll then add
477 # all the structure members as key/vals in the new serialized
478 # dictionary we just created.
479 new_serialized = self.MAP_TYPE()
480 serialized[key] = new_serialized
481 serialized = new_serialized
482 members = shape.members
483 for member_key, member_value in value.items():
484 member_shape = members[member_key]
485 if 'name' in member_shape.serialization:
486 member_key = member_shape.serialization['name']
487 self._serialize(
488 serialized, member_value, member_shape, member_key
489 )
491 def _serialize_type_map(self, serialized, value, shape, key):
492 map_obj = self.MAP_TYPE()
493 serialized[key] = map_obj
494 for sub_key, sub_value in value.items():
495 self._serialize(map_obj, sub_value, shape.value, sub_key)
497 def _serialize_type_list(self, serialized, value, shape, key):
498 list_obj = []
499 serialized[key] = list_obj
500 for list_item in value:
501 wrapper = {}
502 # The JSON list serialization is the only case where we aren't
503 # setting a key on a dict. We handle this by using
504 # a __current__ key on a wrapper dict to serialize each
505 # list item before appending it to the serialized list.
506 self._serialize(wrapper, list_item, shape.member, "__current__")
507 list_obj.append(wrapper["__current__"])
509 def _default_serialize(self, serialized, value, shape, key):
510 serialized[key] = value
512 def _serialize_type_timestamp(self, serialized, value, shape, key):
513 serialized[key] = self._convert_timestamp_to_str(
514 value, shape.serialization.get('timestampFormat')
515 )
517 def _serialize_type_blob(self, serialized, value, shape, key):
518 serialized[key] = self._get_base64(value)
520 def _serialize_type_float(self, serialized, value, shape, prefix=''):
521 if isinstance(value, decimal.Decimal):
522 value = float(value)
523 serialized[prefix] = self._handle_float(value)
525 def _serialize_type_double(self, serialized, value, shape, prefix=''):
526 self._serialize_type_float(serialized, value, shape, prefix)
529class CBORSerializer(Serializer):
530 UNSIGNED_INT_MAJOR_TYPE = 0
531 NEGATIVE_INT_MAJOR_TYPE = 1
532 BLOB_MAJOR_TYPE = 2
533 STRING_MAJOR_TYPE = 3
534 LIST_MAJOR_TYPE = 4
535 MAP_MAJOR_TYPE = 5
536 TAG_MAJOR_TYPE = 6
537 FLOAT_AND_SIMPLE_MAJOR_TYPE = 7
539 def _serialize_data_item(self, serialized, value, shape, key=None):
540 method = getattr(self, f'_serialize_type_{shape.type_name}')
541 if method is None:
542 raise ValueError(
543 f"Unrecognized C2J type: {shape.type_name}, unable to "
544 f"serialize request"
545 )
546 method(serialized, value, shape, key)
548 def _serialize_type_integer(self, serialized, value, shape, key):
549 if value >= 0:
550 major_type = self.UNSIGNED_INT_MAJOR_TYPE
551 else:
552 major_type = self.NEGATIVE_INT_MAJOR_TYPE
553 # The only differences in serializing negative and positive integers is
554 # that for negative, we set the major type to 1 and set the value to -1
555 # minus the value
556 value = -1 - value
557 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
558 value
559 )
560 initial_byte = self._get_initial_byte(major_type, additional_info)
561 if num_bytes == 0:
562 serialized.extend(initial_byte)
563 else:
564 serialized.extend(initial_byte + value.to_bytes(num_bytes, "big"))
566 def _serialize_type_long(self, serialized, value, shape, key):
567 self._serialize_type_integer(serialized, value, shape, key)
569 def _serialize_type_blob(self, serialized, value, shape, key):
570 if isinstance(value, str):
571 value = value.encode('utf-8')
572 elif not isinstance(value, (bytes, bytearray)):
573 # We support file-like objects for blobs; these already have been
574 # validated to ensure they have a read method
575 value = value.read()
576 length = len(value)
577 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
578 length
579 )
580 initial_byte = self._get_initial_byte(
581 self.BLOB_MAJOR_TYPE, additional_info
582 )
583 if num_bytes == 0:
584 serialized.extend(initial_byte)
585 else:
586 serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
587 serialized.extend(value)
589 def _serialize_type_string(self, serialized, value, shape, key):
590 encoded = value.encode('utf-8')
591 length = len(encoded)
592 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
593 length
594 )
595 initial_byte = self._get_initial_byte(
596 self.STRING_MAJOR_TYPE, additional_info
597 )
598 if num_bytes == 0:
599 serialized.extend(initial_byte + encoded)
600 else:
601 serialized.extend(
602 initial_byte + length.to_bytes(num_bytes, "big") + encoded
603 )
605 def _serialize_type_list(self, serialized, value, shape, key):
606 length = len(value)
607 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
608 length
609 )
610 initial_byte = self._get_initial_byte(
611 self.LIST_MAJOR_TYPE, additional_info
612 )
613 if num_bytes == 0:
614 serialized.extend(initial_byte)
615 else:
616 serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
617 for item in value:
618 self._serialize_data_item(serialized, item, shape.member)
620 def _serialize_type_map(self, serialized, value, shape, key):
621 length = len(value)
622 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
623 length
624 )
625 initial_byte = self._get_initial_byte(
626 self.MAP_MAJOR_TYPE, additional_info
627 )
628 if num_bytes == 0:
629 serialized.extend(initial_byte)
630 else:
631 serialized.extend(initial_byte + length.to_bytes(num_bytes, "big"))
632 for key_item, item in value.items():
633 self._serialize_data_item(serialized, key_item, shape.key)
634 self._serialize_data_item(serialized, item, shape.value)
636 def _serialize_type_structure(self, serialized, value, shape, key):
637 if key is not None:
638 # For nested structures, we need to serialize the key first
639 self._serialize_data_item(serialized, key, shape.key_shape)
641 # Remove `None` values from the dictionary
642 value = {k: v for k, v in value.items() if v is not None}
644 map_length = len(value)
645 additional_info, num_bytes = self._get_additional_info_and_num_bytes(
646 map_length
647 )
648 initial_byte = self._get_initial_byte(
649 self.MAP_MAJOR_TYPE, additional_info
650 )
651 if num_bytes == 0:
652 serialized.extend(initial_byte)
653 else:
654 serialized.extend(
655 initial_byte + map_length.to_bytes(num_bytes, "big")
656 )
658 members = shape.members
659 for member_key, member_value in value.items():
660 member_shape = members[member_key]
661 if 'name' in member_shape.serialization:
662 member_key = member_shape.serialization['name']
663 if member_value is not None:
664 self._serialize_type_string(serialized, member_key, None, None)
665 self._serialize_data_item(
666 serialized, member_value, member_shape
667 )
669 def _serialize_type_timestamp(self, serialized, value, shape, key):
670 timestamp = self._convert_timestamp_to_str(value)
671 tag = 1 # Use tag 1 for unix timestamp
672 initial_byte = self._get_initial_byte(self.TAG_MAJOR_TYPE, tag)
673 serialized.extend(initial_byte) # Tagging the timestamp
674 # Tag 1 permits either an integer or a floating-point epoch seconds
675 # value; a float is used when sub-second precision is present.
676 if isinstance(timestamp, float):
677 self._serialize_type_double(serialized, timestamp, shape, key)
678 else:
679 self._serialize_type_integer(serialized, timestamp, shape, key)
681 def _serialize_type_float(self, serialized, value, shape, key):
682 if self._is_special_number(value):
683 serialized.extend(
684 self._get_bytes_for_special_numbers(value)
685 ) # Handle special values like NaN or Infinity
686 else:
687 initial_byte = self._get_initial_byte(
688 self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 26
689 )
690 serialized.extend(initial_byte + struct.pack(">f", value))
692 def _serialize_type_double(self, serialized, value, shape, key):
693 if self._is_special_number(value):
694 serialized.extend(
695 self._get_bytes_for_special_numbers(value)
696 ) # Handle special values like NaN or Infinity
697 else:
698 initial_byte = self._get_initial_byte(
699 self.FLOAT_AND_SIMPLE_MAJOR_TYPE, 27
700 )
701 serialized.extend(initial_byte + struct.pack(">d", value))
703 def _serialize_type_boolean(self, serialized, value, shape, key):
704 additional_info = 21 if value else 20
705 serialized.extend(
706 self._get_initial_byte(
707 self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info
708 )
709 )
711 def _get_additional_info_and_num_bytes(self, value):
712 # Values under 24 can be stored in the initial byte and don't need further
713 # encoding
714 if value < 24:
715 return value, 0
716 # Values between 24 and 255 (inclusive) can be stored in 1 byte and
717 # correspond to additional info 24
718 elif value < 256:
719 return 24, 1
720 # Values up to 65535 can be stored in two bytes and correspond to additional
721 # info 25
722 elif value < 65536:
723 return 25, 2
724 # Values up to 4294967296 can be stored in four bytes and correspond to
725 # additional info 26
726 elif value < 4294967296:
727 return 26, 4
728 # The maximum number of bytes in a definite length data items is 8 which
729 # to additional info 27
730 else:
731 return 27, 8
733 def _get_initial_byte(self, major_type, additional_info):
734 # The highest order three bits are the major type, so we need to bitshift the
735 # major type by 5
736 major_type_bytes = major_type << 5
737 return (major_type_bytes | additional_info).to_bytes(1, "big")
739 def _is_special_number(self, value):
740 return any(
741 [
742 value == float('inf'),
743 value == float('-inf'),
744 math.isnan(value),
745 ]
746 )
748 def _get_bytes_for_special_numbers(self, value):
749 additional_info = 25
750 initial_byte = self._get_initial_byte(
751 self.FLOAT_AND_SIMPLE_MAJOR_TYPE, additional_info
752 )
753 if value == float('inf'):
754 return initial_byte + struct.pack(">H", 0x7C00)
755 elif value == float('-inf'):
756 return initial_byte + struct.pack(">H", 0xFC00)
757 elif math.isnan(value):
758 return initial_byte + struct.pack(">H", 0x7E00)
761class BaseRestSerializer(Serializer):
762 """Base class for rest protocols.
764 The only variance between the various rest protocols is the
765 way that the body is serialized. All other aspects (headers, uri, etc.)
766 are the same and logic for serializing those aspects lives here.
768 Subclasses must implement the ``_serialize_body_params`` method.
770 """
772 QUERY_STRING_TIMESTAMP_FORMAT = 'iso8601'
773 HEADER_TIMESTAMP_FORMAT = 'rfc822'
774 # This is a list of known values for the "location" key in the
775 # serialization dict. The location key tells us where on the request
776 # to put the serialized value.
777 KNOWN_LOCATIONS = ['uri', 'querystring', 'header', 'headers']
779 def serialize_to_request(self, parameters, operation_model):
780 serialized = self._create_default_request()
781 serialized['method'] = operation_model.http.get(
782 'method', self.DEFAULT_METHOD
783 )
784 shape = operation_model.input_shape
786 host_prefix = self._expand_host_prefix(parameters, operation_model)
787 if host_prefix is not None:
788 serialized['host_prefix'] = host_prefix
790 if shape is None:
791 serialized['url_path'] = operation_model.http['requestUri']
792 return serialized
793 shape_members = shape.members
794 # While the ``serialized`` key holds the final serialized request
795 # data, we need interim dicts for the various locations of the
796 # request. We need this for the uri_path_kwargs and the
797 # query_string_kwargs because they are templated, so we need
798 # to gather all the needed data for the string template,
799 # then we render the template. The body_kwargs is needed
800 # because once we've collected them all, we run them through
801 # _serialize_body_params, which for rest-json, creates JSON,
802 # and for rest-xml, will create XML. This is what the
803 # ``partitioned`` dict below is for.
804 partitioned = {
805 'uri_path_kwargs': self.MAP_TYPE(),
806 'query_string_kwargs': self.MAP_TYPE(),
807 'body_kwargs': self.MAP_TYPE(),
808 'headers': self.MAP_TYPE(),
809 }
810 for param_name, param_value in parameters.items():
811 if param_value is None:
812 # Don't serialize any parameter with a None value.
813 continue
814 self._partition_parameters(
815 partitioned, param_name, param_value, shape_members
816 )
817 serialized['url_path'] = self._render_uri_template(
818 operation_model.http['requestUri'], partitioned['uri_path_kwargs']
819 )
821 if 'authPath' in operation_model.http:
822 serialized['auth_path'] = self._render_uri_template(
823 operation_model.http['authPath'],
824 partitioned['uri_path_kwargs'],
825 )
826 # Note that we lean on the http implementation to handle the case
827 # where the requestUri path already has query parameters.
828 # The bundled http client, requests, already supports this.
829 serialized['query_string'] = partitioned['query_string_kwargs']
830 if partitioned['headers']:
831 serialized['headers'] = partitioned['headers']
832 self._serialize_payload(
833 partitioned, parameters, serialized, shape, shape_members
834 )
835 self._serialize_content_type(serialized, shape, shape_members)
837 return serialized
839 def _render_uri_template(self, uri_template, params):
840 # We need to handle two cases::
841 #
842 # /{Bucket}/foo
843 # /{Key+}/bar
844 # A label ending with '+' is greedy. There can only
845 # be one greedy key.
846 encoded_params = {}
847 for template_param in re.findall(r'{(.*?)}', uri_template):
848 if template_param.endswith('+'):
849 encoded_params[template_param] = percent_encode(
850 params[template_param[:-1]], safe='/~'
851 )
852 else:
853 encoded_params[template_param] = percent_encode(
854 params[template_param]
855 )
856 return uri_template.format(**encoded_params)
858 def _serialize_payload(
859 self, partitioned, parameters, serialized, shape, shape_members
860 ):
861 # partitioned - The user input params partitioned by location.
862 # parameters - The user input params.
863 # serialized - The final serialized request dict.
864 # shape - Describes the expected input shape
865 # shape_members - The members of the input struct shape
866 payload_member = shape.serialization.get('payload')
867 if self._has_streaming_payload(payload_member, shape_members):
868 # If it's streaming, then the body is just the
869 # value of the payload.
870 body_payload = parameters.get(payload_member, b'')
871 body_payload = self._encode_payload(body_payload)
872 serialized['body'] = body_payload
873 elif payload_member is not None:
874 # If there's a payload member, we serialized that
875 # member to they body.
876 body_params = parameters.get(payload_member)
877 if body_params is not None:
878 serialized['body'] = self._serialize_body_params(
879 body_params, shape_members[payload_member]
880 )
881 else:
882 serialized['body'] = self._serialize_empty_body()
883 elif partitioned['body_kwargs']:
884 serialized['body'] = self._serialize_body_params(
885 partitioned['body_kwargs'], shape
886 )
887 elif self._requires_empty_body(shape):
888 serialized['body'] = self._serialize_empty_body()
890 def _serialize_empty_body(self):
891 return b''
893 def _serialize_content_type(self, serialized, shape, shape_members):
894 """
895 Some protocols require varied Content-Type headers
896 depending on user input. This allows subclasses to apply
897 this conditionally.
898 """
899 pass
901 def _requires_empty_body(self, shape):
902 """
903 Some protocols require a specific body to represent an empty
904 payload. This allows subclasses to apply this conditionally.
905 """
906 return False
908 def _has_streaming_payload(self, payload, shape_members):
909 """Determine if payload is streaming (a blob or string)."""
910 return payload is not None and shape_members[payload].type_name in (
911 'blob',
912 'string',
913 )
915 def _encode_payload(self, body):
916 if isinstance(body, str):
917 return body.encode(self.DEFAULT_ENCODING)
918 return body
920 def _partition_parameters(
921 self, partitioned, param_name, param_value, shape_members
922 ):
923 # This takes the user provided input parameter (``param``)
924 # and figures out where they go in the request dict.
925 # Some params are HTTP headers, some are used in the URI, some
926 # are in the request body. This method deals with this.
927 member = shape_members[param_name]
928 location = member.serialization.get('location')
929 key_name = member.serialization.get('name', param_name)
930 if location == 'uri':
931 uri_path_value = self._get_uri_and_query_string_value(
932 param_value, member
933 )
934 partitioned['uri_path_kwargs'][key_name] = uri_path_value
935 elif location == 'querystring':
936 if isinstance(param_value, dict):
937 partitioned['query_string_kwargs'].update(param_value)
938 elif member.type_name == 'list':
939 new_param = [
940 self._get_uri_and_query_string_value(value, member.member)
941 for value in param_value
942 ]
943 partitioned['query_string_kwargs'][key_name] = new_param
944 else:
945 new_param = self._get_uri_and_query_string_value(
946 param_value, member
947 )
948 partitioned['query_string_kwargs'][key_name] = new_param
949 elif location == 'header':
950 shape = shape_members[param_name]
951 if not param_value and shape.type_name == 'list':
952 # Empty lists should not be set on the headers
953 return
954 partitioned['headers'][key_name] = self._convert_header_value(
955 shape, param_value
956 )
957 elif location == 'headers':
958 # 'headers' is a bit of an oddball. The ``key_name``
959 # is actually really a prefix for the header names:
960 header_prefix = key_name
961 # The value provided by the user is a dict so we'll be
962 # creating multiple header key/val pairs. The key
963 # name to use for each header is the header_prefix (``key_name``)
964 # plus the key provided by the user.
965 self._do_serialize_header_map(
966 header_prefix, partitioned['headers'], param_value
967 )
968 else:
969 partitioned['body_kwargs'][param_name] = param_value
971 def _get_uri_and_query_string_value(self, param_value, member):
972 if member.type_name == 'boolean':
973 return str(param_value).lower()
974 elif member.type_name == 'timestamp':
975 timestamp_format = member.serialization.get(
976 'timestampFormat', self.QUERY_STRING_TIMESTAMP_FORMAT
977 )
978 return self._convert_timestamp_to_str(
979 param_value, timestamp_format
980 )
981 elif member.type_name in ['float', 'double']:
982 return str(self._handle_float(param_value))
983 return param_value
985 def _do_serialize_header_map(self, header_prefix, headers, user_input):
986 for key, val in user_input.items():
987 full_key = header_prefix + key
988 headers[full_key] = val
990 def _serialize_body_params(self, params, shape):
991 raise NotImplementedError('_serialize_body_params')
993 def _convert_header_value(self, shape, value):
994 if shape.type_name == 'timestamp':
995 datetime_obj = parse_to_aware_datetime(value)
996 timestamp = calendar.timegm(datetime_obj.utctimetuple())
997 timestamp_format = shape.serialization.get(
998 'timestampFormat', self.HEADER_TIMESTAMP_FORMAT
999 )
1000 return str(
1001 self._convert_timestamp_to_str(timestamp, timestamp_format)
1002 )
1003 elif shape.type_name == 'list':
1004 if shape.member.type_name == "string":
1005 converted_value = [
1006 self._escape_header_list_string(v)
1007 for v in value
1008 if v is not None
1009 ]
1010 else:
1011 converted_value = [
1012 self._convert_header_value(shape.member, v)
1013 for v in value
1014 if v is not None
1015 ]
1016 return ",".join(converted_value)
1017 elif is_json_value_header(shape):
1018 # Serialize with no spaces after separators to save space in
1019 # the header.
1020 return self._get_base64(json.dumps(value, separators=(',', ':')))
1021 elif shape.type_name == 'boolean':
1022 return str(value).lower()
1023 elif shape.type_name in ['float', 'double']:
1024 return str(self._handle_float(value))
1025 else:
1026 return str(value)
1028 def _escape_header_list_string(self, value):
1029 # Escapes a header list string by wrapping it in double quotes if it contains
1030 # a comma or a double quote, and escapes any internal double quotes.
1031 if '"' in value or ',' in value:
1032 return '"' + value.replace('"', '\\"') + '"'
1033 else:
1034 return value
1037class BaseRpcV2Serializer(Serializer):
1038 """Base class for RPCv2 protocols.
1040 The only variance between the various RPCv2 protocols is the
1041 way that the body is serialized. All other aspects (headers, uri, etc.)
1042 are the same and logic for serializing those aspects lives here.
1044 Subclasses must implement the ``_serialize_body_params`` and
1045 ``_serialize_headers`` methods.
1047 """
1049 def serialize_to_request(self, parameters, operation_model):
1050 serialized = self._create_default_request()
1051 service_name = operation_model.service_model.metadata['targetPrefix']
1052 operation_name = operation_model.name
1053 serialized['url_path'] = (
1054 f'/service/{service_name}/operation/{operation_name}'
1055 )
1057 input_shape = operation_model.input_shape
1058 if input_shape is not None:
1059 self._serialize_payload(parameters, serialized, input_shape)
1061 host_prefix = self._expand_host_prefix(parameters, operation_model)
1062 if host_prefix is not None:
1063 serialized['host_prefix'] = host_prefix
1065 self._serialize_headers(serialized, operation_model)
1067 return serialized
1069 def _serialize_payload(self, parameters, serialized, shape):
1070 body_payload = self._serialize_body_params(parameters, shape)
1071 serialized['body'] = body_payload
1073 def _serialize_headers(self, serialized, operation_model):
1074 raise NotImplementedError("_serialize_headers")
1076 def _serialize_body_params(self, parameters, shape):
1077 raise NotImplementedError("_serialize_body_params")
1080class RestJSONSerializer(BaseRestSerializer, JSONSerializer):
1081 def _serialize_empty_body(self):
1082 return b'{}'
1084 def _requires_empty_body(self, shape):
1085 """
1086 Serialize an empty JSON object whenever the shape has
1087 members not targeting a location.
1088 """
1089 for member, val in shape.members.items():
1090 if 'location' not in val.serialization:
1091 return True
1092 return False
1094 def _serialize_content_type(self, serialized, shape, shape_members):
1095 """Set Content-Type to application/json for all structured bodies."""
1096 payload = shape.serialization.get('payload')
1097 if self._has_streaming_payload(payload, shape_members):
1098 # Don't apply content-type to streaming bodies
1099 return
1101 has_body = serialized['body'] != b''
1102 has_content_type = has_header('Content-Type', serialized['headers'])
1103 if has_body and not has_content_type:
1104 serialized['headers']['Content-Type'] = 'application/json'
1106 def _serialize_body_params(self, params, shape):
1107 serialized_body = self.MAP_TYPE()
1108 self._serialize(serialized_body, params, shape)
1109 return json.dumps(serialized_body).encode(self.DEFAULT_ENCODING)
1112class RestXMLSerializer(BaseRestSerializer):
1113 TIMESTAMP_FORMAT = 'iso8601'
1115 def _serialize_body_params(self, params, shape):
1116 root_name = shape.serialization['name']
1117 pseudo_root = ElementTree.Element('')
1118 self._serialize(shape, params, pseudo_root, root_name)
1119 real_root = list(pseudo_root)[0]
1120 return ElementTree.tostring(real_root, encoding=self.DEFAULT_ENCODING)
1122 def _serialize(self, shape, params, xmlnode, name):
1123 method = getattr(
1124 self,
1125 f'_serialize_type_{shape.type_name}',
1126 self._default_serialize,
1127 )
1128 method(xmlnode, params, shape, name)
1130 def _serialize_type_structure(self, xmlnode, params, shape, name):
1131 structure_node = ElementTree.SubElement(xmlnode, name)
1133 self._add_xml_namespace(shape, structure_node)
1134 for key, value in params.items():
1135 member_shape = shape.members[key]
1136 member_name = member_shape.serialization.get('name', key)
1137 # We need to special case member shapes that are marked as an
1138 # xmlAttribute. Rather than serializing into an XML child node,
1139 # we instead serialize the shape to an XML attribute of the
1140 # *current* node.
1141 if value is None:
1142 # Don't serialize any param whose value is None.
1143 return
1144 if member_shape.serialization.get('xmlAttribute'):
1145 # xmlAttributes must have a serialization name.
1146 xml_attribute_name = member_shape.serialization['name']
1147 structure_node.attrib[xml_attribute_name] = value
1148 continue
1149 self._serialize(member_shape, value, structure_node, member_name)
1151 def _serialize_type_list(self, xmlnode, params, shape, name):
1152 member_shape = shape.member
1153 if shape.serialization.get('flattened'):
1154 element_name = name
1155 list_node = xmlnode
1156 else:
1157 element_name = member_shape.serialization.get('name', 'member')
1158 list_node = ElementTree.SubElement(xmlnode, name)
1159 self._add_xml_namespace(shape, list_node)
1160 for item in params:
1161 self._serialize(member_shape, item, list_node, element_name)
1163 def _serialize_type_map(self, xmlnode, params, shape, name):
1164 # Given the ``name`` of MyMap, and input of {"key1": "val1"}
1165 # we serialize this as:
1166 # <MyMap>
1167 # <entry>
1168 # <key>key1</key>
1169 # <value>val1</value>
1170 # </entry>
1171 # </MyMap>
1172 if not self._is_shape_flattened(shape):
1173 node = ElementTree.SubElement(xmlnode, name)
1174 self._add_xml_namespace(shape, node)
1176 for key, value in params.items():
1177 sub_node = (
1178 ElementTree.SubElement(xmlnode, name)
1179 if self._is_shape_flattened(shape)
1180 else ElementTree.SubElement(node, 'entry')
1181 )
1182 key_name = self._get_serialized_name(shape.key, default_name='key')
1183 val_name = self._get_serialized_name(
1184 shape.value, default_name='value'
1185 )
1186 self._serialize(shape.key, key, sub_node, key_name)
1187 self._serialize(shape.value, value, sub_node, val_name)
1189 def _serialize_type_boolean(self, xmlnode, params, shape, name):
1190 # For scalar types, the 'params' attr is actually just a scalar
1191 # value representing the data we need to serialize as a boolean.
1192 # It will either be 'true' or 'false'
1193 node = ElementTree.SubElement(xmlnode, name)
1194 if params:
1195 str_value = 'true'
1196 else:
1197 str_value = 'false'
1198 node.text = str_value
1199 self._add_xml_namespace(shape, node)
1201 def _serialize_type_blob(self, xmlnode, params, shape, name):
1202 node = ElementTree.SubElement(xmlnode, name)
1203 node.text = self._get_base64(params)
1204 self._add_xml_namespace(shape, node)
1206 def _serialize_type_timestamp(self, xmlnode, params, shape, name):
1207 node = ElementTree.SubElement(xmlnode, name)
1208 node.text = str(
1209 self._convert_timestamp_to_str(
1210 params, shape.serialization.get('timestampFormat')
1211 )
1212 )
1213 self._add_xml_namespace(shape, node)
1215 def _serialize_type_float(self, xmlnode, params, shape, name):
1216 node = ElementTree.SubElement(xmlnode, name)
1217 node.text = str(self._handle_float(params))
1218 self._add_xml_namespace(shape, node)
1220 def _serialize_type_double(self, xmlnode, params, shape, name):
1221 self._serialize_type_float(xmlnode, params, shape, name)
1223 def _default_serialize(self, xmlnode, params, shape, name):
1224 node = ElementTree.SubElement(xmlnode, name)
1225 node.text = str(params)
1226 self._add_xml_namespace(shape, node)
1228 def _add_xml_namespace(self, shape, structure_node):
1229 if 'xmlNamespace' in shape.serialization:
1230 namespace_metadata = shape.serialization['xmlNamespace']
1231 attribute_name = 'xmlns'
1232 if isinstance(namespace_metadata, dict):
1233 if namespace_metadata.get('prefix'):
1234 attribute_name += f":{namespace_metadata['prefix']}"
1235 structure_node.attrib[attribute_name] = namespace_metadata[
1236 'uri'
1237 ]
1238 elif isinstance(namespace_metadata, str):
1239 structure_node.attrib[attribute_name] = namespace_metadata
1242class RpcV2CBORSerializer(BaseRpcV2Serializer, CBORSerializer):
1243 TIMESTAMP_FORMAT = 'unixtimestamp'
1245 def serialize_to_request(self, parameters, operation_model):
1246 register_feature_id('PROTOCOL_RPC_V2_CBOR')
1247 return super().serialize_to_request(parameters, operation_model)
1249 def _serialize_body_params(self, parameters, input_shape):
1250 body = bytearray()
1251 self._serialize_data_item(body, parameters, input_shape)
1252 return bytes(body)
1254 def _serialize_headers(self, serialized, operation_model):
1255 serialized['headers']['smithy-protocol'] = 'rpc-v2-cbor'
1257 if operation_model.has_event_stream_output:
1258 header_val = 'application/vnd.amazon.eventstream'
1259 else:
1260 header_val = 'application/cbor'
1261 self._handle_query_compatible_trait(operation_model, serialized)
1263 has_body = serialized['body'] != b''
1264 has_content_type = has_header('Content-Type', serialized['headers'])
1266 serialized['headers']['Accept'] = header_val
1267 if not has_content_type and has_body:
1268 serialized['headers']['Content-Type'] = header_val
1271SERIALIZERS = {
1272 'ec2': EC2Serializer,
1273 'query': QuerySerializer,
1274 'json': JSONSerializer,
1275 'rest-json': RestJSONSerializer,
1276 'rest-xml': RestXMLSerializer,
1277 'smithy-rpc-v2-cbor': RpcV2CBORSerializer,
1278}