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

144 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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""" 

25import os 

26import platform 

27from copy import copy 

28from string import ascii_letters, digits 

29from typing import NamedTuple, Optional 

30 

31from botocore import __version__ as botocore_version 

32from botocore.compat import HAS_CRT 

33 

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

35_USERAGENT_ALLOWED_OS_NAMES = ( 

36 'windows', 

37 'linux', 

38 'macos', 

39 'android', 

40 'ios', 

41 'watchos', 

42 'tvos', 

43 'other', 

44) 

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

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

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

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

49# lowercase. 

50_USERAGENT_SDK_NAME = 'Botocore' 

51 

52 

53def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

55 

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

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

58 

59 :type raw_str: str 

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

61 

62 :type allow_hash: bool 

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

64 """ 

65 return ''.join( 

66 c 

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

68 else '-' 

69 for c in raw_str 

70 ) 

71 

72 

73class UserAgentComponent(NamedTuple): 

74 """ 

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

76 

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

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

79 

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

81 """ 

82 

83 prefix: str 

84 name: str 

85 value: Optional[str] = None 

86 

87 def to_string(self): 

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

89 clean_prefix = sanitize_user_agent_string_component( 

90 self.prefix, allow_hash=True 

91 ) 

92 clean_name = sanitize_user_agent_string_component( 

93 self.name, allow_hash=False 

94 ) 

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

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

97 clean_value = sanitize_user_agent_string_component( 

98 self.value, allow_hash=True 

99 ) 

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

101 

102 

103class RawStringUserAgentComponent: 

104 """ 

105 UserAgentComponent interface wrapper around ``str``. 

106 

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

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

109 performed. 

110 """ 

111 

112 def __init__(self, value): 

113 self._value = value 

114 

115 def to_string(self): 

116 return self._value 

117 

118 

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

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

121try: 

122 from botocore.customizations.useragent import modify_components 

123except ImportError: 

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

125 def modify_components(components): 

126 return components 

127 

128 

129class UserAgentString: 

130 """ 

131 Generator for AWS SDK User-Agent header strings. 

132 

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

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

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

136 string format. 

137 

138 Example usage: 

139 

140 ua_session = UserAgentString.from_environment() 

141 ua_session.set_session_config(...) 

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

143 ua_string = ua_request.to_string() 

144 

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

146 time, the methods can be chained: 

147 

148 ua_string = ( 

149 UserAgentString 

150 .from_environment() 

151 .set_session_config(...) 

152 .with_client_config(Config(...)) 

153 .to_string() 

154 ) 

155 

156 """ 

157 

158 def __init__( 

159 self, 

160 platform_name, 

161 platform_version, 

162 platform_machine, 

163 python_version, 

164 python_implementation, 

165 execution_env, 

166 crt_version=None, 

167 ): 

168 """ 

169 :type platform_name: str 

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

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

172 :type platform_version: str 

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

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

175 :type platform_machine: str 

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

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

178 :type python_version: str 

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

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

181 :type python_implementation: str 

182 :param python_implementation: Name of the python implementation. 

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

184 :type execution_env: str 

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

186 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

187 variable. 

188 :type crt_version: str 

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

190 """ 

191 self._platform_name = platform_name 

192 self._platform_version = platform_version 

193 self._platform_machine = platform_machine 

194 self._python_version = python_version 

195 self._python_implementation = python_implementation 

196 self._execution_env = execution_env 

197 self._crt_version = crt_version 

198 

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

200 self._session_user_agent_name = None 

201 self._session_user_agent_version = None 

202 self._session_user_agent_extra = None 

203 

204 self._client_config = None 

205 self._uses_paginator = None 

206 self._uses_waiter = None 

207 self._uses_resource = None 

208 

209 @classmethod 

210 def from_environment(cls): 

211 crt_version = None 

212 if HAS_CRT: 

213 crt_version = _get_crt_version() or 'Unknown' 

214 return cls( 

215 platform_name=platform.system(), 

216 platform_version=platform.release(), 

217 platform_machine=platform.machine(), 

218 python_version=platform.python_version(), 

219 python_implementation=platform.python_implementation(), 

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

221 crt_version=crt_version, 

222 ) 

223 

224 def set_session_config( 

225 self, 

226 session_user_agent_name, 

227 session_user_agent_version, 

228 session_user_agent_extra, 

229 ): 

230 """ 

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

232 

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

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

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

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

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

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

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

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

241 """ 

242 self._session_user_agent_name = session_user_agent_name 

243 self._session_user_agent_version = session_user_agent_version 

244 self._session_user_agent_extra = session_user_agent_extra 

245 return self 

246 

247 def with_client_config(self, client_config): 

248 """ 

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

250 

251 :type client_config: botocore.config.Config 

252 :param client_config: The client configuration object. 

253 """ 

254 cp = copy(self) 

255 cp._client_config = client_config 

256 return cp 

257 

258 def to_string(self): 

259 """ 

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

261 """ 

262 config_ua_override = None 

263 if self._client_config: 

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

265 config_ua_override = self._client_config._supplied_user_agent 

266 else: 

267 config_ua_override = self._client_config.user_agent 

268 

269 if config_ua_override is not None: 

270 return self._build_legacy_ua_string(config_ua_override) 

271 

272 components = [ 

273 *self._build_sdk_metadata(), 

274 RawStringUserAgentComponent('ua/2.0'), 

275 *self._build_os_metadata(), 

276 *self._build_architecture_metadata(), 

277 *self._build_language_metadata(), 

278 *self._build_execution_env_metadata(), 

279 *self._build_feature_metadata(), 

280 *self._build_config_metadata(), 

281 *self._build_app_id(), 

282 *self._build_extra(), 

283 ] 

284 

285 components = modify_components(components) 

286 

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

288 

289 def _build_sdk_metadata(self): 

290 """ 

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

292 

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

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

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

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

297 """ 

298 sdk_md = [] 

299 if ( 

300 self._session_user_agent_name 

301 and self._session_user_agent_version 

302 and ( 

303 self._session_user_agent_name != _USERAGENT_SDK_NAME 

304 or self._session_user_agent_version != botocore_version 

305 ) 

306 ): 

307 sdk_md.extend( 

308 [ 

309 UserAgentComponent( 

310 self._session_user_agent_name, 

311 self._session_user_agent_version, 

312 ), 

313 UserAgentComponent( 

314 'md', _USERAGENT_SDK_NAME, botocore_version 

315 ), 

316 ] 

317 ) 

318 else: 

319 sdk_md.append( 

320 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

321 ) 

322 

323 if self._crt_version is not None: 

324 sdk_md.append( 

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

326 ) 

327 

328 return sdk_md 

329 

330 def _build_os_metadata(self): 

331 """ 

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

333 

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

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

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

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

338 

339 String representations of example return values: 

340 * ``os/macos#10.13.6`` 

341 * ``os/linux`` 

342 * ``os/other`` 

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

344 """ 

345 if self._platform_name is None: 

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

347 

348 plt_name_lower = self._platform_name.lower() 

349 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

350 os_family = plt_name_lower 

351 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

352 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

353 else: 

354 os_family = None 

355 

356 if os_family is not None: 

357 return [ 

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

359 ] 

360 else: 

361 return [ 

362 UserAgentComponent('os', 'other'), 

363 UserAgentComponent( 

364 'md', self._platform_name, self._platform_version 

365 ), 

366 ] 

367 

368 def _build_architecture_metadata(self): 

369 """ 

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

371 

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

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

374 """ 

375 if self._platform_machine: 

376 return [ 

377 UserAgentComponent( 

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

379 ) 

380 ] 

381 return [] 

382 

383 def _build_language_metadata(self): 

384 """ 

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

386 

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

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

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

390 

391 String representation of an example return value: 

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

393 """ 

394 lang_md = [ 

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

396 ] 

397 if self._python_implementation: 

398 lang_md.append( 

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

400 ) 

401 return lang_md 

402 

403 def _build_execution_env_metadata(self): 

404 """ 

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

406 

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

408 from the environment variable AWS_EXECUTION_ENV. 

409 """ 

410 if self._execution_env: 

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

412 else: 

413 return [] 

414 

415 def _build_feature_metadata(self): 

416 """ 

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

418 

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

420 future version. 

421 """ 

422 return [] 

423 

424 def _build_config_metadata(self): 

425 """ 

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

427 

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

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

430 added or removed in future versions. 

431 """ 

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

433 return [] 

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

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

436 if self._client_config.endpoint_discovery_enabled: 

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

438 return cfg_md 

439 

440 def _build_app_id(self): 

441 """ 

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

443 

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

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

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

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

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

449 User-Agent header. 

450 """ 

451 if self._client_config and self._client_config.user_agent_appid: 

452 return [ 

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

454 ] 

455 else: 

456 return [] 

457 

458 def _build_extra(self): 

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

460 

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

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

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

464 

465 Preferred ways to inject application-specific information into 

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

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

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

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

470 """ 

471 extra = [] 

472 if self._session_user_agent_extra: 

473 extra.append( 

474 RawStringUserAgentComponent(self._session_user_agent_extra) 

475 ) 

476 if self._client_config and self._client_config.user_agent_extra: 

477 extra.append( 

478 RawStringUserAgentComponent( 

479 self._client_config.user_agent_extra 

480 ) 

481 ) 

482 return extra 

483 

484 def _build_legacy_ua_string(self, config_ua_override): 

485 components = [config_ua_override] 

486 if self._session_user_agent_extra: 

487 components.append(self._session_user_agent_extra) 

488 if self._client_config.user_agent_extra: 

489 components.append(self._client_config.user_agent_extra) 

490 return ' '.join(components) 

491 

492 

493def _get_crt_version(): 

494 """ 

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

496 changes. 

497 """ 

498 try: 

499 import awscrt 

500 

501 return awscrt.__version__ 

502 except AttributeError: 

503 return None