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 'CLI_V1_TO_V2_MIGRATION_DEBUG_MODE': '-', 

101} 

102 

103 

104def register_feature_id(feature_id): 

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

106 

107 :type feature_id: str 

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

109 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

110 """ 

111 ctx = get_context() 

112 if ctx is None: 

113 # Never register features outside the scope of a 

114 # ``botocore.context.start_as_current_context`` context manager. 

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

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

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

118 return 

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

120 ctx.features.add(val) 

121 

122 

123def register_feature_ids(feature_ids): 

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

125 

126 :type feature_ids: iterable of str 

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

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

129 """ 

130 for feature_id in feature_ids: 

131 register_feature_id(feature_id) 

132 

133 

134def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

136 

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

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

139 

140 :type raw_str: str 

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

142 

143 :type allow_hash: bool 

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

145 """ 

146 return ''.join( 

147 c 

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

149 else '-' 

150 for c in raw_str 

151 ) 

152 

153 

154class UserAgentComponentSizeConfig: 

155 """ 

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

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

158 """ 

159 

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

161 self.max_size_in_bytes = max_size_in_bytes 

162 self.delimiter = delimiter 

163 self._validate_input() 

164 

165 def _validate_input(self): 

166 if self.max_size_in_bytes < 1: 

167 raise ValueError( 

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

169 'Value must be a positive integer.' 

170 ) 

171 

172 

173class UserAgentComponent(NamedTuple): 

174 """ 

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

176 

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

178 In the string representation these are combined in the format 

179 ``prefix/name#value``. 

180 

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

182 built user agent string component. 

183 

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

185 """ 

186 

187 prefix: str 

188 name: str 

189 value: Optional[str] = None 

190 size_config: Optional[UserAgentComponentSizeConfig] = None 

191 

192 def to_string(self): 

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

194 clean_prefix = sanitize_user_agent_string_component( 

195 self.prefix, allow_hash=True 

196 ) 

197 clean_name = sanitize_user_agent_string_component( 

198 self.name, allow_hash=False 

199 ) 

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

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

202 else: 

203 clean_value = sanitize_user_agent_string_component( 

204 self.value, allow_hash=True 

205 ) 

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

207 if self.size_config is not None: 

208 clean_string = self._truncate_string( 

209 clean_string, 

210 self.size_config.max_size_in_bytes, 

211 self.size_config.delimiter, 

212 ) 

213 return clean_string 

214 

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

216 """ 

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

218 equal to ``max_size``. 

219 """ 

220 orig = string 

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

222 parts = string.split(delimiter) 

223 parts.pop() 

224 string = delimiter.join(parts) 

225 

226 if string == '': 

227 logger.debug( 

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

229 "`%s` bytes with delimiter " 

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

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

232 orig, 

233 max_size, 

234 delimiter, 

235 ) 

236 return string 

237 

238 

239class RawStringUserAgentComponent: 

240 """ 

241 UserAgentComponent interface wrapper around ``str``. 

242 

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

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

245 performed. 

246 """ 

247 

248 def __init__(self, value): 

249 self._value = value 

250 

251 def to_string(self): 

252 return self._value 

253 

254 

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

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

257try: 

258 from botocore.customizations.useragent import modify_components 

259except ImportError: 

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

261 def modify_components(components): 

262 return components 

263 

264 

265class UserAgentString: 

266 """ 

267 Generator for AWS SDK User-Agent header strings. 

268 

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

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

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

272 string format. 

273 

274 Example usage: 

275 

276 ua_session = UserAgentString.from_environment() 

277 ua_session.set_session_config(...) 

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

279 ua_string = ua_request.to_string() 

280 

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

282 time, the methods can be chained: 

283 

284 ua_string = ( 

285 UserAgentString 

286 .from_environment() 

287 .set_session_config(...) 

288 .with_client_config(Config(...)) 

289 .to_string() 

290 ) 

291 

292 """ 

293 

294 def __init__( 

295 self, 

296 platform_name, 

297 platform_version, 

298 platform_machine, 

299 python_version, 

300 python_implementation, 

301 execution_env, 

302 crt_version=None, 

303 ): 

304 """ 

305 :type platform_name: str 

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

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

308 :type platform_version: str 

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

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

311 :type platform_machine: str 

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

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

314 :type python_version: str 

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

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

317 :type python_implementation: str 

318 :param python_implementation: Name of the python implementation. 

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

320 :type execution_env: str 

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

322 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

323 variable. 

324 :type crt_version: str 

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

326 """ 

327 self._platform_name = platform_name 

328 self._platform_version = platform_version 

329 self._platform_machine = platform_machine 

330 self._python_version = python_version 

331 self._python_implementation = python_implementation 

332 self._execution_env = execution_env 

333 self._crt_version = crt_version 

334 

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

336 self._session_user_agent_name = None 

337 self._session_user_agent_version = None 

338 self._session_user_agent_extra = None 

339 

340 self._client_config = None 

341 

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

343 self._client_features = None 

344 

345 @classmethod 

346 def from_environment(cls): 

347 crt_version = None 

348 if HAS_CRT: 

349 crt_version = _get_crt_version() or 'Unknown' 

350 return cls( 

351 platform_name=platform.system(), 

352 platform_version=platform.release(), 

353 platform_machine=platform.machine(), 

354 python_version=platform.python_version(), 

355 python_implementation=platform.python_implementation(), 

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

357 crt_version=crt_version, 

358 ) 

359 

360 def set_session_config( 

361 self, 

362 session_user_agent_name, 

363 session_user_agent_version, 

364 session_user_agent_extra, 

365 ): 

366 """ 

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

368 

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

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

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

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

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

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

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

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

377 """ 

378 self._session_user_agent_name = session_user_agent_name 

379 self._session_user_agent_version = session_user_agent_version 

380 self._session_user_agent_extra = session_user_agent_extra 

381 return self 

382 

383 def set_client_features(self, features): 

384 """ 

385 Persist client-specific features registered before or during client 

386 creation. 

387 

388 :type features: Set[str] 

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

390 """ 

391 self._client_features = features 

392 

393 def with_client_config(self, client_config): 

394 """ 

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

396 

397 :type client_config: botocore.config.Config 

398 :param client_config: The client configuration object. 

399 """ 

400 cp = copy(self) 

401 cp._client_config = client_config 

402 return cp 

403 

404 def to_string(self): 

405 """ 

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

407 """ 

408 config_ua_override = None 

409 if self._client_config: 

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

411 config_ua_override = self._client_config._supplied_user_agent 

412 else: 

413 config_ua_override = self._client_config.user_agent 

414 

415 if config_ua_override is not None: 

416 return self._build_legacy_ua_string(config_ua_override) 

417 

418 components = [ 

419 *self._build_sdk_metadata(), 

420 RawStringUserAgentComponent('ua/2.1'), 

421 *self._build_os_metadata(), 

422 *self._build_architecture_metadata(), 

423 *self._build_language_metadata(), 

424 *self._build_execution_env_metadata(), 

425 *self._build_feature_metadata(), 

426 *self._build_config_metadata(), 

427 *self._build_app_id(), 

428 *self._build_extra(), 

429 ] 

430 

431 components = modify_components(components) 

432 

433 return ' '.join( 

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

435 ) 

436 

437 def _build_sdk_metadata(self): 

438 """ 

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

440 

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

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

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

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

445 """ 

446 sdk_md = [] 

447 if ( 

448 self._session_user_agent_name 

449 and self._session_user_agent_version 

450 and ( 

451 self._session_user_agent_name != _USERAGENT_SDK_NAME 

452 or self._session_user_agent_version != botocore_version 

453 ) 

454 ): 

455 sdk_md.extend( 

456 [ 

457 UserAgentComponent( 

458 self._session_user_agent_name, 

459 self._session_user_agent_version, 

460 ), 

461 UserAgentComponent( 

462 'md', _USERAGENT_SDK_NAME, botocore_version 

463 ), 

464 ] 

465 ) 

466 else: 

467 sdk_md.append( 

468 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

469 ) 

470 

471 if self._crt_version is not None: 

472 sdk_md.append( 

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

474 ) 

475 

476 return sdk_md 

477 

478 def _build_os_metadata(self): 

479 """ 

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

481 

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

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

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

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

486 

487 String representations of example return values: 

488 * ``os/macos#10.13.6`` 

489 * ``os/linux`` 

490 * ``os/other`` 

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

492 """ 

493 if self._platform_name is None: 

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

495 

496 plt_name_lower = self._platform_name.lower() 

497 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

498 os_family = plt_name_lower 

499 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

500 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

501 else: 

502 os_family = None 

503 

504 if os_family is not None: 

505 return [ 

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

507 ] 

508 else: 

509 return [ 

510 UserAgentComponent('os', 'other'), 

511 UserAgentComponent( 

512 'md', self._platform_name, self._platform_version 

513 ), 

514 ] 

515 

516 def _build_architecture_metadata(self): 

517 """ 

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

519 

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

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

522 """ 

523 if self._platform_machine: 

524 return [ 

525 UserAgentComponent( 

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

527 ) 

528 ] 

529 return [] 

530 

531 def _build_language_metadata(self): 

532 """ 

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

534 

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

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

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

538 

539 String representation of an example return value: 

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

541 """ 

542 lang_md = [ 

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

544 ] 

545 if self._python_implementation: 

546 lang_md.append( 

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

548 ) 

549 return lang_md 

550 

551 def _build_execution_env_metadata(self): 

552 """ 

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

554 

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

556 from the environment variable AWS_EXECUTION_ENV. 

557 """ 

558 if self._execution_env: 

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

560 else: 

561 return [] 

562 

563 def _build_feature_metadata(self): 

564 """ 

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

566 

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

568 comma-separated metric values. 

569 """ 

570 ctx = get_context() 

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

572 client_features = self._client_features or set() 

573 features = client_features.union(context_features) 

574 if not features: 

575 return [] 

576 size_config = UserAgentComponentSizeConfig(1024, ',') 

577 return [ 

578 UserAgentComponent( 

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

580 ) 

581 ] 

582 

583 def _build_config_metadata(self): 

584 """ 

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

586 

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

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

589 added or removed in future versions. 

590 """ 

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

592 return [] 

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

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

595 if self._client_config.endpoint_discovery_enabled: 

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

597 return cfg_md 

598 

599 def _build_app_id(self): 

600 """ 

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

602 

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

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

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

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

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

608 User-Agent header. 

609 """ 

610 if self._client_config and self._client_config.user_agent_appid: 

611 return [ 

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

613 ] 

614 else: 

615 return [] 

616 

617 def _build_extra(self): 

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

619 

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

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

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

623 

624 Preferred ways to inject application-specific information into 

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

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

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

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

629 """ 

630 extra = [] 

631 if self._session_user_agent_extra: 

632 extra.append( 

633 RawStringUserAgentComponent(self._session_user_agent_extra) 

634 ) 

635 if self._client_config and self._client_config.user_agent_extra: 

636 extra.append( 

637 RawStringUserAgentComponent( 

638 self._client_config.user_agent_extra 

639 ) 

640 ) 

641 return extra 

642 

643 def _build_legacy_ua_string(self, config_ua_override): 

644 components = [config_ua_override] 

645 if self._session_user_agent_extra: 

646 components.append(self._session_user_agent_extra) 

647 if self._client_config.user_agent_extra: 

648 components.append(self._client_config.user_agent_extra) 

649 return ' '.join(components) 

650 

651 def rebuild_and_replace_user_agent_handler( 

652 self, operation_name, request, **kwargs 

653 ): 

654 ua_string = self.to_string() 

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

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

657 

658 

659def _get_crt_version(): 

660 """ 

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

662 changes. 

663 """ 

664 try: 

665 import awscrt 

666 

667 return awscrt.__version__ 

668 except AttributeError: 

669 return None