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

191 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 'CREDENTIALS_PROFILE_LOGIN': 'AC', 

102 'CREDENTIALS_LOGIN': 'AD', 

103} 

104 

105 

106def register_feature_id(feature_id): 

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

108 

109 :type feature_id: str 

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

111 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

112 """ 

113 ctx = get_context() 

114 if ctx is None: 

115 # Never register features outside the scope of a 

116 # ``botocore.context.start_as_current_context`` context manager. 

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

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

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

120 return 

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

122 ctx.features.add(val) 

123 

124 

125def register_feature_ids(feature_ids): 

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

127 

128 :type feature_ids: iterable of str 

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

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

131 """ 

132 for feature_id in feature_ids: 

133 register_feature_id(feature_id) 

134 

135 

136def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

138 

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

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

141 

142 :type raw_str: str 

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

144 

145 :type allow_hash: bool 

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

147 """ 

148 return ''.join( 

149 c 

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

151 else '-' 

152 for c in raw_str 

153 ) 

154 

155 

156class UserAgentComponentSizeConfig: 

157 """ 

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

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

160 """ 

161 

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

163 self.max_size_in_bytes = max_size_in_bytes 

164 self.delimiter = delimiter 

165 self._validate_input() 

166 

167 def _validate_input(self): 

168 if self.max_size_in_bytes < 1: 

169 raise ValueError( 

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

171 'Value must be a positive integer.' 

172 ) 

173 

174 

175class UserAgentComponent(NamedTuple): 

176 """ 

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

178 

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

180 In the string representation these are combined in the format 

181 ``prefix/name#value``. 

182 

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

184 built user agent string component. 

185 

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

187 """ 

188 

189 prefix: str 

190 name: str 

191 value: Optional[str] = None 

192 size_config: Optional[UserAgentComponentSizeConfig] = None 

193 

194 def to_string(self): 

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

196 clean_prefix = sanitize_user_agent_string_component( 

197 self.prefix, allow_hash=True 

198 ) 

199 clean_name = sanitize_user_agent_string_component( 

200 self.name, allow_hash=False 

201 ) 

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

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

204 else: 

205 clean_value = sanitize_user_agent_string_component( 

206 self.value, allow_hash=True 

207 ) 

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

209 if self.size_config is not None: 

210 clean_string = self._truncate_string( 

211 clean_string, 

212 self.size_config.max_size_in_bytes, 

213 self.size_config.delimiter, 

214 ) 

215 return clean_string 

216 

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

218 """ 

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

220 equal to ``max_size``. 

221 """ 

222 orig = string 

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

224 parts = string.split(delimiter) 

225 parts.pop() 

226 string = delimiter.join(parts) 

227 

228 if string == '': 

229 logger.debug( 

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

231 "`%s` bytes with delimiter " 

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

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

234 orig, 

235 max_size, 

236 delimiter, 

237 ) 

238 return string 

239 

240 

241class RawStringUserAgentComponent: 

242 """ 

243 UserAgentComponent interface wrapper around ``str``. 

244 

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

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

247 performed. 

248 """ 

249 

250 def __init__(self, value): 

251 self._value = value 

252 

253 def to_string(self): 

254 return self._value 

255 

256 

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

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

259try: 

260 from botocore.customizations.useragent import modify_components 

261except ImportError: 

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

263 def modify_components(components): 

264 return components 

265 

266 

267class UserAgentString: 

268 """ 

269 Generator for AWS SDK User-Agent header strings. 

270 

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

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

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

274 string format. 

275 

276 Example usage: 

277 

278 ua_session = UserAgentString.from_environment() 

279 ua_session.set_session_config(...) 

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

281 ua_string = ua_request.to_string() 

282 

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

284 time, the methods can be chained: 

285 

286 ua_string = ( 

287 UserAgentString 

288 .from_environment() 

289 .set_session_config(...) 

290 .with_client_config(Config(...)) 

291 .to_string() 

292 ) 

293 

294 """ 

295 

296 def __init__( 

297 self, 

298 platform_name, 

299 platform_version, 

300 platform_machine, 

301 python_version, 

302 python_implementation, 

303 execution_env, 

304 crt_version=None, 

305 ): 

306 """ 

307 :type platform_name: str 

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

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

310 :type platform_version: str 

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

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

313 :type platform_machine: str 

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

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

316 :type python_version: str 

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

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

319 :type python_implementation: str 

320 :param python_implementation: Name of the python implementation. 

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

322 :type execution_env: str 

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

324 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

325 variable. 

326 :type crt_version: str 

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

328 """ 

329 self._platform_name = platform_name 

330 self._platform_version = platform_version 

331 self._platform_machine = platform_machine 

332 self._python_version = python_version 

333 self._python_implementation = python_implementation 

334 self._execution_env = execution_env 

335 self._crt_version = crt_version 

336 

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

338 self._session_user_agent_name = None 

339 self._session_user_agent_version = None 

340 self._session_user_agent_extra = None 

341 

342 self._client_config = None 

343 

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

345 self._client_features = None 

346 

347 @classmethod 

348 def from_environment(cls): 

349 crt_version = None 

350 if HAS_CRT: 

351 crt_version = _get_crt_version() or 'Unknown' 

352 return cls( 

353 platform_name=platform.system(), 

354 platform_version=platform.release(), 

355 platform_machine=platform.machine(), 

356 python_version=platform.python_version(), 

357 python_implementation=platform.python_implementation(), 

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

359 crt_version=crt_version, 

360 ) 

361 

362 def set_session_config( 

363 self, 

364 session_user_agent_name, 

365 session_user_agent_version, 

366 session_user_agent_extra, 

367 ): 

368 """ 

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

370 

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

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

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

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

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

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

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

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

379 """ 

380 self._session_user_agent_name = session_user_agent_name 

381 self._session_user_agent_version = session_user_agent_version 

382 self._session_user_agent_extra = session_user_agent_extra 

383 return self 

384 

385 def set_client_features(self, features): 

386 """ 

387 Persist client-specific features registered before or during client 

388 creation. 

389 

390 :type features: Set[str] 

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

392 """ 

393 self._client_features = features 

394 

395 def with_client_config(self, client_config): 

396 """ 

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

398 

399 :type client_config: botocore.config.Config 

400 :param client_config: The client configuration object. 

401 """ 

402 cp = copy(self) 

403 cp._client_config = client_config 

404 return cp 

405 

406 def to_string(self): 

407 """ 

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

409 """ 

410 config_ua_override = None 

411 if self._client_config: 

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

413 config_ua_override = self._client_config._supplied_user_agent 

414 else: 

415 config_ua_override = self._client_config.user_agent 

416 

417 if config_ua_override is not None: 

418 return self._build_legacy_ua_string(config_ua_override) 

419 

420 components = [ 

421 *self._build_sdk_metadata(), 

422 RawStringUserAgentComponent('ua/2.1'), 

423 *self._build_os_metadata(), 

424 *self._build_architecture_metadata(), 

425 *self._build_language_metadata(), 

426 *self._build_execution_env_metadata(), 

427 *self._build_feature_metadata(), 

428 *self._build_config_metadata(), 

429 *self._build_app_id(), 

430 *self._build_extra(), 

431 ] 

432 

433 components = modify_components(components) 

434 

435 return ' '.join( 

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

437 ) 

438 

439 def _build_sdk_metadata(self): 

440 """ 

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

442 

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

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

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

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

447 """ 

448 sdk_md = [] 

449 if ( 

450 self._session_user_agent_name 

451 and self._session_user_agent_version 

452 and ( 

453 self._session_user_agent_name != _USERAGENT_SDK_NAME 

454 or self._session_user_agent_version != botocore_version 

455 ) 

456 ): 

457 sdk_md.extend( 

458 [ 

459 UserAgentComponent( 

460 self._session_user_agent_name, 

461 self._session_user_agent_version, 

462 ), 

463 UserAgentComponent( 

464 'md', _USERAGENT_SDK_NAME, botocore_version 

465 ), 

466 ] 

467 ) 

468 else: 

469 sdk_md.append( 

470 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

471 ) 

472 

473 if self._crt_version is not None: 

474 sdk_md.append( 

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

476 ) 

477 

478 return sdk_md 

479 

480 def _build_os_metadata(self): 

481 """ 

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

483 

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

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

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

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

488 

489 String representations of example return values: 

490 * ``os/macos#10.13.6`` 

491 * ``os/linux`` 

492 * ``os/other`` 

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

494 """ 

495 if self._platform_name is None: 

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

497 

498 plt_name_lower = self._platform_name.lower() 

499 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

500 os_family = plt_name_lower 

501 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

502 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

503 else: 

504 os_family = None 

505 

506 if os_family is not None: 

507 return [ 

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

509 ] 

510 else: 

511 return [ 

512 UserAgentComponent('os', 'other'), 

513 UserAgentComponent( 

514 'md', self._platform_name, self._platform_version 

515 ), 

516 ] 

517 

518 def _build_architecture_metadata(self): 

519 """ 

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

521 

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

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

524 """ 

525 if self._platform_machine: 

526 return [ 

527 UserAgentComponent( 

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

529 ) 

530 ] 

531 return [] 

532 

533 def _build_language_metadata(self): 

534 """ 

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

536 

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

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

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

540 

541 String representation of an example return value: 

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

543 """ 

544 lang_md = [ 

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

546 ] 

547 if self._python_implementation: 

548 lang_md.append( 

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

550 ) 

551 return lang_md 

552 

553 def _build_execution_env_metadata(self): 

554 """ 

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

556 

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

558 from the environment variable AWS_EXECUTION_ENV. 

559 """ 

560 if self._execution_env: 

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

562 else: 

563 return [] 

564 

565 def _build_feature_metadata(self): 

566 """ 

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

568 

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

570 comma-separated metric values. 

571 """ 

572 ctx = get_context() 

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

574 client_features = self._client_features or set() 

575 features = client_features.union(context_features) 

576 if not features: 

577 return [] 

578 size_config = UserAgentComponentSizeConfig(1024, ',') 

579 return [ 

580 UserAgentComponent( 

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

582 ) 

583 ] 

584 

585 def _build_config_metadata(self): 

586 """ 

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

588 

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

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

591 added or removed in future versions. 

592 """ 

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

594 return [] 

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

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

597 if self._client_config.endpoint_discovery_enabled: 

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

599 return cfg_md 

600 

601 def _build_app_id(self): 

602 """ 

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

604 

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

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

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

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

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

610 User-Agent header. 

611 """ 

612 if self._client_config and self._client_config.user_agent_appid: 

613 appid = sanitize_user_agent_string_component( 

614 raw_str=self._client_config.user_agent_appid, allow_hash=True 

615 ) 

616 return [RawStringUserAgentComponent(f'app/{appid}')] 

617 else: 

618 return [] 

619 

620 def _build_extra(self): 

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

622 

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

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

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

626 

627 Preferred ways to inject application-specific information into 

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

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

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

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

632 """ 

633 extra = [] 

634 if self._session_user_agent_extra: 

635 extra.append( 

636 RawStringUserAgentComponent(self._session_user_agent_extra) 

637 ) 

638 if self._client_config and self._client_config.user_agent_extra: 

639 extra.append( 

640 RawStringUserAgentComponent( 

641 self._client_config.user_agent_extra 

642 ) 

643 ) 

644 return extra 

645 

646 def _build_legacy_ua_string(self, config_ua_override): 

647 components = [config_ua_override] 

648 if self._session_user_agent_extra: 

649 components.append(self._session_user_agent_extra) 

650 if self._client_config.user_agent_extra: 

651 components.append(self._client_config.user_agent_extra) 

652 return ' '.join(components) 

653 

654 def rebuild_and_replace_user_agent_handler( 

655 self, operation_name, request, **kwargs 

656 ): 

657 ua_string = self.to_string() 

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

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

660 

661 

662def _get_crt_version(): 

663 """ 

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

665 changes. 

666 """ 

667 try: 

668 import awscrt 

669 

670 return awscrt.__version__ 

671 except AttributeError: 

672 return None