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