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