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

99 statements  

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

1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/ 

2# Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"). You 

5# may not use this file except in compliance with the License. A copy of 

6# the License is located at 

7# 

8# http://aws.amazon.com/apache2.0/ 

9# 

10# or in the "license" file accompanying this file. This file is 

11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 

12# ANY KIND, either express or implied. See the License for the specific 

13# language governing permissions and limitations under the License. 

14import configparser 

15import copy 

16import os 

17import shlex 

18import sys 

19 

20import botocore.exceptions 

21 

22 

23def multi_file_load_config(*filenames): 

24 """Load and combine multiple INI configs with profiles. 

25 

26 This function will take a list of filesnames and return 

27 a single dictionary that represents the merging of the loaded 

28 config files. 

29 

30 If any of the provided filenames does not exist, then that file 

31 is ignored. It is therefore ok to provide a list of filenames, 

32 some of which may not exist. 

33 

34 Configuration files are **not** deep merged, only the top level 

35 keys are merged. The filenames should be passed in order of 

36 precedence. The first config file has precedence over the 

37 second config file, which has precedence over the third config file, 

38 etc. The only exception to this is that the "profiles" key is 

39 merged to combine profiles from multiple config files into a 

40 single profiles mapping. However, if a profile is defined in 

41 multiple config files, then the config file with the highest 

42 precedence is used. Profile values themselves are not merged. 

43 For example:: 

44 

45 FileA FileB FileC 

46 [foo] [foo] [bar] 

47 a=1 a=2 a=3 

48 b=2 

49 

50 [bar] [baz] [profile a] 

51 a=2 a=3 region=e 

52 

53 [profile a] [profile b] [profile c] 

54 region=c region=d region=f 

55 

56 The final result of ``multi_file_load_config(FileA, FileB, FileC)`` 

57 would be:: 

58 

59 {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3}, 

60 "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}}, 

61 {"c": {"region": "f"}}} 

62 

63 Note that the "foo" key comes from A, even though it's defined in both 

64 FileA and FileB. Because "foo" was defined in FileA first, then the values 

65 for "foo" from FileA are used and the values for "foo" from FileB are 

66 ignored. Also note where the profiles originate from. Profile "a" 

67 comes FileA, profile "b" comes from FileB, and profile "c" comes 

68 from FileC. 

69 

70 """ 

71 configs = [] 

72 profiles = [] 

73 for filename in filenames: 

74 try: 

75 loaded = load_config(filename) 

76 except botocore.exceptions.ConfigNotFound: 

77 continue 

78 profiles.append(loaded.pop('profiles')) 

79 configs.append(loaded) 

80 merged_config = _merge_list_of_dicts(configs) 

81 merged_profiles = _merge_list_of_dicts(profiles) 

82 merged_config['profiles'] = merged_profiles 

83 return merged_config 

84 

85 

86def _merge_list_of_dicts(list_of_dicts): 

87 merged_dicts = {} 

88 for single_dict in list_of_dicts: 

89 for key, value in single_dict.items(): 

90 if key not in merged_dicts: 

91 merged_dicts[key] = value 

92 return merged_dicts 

93 

94 

95def load_config(config_filename): 

96 """Parse a INI config with profiles. 

97 

98 This will parse an INI config file and map top level profiles 

99 into a top level "profile" key. 

100 

101 If you want to parse an INI file and map all section names to 

102 top level keys, use ``raw_config_parse`` instead. 

103 

104 """ 

105 parsed = raw_config_parse(config_filename) 

106 return build_profile_map(parsed) 

107 

108 

109def raw_config_parse(config_filename, parse_subsections=True): 

110 """Returns the parsed INI config contents. 

111 

112 Each section name is a top level key. 

113 

114 :param config_filename: The name of the INI file to parse 

115 

116 :param parse_subsections: If True, parse indented blocks as 

117 subsections that represent their own configuration dictionary. 

118 For example, if the config file had the contents:: 

119 

120 s3 = 

121 signature_version = s3v4 

122 addressing_style = path 

123 

124 The resulting ``raw_config_parse`` would be:: 

125 

126 {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}} 

127 

128 If False, do not try to parse subsections and return the indented 

129 block as its literal value:: 

130 

131 {'s3': '\nsignature_version = s3v4\naddressing_style = path'} 

132 

133 :returns: A dict with keys for each profile found in the config 

134 file and the value of each key being a dict containing name 

135 value pairs found in that profile. 

136 

137 :raises: ConfigNotFound, ConfigParseError 

138 """ 

139 config = {} 

140 path = config_filename 

141 if path is not None: 

142 path = os.path.expandvars(path) 

143 path = os.path.expanduser(path) 

144 if not os.path.isfile(path): 

145 raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path)) 

146 cp = configparser.RawConfigParser() 

147 try: 

148 cp.read([path]) 

149 except (configparser.Error, UnicodeDecodeError) as e: 

150 raise botocore.exceptions.ConfigParseError( 

151 path=_unicode_path(path), error=e 

152 ) from None 

153 else: 

154 for section in cp.sections(): 

155 config[section] = {} 

156 for option in cp.options(section): 

157 config_value = cp.get(section, option) 

158 if parse_subsections and config_value.startswith('\n'): 

159 # Then we need to parse the inner contents as 

160 # hierarchical. We support a single level 

161 # of nesting for now. 

162 try: 

163 config_value = _parse_nested(config_value) 

164 except ValueError as e: 

165 raise botocore.exceptions.ConfigParseError( 

166 path=_unicode_path(path), error=e 

167 ) from None 

168 config[section][option] = config_value 

169 return config 

170 

171 

172def _unicode_path(path): 

173 if isinstance(path, str): 

174 return path 

175 # According to the documentation getfilesystemencoding can return None 

176 # on unix in which case the default encoding is used instead. 

177 filesystem_encoding = sys.getfilesystemencoding() 

178 if filesystem_encoding is None: 

179 filesystem_encoding = sys.getdefaultencoding() 

180 return path.decode(filesystem_encoding, 'replace') 

181 

182 

183def _parse_nested(config_value): 

184 # Given a value like this: 

185 # \n 

186 # foo = bar 

187 # bar = baz 

188 # We need to parse this into 

189 # {'foo': 'bar', 'bar': 'baz} 

190 parsed = {} 

191 for line in config_value.splitlines(): 

192 line = line.strip() 

193 if not line: 

194 continue 

195 # The caller will catch ValueError 

196 # and raise an appropriate error 

197 # if this fails. 

198 key, value = line.split('=', 1) 

199 parsed[key.strip()] = value.strip() 

200 return parsed 

201 

202 

203def _parse_section(key, values): 

204 result = {} 

205 try: 

206 parts = shlex.split(key) 

207 except ValueError: 

208 return result 

209 if len(parts) == 2: 

210 result[parts[1]] = values 

211 return result 

212 

213 

214def build_profile_map(parsed_ini_config): 

215 """Convert the parsed INI config into a profile map. 

216 

217 The config file format requires that every profile except the 

218 default to be prepended with "profile", e.g.:: 

219 

220 [profile test] 

221 aws_... = foo 

222 aws_... = bar 

223 

224 [profile bar] 

225 aws_... = foo 

226 aws_... = bar 

227 

228 # This is *not* a profile 

229 [preview] 

230 otherstuff = 1 

231 

232 # Neither is this 

233 [foobar] 

234 morestuff = 2 

235 

236 The build_profile_map will take a parsed INI config file where each top 

237 level key represents a section name, and convert into a format where all 

238 the profiles are under a single top level "profiles" key, and each key in 

239 the sub dictionary is a profile name. For example, the above config file 

240 would be converted from:: 

241 

242 {"profile test": {"aws_...": "foo", "aws...": "bar"}, 

243 "profile bar": {"aws...": "foo", "aws...": "bar"}, 

244 "preview": {"otherstuff": ...}, 

245 "foobar": {"morestuff": ...}, 

246 } 

247 

248 into:: 

249 

250 {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"}, 

251 "bar": {"aws...": "foo", "aws...": "bar"}, 

252 "preview": {"otherstuff": ...}, 

253 "foobar": {"morestuff": ...}, 

254 } 

255 

256 If there are no profiles in the provided parsed INI contents, then 

257 an empty dict will be the value associated with the ``profiles`` key. 

258 

259 .. note:: 

260 

261 This will not mutate the passed in parsed_ini_config. Instead it will 

262 make a deepcopy and return that value. 

263 

264 """ 

265 parsed_config = copy.deepcopy(parsed_ini_config) 

266 profiles = {} 

267 sso_sessions = {} 

268 services = {} 

269 final_config = {} 

270 for key, values in parsed_config.items(): 

271 if key.startswith("profile"): 

272 profiles.update(_parse_section(key, values)) 

273 elif key.startswith("sso-session"): 

274 sso_sessions.update(_parse_section(key, values)) 

275 elif key.startswith("services"): 

276 services.update(_parse_section(key, values)) 

277 elif key == 'default': 

278 # default section is special and is considered a profile 

279 # name but we don't require you use 'profile "default"' 

280 # as a section. 

281 profiles[key] = values 

282 else: 

283 final_config[key] = values 

284 final_config['profiles'] = profiles 

285 final_config['sso_sessions'] = sso_sessions 

286 final_config['services'] = services 

287 return final_config