def sobject_collections_request(self, method, records, all_or_none=True): # pylint:disable=too-many-locals assert method in ('GET', 'POST', 'PATCH', 'DELETE') if method == 'DELETE': params = dict(ids=','.join(records), allOrNone=str(bool(all_or_none)).lower()) resp = self.handle_api_exceptions(method, 'composite/sobjects', params=params) else: if method in ('POST', 'PATCH'): records = [merge_dict(x, attributes={'type': x['type_']}) for x in records] for x in records: x.pop('type_') post_data = {'records': records, 'allOrNone': all_or_none} else: raise NotSupportedError("Method {} not implemended".format(method)) resp = self.handle_api_exceptions(method, 'composite/sobjects', json=post_data) resp_data = resp.json() x_ok, x_err, x_roll = self._group_results(resp_data, records, all_or_none) is_ok = not x_err if is_ok: return [x['id'] for i, x in x_ok] # for .lastrowid width_type = max(len(type_) for i, errs, type_, id_ in x_err) width_type = max(width_type, len('sobject')) messages = ['', 'index {} sobject{:{width}s}error_info'.format( ('ID' + 16 * ' ' if x_err[0][3] else ''), '', width=(width_type + 2 - len('sobject')))] for i, errs, type_, id_ in x_err: field_info = 'FIELDS: {}'.format(errs[0]['fields']) if errs[0].get('fields') else '' msg = '{:5d} {} {:{width_type}s} {}: {} {}'.format( i, id_ or '', type_, errs[0]['statusCode'], errs[0]['message'], field_info, width_type=width_type) messages.append(msg) raise SalesforceError(messages)
def handle_api_exceptions_inter(self, method: str, *url_parts: str, **kwargs: Any) -> requests.Response: """The main (middle) part - it is enough if no error occurs.""" global request_count # used only in single thread tests - OK # pylint:disable=global-statement # log.info("request %s %s", method, '/'.join(url_parts)) api_ver = kwargs.pop('api_ver', None) url = self.rest_api_url(*url_parts, api_ver=api_ver) # The 'verify' option is about verifying TLS certificates kwargs_in = { 'timeout': getattr(settings, 'SALESFORCE_QUERY_TIMEOUT', (4, 15)), 'verify': True } kwargs_in.update(kwargs) log.debug('Request API URL: %s', url) request_count += 1 session = self.sf_session try: time_statistics.update_callback(url, self.ping_connection) response = session.request(method, url, **kwargs_in) except requests.exceptions.Timeout: raise SalesforceError("Timeout, URL=%s" % url) if (response.status_code == 401 # Unauthorized and 'json' in response.headers['content-type'] and response.json()[0]['errorCode'] == 'INVALID_SESSION_ID'): # Reauthenticate and retry (expired or invalid session ID or OAuth) token = session.auth.reauthenticate() if token: if 'headers' in kwargs: kwargs['headers'].update(Authorization='OAuth %s' % token) try: response = session.request(method, url, **kwargs_in) except requests.exceptions.Timeout: raise SalesforceError("Timeout, URL=%s" % url) if response.status_code < 400: # OK # 200 "OK" (GET, POST) # 201 "Created" (POST) # 204 "No Content" (DELETE) # 300 ambiguous items for external ID. # 304 "Not Modified" (after conditional HEADER request for metadata), return response # status codes docs (400, 403, 404, 405, 415, 500) # https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/errorcodes.htm self.raise_errors(response) return # type: ignore[return-value] # noqa
def raise_errors(self, response): """The innermost part - report errors by exceptions""" # Errors: 400, 403 permissions or REQUEST_LIMIT_EXCEEDED, 404, 405, 415, 500) # TODO extract a case ID for Salesforce support from code 500 messages # TODO disabled 'debug_verbs' temporarily, after writing better default messages verb = self.debug_verbs # NOQA pylint:disable=unused-variable method = response.request.method data = None is_json = 'json' in response.headers.get('Content-Type', '') and response.text if is_json: data = json.loads(response.text) if not (isinstance(data, list) and data and 'errorCode' in data[0]): messages = [response.text] if is_json else [] raise OperationalError([ 'HTTP error "%d %s":' % (response.status_code, response.reason) ] + messages, response, ['method+url']) # Other Errors are reported in the json body err_msg = data[0]['message'] err_code = data[0]['errorCode'] if response.status_code == 404: # ResourceNotFound if method == 'DELETE' and err_code in ( 'ENTITY_IS_DELETED', 'INVALID_CROSS_REFERENCE_KEY'): # It was a delete command and the object is in trash bin or it is # completely deleted or it could be a valid Id for this sobject type. # Then we accept it with a warning, similarly to delete by a classic database query: # DELETE FROM xy WHERE id = 'something_deleted_yet' warn_sf([err_msg, "Object is deleted before delete or update"], response, ['method+url']) # TODO add a warning and add it to messages return None if err_code in ( 'NOT_FOUND', # 404 e.g. invalid object type in url path or url query?q=select ... 'METHOD_NOT_ALLOWED', # 405 e.g. patch instead of post ): # both need to report the url raise SalesforceError([err_msg], response, ['method+url']) # it is good e.g for these errorCode: ('INVALID_FIELD', 'MALFORMED_QUERY', 'INVALID_FIELD_FOR_INSERT_UPDATE') raise SalesforceError([err_msg], response)