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

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

190 statements  

1# Copyright 2023 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""" 

14NOTE: All classes and functions in this module are considered private and are 

15subject to abrupt breaking changes. Please do not use them directly. 

16 

17To modify the User-Agent header sent by botocore, use one of these 

18configuration options: 

19* The ``AWS_SDK_UA_APP_ID`` environment variable. 

20* The ``sdk_ua_app_id`` setting in the shared AWS config file. 

21* The ``user_agent_appid`` field in the :py:class:`botocore.config.Config`. 

22* The ``user_agent_extra`` field in the :py:class:`botocore.config.Config`. 

23 

24""" 

25 

26import logging 

27import os 

28import platform 

29from copy import copy 

30from string import ascii_letters, digits 

31from typing import NamedTuple, Optional 

32 

33from botocore import __version__ as botocore_version 

34from botocore.compat import HAS_CRT 

35from botocore.context import get_context 

36 

37logger = logging.getLogger(__name__) 

38 

39 

40_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~," 

41_USERAGENT_ALLOWED_OS_NAMES = ( 

42 'windows', 

43 'linux', 

44 'macos', 

45 'android', 

46 'ios', 

47 'watchos', 

48 'tvos', 

49 'other', 

50) 

51_USERAGENT_PLATFORM_NAME_MAPPINGS = {'darwin': 'macos'} 

52# The name by which botocore is identified in the User-Agent header. While most 

53# AWS SDKs follow a naming pattern of "aws-sdk-*", botocore and boto3 continue 

54# using their existing values. Uses uppercase "B" with all other characters 

55# lowercase. 

56_USERAGENT_SDK_NAME = 'Botocore' 

57_USERAGENT_FEATURE_MAPPINGS = { 

58 'WAITER': 'B', 

59 'PAGINATOR': 'C', 

60 "RETRY_MODE_LEGACY": "D", 

61 "RETRY_MODE_STANDARD": "E", 

62 "RETRY_MODE_ADAPTIVE": "F", 

63 'S3_TRANSFER': 'G', 

64 'GZIP_REQUEST_COMPRESSION': 'L', 

65 'PROTOCOL_RPC_V2_CBOR': 'M', 

66 'ENDPOINT_OVERRIDE': 'N', 

67 'ACCOUNT_ID_MODE_PREFERRED': 'P', 

68 'ACCOUNT_ID_MODE_DISABLED': 'Q', 

69 'ACCOUNT_ID_MODE_REQUIRED': 'R', 

70 'SIGV4A_SIGNING': 'S', 

71 'RESOLVED_ACCOUNT_ID': 'T', 

72 'FLEXIBLE_CHECKSUMS_REQ_CRC32': 'U', 

73 'FLEXIBLE_CHECKSUMS_REQ_CRC32C': 'V', 

74 'FLEXIBLE_CHECKSUMS_REQ_CRC64': 'W', 

75 'FLEXIBLE_CHECKSUMS_REQ_SHA1': 'X', 

76 'FLEXIBLE_CHECKSUMS_REQ_SHA256': 'Y', 

77 'FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED': 'Z', 

78 'FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED': 'a', 

79 'FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED': 'b', 

80 'FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED': 'c', 

81 'CREDENTIALS_CODE': 'e', 

82 'CREDENTIALS_ENV_VARS': 'g', 

83 'CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN': 'h', 

84 'CREDENTIALS_STS_ASSUME_ROLE': 'i', 

85 'CREDENTIALS_STS_ASSUME_ROLE_WEB_ID': 'k', 

86 'CREDENTIALS_PROFILE': 'n', 

87 'CREDENTIALS_PROFILE_SOURCE_PROFILE': 'o', 

88 'CREDENTIALS_PROFILE_NAMED_PROVIDER': 'p', 

89 'CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN': 'q', 

90 'CREDENTIALS_PROFILE_SSO': 'r', 

91 'CREDENTIALS_SSO': 's', 

92 'CREDENTIALS_PROFILE_SSO_LEGACY': 't', 

93 'CREDENTIALS_SSO_LEGACY': 'u', 

94 'CREDENTIALS_PROFILE_PROCESS': 'v', 

95 'CREDENTIALS_PROCESS': 'w', 

96 'CREDENTIALS_BOTO2_CONFIG_FILE': 'x', 

97 'CREDENTIALS_HTTP': 'z', 

98 'CREDENTIALS_IMDS': '0', 

99 'BEARER_SERVICE_ENV_VARS': '3', 

100} 

101 

102 

103def register_feature_id(feature_id): 

104 """Adds metric value to the current context object's ``features`` set. 

105 

106 :type feature_id: str 

107 :param feature_id: The name of the feature to register. Value must be a key 

108 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

109 """ 

110 ctx = get_context() 

111 if ctx is None: 

112 # Never register features outside the scope of a 

113 # ``botocore.context.start_as_current_context`` context manager. 

114 # Otherwise, the context variable won't be reset and features will 

115 # bleed into all subsequent requests. Return instead of raising an 

116 # exception since this function could be invoked in a public interface. 

117 return 

118 if val := _USERAGENT_FEATURE_MAPPINGS.get(feature_id): 

119 ctx.features.add(val) 

120 

121 

122def register_feature_ids(feature_ids): 

123 """Adds multiple feature IDs to the current context object's ``features`` set. 

124 

125 :type feature_ids: iterable of str 

126 :param feature_ids: An iterable of feature ID strings to register. Each 

127 value must be a key in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

128 """ 

129 for feature_id in feature_ids: 

130 register_feature_id(feature_id) 

131 

132 

133def sanitize_user_agent_string_component(raw_str, allow_hash): 

134 """Replaces all not allowed characters in the string with a dash ("-"). 

135 

136 Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~,``. If 

137 ``allow_hash`` is ``True``, "#"``" is also allowed. 

138 

139 :type raw_str: str 

140 :param raw_str: The input string to be sanitized. 

141 

142 :type allow_hash: bool 

143 :param allow_hash: Whether "#" is considered an allowed character. 

144 """ 

145 return ''.join( 

146 c 

147 if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == '#') 

148 else '-' 

149 for c in raw_str 

150 ) 

151 

152 

153class UserAgentComponentSizeConfig: 

154 """ 

155 Configures the max size of a built user agent string component and the 

156 delimiter used to truncate the string if the size is above the max. 

157 """ 

158 

159 def __init__(self, max_size_in_bytes: int, delimiter: str): 

160 self.max_size_in_bytes = max_size_in_bytes 

161 self.delimiter = delimiter 

162 self._validate_input() 

163 

164 def _validate_input(self): 

165 if self.max_size_in_bytes < 1: 

166 raise ValueError( 

167 f'Invalid `max_size_in_bytes`: {self.max_size_in_bytes}. ' 

168 'Value must be a positive integer.' 

169 ) 

170 

171 

172class UserAgentComponent(NamedTuple): 

173 """ 

174 Component of a Botocore User-Agent header string in the standard format. 

175 

176 Each component consists of a prefix, a name, a value, and a size_config. 

177 In the string representation these are combined in the format 

178 ``prefix/name#value``. 

179 

180 ``size_config`` configures the max size and truncation strategy for the 

181 built user agent string component. 

182 

183 This class is considered private and is subject to abrupt breaking changes. 

184 """ 

185 

186 prefix: str 

187 name: str 

188 value: Optional[str] = None 

189 size_config: Optional[UserAgentComponentSizeConfig] = None 

190 

191 def to_string(self): 

192 """Create string like 'prefix/name#value' from a UserAgentComponent.""" 

193 clean_prefix = sanitize_user_agent_string_component( 

194 self.prefix, allow_hash=True 

195 ) 

196 clean_name = sanitize_user_agent_string_component( 

197 self.name, allow_hash=False 

198 ) 

199 if self.value is None or self.value == '': 

200 clean_string = f'{clean_prefix}/{clean_name}' 

201 else: 

202 clean_value = sanitize_user_agent_string_component( 

203 self.value, allow_hash=True 

204 ) 

205 clean_string = f'{clean_prefix}/{clean_name}#{clean_value}' 

206 if self.size_config is not None: 

207 clean_string = self._truncate_string( 

208 clean_string, 

209 self.size_config.max_size_in_bytes, 

210 self.size_config.delimiter, 

211 ) 

212 return clean_string 

213 

214 def _truncate_string(self, string, max_size, delimiter): 

215 """ 

216 Pop ``delimiter``-separated values until encoded string is less than or 

217 equal to ``max_size``. 

218 """ 

219 orig = string 

220 while len(string.encode('utf-8')) > max_size: 

221 parts = string.split(delimiter) 

222 parts.pop() 

223 string = delimiter.join(parts) 

224 

225 if string == '': 

226 logger.debug( 

227 "User agent component `%s` could not be truncated to " 

228 "`%s` bytes with delimiter " 

229 "`%s` without losing all contents. " 

230 "Value will be omitted from user agent string.", 

231 orig, 

232 max_size, 

233 delimiter, 

234 ) 

235 return string 

236 

237 

238class RawStringUserAgentComponent: 

239 """ 

240 UserAgentComponent interface wrapper around ``str``. 

241 

242 Use for User-Agent header components that are not constructed from 

243 prefix+name+value but instead are provided as strings. No sanitization is 

244 performed. 

245 """ 

246 

247 def __init__(self, value): 

248 self._value = value 

249 

250 def to_string(self): 

251 return self._value 

252 

253 

254# This is not a public interface and is subject to abrupt breaking changes. 

255# Any usage is not advised or supported in external code bases. 

256try: 

257 from botocore.customizations.useragent import modify_components 

258except ImportError: 

259 # Default implementation that returns unmodified User-Agent components. 

260 def modify_components(components): 

261 return components 

262 

263 

264class UserAgentString: 

265 """ 

266 Generator for AWS SDK User-Agent header strings. 

267 

268 The User-Agent header format contains information from session, client, and 

269 request context. ``UserAgentString`` provides methods for collecting the 

270 information and ``to_string`` for assembling it into the standardized 

271 string format. 

272 

273 Example usage: 

274 

275 ua_session = UserAgentString.from_environment() 

276 ua_session.set_session_config(...) 

277 ua_client = ua_session.with_client_config(Config(...)) 

278 ua_string = ua_request.to_string() 

279 

280 For testing or when information from all sources is available at the same 

281 time, the methods can be chained: 

282 

283 ua_string = ( 

284 UserAgentString 

285 .from_environment() 

286 .set_session_config(...) 

287 .with_client_config(Config(...)) 

288 .to_string() 

289 ) 

290 

291 """ 

292 

293 def __init__( 

294 self, 

295 platform_name, 

296 platform_version, 

297 platform_machine, 

298 python_version, 

299 python_implementation, 

300 execution_env, 

301 crt_version=None, 

302 ): 

303 """ 

304 :type platform_name: str 

305 :param platform_name: Name of the operating system or equivalent 

306 platform name. Should be sourced from :py:meth:`platform.system`. 

307 :type platform_version: str 

308 :param platform_version: Version of the operating system or equivalent 

309 platform name. Should be sourced from :py:meth:`platform.version`. 

310 :type platform_machine: str 

311 :param platform_version: Processor architecture or machine type. For 

312 example "x86_64". Should be sourced from :py:meth:`platform.machine`. 

313 :type python_version: str 

314 :param python_version: Version of the python implementation as str. 

315 Should be sourced from :py:meth:`platform.python_version`. 

316 :type python_implementation: str 

317 :param python_implementation: Name of the python implementation. 

318 Should be sourced from :py:meth:`platform.python_implementation`. 

319 :type execution_env: str 

320 :param execution_env: The value of the AWS execution environment. 

321 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

322 variable. 

323 :type crt_version: str 

324 :param crt_version: Version string of awscrt package, if installed. 

325 """ 

326 self._platform_name = platform_name 

327 self._platform_version = platform_version 

328 self._platform_machine = platform_machine 

329 self._python_version = python_version 

330 self._python_implementation = python_implementation 

331 self._execution_env = execution_env 

332 self._crt_version = crt_version 

333 

334 # Components that can be added with ``set_session_config()`` 

335 self._session_user_agent_name = None 

336 self._session_user_agent_version = None 

337 self._session_user_agent_extra = None 

338 

339 self._client_config = None 

340 

341 # Component that can be set with ``set_client_features()`` 

342 self._client_features = None 

343 

344 @classmethod 

345 def from_environment(cls): 

346 crt_version = None 

347 if HAS_CRT: 

348 crt_version = _get_crt_version() or 'Unknown' 

349 return cls( 

350 platform_name=platform.system(), 

351 platform_version=platform.release(), 

352 platform_machine=platform.machine(), 

353 python_version=platform.python_version(), 

354 python_implementation=platform.python_implementation(), 

355 execution_env=os.environ.get('AWS_EXECUTION_ENV'), 

356 crt_version=crt_version, 

357 ) 

358 

359 def set_session_config( 

360 self, 

361 session_user_agent_name, 

362 session_user_agent_version, 

363 session_user_agent_extra, 

364 ): 

365 """ 

366 Set the user agent configuration values that apply at session level. 

367 

368 :param user_agent_name: The user agent name configured in the 

369 :py:class:`botocore.session.Session` object. For backwards 

370 compatibility, this will always be at the beginning of the 

371 User-Agent string, together with ``user_agent_version``. 

372 :param user_agent_version: The user agent version configured in the 

373 :py:class:`botocore.session.Session` object. 

374 :param user_agent_extra: The user agent "extra" configured in the 

375 :py:class:`botocore.session.Session` object. 

376 """ 

377 self._session_user_agent_name = session_user_agent_name 

378 self._session_user_agent_version = session_user_agent_version 

379 self._session_user_agent_extra = session_user_agent_extra 

380 return self 

381 

382 def set_client_features(self, features): 

383 """ 

384 Persist client-specific features registered before or during client 

385 creation. 

386 

387 :type features: Set[str] 

388 :param features: A set of client-specific features. 

389 """ 

390 self._client_features = features 

391 

392 def with_client_config(self, client_config): 

393 """ 

394 Create a copy with all original values and client-specific values. 

395 

396 :type client_config: botocore.config.Config 

397 :param client_config: The client configuration object. 

398 """ 

399 cp = copy(self) 

400 cp._client_config = client_config 

401 return cp 

402 

403 def to_string(self): 

404 """ 

405 Build User-Agent header string from the object's properties. 

406 """ 

407 config_ua_override = None 

408 if self._client_config: 

409 if hasattr(self._client_config, '_supplied_user_agent'): 

410 config_ua_override = self._client_config._supplied_user_agent 

411 else: 

412 config_ua_override = self._client_config.user_agent 

413 

414 if config_ua_override is not None: 

415 return self._build_legacy_ua_string(config_ua_override) 

416 

417 components = [ 

418 *self._build_sdk_metadata(), 

419 RawStringUserAgentComponent('ua/2.1'), 

420 *self._build_os_metadata(), 

421 *self._build_architecture_metadata(), 

422 *self._build_language_metadata(), 

423 *self._build_execution_env_metadata(), 

424 *self._build_feature_metadata(), 

425 *self._build_config_metadata(), 

426 *self._build_app_id(), 

427 *self._build_extra(), 

428 ] 

429 

430 components = modify_components(components) 

431 

432 return ' '.join( 

433 [comp.to_string() for comp in components if comp.to_string()] 

434 ) 

435 

436 def _build_sdk_metadata(self): 

437 """ 

438 Build the SDK name and version component of the User-Agent header. 

439 

440 For backwards-compatibility both session-level and client-level config 

441 of custom tool names are honored. If this removes the Botocore 

442 information from the start of the string, Botocore's name and version 

443 are included as a separate field with "md" prefix. 

444 """ 

445 sdk_md = [] 

446 if ( 

447 self._session_user_agent_name 

448 and self._session_user_agent_version 

449 and ( 

450 self._session_user_agent_name != _USERAGENT_SDK_NAME 

451 or self._session_user_agent_version != botocore_version 

452 ) 

453 ): 

454 sdk_md.extend( 

455 [ 

456 UserAgentComponent( 

457 self._session_user_agent_name, 

458 self._session_user_agent_version, 

459 ), 

460 UserAgentComponent( 

461 'md', _USERAGENT_SDK_NAME, botocore_version 

462 ), 

463 ] 

464 ) 

465 else: 

466 sdk_md.append( 

467 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

468 ) 

469 

470 if self._crt_version is not None: 

471 sdk_md.append( 

472 UserAgentComponent('md', 'awscrt', self._crt_version) 

473 ) 

474 

475 return sdk_md 

476 

477 def _build_os_metadata(self): 

478 """ 

479 Build the OS/platform components of the User-Agent header string. 

480 

481 For recognized platform names that match or map to an entry in the list 

482 of standardized OS names, a single component with prefix "os" is 

483 returned. Otherwise, one component "os/other" is returned and a second 

484 with prefix "md" and the raw platform name. 

485 

486 String representations of example return values: 

487 * ``os/macos#10.13.6`` 

488 * ``os/linux`` 

489 * ``os/other`` 

490 * ``os/other md/foobar#1.2.3`` 

491 """ 

492 if self._platform_name is None: 

493 return [UserAgentComponent('os', 'other')] 

494 

495 plt_name_lower = self._platform_name.lower() 

496 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

497 os_family = plt_name_lower 

498 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

499 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

500 else: 

501 os_family = None 

502 

503 if os_family is not None: 

504 return [ 

505 UserAgentComponent('os', os_family, self._platform_version) 

506 ] 

507 else: 

508 return [ 

509 UserAgentComponent('os', 'other'), 

510 UserAgentComponent( 

511 'md', self._platform_name, self._platform_version 

512 ), 

513 ] 

514 

515 def _build_architecture_metadata(self): 

516 """ 

517 Build architecture component of the User-Agent header string. 

518 

519 Returns the machine type with prefix "md" and name "arch", if one is 

520 available. Common values include "x86_64", "arm64", "i386". 

521 """ 

522 if self._platform_machine: 

523 return [ 

524 UserAgentComponent( 

525 'md', 'arch', self._platform_machine.lower() 

526 ) 

527 ] 

528 return [] 

529 

530 def _build_language_metadata(self): 

531 """ 

532 Build the language components of the User-Agent header string. 

533 

534 Returns the Python version in a component with prefix "lang" and name 

535 "python". The Python implementation (e.g. CPython, PyPy) is returned as 

536 separate metadata component with prefix "md" and name "pyimpl". 

537 

538 String representation of an example return value: 

539 ``lang/python#3.10.4 md/pyimpl#CPython`` 

540 """ 

541 lang_md = [ 

542 UserAgentComponent('lang', 'python', self._python_version), 

543 ] 

544 if self._python_implementation: 

545 lang_md.append( 

546 UserAgentComponent('md', 'pyimpl', self._python_implementation) 

547 ) 

548 return lang_md 

549 

550 def _build_execution_env_metadata(self): 

551 """ 

552 Build the execution environment component of the User-Agent header. 

553 

554 Returns a single component prefixed with "exec-env", usually sourced 

555 from the environment variable AWS_EXECUTION_ENV. 

556 """ 

557 if self._execution_env: 

558 return [UserAgentComponent('exec-env', self._execution_env)] 

559 else: 

560 return [] 

561 

562 def _build_feature_metadata(self): 

563 """ 

564 Build the features component of the User-Agent header string. 

565 

566 Returns a single component with prefix "m" followed by a list of 

567 comma-separated metric values. 

568 """ 

569 ctx = get_context() 

570 context_features = set() if ctx is None else ctx.features 

571 client_features = self._client_features or set() 

572 features = client_features.union(context_features) 

573 if not features: 

574 return [] 

575 size_config = UserAgentComponentSizeConfig(1024, ',') 

576 return [ 

577 UserAgentComponent( 

578 'm', ','.join(features), size_config=size_config 

579 ) 

580 ] 

581 

582 def _build_config_metadata(self): 

583 """ 

584 Build the configuration components of the User-Agent header string. 

585 

586 Returns a list of components with prefix "cfg" followed by the config 

587 setting name and its value. Tracked configuration settings may be 

588 added or removed in future versions. 

589 """ 

590 if not self._client_config or not self._client_config.retries: 

591 return [] 

592 retry_mode = self._client_config.retries.get('mode') 

593 cfg_md = [UserAgentComponent('cfg', 'retry-mode', retry_mode)] 

594 if self._client_config.endpoint_discovery_enabled: 

595 cfg_md.append(UserAgentComponent('cfg', 'endpoint-discovery')) 

596 return cfg_md 

597 

598 def _build_app_id(self): 

599 """ 

600 Build app component of the User-Agent header string. 

601 

602 Returns a single component with prefix "app" and value sourced from the 

603 ``user_agent_appid`` field in :py:class:`botocore.config.Config` or 

604 the ``sdk_ua_app_id`` setting in the shared configuration file, or the 

605 ``AWS_SDK_UA_APP_ID`` environment variable. These are the recommended 

606 ways for apps built with Botocore to insert their identifer into the 

607 User-Agent header. 

608 """ 

609 if self._client_config and self._client_config.user_agent_appid: 

610 return [ 

611 UserAgentComponent('app', self._client_config.user_agent_appid) 

612 ] 

613 else: 

614 return [] 

615 

616 def _build_extra(self): 

617 """User agent string components based on legacy "extra" settings. 

618 

619 Creates components from the session-level and client-level 

620 ``user_agent_extra`` setting, if present. Both are passed through 

621 verbatim and should be appended at the end of the string. 

622 

623 Preferred ways to inject application-specific information into 

624 botocore's User-Agent header string are the ``user_agent_appid` field 

625 in :py:class:`botocore.config.Config`. The ``AWS_SDK_UA_APP_ID`` 

626 environment variable and the ``sdk_ua_app_id`` configuration file 

627 setting are alternative ways to set the ``user_agent_appid`` config. 

628 """ 

629 extra = [] 

630 if self._session_user_agent_extra: 

631 extra.append( 

632 RawStringUserAgentComponent(self._session_user_agent_extra) 

633 ) 

634 if self._client_config and self._client_config.user_agent_extra: 

635 extra.append( 

636 RawStringUserAgentComponent( 

637 self._client_config.user_agent_extra 

638 ) 

639 ) 

640 return extra 

641 

642 def _build_legacy_ua_string(self, config_ua_override): 

643 components = [config_ua_override] 

644 if self._session_user_agent_extra: 

645 components.append(self._session_user_agent_extra) 

646 if self._client_config.user_agent_extra: 

647 components.append(self._client_config.user_agent_extra) 

648 return ' '.join(components) 

649 

650 def rebuild_and_replace_user_agent_handler( 

651 self, operation_name, request, **kwargs 

652 ): 

653 ua_string = self.to_string() 

654 if request.headers.get('User-Agent'): 

655 request.headers.replace_header('User-Agent', ua_string) 

656 

657 

658def _get_crt_version(): 

659 """ 

660 This function is considered private and is subject to abrupt breaking 

661 changes. 

662 """ 

663 try: 

664 import awscrt 

665 

666 return awscrt.__version__ 

667 except AttributeError: 

668 return None