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

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

92 statements  

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 f'Search path hits shape type {shape.type_name} from {item}' 

114 ) 

115 

116 # Anything not handled here is set to None 

117 if shape.type_name == 'structure': 

118 response = {} 

119 elif shape.type_name == 'list': 

120 response = [] 

121 elif shape.type_name == 'map': 

122 response = {} 

123 

124 return response 

125 

126 

127class RawHandler: 

128 """ 

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

130 dictionary, optionally after performing a JMESPath search if one 

131 has been defined for the action. 

132 

133 :type search_path: string 

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

135 :rtype: dict 

136 :return: Service response 

137 """ 

138 

139 def __init__(self, search_path): 

140 self.search_path = search_path 

141 

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

143 """ 

144 :type parent: ServiceResource 

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

146 :type params: dict 

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

148 :type response: dict 

149 :param response: Low-level operation response. 

150 """ 

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

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

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

154 

155 return response 

156 

157 

158class ResourceHandler: 

159 """ 

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

161 response based on the given response resource definition. 

162 

163 :type search_path: string 

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

165 

166 :type factory: ResourceFactory 

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

168 this action is attached. 

169 

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

171 :param resource_model: Response resource model. 

172 

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

174 :param service_context: Context about the AWS service 

175 

176 :type operation_name: string 

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

178 exists. 

179 

180 :rtype: ServiceResource or list 

181 :return: New resource instance(s). 

182 """ 

183 

184 def __init__( 

185 self, 

186 search_path, 

187 factory, 

188 resource_model, 

189 service_context, 

190 operation_name=None, 

191 ): 

192 self.search_path = search_path 

193 self.factory = factory 

194 self.resource_model = resource_model 

195 self.operation_name = operation_name 

196 self.service_context = service_context 

197 

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

199 """ 

200 :type parent: ServiceResource 

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

202 :type params: dict 

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

204 :type response: dict 

205 :param response: Low-level operation response. 

206 """ 

207 resource_name = self.resource_model.type 

208 json_definition = self.service_context.resource_json_definitions.get( 

209 resource_name 

210 ) 

211 

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

213 resource_cls = self.factory.load_from_definition( 

214 resource_name=resource_name, 

215 single_resource_json_definition=json_definition, 

216 service_context=self.service_context, 

217 ) 

218 raw_response = response 

219 search_response = None 

220 

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

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

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

224 # the attribute properties look for data. 

225 if self.search_path: 

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

227 

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

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

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

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

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

233 identifiers = dict( 

234 build_identifiers( 

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

236 ) 

237 ) 

238 

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

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

241 

242 if plural: 

243 response = [] 

244 

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

246 # determine how many resource instances to create. 

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

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

249 # was given. This prevents accidentally loading unrelated 

250 # data that may be in the response. 

251 response_item = None 

252 if search_response: 

253 response_item = search_response[i] 

254 response.append( 

255 self.handle_response_item( 

256 resource_cls, parent, identifiers, response_item 

257 ) 

258 ) 

259 elif all_not_none(identifiers.values()): 

260 # All identifiers must always exist, otherwise the resource 

261 # cannot be instantiated. 

262 response = self.handle_response_item( 

263 resource_cls, parent, identifiers, search_response 

264 ) 

265 else: 

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

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

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

269 # to return. 

270 response = None 

271 if self.operation_name is not None: 

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

273 # its shape. 

274 response = build_empty_response( 

275 self.search_path, 

276 self.operation_name, 

277 self.service_context.service_model, 

278 ) 

279 

280 return response 

281 

282 def handle_response_item( 

283 self, resource_cls, parent, identifiers, resource_data 

284 ): 

285 """ 

286 Handles the creation of a single response item by setting 

287 parameters and creating the appropriate resource instance. 

288 

289 :type resource_cls: ServiceResource subclass 

290 :param resource_cls: The resource class to instantiate. 

291 :type parent: ServiceResource 

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

293 :type identifiers: dict 

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

295 :type resource_data: dict or None 

296 :param resource_data: Data for resource attributes. 

297 :rtype: ServiceResource 

298 :return: New resource instance. 

299 """ 

300 kwargs = { 

301 'client': parent.meta.client, 

302 } 

303 

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

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

306 if isinstance(value, list): 

307 value = value.pop(0) 

308 

309 kwargs[name] = value 

310 

311 resource = resource_cls(**kwargs) 

312 

313 if resource_data is not None: 

314 resource.meta.data = resource_data 

315 

316 return resource