Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/botocore/serialize.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

627 statements  

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}