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 'FLEXIBLE_CHECKSUMS_REQ_MD5': 'AE', 

104 'FLEXIBLE_CHECKSUMS_REQ_SHA512': 'AF', 

105 'FLEXIBLE_CHECKSUMS_REQ_XXHASH3': 'AG', 

106 'FLEXIBLE_CHECKSUMS_REQ_XXHASH64': 'AH', 

107 'FLEXIBLE_CHECKSUMS_REQ_XXHASH128': 'AI', 

108} 

109 

110 

111def register_feature_id(feature_id): 

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

113 

114 :type feature_id: str 

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

116 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

117 """ 

118 ctx = get_context() 

119 if ctx is None: 

120 # Never register features outside the scope of a 

121 # ``botocore.context.start_as_current_context`` context manager. 

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

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

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

125 return 

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

127 ctx.features.add(val) 

128 

129 

130def register_feature_ids(feature_ids): 

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

132 

133 :type feature_ids: iterable of str 

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

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

136 """ 

137 for feature_id in feature_ids: 

138 register_feature_id(feature_id) 

139 

140 

141def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

143 

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

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

146 

147 :type raw_str: str 

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

149 

150 :type allow_hash: bool 

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

152 """ 

153 return ''.join( 

154 c 

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

156 else '-' 

157 for c in raw_str 

158 ) 

159 

160 

161class UserAgentComponentSizeConfig: 

162 """ 

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

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

165 """ 

166 

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

168 self.max_size_in_bytes = max_size_in_bytes 

169 self.delimiter = delimiter 

170 self._validate_input() 

171 

172 def _validate_input(self): 

173 if self.max_size_in_bytes < 1: 

174 raise ValueError( 

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

176 'Value must be a positive integer.' 

177 ) 

178 

179 

180class UserAgentComponent(NamedTuple): 

181 """ 

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

183 

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

185 In the string representation these are combined in the format 

186 ``prefix/name#value``. 

187 

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

189 built user agent string component. 

190 

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

192 """ 

193 

194 prefix: str 

195 name: str 

196 value: Optional[str] = None 

197 size_config: Optional[UserAgentComponentSizeConfig] = None 

198 

199 def to_string(self): 

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

201 clean_prefix = sanitize_user_agent_string_component( 

202 self.prefix, allow_hash=True 

203 ) 

204 clean_name = sanitize_user_agent_string_component( 

205 self.name, allow_hash=False 

206 ) 

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

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

209 else: 

210 clean_value = sanitize_user_agent_string_component( 

211 self.value, allow_hash=True 

212 ) 

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

214 if self.size_config is not None: 

215 clean_string = self._truncate_string( 

216 clean_string, 

217 self.size_config.max_size_in_bytes, 

218 self.size_config.delimiter, 

219 ) 

220 return clean_string 

221 

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

223 """ 

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

225 equal to ``max_size``. 

226 """ 

227 orig = string 

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

229 parts = string.split(delimiter) 

230 parts.pop() 

231 string = delimiter.join(parts) 

232 

233 if string == '': 

234 logger.debug( 

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

236 "`%s` bytes with delimiter " 

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

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

239 orig, 

240 max_size, 

241 delimiter, 

242 ) 

243 return string 

244 

245 

246class RawStringUserAgentComponent: 

247 """ 

248 UserAgentComponent interface wrapper around ``str``. 

249 

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

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

252 performed. 

253 """ 

254 

255 def __init__(self, value): 

256 self._value = value 

257 

258 def to_string(self): 

259 return self._value 

260 

261 

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

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

264try: 

265 from botocore.customizations.useragent import modify_components 

266except ImportError: 

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

268 def modify_components(components): 

269 return components 

270 

271 

272class UserAgentString: 

273 """ 

274 Generator for AWS SDK User-Agent header strings. 

275 

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

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

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

279 string format. 

280 

281 Example usage: 

282 

283 ua_session = UserAgentString.from_environment() 

284 ua_session.set_session_config(...) 

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

286 ua_string = ua_request.to_string() 

287 

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

289 time, the methods can be chained: 

290 

291 ua_string = ( 

292 UserAgentString 

293 .from_environment() 

294 .set_session_config(...) 

295 .with_client_config(Config(...)) 

296 .to_string() 

297 ) 

298 

299 """ 

300 

301 def __init__( 

302 self, 

303 platform_name, 

304 platform_version, 

305 platform_machine, 

306 python_version, 

307 python_implementation, 

308 execution_env, 

309 crt_version=None, 

310 ): 

311 """ 

312 :type platform_name: str 

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

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

315 :type platform_version: str 

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

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

318 :type platform_machine: str 

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

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

321 :type python_version: str 

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

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

324 :type python_implementation: str 

325 :param python_implementation: Name of the python implementation. 

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

327 :type execution_env: str 

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

329 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

330 variable. 

331 :type crt_version: str 

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

333 """ 

334 self._platform_name = platform_name 

335 self._platform_version = platform_version 

336 self._platform_machine = platform_machine 

337 self._python_version = python_version 

338 self._python_implementation = python_implementation 

339 self._execution_env = execution_env 

340 self._crt_version = crt_version 

341 

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

343 self._session_user_agent_name = None 

344 self._session_user_agent_version = None 

345 self._session_user_agent_extra = None 

346 

347 self._client_config = None 

348 

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

350 self._client_features = None 

351 

352 @classmethod 

353 def from_environment(cls): 

354 crt_version = None 

355 if HAS_CRT: 

356 crt_version = _get_crt_version() or 'Unknown' 

357 return cls( 

358 platform_name=platform.system(), 

359 platform_version=platform.release(), 

360 platform_machine=platform.machine(), 

361 python_version=platform.python_version(), 

362 python_implementation=platform.python_implementation(), 

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

364 crt_version=crt_version, 

365 ) 

366 

367 def set_session_config( 

368 self, 

369 session_user_agent_name, 

370 session_user_agent_version, 

371 session_user_agent_extra, 

372 ): 

373 """ 

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

375 

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

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

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

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

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

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

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

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

384 """ 

385 self._session_user_agent_name = session_user_agent_name 

386 self._session_user_agent_version = session_user_agent_version 

387 self._session_user_agent_extra = session_user_agent_extra 

388 return self 

389 

390 def set_client_features(self, features): 

391 """ 

392 Persist client-specific features registered before or during client 

393 creation. 

394 

395 :type features: Set[str] 

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

397 """ 

398 self._client_features = features 

399 

400 def with_client_config(self, client_config): 

401 """ 

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

403 

404 :type client_config: botocore.config.Config 

405 :param client_config: The client configuration object. 

406 """ 

407 cp = copy(self) 

408 cp._client_config = client_config 

409 return cp 

410 

411 def to_string(self): 

412 """ 

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

414 """ 

415 config_ua_override = None 

416 if self._client_config: 

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

418 config_ua_override = self._client_config._supplied_user_agent 

419 else: 

420 config_ua_override = self._client_config.user_agent 

421 

422 if config_ua_override is not None: 

423 return self._build_legacy_ua_string(config_ua_override) 

424 

425 components = [ 

426 *self._build_sdk_metadata(), 

427 RawStringUserAgentComponent('ua/2.1'), 

428 *self._build_os_metadata(), 

429 *self._build_architecture_metadata(), 

430 *self._build_language_metadata(), 

431 *self._build_execution_env_metadata(), 

432 *self._build_feature_metadata(), 

433 *self._build_config_metadata(), 

434 *self._build_app_id(), 

435 *self._build_extra(), 

436 ] 

437 

438 components = modify_components(components) 

439 

440 return ' '.join( 

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

442 ) 

443 

444 def _build_sdk_metadata(self): 

445 """ 

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

447 

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

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

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

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

452 """ 

453 sdk_md = [] 

454 if ( 

455 self._session_user_agent_name 

456 and self._session_user_agent_version 

457 and ( 

458 self._session_user_agent_name != _USERAGENT_SDK_NAME 

459 or self._session_user_agent_version != botocore_version 

460 ) 

461 ): 

462 sdk_md.extend( 

463 [ 

464 UserAgentComponent( 

465 self._session_user_agent_name, 

466 self._session_user_agent_version, 

467 ), 

468 UserAgentComponent( 

469 'md', _USERAGENT_SDK_NAME, botocore_version 

470 ), 

471 ] 

472 ) 

473 else: 

474 sdk_md.append( 

475 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

476 ) 

477 

478 if self._crt_version is not None: 

479 sdk_md.append( 

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

481 ) 

482 

483 return sdk_md 

484 

485 def _build_os_metadata(self): 

486 """ 

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

488 

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

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

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

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

493 

494 String representations of example return values: 

495 * ``os/macos#10.13.6`` 

496 * ``os/linux`` 

497 * ``os/other`` 

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

499 """ 

500 if self._platform_name is None: 

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

502 

503 plt_name_lower = self._platform_name.lower() 

504 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

505 os_family = plt_name_lower 

506 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

507 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

508 else: 

509 os_family = None 

510 

511 if os_family is not None: 

512 return [ 

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

514 ] 

515 else: 

516 return [ 

517 UserAgentComponent('os', 'other'), 

518 UserAgentComponent( 

519 'md', self._platform_name, self._platform_version 

520 ), 

521 ] 

522 

523 def _build_architecture_metadata(self): 

524 """ 

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

526 

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

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

529 """ 

530 if self._platform_machine: 

531 return [ 

532 UserAgentComponent( 

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

534 ) 

535 ] 

536 return [] 

537 

538 def _build_language_metadata(self): 

539 """ 

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

541 

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

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

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

545 

546 String representation of an example return value: 

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

548 """ 

549 lang_md = [ 

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

551 ] 

552 if self._python_implementation: 

553 lang_md.append( 

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

555 ) 

556 return lang_md 

557 

558 def _build_execution_env_metadata(self): 

559 """ 

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

561 

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

563 from the environment variable AWS_EXECUTION_ENV. 

564 """ 

565 if self._execution_env: 

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

567 else: 

568 return [] 

569 

570 def _build_feature_metadata(self): 

571 """ 

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

573 

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

575 comma-separated metric values. 

576 """ 

577 ctx = get_context() 

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

579 client_features = self._client_features or set() 

580 features = client_features.union(context_features) 

581 if not features: 

582 return [] 

583 size_config = UserAgentComponentSizeConfig(1024, ',') 

584 return [ 

585 UserAgentComponent( 

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

587 ) 

588 ] 

589 

590 def _build_config_metadata(self): 

591 """ 

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

593 

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

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

596 added or removed in future versions. 

597 """ 

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

599 return [] 

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

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

602 if self._client_config.endpoint_discovery_enabled: 

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

604 return cfg_md 

605 

606 def _build_app_id(self): 

607 """ 

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

609 

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

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

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

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

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

615 User-Agent header. 

616 """ 

617 if self._client_config and self._client_config.user_agent_appid: 

618 appid = sanitize_user_agent_string_component( 

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

620 ) 

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

622 else: 

623 return [] 

624 

625 def _build_extra(self): 

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

627 

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

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

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

631 

632 Preferred ways to inject application-specific information into 

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

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

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

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

637 """ 

638 extra = [] 

639 if self._session_user_agent_extra: 

640 extra.append( 

641 RawStringUserAgentComponent(self._session_user_agent_extra) 

642 ) 

643 if self._client_config and self._client_config.user_agent_extra: 

644 extra.append( 

645 RawStringUserAgentComponent( 

646 self._client_config.user_agent_extra 

647 ) 

648 ) 

649 return extra 

650 

651 def _build_legacy_ua_string(self, config_ua_override): 

652 components = [config_ua_override] 

653 if self._session_user_agent_extra: 

654 components.append(self._session_user_agent_extra) 

655 if self._client_config.user_agent_extra: 

656 components.append(self._client_config.user_agent_extra) 

657 return ' '.join(components) 

658 

659 def rebuild_and_replace_user_agent_handler( 

660 self, operation_name, request, **kwargs 

661 ): 

662 ua_string = self.to_string() 

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

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

665 

666 

667def _get_crt_version(): 

668 """ 

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

670 changes. 

671 """ 

672 try: 

673 import awscrt 

674 

675 return awscrt.__version__ 

676 except AttributeError: 

677 return None