Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/boto3/resources/response.py: 14%

92 statements  

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

1# Copyright 2014 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# https://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 

14import jmespath 

15from botocore import xform_name 

16 

17from .params import get_data_member 

18 

19 

20def all_not_none(iterable): 

21 """ 

22 Return True if all elements of the iterable are not None (or if the 

23 iterable is empty). This is like the built-in ``all``, except checks 

24 against None, so 0 and False are allowable values. 

25 """ 

26 for element in iterable: 

27 if element is None: 

28 return False 

29 return True 

30 

31 

32def build_identifiers(identifiers, parent, params=None, raw_response=None): 

33 """ 

34 Builds a mapping of identifier names to values based on the 

35 identifier source location, type, and target. Identifier 

36 values may be scalars or lists depending on the source type 

37 and location. 

38 

39 :type identifiers: list 

40 :param identifiers: List of :py:class:`~boto3.resources.model.Parameter` 

41 definitions 

42 :type parent: ServiceResource 

43 :param parent: The resource instance to which this action is attached. 

44 :type params: dict 

45 :param params: Request parameters sent to the service. 

46 :type raw_response: dict 

47 :param raw_response: Low-level operation response. 

48 :rtype: list 

49 :return: An ordered list of ``(name, value)`` identifier tuples. 

50 """ 

51 results = [] 

52 

53 for identifier in identifiers: 

54 source = identifier.source 

55 target = identifier.target 

56 

57 if source == 'response': 

58 value = jmespath.search(identifier.path, raw_response) 

59 elif source == 'requestParameter': 

60 value = jmespath.search(identifier.path, params) 

61 elif source == 'identifier': 

62 value = getattr(parent, xform_name(identifier.name)) 

63 elif source == 'data': 

64 # If this is a data member then it may incur a load 

65 # action before returning the value. 

66 value = get_data_member(parent, identifier.path) 

67 elif source == 'input': 

68 # This value is set by the user, so ignore it here 

69 continue 

70 else: 

71 raise NotImplementedError(f'Unsupported source type: {source}') 

72 

73 results.append((xform_name(target), value)) 

74 

75 return results 

76 

77 

78def build_empty_response(search_path, operation_name, service_model): 

79 """ 

80 Creates an appropriate empty response for the type that is expected, 

81 based on the service model's shape type. For example, a value that 

82 is normally a list would then return an empty list. A structure would 

83 return an empty dict, and a number would return None. 

84 

85 :type search_path: string 

86 :param search_path: JMESPath expression to search in the response 

87 :type operation_name: string 

88 :param operation_name: Name of the underlying service operation. 

89 :type service_model: :ref:`botocore.model.ServiceModel` 

90 :param service_model: The Botocore service model 

91 :rtype: dict, list, or None 

92 :return: An appropriate empty value 

93 """ 

94 response = None 

95 

96 operation_model = service_model.operation_model(operation_name) 

97 shape = operation_model.output_shape 

98 

99 if search_path: 

100 # Walk the search path and find the final shape. For example, given 

101 # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``, 

102 # then the shape for ``bar`` (ignoring the indexing), and finally 

103 # the shape for ``baz``. 

104 for item in search_path.split('.'): 

105 item = item.strip('[0123456789]$') 

106 

107 if shape.type_name == 'structure': 

108 shape = shape.members[item] 

109 elif shape.type_name == 'list': 

110 shape = shape.member 

111 else: 

112 raise NotImplementedError( 

113 'Search path hits shape type {} from {}'.format( 

114 shape.type_name, item 

115 ) 

116 ) 

117 

118 # Anything not handled here is set to None 

119 if shape.type_name == 'structure': 

120 response = {} 

121 elif shape.type_name == 'list': 

122 response = [] 

123 elif shape.type_name == 'map': 

124 response = {} 

125 

126 return response 

127 

128 

129class RawHandler: 

130 """ 

131 A raw action response handler. This passed through the response 

132 dictionary, optionally after performing a JMESPath search if one 

133 has been defined for the action. 

134 

135 :type search_path: string 

136 :param search_path: JMESPath expression to search in the response 

137 :rtype: dict 

138 :return: Service response 

139 """ 

140 

141 def __init__(self, search_path): 

142 self.search_path = search_path 

143 

144 def __call__(self, parent, params, response): 

145 """ 

146 :type parent: ServiceResource 

147 :param parent: The resource instance to which this action is attached. 

148 :type params: dict 

149 :param params: Request parameters sent to the service. 

150 :type response: dict 

151 :param response: Low-level operation response. 

152 """ 

153 # TODO: Remove the '$' check after JMESPath supports it 

154 if self.search_path and self.search_path != '$': 

155 response = jmespath.search(self.search_path, response) 

156 

157 return response 

158 

159 

160class ResourceHandler: 

161 """ 

162 Creates a new resource or list of new resources from the low-level 

163 response based on the given response resource definition. 

164 

165 :type search_path: string 

166 :param search_path: JMESPath expression to search in the response 

167 

168 :type factory: ResourceFactory 

169 :param factory: The factory that created the resource class to which 

170 this action is attached. 

171 

172 :type resource_model: :py:class:`~boto3.resources.model.ResponseResource` 

173 :param resource_model: Response resource model. 

174 

175 :type service_context: :py:class:`~boto3.utils.ServiceContext` 

176 :param service_context: Context about the AWS service 

177 

178 :type operation_name: string 

179 :param operation_name: Name of the underlying service operation, if it 

180 exists. 

181 

182 :rtype: ServiceResource or list 

183 :return: New resource instance(s). 

184 """ 

185 

186 def __init__( 

187 self, 

188 search_path, 

189 factory, 

190 resource_model, 

191 service_context, 

192 operation_name=None, 

193 ): 

194 self.search_path = search_path 

195 self.factory = factory 

196 self.resource_model = resource_model 

197 self.operation_name = operation_name 

198 self.service_context = service_context 

199 

200 def __call__(self, parent, params, response): 

201 """ 

202 :type parent: ServiceResource 

203 :param parent: The resource instance to which this action is attached. 

204 :type params: dict 

205 :param params: Request parameters sent to the service. 

206 :type response: dict 

207 :param response: Low-level operation response. 

208 """ 

209 resource_name = self.resource_model.type 

210 json_definition = self.service_context.resource_json_definitions.get( 

211 resource_name 

212 ) 

213 

214 # Load the new resource class that will result from this action. 

215 resource_cls = self.factory.load_from_definition( 

216 resource_name=resource_name, 

217 single_resource_json_definition=json_definition, 

218 service_context=self.service_context, 

219 ) 

220 raw_response = response 

221 search_response = None 

222 

223 # Anytime a path is defined, it means the response contains the 

224 # resource's attributes, so resource_data gets set here. It 

225 # eventually ends up in resource.meta.data, which is where 

226 # the attribute properties look for data. 

227 if self.search_path: 

228 search_response = jmespath.search(self.search_path, raw_response) 

229 

230 # First, we parse all the identifiers, then create the individual 

231 # response resources using them. Any identifiers that are lists 

232 # will have one item consumed from the front of the list for each 

233 # resource that is instantiated. Items which are not a list will 

234 # be set as the same value on each new resource instance. 

235 identifiers = dict( 

236 build_identifiers( 

237 self.resource_model.identifiers, parent, params, raw_response 

238 ) 

239 ) 

240 

241 # If any of the identifiers is a list, then the response is plural 

242 plural = [v for v in identifiers.values() if isinstance(v, list)] 

243 

244 if plural: 

245 response = [] 

246 

247 # The number of items in an identifier that is a list will 

248 # determine how many resource instances to create. 

249 for i in range(len(plural[0])): 

250 # Response item data is *only* available if a search path 

251 # was given. This prevents accidentally loading unrelated 

252 # data that may be in the response. 

253 response_item = None 

254 if search_response: 

255 response_item = search_response[i] 

256 response.append( 

257 self.handle_response_item( 

258 resource_cls, parent, identifiers, response_item 

259 ) 

260 ) 

261 elif all_not_none(identifiers.values()): 

262 # All identifiers must always exist, otherwise the resource 

263 # cannot be instantiated. 

264 response = self.handle_response_item( 

265 resource_cls, parent, identifiers, search_response 

266 ) 

267 else: 

268 # The response should be empty, but that may mean an 

269 # empty dict, list, or None based on whether we make 

270 # a remote service call and what shape it is expected 

271 # to return. 

272 response = None 

273 if self.operation_name is not None: 

274 # A remote service call was made, so try and determine 

275 # its shape. 

276 response = build_empty_response( 

277 self.search_path, 

278 self.operation_name, 

279 self.service_context.service_model, 

280 ) 

281 

282 return response 

283 

284 def handle_response_item( 

285 self, resource_cls, parent, identifiers, resource_data 

286 ): 

287 """ 

288 Handles the creation of a single response item by setting 

289 parameters and creating the appropriate resource instance. 

290 

291 :type resource_cls: ServiceResource subclass 

292 :param resource_cls: The resource class to instantiate. 

293 :type parent: ServiceResource 

294 :param parent: The resource instance to which this action is attached. 

295 :type identifiers: dict 

296 :param identifiers: Map of identifier names to value or values. 

297 :type resource_data: dict or None 

298 :param resource_data: Data for resource attributes. 

299 :rtype: ServiceResource 

300 :return: New resource instance. 

301 """ 

302 kwargs = { 

303 'client': parent.meta.client, 

304 } 

305 

306 for name, value in identifiers.items(): 

307 # If value is a list, then consume the next item 

308 if isinstance(value, list): 

309 value = value.pop(0) 

310 

311 kwargs[name] = value 

312 

313 resource = resource_cls(**kwargs) 

314 

315 if resource_data is not None: 

316 resource.meta.data = resource_data 

317 

318 return resource