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