|
|
@@ -0,0 +1,855 @@
|
|
|
+"""Core classes and exceptions for Simple-Salesforce"""
|
|
|
+
|
|
|
+
|
|
|
+# has to be defined prior to login import
|
|
|
+DEFAULT_API_VERSION = '38.0'
|
|
|
+
|
|
|
+
|
|
|
+import logging
|
|
|
+import warnings
|
|
|
+import requests
|
|
|
+import json
|
|
|
+import re
|
|
|
+from collections import namedtuple
|
|
|
+
|
|
|
+try:
|
|
|
+ from urlparse import urlparse, urljoin
|
|
|
+except ImportError:
|
|
|
+ # Python 3+
|
|
|
+ from urllib.parse import urlparse, urljoin
|
|
|
+
|
|
|
+from simple_salesforce.login import SalesforceLogin
|
|
|
+from simple_salesforce.util import date_to_iso8601, exception_handler
|
|
|
+from simple_salesforce.exceptions import (
|
|
|
+ SalesforceGeneralError
|
|
|
+)
|
|
|
+from simple_salesforce.bulk import SFBulkHandler
|
|
|
+
|
|
|
+try:
|
|
|
+ from collections import OrderedDict
|
|
|
+except ImportError:
|
|
|
+ # Python < 2.7
|
|
|
+ from ordereddict import OrderedDict
|
|
|
+
|
|
|
+#pylint: disable=invalid-name
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+
|
|
|
+def _warn_request_deprecation():
|
|
|
+ """Deprecation for (Salesforce/SFType).request attribute"""
|
|
|
+ warnings.warn(
|
|
|
+ 'The request attribute has been deprecated and will be removed in a '
|
|
|
+ 'future version. Please use Salesforce.session instead.',
|
|
|
+ DeprecationWarning
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+Usage = namedtuple('Usage', 'used total')
|
|
|
+PerAppUsage = namedtuple('PerAppUsage', 'used total name')
|
|
|
+
|
|
|
+
|
|
|
+# pylint: disable=too-many-instance-attributes
|
|
|
+class Salesforce(object):
|
|
|
+ """Salesforce Instance
|
|
|
+
|
|
|
+ An instance of Salesforce is a handy way to wrap a Salesforce session
|
|
|
+ for easy use of the Salesforce REST API.
|
|
|
+ """
|
|
|
+ # pylint: disable=too-many-arguments
|
|
|
+ def __init__(
|
|
|
+ self, username=None, password=None, security_token=None,
|
|
|
+ session_id=None, instance=None, instance_url=None,
|
|
|
+ organizationId=None, sandbox=None, version=DEFAULT_API_VERSION,
|
|
|
+ proxies=None, session=None, client_id=None, domain=None):
|
|
|
+ """Initialize the instance with the given parameters.
|
|
|
+
|
|
|
+ Available kwargs
|
|
|
+
|
|
|
+ Password Authentication:
|
|
|
+
|
|
|
+ * username -- the Salesforce username to use for authentication
|
|
|
+ * password -- the password for the username
|
|
|
+ * security_token -- the security token for the username
|
|
|
+ * sandbox -- DEPRECATED: Use domain instead.
|
|
|
+ * domain -- The domain to using for connecting to Salesforce. Use
|
|
|
+ common domains, such as 'login' or 'test', or
|
|
|
+ Salesforce My domain. If not used, will default to
|
|
|
+ 'login'.
|
|
|
+
|
|
|
+ Direct Session and Instance Access:
|
|
|
+
|
|
|
+ * session_id -- Access token for this session
|
|
|
+
|
|
|
+ Then either
|
|
|
+ * instance -- Domain of your Salesforce instance, i.e.
|
|
|
+ `na1.salesforce.com`
|
|
|
+ OR
|
|
|
+ * instance_url -- Full URL of your instance i.e.
|
|
|
+ `https://na1.salesforce.com
|
|
|
+
|
|
|
+ Universal Kwargs:
|
|
|
+ * version -- the version of the Salesforce API to use, for example
|
|
|
+ `29.0`
|
|
|
+ * proxies -- the optional map of scheme to proxy server
|
|
|
+ * session -- Custom requests session, created in calling code. This
|
|
|
+ enables the use of requests Session features not otherwise
|
|
|
+ exposed by simple_salesforce.
|
|
|
+
|
|
|
+ """
|
|
|
+ if (sandbox is not None) and (domain is not None):
|
|
|
+ raise ValueError("Both 'sandbox' and 'domain' arguments were "
|
|
|
+ "supplied. Either may be supplied, but not "
|
|
|
+ "both.")
|
|
|
+
|
|
|
+ if sandbox is not None:
|
|
|
+ warnings.warn("'sandbox' argument is deprecated. Use "
|
|
|
+ "'domain' instead. Overriding 'domain' "
|
|
|
+ "with 'sandbox' value.",
|
|
|
+ DeprecationWarning)
|
|
|
+
|
|
|
+ domain = 'test' if sandbox else 'login'
|
|
|
+
|
|
|
+ if domain is None:
|
|
|
+ domain = 'login'
|
|
|
+
|
|
|
+ # Determine if the user passed in the optional version and/or
|
|
|
+ # domain kwargs
|
|
|
+ self.sf_version = version
|
|
|
+ self.domain = domain
|
|
|
+ self.session = session or requests.Session()
|
|
|
+ self.proxies = self.session.proxies
|
|
|
+ # override custom session proxies dance
|
|
|
+ if proxies is not None:
|
|
|
+ if not session:
|
|
|
+ self.session.proxies = self.proxies = proxies
|
|
|
+ else:
|
|
|
+ logger.warning(
|
|
|
+ 'Proxies must be defined on custom session object, '
|
|
|
+ 'ignoring proxies: %s', proxies
|
|
|
+ )
|
|
|
+
|
|
|
+ # Determine if the user wants to use our username/password auth or pass
|
|
|
+ # in their own information
|
|
|
+ if all(arg is not None for arg in (
|
|
|
+ username, password, security_token)):
|
|
|
+ self.auth_type = "password"
|
|
|
+
|
|
|
+ # Pass along the username/password to our login helper
|
|
|
+ self.session_id, self.sf_instance = SalesforceLogin(
|
|
|
+ session=self.session,
|
|
|
+ username=username,
|
|
|
+ password=password,
|
|
|
+ security_token=security_token,
|
|
|
+ sf_version=self.sf_version,
|
|
|
+ proxies=self.proxies,
|
|
|
+ client_id=client_id,
|
|
|
+ domain=self.domain)
|
|
|
+
|
|
|
+ elif all(arg is not None for arg in (
|
|
|
+ session_id, instance or instance_url)):
|
|
|
+ self.auth_type = "direct"
|
|
|
+ self.session_id = session_id
|
|
|
+
|
|
|
+ # If the user provides the full url (as returned by the OAuth
|
|
|
+ # interface for example) extract the hostname (which we rely on)
|
|
|
+ if instance_url is not None:
|
|
|
+ self.sf_instance = urlparse(instance_url).hostname
|
|
|
+ else:
|
|
|
+ self.sf_instance = instance
|
|
|
+
|
|
|
+ elif all(arg is not None for arg in (
|
|
|
+ username, password, organizationId)):
|
|
|
+ self.auth_type = 'ipfilter'
|
|
|
+
|
|
|
+ # Pass along the username/password to our login helper
|
|
|
+ self.session_id, self.sf_instance = SalesforceLogin(
|
|
|
+ session=self.session,
|
|
|
+ username=username,
|
|
|
+ password=password,
|
|
|
+ organizationId=organizationId,
|
|
|
+ sf_version=self.sf_version,
|
|
|
+ proxies=self.proxies,
|
|
|
+ client_id=client_id,
|
|
|
+ domain=self.domain)
|
|
|
+
|
|
|
+ else:
|
|
|
+ raise TypeError(
|
|
|
+ 'You must provide login information or an instance and token'
|
|
|
+ )
|
|
|
+
|
|
|
+ self.auth_site = ('https://{domain}.salesforce.com'
|
|
|
+ .format(domain=self.domain))
|
|
|
+
|
|
|
+ self.headers = {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': 'Bearer ' + self.session_id,
|
|
|
+ 'X-PrettyPrint': '1'
|
|
|
+ }
|
|
|
+
|
|
|
+ self.base_url = ('https://{instance}/services/data/v{version}/'
|
|
|
+ .format(instance=self.sf_instance,
|
|
|
+ version=self.sf_version))
|
|
|
+ self.apex_url = ('https://{instance}/services/apexrest/'
|
|
|
+ .format(instance=self.sf_instance))
|
|
|
+ self.bulk_url = ('https://{instance}/services/async/{version}/'
|
|
|
+ .format(instance=self.sf_instance,
|
|
|
+ version=self.sf_version))
|
|
|
+
|
|
|
+ self.api_usage = {}
|
|
|
+
|
|
|
+ def describe(self):
|
|
|
+ """Describes all available objects
|
|
|
+ """
|
|
|
+ url = self.base_url + "sobjects"
|
|
|
+ result = self._call_salesforce('GET', url, name='describe')
|
|
|
+
|
|
|
+ json_result = result.json(object_pairs_hook=OrderedDict)
|
|
|
+ if len(json_result) == 0:
|
|
|
+ return None
|
|
|
+
|
|
|
+ return json_result
|
|
|
+
|
|
|
+ # SObject Handler
|
|
|
+ def __getattr__(self, name):
|
|
|
+ """Returns an `SFType` instance for the given Salesforce object type
|
|
|
+ (given in `name`).
|
|
|
+
|
|
|
+ The magic part of the SalesforceAPI, this function translates
|
|
|
+ calls such as `salesforce_api_instance.Lead.metadata()` into fully
|
|
|
+ constituted `SFType` instances to make a nice Python API wrapper
|
|
|
+ for the REST API.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * name -- the name of a Salesforce object type, e.g. Lead or Contact
|
|
|
+ """
|
|
|
+
|
|
|
+ # fix to enable serialization
|
|
|
+ # (https://github.com/heroku/simple-salesforce/issues/60)
|
|
|
+ if name.startswith('__'):
|
|
|
+ return super(Salesforce, self).__getattr__(name)
|
|
|
+
|
|
|
+ if name == 'bulk':
|
|
|
+ # Deal with bulk API functions
|
|
|
+ return SFBulkHandler(self.session_id, self.bulk_url, self.proxies,
|
|
|
+ self.session)
|
|
|
+
|
|
|
+ return SFType(
|
|
|
+ name, self.session_id, self.sf_instance, sf_version=self.sf_version,
|
|
|
+ proxies=self.proxies, session=self.session)
|
|
|
+
|
|
|
+ # User utility methods
|
|
|
+ def set_password(self, user, password):
|
|
|
+ """Sets the password of a user
|
|
|
+
|
|
|
+ salesforce dev documentation link:
|
|
|
+ https://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_user_password.htm
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * user: the userID of the user to set
|
|
|
+ * password: the new password
|
|
|
+ """
|
|
|
+
|
|
|
+ url = self.base_url + 'sobjects/User/%s/password' % user
|
|
|
+ params = {'NewPassword': password}
|
|
|
+
|
|
|
+ result = self._call_salesforce('POST', url, data=json.dumps(params))
|
|
|
+
|
|
|
+ # salesforce return 204 No Content when the request is successful
|
|
|
+ if result.status_code != 200 and result.status_code != 204:
|
|
|
+ raise SalesforceGeneralError(url,
|
|
|
+ result.status_code,
|
|
|
+ 'User',
|
|
|
+ result.content)
|
|
|
+ json_result = result.json(object_pairs_hook=OrderedDict)
|
|
|
+ if len(json_result) == 0:
|
|
|
+ return None
|
|
|
+
|
|
|
+ return json_result
|
|
|
+
|
|
|
+ # pylint: disable=invalid-name
|
|
|
+ def setPassword(self, user, password):
|
|
|
+ # pylint: disable=line-too-long
|
|
|
+ """Sets the password of a user
|
|
|
+
|
|
|
+ salesforce dev documentation link:
|
|
|
+ https://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_user_password.htm
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * user: the userID of the user to set
|
|
|
+ * password: the new password
|
|
|
+ """
|
|
|
+ warnings.warn(
|
|
|
+ "This method has been deprecated."
|
|
|
+ "Please use set_password instead.",
|
|
|
+ DeprecationWarning)
|
|
|
+ return self.set_password(user, password)
|
|
|
+
|
|
|
+ # Generic Rest Function
|
|
|
+ def restful(self, path, params=None, method='GET', **kwargs):
|
|
|
+ """Allows you to make a direct REST call if you know the path
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * path: The path of the request
|
|
|
+ Example: sobjects/User/ABC123/password'
|
|
|
+ * params: dict of parameters to pass to the path
|
|
|
+ * method: HTTP request method, default GET
|
|
|
+ * other arguments supported by requests.request (e.g. json, timeout)
|
|
|
+ """
|
|
|
+
|
|
|
+ url = self.base_url + path
|
|
|
+ result = self._call_salesforce(method, url, name=path, params=params,
|
|
|
+ **kwargs)
|
|
|
+
|
|
|
+ json_result = result.json(object_pairs_hook=OrderedDict)
|
|
|
+ if len(json_result) == 0:
|
|
|
+ return None
|
|
|
+
|
|
|
+ return json_result
|
|
|
+
|
|
|
+ # Search Functions
|
|
|
+ def search(self, search):
|
|
|
+ """Returns the result of a Salesforce search as a dict decoded from
|
|
|
+ the Salesforce response JSON payload.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * search -- the fully formatted SOSL search string, e.g.
|
|
|
+ `FIND {Waldo}`
|
|
|
+ """
|
|
|
+ url = self.base_url + 'search/'
|
|
|
+
|
|
|
+ # `requests` will correctly encode the query string passed as `params`
|
|
|
+ params = {'q': search}
|
|
|
+ result = self._call_salesforce('GET', url, name='search', params=params)
|
|
|
+
|
|
|
+ json_result = result.json(object_pairs_hook=OrderedDict)
|
|
|
+ if len(json_result) == 0:
|
|
|
+ return None
|
|
|
+
|
|
|
+ return json_result
|
|
|
+
|
|
|
+ def quick_search(self, search):
|
|
|
+ """Returns the result of a Salesforce search as a dict decoded from
|
|
|
+ the Salesforce response JSON payload.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * search -- the non-SOSL search string, e.g. `Waldo`. This search
|
|
|
+ string will be wrapped to read `FIND {Waldo}` before being
|
|
|
+ sent to Salesforce
|
|
|
+ """
|
|
|
+ search_string = u'FIND {{{search_string}}}'.format(search_string=search)
|
|
|
+ return self.search(search_string)
|
|
|
+
|
|
|
+ def limits(self, **kwargs):
|
|
|
+ """Return the result of a Salesforce request to list Organization
|
|
|
+ limits.
|
|
|
+ """
|
|
|
+ url = self.base_url + 'limits/'
|
|
|
+ result = self._call_salesforce('GET', url, **kwargs)
|
|
|
+
|
|
|
+ if result.status_code != 200:
|
|
|
+ exception_handler(result)
|
|
|
+
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ # Query Handler
|
|
|
+ def query(self, query, include_deleted=False, **kwargs):
|
|
|
+ """Return the result of a Salesforce SOQL query as a dict decoded from
|
|
|
+ the Salesforce response JSON payload.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * query -- the SOQL query to send to Salesforce, e.g.
|
|
|
+ SELECT Id FROM Lead WHERE Email = "waldo@somewhere.com"
|
|
|
+ * include_deleted -- True if deleted records should be included
|
|
|
+ """
|
|
|
+ url = self.base_url + ('queryAll/' if include_deleted else 'query/')
|
|
|
+ params = {'q': query}
|
|
|
+ # `requests` will correctly encode the query string passed as `params`
|
|
|
+ result = self._call_salesforce('GET', url, name='query',
|
|
|
+ params=params, **kwargs)
|
|
|
+
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def query_more(
|
|
|
+ self, next_records_identifier, identifier_is_url=False,
|
|
|
+ include_deleted=False, **kwargs):
|
|
|
+ """Retrieves more results from a query that returned more results
|
|
|
+ than the batch maximum. Returns a dict decoded from the Salesforce
|
|
|
+ response JSON payload.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * next_records_identifier -- either the Id of the next Salesforce
|
|
|
+ object in the result, or a URL to the
|
|
|
+ next record in the result.
|
|
|
+ * identifier_is_url -- True if `next_records_identifier` should be
|
|
|
+ treated as a URL, False if
|
|
|
+ `next_records_identifier` should be treated as
|
|
|
+ an Id.
|
|
|
+ * include_deleted -- True if the `next_records_identifier` refers to a
|
|
|
+ query that includes deleted records. Only used if
|
|
|
+ `identifier_is_url` is False
|
|
|
+ """
|
|
|
+ if identifier_is_url:
|
|
|
+ # Don't use `self.base_url` here because the full URI is provided
|
|
|
+ url = (u'https://{instance}{next_record_url}'
|
|
|
+ .format(instance=self.sf_instance,
|
|
|
+ next_record_url=next_records_identifier))
|
|
|
+ else:
|
|
|
+ endpoint = 'queryAll' if include_deleted else 'query'
|
|
|
+ url = self.base_url + '{query_endpoint}/{next_record_id}'
|
|
|
+ url = url.format(query_endpoint=endpoint,
|
|
|
+ next_record_id=next_records_identifier)
|
|
|
+ result = self._call_salesforce('GET', url, name='query_more', **kwargs)
|
|
|
+
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def query_all(self, query, include_deleted=False, **kwargs):
|
|
|
+ """Returns the full set of results for the `query`. This is a
|
|
|
+ convenience
|
|
|
+ wrapper around `query(...)` and `query_more(...)`.
|
|
|
+
|
|
|
+ The returned dict is the decoded JSON payload from the final call to
|
|
|
+ Salesforce, but with the `totalSize` field representing the full
|
|
|
+ number of results retrieved and the `records` list representing the
|
|
|
+ full list of records retrieved.
|
|
|
+
|
|
|
+ Arguments
|
|
|
+
|
|
|
+ * query -- the SOQL query to send to Salesforce, e.g.
|
|
|
+ SELECT Id FROM Lead WHERE Email = "waldo@somewhere.com"
|
|
|
+ * include_deleted -- True if the query should include deleted records.
|
|
|
+ """
|
|
|
+
|
|
|
+ result = self.query(query, include_deleted=include_deleted, **kwargs)
|
|
|
+ all_records = []
|
|
|
+
|
|
|
+ while True:
|
|
|
+ all_records.extend(result['records'])
|
|
|
+ # fetch next batch if we're not done else break out of loop
|
|
|
+ if not result['done']:
|
|
|
+ result = self.query_more(result['nextRecordsUrl'],
|
|
|
+ identifier_is_url=True)
|
|
|
+ else:
|
|
|
+ break
|
|
|
+
|
|
|
+ result['records'] = all_records
|
|
|
+ return result
|
|
|
+
|
|
|
+ def apexecute(self, action, method='GET', data=None, **kwargs):
|
|
|
+ """Makes an HTTP request to an APEX REST endpoint
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * action -- The REST endpoint for the request.
|
|
|
+ * method -- HTTP method for the request (default GET)
|
|
|
+ * data -- A dict of parameters to send in a POST / PUT request
|
|
|
+ * kwargs -- Additional kwargs to pass to `requests.request`
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method,
|
|
|
+ self.apex_url + action,
|
|
|
+ name="apexexcute",
|
|
|
+ data=json.dumps(data), **kwargs
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ response_content = result.json()
|
|
|
+ # pylint: disable=broad-except
|
|
|
+ except Exception:
|
|
|
+ response_content = result.text
|
|
|
+
|
|
|
+ return response_content
|
|
|
+
|
|
|
+ def _call_salesforce(self, method, url, name="", **kwargs):
|
|
|
+ """Utility method for performing HTTP call to Salesforce.
|
|
|
+
|
|
|
+ Returns a `requests.result` object.
|
|
|
+ """
|
|
|
+ result = self.session.request(
|
|
|
+ method, url, headers=self.headers, **kwargs)
|
|
|
+
|
|
|
+ if result.status_code >= 300:
|
|
|
+ exception_handler(result, name=name)
|
|
|
+
|
|
|
+ sforce_limit_info = result.headers.get('Sforce-Limit-Info')
|
|
|
+ if sforce_limit_info:
|
|
|
+ self.api_usage = self.parse_api_usage(sforce_limit_info)
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ @property
|
|
|
+ def request(self):
|
|
|
+ """Deprecated access to self.session for backwards compatibility"""
|
|
|
+ _warn_request_deprecation()
|
|
|
+ return self.session
|
|
|
+
|
|
|
+ @request.setter
|
|
|
+ def request(self, session):
|
|
|
+ """Deprecated setter for self.session"""
|
|
|
+ _warn_request_deprecation()
|
|
|
+ self.session = session
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def parse_api_usage(sforce_limit_info):
|
|
|
+ """parse API usage and limits out of the Sforce-Limit-Info header
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * sforce_limit_info: The value of response header 'Sforce-Limit-Info'
|
|
|
+ Example 1: 'api-usage=18/5000'
|
|
|
+ Example 2: 'api-usage=25/5000;
|
|
|
+ per-app-api-usage=17/250(appName=sample-connected-app)'
|
|
|
+ """
|
|
|
+ result = {}
|
|
|
+
|
|
|
+ api_usage = re.match(r'[^-]?api-usage=(?P<used>\d+)/(?P<tot>\d+)',
|
|
|
+ sforce_limit_info)
|
|
|
+ pau = r'.+per-app-api-usage=(?P<u>\d+)/(?P<t>\d+)\(appName=(?P<n>.+)\)'
|
|
|
+ per_app_api_usage = re.match(pau, sforce_limit_info)
|
|
|
+
|
|
|
+ if api_usage and api_usage.groups():
|
|
|
+ groups = api_usage.groups()
|
|
|
+ result['api-usage'] = Usage(used=int(groups[0]),
|
|
|
+ total=int(groups[1]))
|
|
|
+ if per_app_api_usage and per_app_api_usage.groups():
|
|
|
+ groups = per_app_api_usage.groups()
|
|
|
+ result['per-app-api-usage'] = PerAppUsage(used=int(groups[0]),
|
|
|
+ total=int(groups[1]),
|
|
|
+ name=groups[2])
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+class SFType(object):
|
|
|
+ """An interface to a specific type of SObject"""
|
|
|
+
|
|
|
+ # pylint: disable=too-many-arguments
|
|
|
+ def __init__(
|
|
|
+ self, object_name, session_id, sf_instance,
|
|
|
+ sf_version=DEFAULT_API_VERSION, proxies=None, session=None):
|
|
|
+ """Initialize the instance with the given parameters.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * object_name -- the name of the type of SObject this represents,
|
|
|
+ e.g. `Lead` or `Contact`
|
|
|
+ * session_id -- the session ID for authenticating to Salesforce
|
|
|
+ * sf_instance -- the domain of the instance of Salesforce to use
|
|
|
+ * sf_version -- the version of the Salesforce API to use
|
|
|
+ * proxies -- the optional map of scheme to proxy server
|
|
|
+ * session -- Custom requests session, created in calling code. This
|
|
|
+ enables the use of requests Session features not otherwise
|
|
|
+ exposed by simple_salesforce.
|
|
|
+ """
|
|
|
+ self.session_id = session_id
|
|
|
+ self.name = object_name
|
|
|
+ self.session = session or requests.Session()
|
|
|
+ # don't wipe out original proxies with None
|
|
|
+ if not session and proxies is not None:
|
|
|
+ self.session.proxies = proxies
|
|
|
+ self.api_usage = {}
|
|
|
+
|
|
|
+ self.base_url = (
|
|
|
+ u'https://{instance}/services/data/v{sf_version}/sobjects'
|
|
|
+ '/{object_name}/'.format(instance=sf_instance,
|
|
|
+ object_name=object_name,
|
|
|
+ sf_version=sf_version))
|
|
|
+
|
|
|
+ def metadata(self, headers=None):
|
|
|
+ """Returns the result of a GET to `.../{object_name}/` as a dict
|
|
|
+ decoded from the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce('GET', self.base_url, headers=headers)
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def describe(self, headers=None):
|
|
|
+ """Returns the result of a GET to `.../{object_name}/describe` as a
|
|
|
+ dict decoded from the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='GET', url=urljoin(self.base_url, 'describe'),
|
|
|
+ headers=headers
|
|
|
+ )
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def describe_layout(self, record_id, headers=None):
|
|
|
+ """Returns the layout of the object
|
|
|
+
|
|
|
+ Returns the result of a GET to
|
|
|
+ `.../{object_name}/describe/layouts/<recordid>` as a dict decoded from
|
|
|
+ the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * record_id -- the Id of the SObject to get
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ custom_url_part = 'describe/layouts/{record_id}'.format(
|
|
|
+ record_id=record_id
|
|
|
+ )
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='GET',
|
|
|
+ url=urljoin(self.base_url, custom_url_part),
|
|
|
+ headers=headers
|
|
|
+ )
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def get(self, record_id, headers=None):
|
|
|
+ """Returns the result of a GET to `.../{object_name}/{record_id}` as a
|
|
|
+ dict decoded from the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * record_id -- the Id of the SObject to get
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='GET', url=urljoin(self.base_url, record_id),
|
|
|
+ headers=headers
|
|
|
+ )
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def get_by_custom_id(self, custom_id_field, custom_id, headers=None):
|
|
|
+ """Return an ``SFType`` by custom ID
|
|
|
+
|
|
|
+ Returns the result of a GET to
|
|
|
+ `.../{object_name}/{custom_id_field}/{custom_id}` as a dict decoded
|
|
|
+ from the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * custom_id_field -- the API name of a custom field that was defined
|
|
|
+ as an External ID
|
|
|
+ * custom_id - the External ID value of the SObject to get
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ custom_url = urljoin(
|
|
|
+ self.base_url, '{custom_id_field}/{custom_id}'.format(
|
|
|
+ custom_id_field=custom_id_field, custom_id=custom_id
|
|
|
+ )
|
|
|
+ )
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='GET', url=custom_url, headers=headers
|
|
|
+ )
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def create(self, data, headers=None):
|
|
|
+ """Creates a new SObject using a POST to `.../{object_name}/`.
|
|
|
+
|
|
|
+ Returns a dict decoded from the JSON payload returned by Salesforce.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * data -- a dict of the data to create the SObject from. It will be
|
|
|
+ JSON-encoded before being transmitted.
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='POST', url=self.base_url,
|
|
|
+ data=json.dumps(data), headers=headers
|
|
|
+ )
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def upsert(self, record_id, data, raw_response=False, headers=None):
|
|
|
+ """Creates or updates an SObject using a PATCH to
|
|
|
+ `.../{object_name}/{record_id}`.
|
|
|
+
|
|
|
+ If `raw_response` is false (the default), returns the status code
|
|
|
+ returned by Salesforce. Otherwise, return the `requests.Response`
|
|
|
+ object.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * record_id -- an identifier for the SObject as described in the
|
|
|
+ Salesforce documentation
|
|
|
+ * data -- a dict of the data to create or update the SObject from. It
|
|
|
+ will be JSON-encoded before being transmitted.
|
|
|
+ * raw_response -- a boolean indicating whether to return the response
|
|
|
+ directly, instead of the status code.
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='PATCH', url=urljoin(self.base_url, record_id),
|
|
|
+ data=json.dumps(data), headers=headers
|
|
|
+ )
|
|
|
+ return self._raw_response(result, raw_response)
|
|
|
+
|
|
|
+ def update(self, record_id, data, raw_response=False, headers=None):
|
|
|
+ """Updates an SObject using a PATCH to
|
|
|
+ `.../{object_name}/{record_id}`.
|
|
|
+
|
|
|
+ If `raw_response` is false (the default), returns the status code
|
|
|
+ returned by Salesforce. Otherwise, return the `requests.Response`
|
|
|
+ object.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * record_id -- the Id of the SObject to update
|
|
|
+ * data -- a dict of the data to update the SObject from. It will be
|
|
|
+ JSON-encoded before being transmitted.
|
|
|
+ * raw_response -- a boolean indicating whether to return the response
|
|
|
+ directly, instead of the status code.
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='PATCH', url=urljoin(self.base_url, record_id),
|
|
|
+ data=json.dumps(data), headers=headers
|
|
|
+ )
|
|
|
+ return self._raw_response(result, raw_response)
|
|
|
+
|
|
|
+ def delete(self, record_id, raw_response=False, headers=None):
|
|
|
+ """Deletes an SObject using a DELETE to
|
|
|
+ `.../{object_name}/{record_id}`.
|
|
|
+
|
|
|
+ If `raw_response` is false (the default), returns the status code
|
|
|
+ returned by Salesforce. Otherwise, return the `requests.Response`
|
|
|
+ object.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * record_id -- the Id of the SObject to delete
|
|
|
+ * raw_response -- a boolean indicating whether to return the response
|
|
|
+ directly, instead of the status code.
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ result = self._call_salesforce(
|
|
|
+ method='DELETE', url=urljoin(self.base_url, record_id),
|
|
|
+ headers=headers
|
|
|
+ )
|
|
|
+ return self._raw_response(result, raw_response)
|
|
|
+
|
|
|
+ def deleted(self, start, end, headers=None):
|
|
|
+ # pylint: disable=line-too-long
|
|
|
+ """Gets a list of deleted records
|
|
|
+
|
|
|
+ Use the SObject Get Deleted resource to get a list of deleted records
|
|
|
+ for the specified object.
|
|
|
+ .../deleted/?start=2013-05-05T00:00:00+00:00&end=2013-05-10T00:00:00+00:00
|
|
|
+
|
|
|
+ * start -- start datetime object
|
|
|
+ * end -- end datetime object
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ url = urljoin(
|
|
|
+ self.base_url, 'deleted/?start={start}&end={end}'.format(
|
|
|
+ start=date_to_iso8601(start), end=date_to_iso8601(end)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ result = self._call_salesforce(method='GET', url=url, headers=headers)
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def updated(self, start, end, headers=None):
|
|
|
+ # pylint: disable=line-too-long
|
|
|
+ """Gets a list of updated records
|
|
|
+
|
|
|
+ Use the SObject Get Updated resource to get a list of updated
|
|
|
+ (modified or added) records for the specified object.
|
|
|
+
|
|
|
+ .../updated/?start=2014-03-20T00:00:00+00:00&end=2014-03-22T00:00:00+00:00
|
|
|
+
|
|
|
+ * start -- start datetime object
|
|
|
+ * end -- end datetime object
|
|
|
+ * headers -- a dict with additional request headers.
|
|
|
+ """
|
|
|
+ url = urljoin(
|
|
|
+ self.base_url, 'updated/?start={start}&end={end}'.format(
|
|
|
+ start=date_to_iso8601(start), end=date_to_iso8601(end)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ result = self._call_salesforce(method='GET', url=url, headers=headers)
|
|
|
+ return result.json(object_pairs_hook=OrderedDict)
|
|
|
+
|
|
|
+ def _call_salesforce(self, method, url, **kwargs):
|
|
|
+ """Utility method for performing HTTP call to Salesforce.
|
|
|
+
|
|
|
+ Returns a `requests.result` object.
|
|
|
+ """
|
|
|
+ headers = {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': 'Bearer ' + self.session_id,
|
|
|
+ 'X-PrettyPrint': '1'
|
|
|
+ }
|
|
|
+ additional_headers = kwargs.pop('headers', dict())
|
|
|
+ headers.update(additional_headers or dict())
|
|
|
+ result = self.session.request(method, url, headers=headers, **kwargs)
|
|
|
+
|
|
|
+ if result.status_code >= 300:
|
|
|
+ exception_handler(result, self.name)
|
|
|
+
|
|
|
+ sforce_limit_info = result.headers.get('Sforce-Limit-Info')
|
|
|
+ if sforce_limit_info:
|
|
|
+ self.api_usage = Salesforce.parse_api_usage(sforce_limit_info)
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ # pylint: disable=no-self-use
|
|
|
+ def _raw_response(self, response, body_flag):
|
|
|
+ """Utility method for processing the response and returning either the
|
|
|
+ status code or the response object.
|
|
|
+
|
|
|
+ Returns either an `int` or a `requests.Response` object.
|
|
|
+ """
|
|
|
+ if not body_flag:
|
|
|
+ return response.status_code
|
|
|
+
|
|
|
+ return response
|
|
|
+
|
|
|
+ @property
|
|
|
+ def request(self):
|
|
|
+ """Deprecated access to self.session for backwards compatibility"""
|
|
|
+ _warn_request_deprecation()
|
|
|
+ return self.session
|
|
|
+
|
|
|
+ @request.setter
|
|
|
+ def request(self, session):
|
|
|
+ """Deprecated setter for self.session"""
|
|
|
+ _warn_request_deprecation()
|
|
|
+ self.session = session
|
|
|
+
|
|
|
+
|
|
|
+class SalesforceAPI(Salesforce):
|
|
|
+ """Deprecated SalesforceAPI Instance
|
|
|
+
|
|
|
+ This class implements the Username/Password Authentication Mechanism using
|
|
|
+ Arguments It has since been surpassed by the 'Salesforce' class, which
|
|
|
+ relies on kwargs
|
|
|
+
|
|
|
+ """
|
|
|
+ # pylint: disable=too-many-arguments
|
|
|
+ def __init__(self, username, password, security_token, sandbox=False,
|
|
|
+ sf_version='27.0'):
|
|
|
+ """Initialize the instance with the given parameters.
|
|
|
+
|
|
|
+ Arguments:
|
|
|
+
|
|
|
+ * username -- the Salesforce username to use for authentication
|
|
|
+ * password -- the password for the username
|
|
|
+ * security_token -- the security token for the username
|
|
|
+ * sandbox -- True if you want to login to `test.salesforce.com`, False
|
|
|
+ if you want to login to `login.salesforce.com`.
|
|
|
+ * sf_version -- the version of the Salesforce API to use, for example
|
|
|
+ "27.0"
|
|
|
+ """
|
|
|
+ warnings.warn(
|
|
|
+ "Use of login arguments has been deprecated. Please use kwargs",
|
|
|
+ DeprecationWarning
|
|
|
+ )
|
|
|
+
|
|
|
+ super(SalesforceAPI, self).__init__(username=username,
|
|
|
+ password=password,
|
|
|
+ security_token=security_token,
|
|
|
+ sandbox=sandbox,
|
|
|
+ version=sf_version)
|