""" .. module:: sfdc :synopsis: A Salesforce Rest API module .. moduleauthor:: Aaron Caffrey <acaffrey@salesforce.com>, Jose Garcia Ponce <jgarciaponce@salesforce.com>, Colin Cheevers <ccheevers@salesforce.com>, Tania Prince <tania.prince@salesforce.com> """ from __future__ import absolute_import from . import chatter from . import commons from . import jobs from . import wave import json import logging import re import requests try: from urllib.parse import urlencode except ImportError: from urllib import urlencode DEFAULT_API_VERSION = commons.DEFAULT_API_VERSION SOBJ_SERVICE = '/services/data/v%s/sobjects%s' REVOKE_SERVICE = '/services/oauth2/revoke' VERSIONS_SERVICE = '/services/data/' QUERY_SERVICE = '/services/data/v%s/query/?%s' SEARCH_SERVICE = '/services/data/v%s/search/?%s' TOOLING_ANONYMOUS = '/services/data/v%s/tooling/executeAnonymous/?%s' APPROVAL_SERVICE = '/services/data/v%s/process/approvals/' INSERT_BINARY_BODY_TEMPLATE = """--boundary_string Content-Disposition: form-data; name="entity_%s"; Content-Type: application/json %s --boundary_string Content-Type: %s Content-Disposition: form-data; name="%s"; filename="%s" %s --boundary_string--""" class ApprovalProcess(commons.BaseRequest): """ Returns a list of all approval processes. Can also be used to submit a particular record # noqa if that entity supports an approval process and one has already been defined. It also supports # noqa specifying a collection of different Process Approvals requests to have them all executed in bulk. .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, **kwargs): super( ApprovalProcess, self).__init__( session_id, instance_url, **kwargs) self.service = APPROVAL_SERVICE % self.api_version if self.request_body is None: self.http_method = 'GET' elif self.request_body is not None: self.http_method = 'POST' class Client(object): """ The client class from which all API calls to a Salesforce organisation are made. .. versionadded:: 1.0.0 """ def __init__(self, *args, **kwargs): """ Constructor. :Parameters: - `*username` (`string`) - Salesforce username. - `*password` (`string`) - Salesforce password. - `*login_url` (`string`) - Salesforce login URL. - `*client_id` (`string`) - Salesforce client ID. - `*client_secret` (`string`) - Salesforce client secret. - `\**kwargs` - kwargs (see below) :Keyword Arguments: * *protocol* (`string`) -- Protocol (future use) * *proxies* (`dict`) -- A dict containing proxies to be used by `requests` module. Ex: `{"https": "example.org:443"}` Default: `None` * *version* (`string`) -- SFDC API version to use e.g. '39.0' """ self.username = args[0] self.password = args[1] self.client_id = args[2] self.client_secret = args[3] self.protocol = kwargs.get('protocol') self.proxies = kwargs.get('proxies') self.instance_url = None self.logger = logging.getLogger('sfdc_py') self.logger.setLevel(logging.FATAL) self.logger.addHandler(logging.StreamHandler()) self.client_api_version = None self.client_kwargs = kwargs self.session_id = None self.chatter = chatter.Chatter(self) self.jobs = jobs.Jobs(self) self.wave = wave.Wave(self) def set_instance_url(self, url): """ Strips the protocol from `url` and assigns the value to `self.instance_url` :param url: Instance URL used to make requests (eg. `'https://eu11.salesforce.com'`) :type url: string """ host_only_regex = re.compile('(?:https://)(.*)(?:/*)') match = re.match(host_only_regex, url) instance_url = match.group(1) self.instance_url = instance_url @commons.kwarg_adder def login(self, **kwargs): """ Performs a login request. :param: **kwargs: kwargs :type: **kwargs: dict :return: Login response :rtype: (dict, Login) """ login_response = Login( self.username, self.password, self.client_id, self.client_secret, **kwargs ) req = login_response.request() if req is not None: self.session_id = login_response.get_session_id() self.set_instance_url(req.get('instance_url', str())) self.set_api_version() return req, login_response @commons.kwarg_adder def set_api_version(self, **kwargs): """ Sets the api version to be used by the client. If not provided, it will get the latest version available :return: set version kwarg on client if not defined """ # If 'version' was already in the client kwargs, then 'commons.kwarg_adder' decorator will take care of # passing it around between functions. Therefore, an else statement is not needed here. if 'version' not in self.client_kwargs: service = 'https://' + self.instance_url + VERSIONS_SERVICE headers = {'Content-Type': 'application/json'} r = requests.get(service, headers=headers, proxies=self.proxies) if r.status_code == 200: versions = [] for i in r.json(): versions.append(i['version']) self.client_kwargs.update({'version': max(versions)}) else: # return a known recent api version self.client_kwargs.update({'version': DEFAULT_API_VERSION}) @commons.kwarg_adder def logout(self, **kwargs): """ Performs a logout request. :param: **kwargs: kwargs :type: **kwargs: dict :return: Logout response :rtype: (dict, Logout) """ logout_response = Logout(self.session_id, self.instance_url, **kwargs) req = logout_response.request() return req, logout_response @commons.kwarg_adder def query(self, qs, **kwargs): """ Performs a query request. :param: qs: Query string. eg `'SELECT Id FROM Account LIMIT 10'` :type: qs: string :param: **kwargs: kwargs :type: **kwargs: dict :return: Query response :rtype: (dict, Query) """ q = Query(self.session_id, self.instance_url, qs, **kwargs) req = q.request() return req, q @commons.kwarg_adder def query_more(self, qs, **kwargs): """ Performs a query more request. :param: qs: Query string. eg `'SELECT Id FROM Lead'` :type: qs: string :param: **kwargs: kwargs :type: **kwargs: dict :return: QueryMore response :rtype: ([dict], QueryMore) """ qm = QueryMore(self.session_id, self.instance_url, qs, **kwargs) req = qm.request() return req, qm @commons.kwarg_adder def sobjects(self, **kwargs): """ Prepares an SObject controller with which make various API requests. :param: **kwargs: kwargs :type: **kwargs: dict :return: SObjects response :rtype: SObjectController """ _id = kwargs['id'] if 'id' in kwargs else None object_type = kwargs['object_type'] if 'object_type' in kwargs else None binary_field = kwargs['binary_field'] if 'binary_field' in kwargs else None api_version = kwargs.get('version') external_id = kwargs['external_id'] if 'external_id' in kwargs else None return SObjectController(self, object_type, _id, binary_field, api_version, external_id) @commons.kwarg_adder def search(self, ss, **kwargs): """ Performs a search request. :param: ss: Search string. eg `'FIND {sfdc_py} RETURNING Account(Id, Name) LIMIT 5'` :type: ss: string :param: **kwargs: kwargs :type: **kwargs: dict :return: Search response :rtype: (dict, Search) """ s = Search(self.session_id, self.instance_url, ss, **kwargs) req = s.request() return req, s @commons.kwarg_adder def execute_anonymous(self, ab, **kwargs): """ Performs an anonymous Apex execution request. :param: ab: Anonymous block of Apex code, eg: `'system.debug("Hello world")'` :type: ab: string :param: **kwargs: kwargs :type: **kwargs: dict :return: Execute anonymous response :rtype: (dict, ExecuteAnonymous) """ ea = ExecuteAnonymous( self.session_id, self.instance_url, ab, **kwargs) req = ea.request() return req, ea @commons.kwarg_adder def approvals(self, body=None, **kwargs): """ Performs an approval process request. :param: body: Body of approval process request, if any. :type: body: dict :return: Approval response :rtype: (dict, ApprovalProcess) """ k = { 'request_body': body, 'proxies': self.proxies } k.update(kwargs) ar = ApprovalProcess( session_id=self.session_id, instance_url=self.instance_url, **k) req = ar.request() return req, ar @commons.kwarg_adder def debug(self, **kwargs): """ Sets up debugging for the client at the level provided in the `level` kwarg. If this method is called but no `level` kwarg is provided, the client sets the debug level to `logging.INFO` by default. .. versionadded:: 1.0.0 :param: **kwargs: kwargs :type: **kwargs: dict """ logger = self.logger if 'level' in kwargs: level = kwargs['level'] logger.setLevel(level) else: logger.setLevel(logging.INFO) def __enter__(self): """ Invoked on entry to this class, handle login automatically for context managers :return: self """ self.login() return self def __exit__(self, _type, value, traceback): """ Handle logout automatically upon exiting the statement's body https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers :param type: exception type :param value: exception value :param traceback: traceback for the exception :return:None """ try: self.logout() except Exception as e: self.logger.warning('Unable to logout. Reason: {}'.format(e.args[0])) self.logger.info('__exit__ params: (%s, %s, %s)' % (_type, value, traceback)) class ExecuteAnonymous(commons.BaseRequest): """ Performs a request to `/services/data/vX.XX/tooling/executeAnonymous/` .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, ab, **kwargs): """ Constructor. Calls `super`, then encodes the `service` query string including the abstract block (`ab`) passed in. :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: ab: Anonymous block of Apex code, eg: `'system.debug("Hello world")'` :type: ab: string :param: **kwargs: kwargs :type: **kwargs: dict """ super( ExecuteAnonymous, self).__init__( session_id, instance_url, **kwargs) exec_anon = urlencode( {'anonymousBody': ab.encode('utf-8')}) self.service = TOOLING_ANONYMOUS % (self.api_version, exec_anon) class Login(commons.OAuthRequest): """ Performs a request to `'/services/oauth2/token'` .. versionadded:: 1.0.0 """ def __init__( self, username, password, client_id, client_secret, **kwargs): """ Constructor. Calls `super`, assigns all params to their equivalent instance variables, sets `http_method` to POST, and prepares the request service and payload. :param: username: Salesforce username :type: username: string :param: password: Salesforce password :type: password: string :param: login_url: Salesforce login URL :type: login_url: string :param: client_id: Salesforce client ID :type: client_id: string :param: client_secret: Salesforce client secret :type: client_secret: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(Login, self).__init__(None, None, **kwargs) self.username = username self.password = password self.login_url = kwargs['login_url'] if 'login_url' in kwargs else 'login.salesforce.com' self.client_id = client_id self.client_secret = client_secret self.http_method = 'POST' self.service = '/services/oauth2/token' self.payload = self.get_payload() def get_payload(self): """ Returns the payload dict to be used in the request. :return: OAuth2 request body required to obtain access token. :rtype: dict """ return { 'grant_type': 'password', 'client_id': self.client_id, 'client_secret': self.client_secret, 'username': self.username, 'password': self.password } def request(self): """ Gets the result of `super` for this method, then assigns the `access_token` to `session_id`. Returns request response. :return: Response dict :rtype: dict """ response = super(Login, self).request() if response is not None: if 'access_token' in response: self.session_id = response['access_token'] return response def get_session_id(self): """ Returns the session ID obtained if the login request was successful :return: Session ID :rtype: string """ return self.session_id class LoginException(Exception): """ Exception thrown during due to login failure. .. versionadded:: 1.0.0 """ pass class Logout(commons.OAuthRequest): """ Performs a request to `'/services/oauth2/revoke'` .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, **kwargs): """ Constructor. Calls `super`, assigns the service from the hardcoded value, and sets a `payload` instance variable with a dict where key is `'token'` and value is `session_id` :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(Logout, self).__init__(session_id, instance_url, **kwargs) self.service = REVOKE_SERVICE self.payload = {'token': self.session_id} class Query(commons.BaseRequest): """ Performs a request to `'/services/data/vX.XX/query/'` .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, query_string, **kwargs): """ Constructor. Calls `super`, then encodes the `service` including the `query_string` provided :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: query_string: Query string. eg `'SELECT Id FROM Account LIMIT 10'` :type: query_string: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(Query, self).__init__(session_id, instance_url, **kwargs) qry = urlencode({'q': query_string.encode('utf-8')}) self.service = QUERY_SERVICE % (self.api_version, qry) class QueryMore(commons.BaseRequest): """ Performs recursive requests to `'/services/data/vX.XX/query/'` when there are multiple batches to process. .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, query_string, **kwargs): """ Constructor. Calls `super`, then assigns the `query_string` to an instance variable. :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: query_string: Query string. eg `'SELECT Id FROM Account LIMIT 10'` :type: query_string: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(QueryMore, self).__init__(session_id, instance_url, **kwargs) self.query_string = query_string def request(self, *args): """ Makes a `Query` request for the initial query string, then calls itself recursively to request all remaining batches, if there are any. This method will break the recursion and return all when the last batch processed contains a `done` value equal to `True`. :param: results: All queried batch results obtained in prior recursions of this method. :type: results: [dict] :return: A list of dicts where each dict is a batch of query results :rtype: [dict] """ (last, results) = ( dict(), list() if len(args) is 0 else args[0], ) len_results = len(results) if len_results == 0: q = Query(self.session_id, self.instance_url, self.query_string) q.set_proxies(self.proxies) response = q.request() results.append(response) last = response elif len_results > 0: last = results[len_results - 1] if last.get('done') is False: (headers, logger, request_object, response, service) = self.get_request_vars() service = 'https://%s%s' % (self.instance_url, last.get('nextRecordsUrl')) logging.getLogger('sfdc_py').info('%s %s' % (self.http_method, service)) try: request_object = requests.get( service, headers=headers, proxies=self.proxies) self.status = request_object.status_code if request_object.content.decode('utf-8') == 'null': raise commons.SFDCRequestException('Request body is null') else: last = request_object.json() except Exception as e: self.exceptions.append(e) logger.error('%s %s %s' % (self.http_method, service, self.status)) logger.error(e.args[0]) return else: results.append(last) if last.get('done') is True: return results elif last.get('done') is False: return self.request(results) class Search(commons.BaseRequest): """ Performs a request to `'/services/data/vX.XX/search/'` .. versionadded:: 1.0.0 """ def __init__(self, session_id, instance_url, search_string, **kwargs): """ Constructor. Calls `super`, then encodes the `service` including the `search_string` provided :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: search_string: Search string. eg `'FIND {sfdc_py} RETURNING Account(Id, Name) LIMIT 5'` :type: search_string: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(Search, self).__init__(session_id, instance_url, **kwargs) s = urlencode({'q': search_string.encode('utf-8')}) self.service = SEARCH_SERVICE % (self.api_version, s) class SObjectBlob(commons.BaseRequest): """ Perform a request to `'/services/data/vX.XX/sobjects'` where file i/o is necessary. .. versionadded:: 1.0.0 """ def __init__(self, _client, service, http_method): """ Constructor. Calls `super`, then sets `service` and `http_method` instance variables. :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: service: The service to append to /services/data/vX.XX/sobjects :type: service: string :param: http_method: Method to use with request (`'GET'` and `'POST'` currently supported.) :type: http_method: string :param: **kwargs: kwargs :type: **kwargs: dict """ k = {'http_method': http_method} k.update(_client.client_kwargs) super(SObjectBlob, self).__init__(_client.session_id, _client.instance_url, **k) self.service = service def set_request_body(self, **kwargs): """ Creates binary request body by merging `entity`, `json_body`, `file_content_type`, `field`, `filename` and `content` from `**kwargs**` into a binary body template. Sets `request_body` instance variable with the result. Note: `content` can be either a `file` or a raw value. :param: **kwargs: :type: **kwargs: string """ if self.http_method == 'POST': content = kwargs['content'] self.request_body = INSERT_BINARY_BODY_TEMPLATE % ( kwargs['entity'], kwargs['json_body'], kwargs['file_content_type'], kwargs['field'], kwargs['filename'], content) def request(self): """ Returns the request response. :return: response :rtype: dict """ (headers, logger, request_object, response, service) = self.get_request_vars() logging.getLogger('sfdc_py').info('%s %s' % (self.http_method, service)) if self.http_method == 'GET': headers['Content-Type'] = 'application/octet-stream' try: request_object = requests.get( service, headers=headers, proxies=self.proxies, stream=True) self.status = request_object.status_code if request_object.content.decode('utf-8') == 'null': raise commons.SFDCRequestException('Request body is null') else: self.response = response = request_object except Exception as e: self.exceptions.append(e) logger.error('%s %s %s' % (self.http_method, service, self.status)) logger.error(e.args[0]) return finally: return response elif self.http_method == 'POST': headers['Content-Type'] = 'multipart/form-data;boundary="boundary_string"' try: request_object = requests.post( service, headers=headers, proxies=self.proxies, data=self.request_body) self.status = request_object.status_code if request_object.content.decode('utf-8') == 'null': raise commons.SFDCRequestException('Request body is null') else: self.response = response = request_object.json() except Exception as e: self.exceptions.append(e) logger.error('%s %s %s' % (self.http_method, service, self.status)) logger.error(e.args[0]) return finally: return response class SObjectController(object): """ A special class that controls insert/update/delete/query/describe of SObject resources. .. versionadded:: 1.0.0 """ def __init__(self, _client, object_type, _id, binary_field, api_version, external_id): """ Constructor. :param: _client: Salesforce client object :type: _client: Client :param: object_type: Name of the SObject, eg. `'Case'` :type: object_type: string :param: _id: Resource ID, if available :type: _id: string :param: binary_field: Binary field name, if available on object, eg. `'Body'` :type: binary_field: string """ self.__client__ = _client self.id = _id self.object_type = object_type self.binary_field = binary_field self.api_version = api_version self.external_id = external_id # Maintain client kwargs self.client_kwargs = _client.client_kwargs def get_service(self): """ Returns the correct sobject service depending on whether the countroller contains an `id` instance variable :return: service :rtype: string """ if self.binary_field is not None and self.object_type is not None and self.id is None: return SOBJ_SERVICE % ( self.api_version, '/' + self.object_type) elif self.id is not None and self.object_type is not None and self.external_id is None: return '/%s/%s' % (self.object_type, self.id) elif self.id is not None and self.object_type is not None and self.external_id is not None: return '/%s/%s/%s' % (self.object_type, self.external_id, self.id) elif self.object_type is None: return '' return '/%s' % self.object_type @commons.kwarg_adder def insert(self, body, **kwargs): """ Creates an SObject in Salesforce. Note: if `binary_field` is defined in kwargs, an `SObjectBlob` request will be made and returned, otherwise an `SObject` request will be made. :param: body: Body of SObject request. :type: body: dict :param: **kwargs: kwargs :type: **kwargs: dict :return: Insert result from Salesforce :rtype: (dict, SObject|SObjectBlob) """ sobj = None _client = self.__client__ resource_id = self.get_service() if self.binary_field is None: k = { 'http_method': 'POST', 'request_body': body, 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) elif self.binary_field is not None and 'binary' in kwargs: sobj = SObjectBlob( _client, resource_id, 'POST') # Prep request body properties entity = self.object_type.lower() json_body = json.dumps(body) field = self.binary_field filename = kwargs['binary'][0] content = kwargs['binary'][1] file_content_type = kwargs['binary'][2] # Set the request body sobj.set_request_body( entity=entity, json_body=json_body, field=field, filename=filename, content=content, file_content_type=file_content_type) req = sobj.request() return req, sobj @commons.kwarg_adder def update(self, body, **kwargs): """ Updates an SObject in Salesforce. :param: body: Body of SObject request. :type: body: dict :return: Update result from Salesforce :rtype: (None, SObject) """ _client = self.__client__ resource_id = self.get_service() k = { 'http_method': 'PATCH', 'request_body': body, 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() return req, sobj @commons.kwarg_adder def upsert(self, body, **kwargs): """ Upserts an SObject in Salesforce. :param: body: Body of SObject request. :type: body: dict :return: Upserts result from Salesforce :rtype: (None, SObject) """ _client = self.__client__ resource_id = self.get_service() k = { 'http_method': 'PATCH', 'request_body': body, 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() return req, sobj @commons.kwarg_adder def delete(self, **kwargs): """ Deletes an SObject in Salesforce. :return: Delete result from Salesforce :rtype: (None, SObject) """ _client = self.__client__ resource_id = self.get_service() k = { 'http_method': 'DELETE', 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() return req, sobj @commons.kwarg_adder def query(self, **kwargs): """ Queries an SObject in Salesforce. If a `binary_field` instance variable is defined, this method will further query the binary field content and return it accordingly. :return: Query result from Salesforce :rtype: (dict, SObject)|(dict, SObject, SObjectBlob) """ (_client, resource_id) = (self.__client__, self.get_service()) k = { 'http_method': 'GET', 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() if self.binary_field is not None and isinstance( req, dict) and self.binary_field in req: bin_service = req[self.binary_field] sob_blob = SObjectBlob( _client, bin_service, 'GET') sob_blob.request() return req, sobj, sob_blob return req, sobj @commons.kwarg_adder def describe(self, **kwargs): """ Describes the metadata for an SObject in Salesforce. .. versionadded:: 1.0.0 :return: Describe result from Salesforce :rtype: (dict, SObject) """ _client = self.__client__ resource_id = self.get_service() k = { 'http_method': 'GET', 'resource_id': resource_id + '/describe' } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() return req, sobj @commons.kwarg_adder def describe_global(self, **kwargs): """ Lists the available objects and their metadata for the organizations data. .. versionadded:: 1.0.0 :return: Describe global result from Salesforce :rtype: (dict, SObject) """ _client = self.__client__ resource_id = self.get_service() k = { 'http_method': 'GET', 'resource_id': resource_id } k.update(kwargs) sobj = SObjects(_client, **k) req = sobj.request() return req, sobj class SObjects(commons.BaseRequest): """ Perform a request to `'/services/data/vX.XX/sobjects'` .. versionadded:: 1.0.0 """ def __init__(self, _client, **kwargs): """ Constructor. Calls `super`, retrieves `resource_id` from `**kwargs` if present, then creates `self.service` instance variable. :param: session_id: Session ID used to make request :type: session_id: string :param: instance_url: Instance URL used to make the request (eg. `'eu11.salesforce.com'`) :type: instance_url: string :param: **kwargs: kwargs :type: **kwargs: dict """ super(SObjects, self).__init__(_client.session_id, _client.instance_url, **kwargs) resource_id = kwargs.get('resource_id') self.service = SOBJ_SERVICE % (self.api_version, resource_id) def request(self): """ Makes the appropriate request depending on the `http_method`. Supported now are: `'GET'`, `'POST'`, `'PATCH'`, and `'DELETE'`. Returns request response. Note: As successful `'PATCH'` and `'DELETE'` responses return `NO CONTENT`, this method will return `None`. It may be advisable to check the `status` of the `SObject` instance returned as an additional factor in determining whether the request succeeded. :return: response dict :rtype: dict|None """ sobjects_headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'application/json', 'Sforce-Auto-Assign': 'FALSE' } (headers, logger, request_object, response, service) = ( sobjects_headers, logging.getLogger('sfdc_py'), None, None, 'https://%s%s' % (self.instance_url, self.service) ) headers['Authorization'] = 'OAuth %s' % self.session_id logger.info('%s %s' % (self.http_method, service)) if self.http_method == 'POST': request_object = requests.post( service, headers=headers, json=self.request_body, proxies=self.proxies) elif self.http_method == 'PATCH': request_object = requests.patch( service, headers=headers, json=self.request_body, proxies=self.proxies) self.status = request_object.status_code if request_object.status_code == requests.codes.no_content: return None elif self.http_method == 'DELETE': request_object = requests.delete( service, headers=headers, proxies=self.proxies) self.status = request_object.status_code if request_object.status_code == requests.codes.no_content: return None elif self.http_method == 'GET': request_object = requests.get( service, headers=headers, proxies=self.proxies) self.status = request_object.status_code try: if request_object.content.decode('utf-8') == 'null': raise commons.SFDCRequestException('Request body is null') else: response = request_object.json() except Exception as e: self.exceptions.append(e) logger.error('%s %s %s' % (self.http_method, service, self.status)) logger.error(e.args[0]) return finally: return response def client(username, password, client_id, client_secret, **kwargs): """ Builds a `Client` and returns it. .. versionadded:: 1.0.0 Note: if any of the required parameters are missing, a `ValueError` will be raised. :Parameters: `*username` (`string`) Salesforce username. `*password` (`string`) Salesforce password. `*client_id` (`string`) Salesforce client ID. `*client_secret` (`string`) Salesforce client secret. `\**kwargs` kwargs (see below) :Keyword Arguments: `*login_url` (`string`) Salesforce login URL without protocol Default: `'login.salesforce.com'` `*protocol` (`string`) Protocol (future use) `*proxies` (`dict`) A dict containing proxies to be used by `requests` module. Example: `{"https": "example.org:443"}` Default: `None` `*timeout` ('string') Tell Requests to stop waiting for a response after a given number of seconds :returns: client :rtype: Client :raises: ValueError """ if username is None: raise ValueError('`username` cannot be None') elif password is None: raise ValueError('`password` cannot be None') elif client_id is None: raise ValueError('`client_id` cannot be None') elif client_secret is None: raise ValueError('`client_secret` cannot be None') return Client( username, password, client_id, client_secret, **kwargs)