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

622 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 

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}