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

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

144 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 os 

27import platform 

28from copy import copy 

29from string import ascii_letters, digits 

30from typing import NamedTuple, Optional 

31 

32from botocore import __version__ as botocore_version 

33from botocore.compat import HAS_CRT 

34 

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

36_USERAGENT_ALLOWED_OS_NAMES = ( 

37 'windows', 

38 'linux', 

39 'macos', 

40 'android', 

41 'ios', 

42 'watchos', 

43 'tvos', 

44 'other', 

45) 

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

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

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

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

50# lowercase. 

51_USERAGENT_SDK_NAME = 'Botocore' 

52 

53 

54def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

56 

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

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

59 

60 :type raw_str: str 

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

62 

63 :type allow_hash: bool 

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

65 """ 

66 return ''.join( 

67 c 

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

69 else '-' 

70 for c in raw_str 

71 ) 

72 

73 

74class UserAgentComponent(NamedTuple): 

75 """ 

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

77 

78 Each component consists of a prefix, a name, and a value. In the string 

79 representation these are combined in the format ``prefix/name#value``. 

80 

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

82 """ 

83 

84 prefix: str 

85 name: str 

86 value: Optional[str] = None 

87 

88 def to_string(self): 

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

90 clean_prefix = sanitize_user_agent_string_component( 

91 self.prefix, allow_hash=True 

92 ) 

93 clean_name = sanitize_user_agent_string_component( 

94 self.name, allow_hash=False 

95 ) 

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

97 return f'{clean_prefix}/{clean_name}' 

98 clean_value = sanitize_user_agent_string_component( 

99 self.value, allow_hash=True 

100 ) 

101 return f'{clean_prefix}/{clean_name}#{clean_value}' 

102 

103 

104class RawStringUserAgentComponent: 

105 """ 

106 UserAgentComponent interface wrapper around ``str``. 

107 

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

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

110 performed. 

111 """ 

112 

113 def __init__(self, value): 

114 self._value = value 

115 

116 def to_string(self): 

117 return self._value 

118 

119 

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

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

122try: 

123 from botocore.customizations.useragent import modify_components 

124except ImportError: 

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

126 def modify_components(components): 

127 return components 

128 

129 

130class UserAgentString: 

131 """ 

132 Generator for AWS SDK User-Agent header strings. 

133 

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

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

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

137 string format. 

138 

139 Example usage: 

140 

141 ua_session = UserAgentString.from_environment() 

142 ua_session.set_session_config(...) 

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

144 ua_string = ua_request.to_string() 

145 

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

147 time, the methods can be chained: 

148 

149 ua_string = ( 

150 UserAgentString 

151 .from_environment() 

152 .set_session_config(...) 

153 .with_client_config(Config(...)) 

154 .to_string() 

155 ) 

156 

157 """ 

158 

159 def __init__( 

160 self, 

161 platform_name, 

162 platform_version, 

163 platform_machine, 

164 python_version, 

165 python_implementation, 

166 execution_env, 

167 crt_version=None, 

168 ): 

169 """ 

170 :type platform_name: str 

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

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

173 :type platform_version: str 

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

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

176 :type platform_machine: str 

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

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

179 :type python_version: str 

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

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

182 :type python_implementation: str 

183 :param python_implementation: Name of the python implementation. 

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

185 :type execution_env: str 

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

187 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

188 variable. 

189 :type crt_version: str 

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

191 """ 

192 self._platform_name = platform_name 

193 self._platform_version = platform_version 

194 self._platform_machine = platform_machine 

195 self._python_version = python_version 

196 self._python_implementation = python_implementation 

197 self._execution_env = execution_env 

198 self._crt_version = crt_version 

199 

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

201 self._session_user_agent_name = None 

202 self._session_user_agent_version = None 

203 self._session_user_agent_extra = None 

204 

205 self._client_config = None 

206 self._uses_paginator = None 

207 self._uses_waiter = None 

208 self._uses_resource = None 

209 

210 @classmethod 

211 def from_environment(cls): 

212 crt_version = None 

213 if HAS_CRT: 

214 crt_version = _get_crt_version() or 'Unknown' 

215 return cls( 

216 platform_name=platform.system(), 

217 platform_version=platform.release(), 

218 platform_machine=platform.machine(), 

219 python_version=platform.python_version(), 

220 python_implementation=platform.python_implementation(), 

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

222 crt_version=crt_version, 

223 ) 

224 

225 def set_session_config( 

226 self, 

227 session_user_agent_name, 

228 session_user_agent_version, 

229 session_user_agent_extra, 

230 ): 

231 """ 

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

233 

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

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

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

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

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

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

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

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

242 """ 

243 self._session_user_agent_name = session_user_agent_name 

244 self._session_user_agent_version = session_user_agent_version 

245 self._session_user_agent_extra = session_user_agent_extra 

246 return self 

247 

248 def with_client_config(self, client_config): 

249 """ 

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

251 

252 :type client_config: botocore.config.Config 

253 :param client_config: The client configuration object. 

254 """ 

255 cp = copy(self) 

256 cp._client_config = client_config 

257 return cp 

258 

259 def to_string(self): 

260 """ 

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

262 """ 

263 config_ua_override = None 

264 if self._client_config: 

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

266 config_ua_override = self._client_config._supplied_user_agent 

267 else: 

268 config_ua_override = self._client_config.user_agent 

269 

270 if config_ua_override is not None: 

271 return self._build_legacy_ua_string(config_ua_override) 

272 

273 components = [ 

274 *self._build_sdk_metadata(), 

275 RawStringUserAgentComponent('ua/2.0'), 

276 *self._build_os_metadata(), 

277 *self._build_architecture_metadata(), 

278 *self._build_language_metadata(), 

279 *self._build_execution_env_metadata(), 

280 *self._build_feature_metadata(), 

281 *self._build_config_metadata(), 

282 *self._build_app_id(), 

283 *self._build_extra(), 

284 ] 

285 

286 components = modify_components(components) 

287 

288 return ' '.join([comp.to_string() for comp in components]) 

289 

290 def _build_sdk_metadata(self): 

291 """ 

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

293 

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

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

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

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

298 """ 

299 sdk_md = [] 

300 if ( 

301 self._session_user_agent_name 

302 and self._session_user_agent_version 

303 and ( 

304 self._session_user_agent_name != _USERAGENT_SDK_NAME 

305 or self._session_user_agent_version != botocore_version 

306 ) 

307 ): 

308 sdk_md.extend( 

309 [ 

310 UserAgentComponent( 

311 self._session_user_agent_name, 

312 self._session_user_agent_version, 

313 ), 

314 UserAgentComponent( 

315 'md', _USERAGENT_SDK_NAME, botocore_version 

316 ), 

317 ] 

318 ) 

319 else: 

320 sdk_md.append( 

321 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

322 ) 

323 

324 if self._crt_version is not None: 

325 sdk_md.append( 

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

327 ) 

328 

329 return sdk_md 

330 

331 def _build_os_metadata(self): 

332 """ 

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

334 

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

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

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

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

339 

340 String representations of example return values: 

341 * ``os/macos#10.13.6`` 

342 * ``os/linux`` 

343 * ``os/other`` 

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

345 """ 

346 if self._platform_name is None: 

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

348 

349 plt_name_lower = self._platform_name.lower() 

350 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

351 os_family = plt_name_lower 

352 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

353 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

354 else: 

355 os_family = None 

356 

357 if os_family is not None: 

358 return [ 

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

360 ] 

361 else: 

362 return [ 

363 UserAgentComponent('os', 'other'), 

364 UserAgentComponent( 

365 'md', self._platform_name, self._platform_version 

366 ), 

367 ] 

368 

369 def _build_architecture_metadata(self): 

370 """ 

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

372 

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

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

375 """ 

376 if self._platform_machine: 

377 return [ 

378 UserAgentComponent( 

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

380 ) 

381 ] 

382 return [] 

383 

384 def _build_language_metadata(self): 

385 """ 

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

387 

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

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

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

391 

392 String representation of an example return value: 

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

394 """ 

395 lang_md = [ 

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

397 ] 

398 if self._python_implementation: 

399 lang_md.append( 

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

401 ) 

402 return lang_md 

403 

404 def _build_execution_env_metadata(self): 

405 """ 

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

407 

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

409 from the environment variable AWS_EXECUTION_ENV. 

410 """ 

411 if self._execution_env: 

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

413 else: 

414 return [] 

415 

416 def _build_feature_metadata(self): 

417 """ 

418 Build the features components of the User-Agent header string. 

419 

420 Botocore currently does not report any features. This may change in a 

421 future version. 

422 """ 

423 return [] 

424 

425 def _build_config_metadata(self): 

426 """ 

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

428 

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

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

431 added or removed in future versions. 

432 """ 

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

434 return [] 

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

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

437 if self._client_config.endpoint_discovery_enabled: 

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

439 return cfg_md 

440 

441 def _build_app_id(self): 

442 """ 

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

444 

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

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

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

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

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

450 User-Agent header. 

451 """ 

452 if self._client_config and self._client_config.user_agent_appid: 

453 return [ 

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

455 ] 

456 else: 

457 return [] 

458 

459 def _build_extra(self): 

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

461 

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

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

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

465 

466 Preferred ways to inject application-specific information into 

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

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

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

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

471 """ 

472 extra = [] 

473 if self._session_user_agent_extra: 

474 extra.append( 

475 RawStringUserAgentComponent(self._session_user_agent_extra) 

476 ) 

477 if self._client_config and self._client_config.user_agent_extra: 

478 extra.append( 

479 RawStringUserAgentComponent( 

480 self._client_config.user_agent_extra 

481 ) 

482 ) 

483 return extra 

484 

485 def _build_legacy_ua_string(self, config_ua_override): 

486 components = [config_ua_override] 

487 if self._session_user_agent_extra: 

488 components.append(self._session_user_agent_extra) 

489 if self._client_config.user_agent_extra: 

490 components.append(self._client_config.user_agent_extra) 

491 return ' '.join(components) 

492 

493 

494def _get_crt_version(): 

495 """ 

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

497 changes. 

498 """ 

499 try: 

500 import awscrt 

501 

502 return awscrt.__version__ 

503 except AttributeError: 

504 return None