├── .gitignore ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── salesforce ├── __init__.py ├── api.py ├── exception.py ├── httpClient.py ├── login.py ├── sObject.py ├── salesforceApi.py ├── salesforceRestApi.py ├── salesforceSoapApi.py ├── urlResources.py ├── utils.py └── version.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.egg 4 | .idea/* 5 | test* 6 | build/ 7 | dist/ -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright © 2014, Hormoz Tarevern and salesforce.com 2 | ==================================================== 3 | 4 | All rights reserved. 5 | -------------------- 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | - Neither the name of the salesforce-python-sdk nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS OF PYTHON-GEOJSON BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | recursive-include *.txt *.py -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | salesforce-python-sdk 2 | ===================== 3 | 4 | Salesforce Python SDK supports Salesforce REST and Partner SOAP APIs. 5 | 6 | Install 7 | ------- 8 | python setup.py install 9 | 10 | Example 11 | ------- 12 | import salesforce as sf 13 | sfdc = sf.Salesforce() 14 | 15 | sfdc.authenticate(client_id=client_id, 16 | client_secret=client_secret, 17 | username=username, 18 | password=password) 19 | 20 | #SOAP call 21 | sfdc.Contact.create( 22 | [ 23 | { 24 | 'FirstName': 'John', 25 | 'LastName': 'Varges', 26 | }, 27 | { 28 | 'FirstName': 'Clark', 29 | 'LastName': 'Fisher', 30 | } 31 | ], 32 | soap=True) 33 | 34 | #REST Call 35 | sfdc.Contact.create({ 36 | 'FirstName': 'John', 37 | 'LastName': 'Varges'}) 38 | 39 | You can switch between REST and SOAP by passing soap parameter. 40 | 41 | 42 | Supported APIs 43 | -------------- 44 | get_auth_uri(self, **kwargs) 45 | 46 | authenticate(self, soap=None, **kwargs) 47 | 48 | query(self, query_string, soap=None) 49 | 50 | query_all(self, query_string, soap=None) 51 | 52 | query_more(self, query_url, soap=None) 53 | 54 | search(self, search_string, soap=None) 55 | 56 | get(self, get_url, params=None, soap=None, **kwargs) 57 | 58 | post(self, post_url, data, soap=None) 59 | 60 | On sObject: 61 | 62 | describe(self, soap=None) 63 | 64 | create(self, data, soap=None) 65 | 66 | update(self, data, soap=None) 67 | 68 | delete(self, record_id, soap=None) 69 | 70 | post(self, data, record_id=None, soap=None) 71 | 72 | get(self, record_id=None, params=None, soap=None) -------------------------------------------------------------------------------- /salesforce/__init__.py: -------------------------------------------------------------------------------- 1 | from salesforce.api import Salesforce 2 | from salesforce.exception import RequestFailed -------------------------------------------------------------------------------- /salesforce/api.py: -------------------------------------------------------------------------------- 1 | from salesforceSoapApi import SalesforceSoapAPI 2 | from salesforceRestApi import SalesforceRestAPI 3 | from version import Version 4 | from httpClient import HTTPConnection 5 | from httpClient import Requests 6 | from urlResources import RestUrlResources, SoapUrlResources 7 | import utils 8 | 9 | 10 | class Salesforce(object): 11 | def __init__(self, **kwargs): 12 | super(Salesforce, self).__init__() 13 | 14 | self.__api = None 15 | 16 | self.__sandbox = None 17 | self.__soap = None 18 | self.__httplib = None 19 | self.__version = None 20 | self.__domain = None 21 | 22 | self.sandbox = kwargs.get('sandbox', False) 23 | self.soap = kwargs.get('soap', False) 24 | self.httplib = kwargs.get('httplib', Requests()) 25 | self.domain = kwargs.get('domain', 'test' if self.sandbox else 'login') 26 | self.version = kwargs.get('version', Version.get_latest_version(self.httplib)) 27 | 28 | self.__api = self.__get_api(self.soap) 29 | 30 | def get_auth_uri(self, **kwargs): 31 | return self.__get_api(False).get_auth_uri(**kwargs) 32 | 33 | def authenticate(self, soap=None, **kwargs): 34 | self.__api.auth = self.__get_api(soap).authenticate(**kwargs) 35 | 36 | def query(self, query_string, soap=None): 37 | return self.__get_api(soap).query(query_string) 38 | 39 | def query_all(self, query_string, soap=None): 40 | return self.__get_api(soap).query_all(query_string) 41 | 42 | def query_more(self, query_url, soap=None): 43 | return self.__get_api(soap).query_more(query_url) 44 | 45 | def search(self, search_string, soap=None): 46 | return self.__get_api(soap).search(search_string) 47 | 48 | def get(self, get_url, params=None, soap=None, **kwargs): 49 | return self.__get_api(soap).get(get_url, params) 50 | 51 | def post(self, post_url, data, soap=None): 52 | return self.__get_api(soap).post(post_url, data) 53 | 54 | def __getattr__(self, name): 55 | if not name[0].isalpha(): 56 | return super(Salesforce, self).__getattribute__(name) 57 | 58 | return SObjectFacade( 59 | name, self.__api, self.domain, self.sandbox, self.version, self.soap) 60 | 61 | @property 62 | def sandbox(self): 63 | return self.__sandbox 64 | 65 | @sandbox.setter 66 | def sandbox(self, sandbox): 67 | utils.validate_boolean_input(sandbox, 'sanbox') 68 | 69 | self.__sandbox = sandbox 70 | 71 | if self.__api is not None: 72 | self.__api.url_resources.sandbox = sandbox 73 | self.__api.url_resources.domain = 'test' if self.sandbox else 'login' 74 | 75 | @property 76 | def soap(self): 77 | return self.__soap 78 | 79 | @soap.setter 80 | def soap(self, soap): 81 | utils.validate_boolean_input(soap, 'soap') 82 | 83 | if self.__api is not None: 84 | self.__api = self.__get_api(soap) 85 | 86 | self.__soap = soap 87 | 88 | @property 89 | def httplib(self): 90 | return self.__httplib 91 | 92 | @httplib.setter 93 | def httplib(self, httplib): 94 | if not isinstance(httplib, HTTPConnection): 95 | raise TypeError("Must be a subclass of HTTPConnection!") 96 | 97 | self.__httplib = httplib 98 | 99 | if self.__api is not None: 100 | self.__api.httplib = httplib 101 | 102 | @property 103 | def version(self): 104 | return self.__version 105 | 106 | @version.setter 107 | def version(self, version): 108 | try: 109 | round_version = round(version, 1) 110 | except TypeError: 111 | raise TypeError('Version should be a number!') 112 | 113 | self.__version = round_version 114 | 115 | if self.__api is not None: 116 | self.__api.url_resources.version = round_version 117 | 118 | def __getstate__(self): 119 | return self.__dict__ 120 | 121 | def __setstate__(self, d): 122 | self.__dict__.update(d) 123 | 124 | def __get_api(self, soap): 125 | if soap is None: 126 | soap = self.soap 127 | 128 | if soap == self.soap and self.__api is not None: 129 | return self.__api 130 | else: 131 | auth = None if self.__api is None else self.__api.auth 132 | 133 | if soap: 134 | url_resources = SoapUrlResources(self.domain, self.sandbox, self.version) 135 | return SalesforceSoapAPI(url_resources=url_resources, 136 | httplib=self.httplib, 137 | auth=auth) 138 | else: 139 | url_resources = RestUrlResources(self.domain, self.sandbox, self.version) 140 | return SalesforceRestAPI(url_resources=url_resources, 141 | httplib=self.httplib, 142 | auth=auth) 143 | 144 | 145 | class SObjectFacade(object): 146 | def __init__(self, name, api, domain, sandbox, version, soap): 147 | super(SObjectFacade, self).__init__() 148 | self.__api = api 149 | 150 | self.name = name 151 | self.domain = domain 152 | self.sandbox = sandbox 153 | self.version = version 154 | self.soap = soap 155 | 156 | def describe(self, soap=None): 157 | return self.__get_api(soap).__getattr__(self.name).describe() 158 | 159 | def create(self, data, soap=None): 160 | return self.__get_api(soap).__getattr__(self.name).create(data) 161 | 162 | def update(self, data, soap=None): 163 | return self.__get_api(soap).__getattr__(self.name).update(data) 164 | 165 | def delete(self, record_id, soap=None): 166 | return self.__get_api(soap).__getattr__(self.name).delete(record_id) 167 | 168 | def post(self, data, record_id=None, soap=None): 169 | return self.__get_api(soap).__getattr__(self.name).post(data, record_id) 170 | 171 | def get(self, record_id=None, params=None, soap=None): 172 | return self.__get_api(soap).__getattr__(self.name).get(record_id, params) 173 | 174 | def __get_api(self, soap): 175 | if soap is None: 176 | soap = self.soap 177 | 178 | if soap == self.soap and self.__api is not None: 179 | return self.__api 180 | else: 181 | if soap: 182 | url_resources = SoapUrlResources(self.domain, self.sandbox, self.version) 183 | return SalesforceSoapAPI(url_resources=url_resources, 184 | httplib=self.__api.httplib, 185 | auth=self.__api.auth) 186 | else: 187 | url_resources = RestUrlResources(self.domain, self.sandbox, self.version) 188 | return SalesforceRestAPI(url_resources=url_resources, 189 | httplib=self.__api.httplib, 190 | auth=self.__api.auth) 191 | -------------------------------------------------------------------------------- /salesforce/exception.py: -------------------------------------------------------------------------------- 1 | 2 | class AuthenticationFailed(Exception): 3 | """ 4 | Thrown to indicate that authentication has failed. 5 | """ 6 | pass 7 | 8 | 9 | class RequestFailed(Exception): 10 | """ 11 | Thrown to indicate that request has failed. 12 | """ 13 | def __init__(self, error_code, message): 14 | # Set some exception infomation 15 | self.error_code = error_code 16 | self.message = message 17 | -------------------------------------------------------------------------------- /salesforce/httpClient.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class HTTPConnection(object): 5 | def __init__(self): 6 | super(HTTPConnection, self).__init__() 7 | 8 | def __call__(self, method, url, **kwargs): 9 | raise NotImplementedError 10 | 11 | def get(self, url, **kwargs): 12 | raise NotImplementedError 13 | 14 | def post(self, url, **kwargs): 15 | raise NotImplementedError 16 | 17 | 18 | class Requests(HTTPConnection): 19 | __MAX_REQUEST = 15 20 | 21 | def __init__(self): 22 | super(Requests, self).__init__() 23 | 24 | self.__request = requests.Session() 25 | self.__request_count = 0 26 | 27 | def __call__(self, method, url, **kwargs): 28 | return self.__request.request(method, url, **kwargs) 29 | 30 | def get(self, url, **kwargs): 31 | self.__request_count += 1 32 | if self.__is_exceeded(): 33 | self.__request.close() 34 | 35 | return self.__request.get(url, **kwargs) 36 | 37 | def post(self, url, **kwargs): 38 | self.__request_count += 1 39 | if self.__is_exceeded(): 40 | self.__request.close() 41 | 42 | return self.__request.post(url, **kwargs) 43 | 44 | def set_max_request(self, max_request): 45 | self.__MAX_REQUEST = max_request 46 | 47 | def __is_exceeded(self): 48 | return self.__request_count > self.__MAX_REQUEST 49 | -------------------------------------------------------------------------------- /salesforce/login.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | from exception import AuthenticationFailed 3 | import urllib 4 | import utils 5 | import xml.dom.minidom 6 | 7 | 8 | class Authentication(object): 9 | def __init__(self, access_token='', instance_url=''): 10 | super(Authentication, self).__init__() 11 | 12 | self.__access_token = access_token 13 | self.__instance_url = instance_url 14 | 15 | @property 16 | def access_token(self): 17 | return self.__access_token 18 | 19 | @property 20 | def instance_url(self): 21 | return self.__instance_url 22 | 23 | def is_authenticated(self): 24 | return self.access_token != '' and self.instance_url != '' 25 | 26 | 27 | class Login(object): 28 | def __init__(self, httplib, url_resources): 29 | super(Login, self).__init__() 30 | 31 | self.httplib = httplib 32 | self.url_resources = url_resources 33 | 34 | 35 | class LoginWithRestAPI(Login): 36 | AUTH_SITE = 'https://{domain}.salesforce.com' 37 | TOKEN_PATH = '/services/oauth2/token' 38 | AUTH_PATH = '/services/oauth2/authorize' 39 | 40 | def __init__(self, httplib, url_resources, **kwargs): 41 | super(LoginWithRestAPI, self).__init__(httplib, url_resources) 42 | 43 | self.__validate_kwargs(**kwargs) 44 | 45 | if 'response_type' in kwargs and kwargs['response_type'] == 'code': 46 | self.redirect_uri = kwargs['redirect_uri'] 47 | self.response_type = kwargs['response_type'] 48 | self.client_secret = kwargs['client_secret'] 49 | self.client_id = kwargs['client_id'] 50 | self.redirect = True 51 | 52 | else: 53 | self.username = kwargs['username'] 54 | self.password = kwargs['password'] 55 | self.client_secret = kwargs['client_secret'] 56 | self.client_id = kwargs['client_id'] 57 | self.redirect = False 58 | 59 | def get_auth_uri(self): 60 | headers = {'content-type': 'application/x-www-form-urlencoded'} 61 | endpoint_url = self.__get_auth_endpoint() 62 | params = self.__get_server_or_user_params() 63 | 64 | return endpoint_url + '?' + urllib.urlencode(params) 65 | 66 | def authenticate(self, **kwargs): 67 | headers = {'content-type': 'application/x-www-form-urlencoded'} 68 | endpoint_url = self.__get_token_endpoint() 69 | data = None 70 | 71 | if self.redirect: 72 | if 'code' not in kwargs: 73 | raise AuthenticationFailed("You first need to use the get_auth_uri() to get the 'code'") 74 | 75 | code = kwargs['code'] 76 | data = self.__get_token_using_code_params(code) 77 | else: 78 | data = self.__get_token_using_password_params() 79 | 80 | response = utils.send_request('POST', 81 | self.httplib, 82 | endpoint_url, 83 | headers, 84 | data=data) 85 | 86 | access_token = response['access_token'] 87 | instance_url = response['instance_url'] 88 | 89 | return Authentication(access_token, instance_url) 90 | 91 | def __get_token_endpoint(self): 92 | domain_name = self.url_resources.domain 93 | 94 | return '{0}{1}'.format( 95 | self.AUTH_SITE.format(domain=domain_name), 96 | self.TOKEN_PATH) 97 | 98 | def __get_auth_endpoint(self): 99 | domain_name = self.url_resources.domain 100 | 101 | return '{0}{1}'.format( 102 | self.AUTH_SITE.format(domain=domain_name), 103 | self.AUTH_PATH) 104 | 105 | @staticmethod 106 | def __validate_kwargs(**kwargs): 107 | if 'response_type' in kwargs: 108 | if 'response_type' not in kwargs or \ 109 | 'client_id' not in kwargs or \ 110 | 'client_secret' not in kwargs or \ 111 | 'redirect_uri' not in kwargs: 112 | 113 | raise ValueError("Required fields: 'response_type', 'client_id'," 114 | "'client_secret', and 'redirect_uri'") 115 | 116 | if kwargs['response_type'] != 'code': 117 | raise ValueError("Required fields: 'response_type': 'code' or 'token'") 118 | else: 119 | if 'client_id' not in kwargs or \ 120 | 'client_secret' not in kwargs or \ 121 | 'username' not in kwargs or \ 122 | 'password' not in kwargs: 123 | 124 | raise ValueError("Required fields: 'client_id', 'client_secret', 'username', and 'password'") 125 | 126 | def __get_token_using_password_params(self): 127 | return {'grant_type': 'password', 128 | 'client_id': self.client_id, 129 | 'client_secret': self.client_secret, 130 | 'username': self.username, 131 | 'password': self.password} 132 | 133 | def __get_token_using_code_params(self, code): 134 | return {'grant_type': 'authorization_code', 135 | 'client_id': self.client_id, 136 | 'client_secret': self.client_secret, 137 | 'redirect_uri': self.redirect_uri, 138 | 'code': code} 139 | 140 | def __get_server_or_user_params(self): 141 | return {'response_type': self.response_type, 142 | 'client_id': self.client_id, 143 | 'redirect_uri': self.redirect_uri} 144 | 145 | 146 | class LoginWithSoapAPI(Login): 147 | AUTH_SITE = "https://{domain}.salesforce.com/services/Soap/u/{version}" 148 | 149 | def __init__(self, httplib, url_resources, **kwargs): 150 | super(LoginWithSoapAPI, self).__init__(httplib, url_resources) 151 | 152 | self.__validate_kwargs(**kwargs) 153 | 154 | self.username = kwargs['username'] 155 | self.password = kwargs['password'] 156 | 157 | def authenticate(self): 158 | login_url = self.__get_token_endpoint() 159 | data = self.__get_params() 160 | headers = utils.xml_content_headers(len(data), 'login') 161 | 162 | response = utils.send_request('POST', 163 | self.httplib, 164 | login_url, 165 | headers, 166 | data=data) 167 | 168 | xml_value = xml.dom.minidom.parseString(response.text) 169 | access_token = utils.get_element_by_name(xml_value, 'sessionId') 170 | url = urlparse(utils.get_element_by_name(xml_value, 'serverUrl')) 171 | instance_url = '{0}://{1}'.format(url.scheme, url.netloc) 172 | 173 | return Authentication(access_token, instance_url) 174 | 175 | def __get_token_endpoint(self): 176 | domain_name = self.url_resources.domain 177 | 178 | return self.AUTH_SITE.format( 179 | domain=domain_name, 180 | version=self.url_resources.version) 181 | 182 | @staticmethod 183 | def __validate_kwargs(**kwargs): 184 | if 'username' not in kwargs or \ 185 | 'password' not in kwargs: 186 | 187 | raise ValueError("Required fields: 'username', and 'password'") 188 | 189 | def __get_params(self): 190 | login_body = utils.get_soap_login_body( 191 | self.username, 192 | self.password) 193 | 194 | return utils.soap_login_header().format( 195 | request=login_body) 196 | -------------------------------------------------------------------------------- /salesforce/sObject.py: -------------------------------------------------------------------------------- 1 | class SObject(object): 2 | def __init__(self, httplib, auth, url_resources): 3 | super(SObject, self).__init__() 4 | 5 | self.__httplib = httplib 6 | self.__auth = auth 7 | self.__url_resources = url_resources 8 | 9 | @property 10 | def httplib(self): 11 | return self.__httplib 12 | 13 | @property 14 | def auth(self): 15 | return self.__auth 16 | 17 | @property 18 | def url_resources(self): 19 | return self.__url_resources 20 | 21 | def describe(self): 22 | raise NotImplementedError 23 | 24 | def create(self, data): 25 | raise NotImplementedError 26 | 27 | def update(self, data): 28 | raise NotImplementedError 29 | 30 | def delete(self, data): 31 | raise NotImplementedError 32 | 33 | def post(self, data, **kwargs): 34 | raise NotImplementedError 35 | -------------------------------------------------------------------------------- /salesforce/salesforceApi.py: -------------------------------------------------------------------------------- 1 | from login import Authentication 2 | from httpClient import Requests 3 | from httpClient import HTTPConnection 4 | 5 | 6 | class SalesforceAPI(object): 7 | def __init__(self, url_resources, httplib=Requests(), auth=None): 8 | super(SalesforceAPI, self).__init__() 9 | 10 | self.__httplib = httplib 11 | self.__url_resources = url_resources 12 | self.__auth = auth 13 | self.__login = None 14 | 15 | @property 16 | def url_resources(self): 17 | return self.__url_resources 18 | 19 | @property 20 | def auth(self): 21 | return self.__auth 22 | 23 | @auth.setter 24 | def auth(self, auth): 25 | if not isinstance(auth, Authentication): 26 | raise TypeError("Must be a subclass of Authentication!") 27 | 28 | self.__auth = auth 29 | 30 | @property 31 | def httplib(self): 32 | return self.__httplib 33 | 34 | @httplib.setter 35 | def httplib(self, httplib): 36 | if not isinstance(httplib, HTTPConnection): 37 | raise TypeError("Must be a subclass of HTTPConnection!") 38 | 39 | self.__httplib = httplib 40 | 41 | def __getattr__(self, name): 42 | raise NotImplementedError 43 | 44 | def authenticate(self, **kwargs): 45 | raise NotImplementedError 46 | 47 | def query(self, query_string): 48 | raise NotImplementedError 49 | 50 | def query_all(self, query_string): 51 | raise NotImplementedError 52 | 53 | def query_more(self, query_url): 54 | raise NotImplementedError 55 | 56 | def search(self, search_string): 57 | raise NotImplementedError 58 | 59 | def get(self, params, **kwargs): 60 | raise NotImplementedError 61 | 62 | def post(self, data, url_or_action): 63 | raise NotImplementedError -------------------------------------------------------------------------------- /salesforce/salesforceRestApi.py: -------------------------------------------------------------------------------- 1 | from login import LoginWithRestAPI 2 | from urlResources import ResourcesName 3 | from salesforceApi import SalesforceAPI 4 | from exception import AuthenticationFailed 5 | from sObject import SObject 6 | import utils 7 | import json 8 | 9 | 10 | class SalesforceRestAPI(SalesforceAPI): 11 | def __init__(self, 12 | httplib, 13 | url_resources, 14 | auth=None): 15 | super(SalesforceRestAPI, self).__init__(url_resources, httplib, auth) 16 | 17 | self.__login_api = None 18 | 19 | def authenticate(self, **kwargs): 20 | if 'code' in kwargs: 21 | if not self.__login_api: 22 | raise AuthenticationFailed("You first need to use the get_auth_uri() to get the 'code'") 23 | 24 | else: 25 | self.__login_api = login_api = LoginWithRestAPI( 26 | self.httplib, 27 | self.url_resources, 28 | **kwargs) 29 | 30 | return self.__login_api.authenticate(**kwargs) 31 | 32 | def get_auth_uri(self, **kwargs): 33 | self.__login_api = login_api = LoginWithRestAPI( 34 | self.httplib, 35 | self.url_resources, 36 | **kwargs) 37 | 38 | return login_api.get_auth_uri() 39 | 40 | @utils.authenticate 41 | def query(self, query_string): 42 | query_url = self.url_resources.get_full_resource_url( 43 | self.auth.instance_url, 44 | ResourcesName.get_resource_name("query")) 45 | 46 | params = {'q': query_string} 47 | return self.get(query_url, params) 48 | 49 | @utils.authenticate 50 | def query_all(self, query_string): 51 | query_url = self.url_resources.get_full_resource_url( 52 | self.auth.instance_url, 53 | ResourcesName.get_resource_name("queryAll")) 54 | 55 | params = {'q': query_string} 56 | resp = self.get(query_url, params) 57 | 58 | def do_query_all(response): 59 | if response['done']: 60 | return response 61 | else: 62 | result = self.query_more(response['nextRecordsUrl']) 63 | 64 | response['done'] = result['done'] 65 | response['totalSize'] += result['totalSize'] 66 | response['records'].extend(result['records']) 67 | 68 | return do_query_all(response) 69 | 70 | return do_query_all(resp) 71 | 72 | @utils.authenticate 73 | def query_more(self, url): 74 | query_url = self.url_resources.get_full_resource_url( 75 | self.auth.instance_url, 76 | ResourcesName.get_resource_name("query")) 77 | 78 | if url.startswith(query_url, 79 | len(self.auth.instance_url)): 80 | get_url = '{0}/{1}'.format(self.auth.instance_url, url) 81 | else: 82 | get_url = '{0}/{1}'.format(query_url, url) 83 | 84 | return self.get(get_url) 85 | 86 | @utils.authenticate 87 | def search(self, search_string): 88 | search_url = self.url_resources.get_full_resource_url( 89 | self.auth.instance_url, 90 | ResourcesName.get_resource_name("search")) 91 | 92 | params = {'q': search_string} 93 | 94 | return self.get(search_url, params) 95 | 96 | @utils.authenticate 97 | def quick_search(self, search_string): 98 | return self.search('FIND {%s}' % search_string) 99 | 100 | @utils.authenticate 101 | def get(self, get_url, params=None): 102 | return self.__send_request('GET', 103 | get_url, 104 | params=params) 105 | 106 | @utils.authenticate 107 | def post(self, post_url, data): 108 | return self.__send_request('POST', 109 | post_url, 110 | data=json.dumps(data)) 111 | 112 | @utils.authenticate 113 | def __getattr__(self, name): 114 | if not name[0].isalpha(): 115 | return object.__getattribute__(self, name) 116 | 117 | return RestSObject(name, 118 | self.httplib, 119 | self.auth, 120 | self.url_resources) 121 | 122 | def __getstate__(self): 123 | return self.__dict__ 124 | 125 | def __setstate__(self, d): 126 | self.__dict__.update(d) 127 | 128 | def __send_request(self, method, url, **kwargs): 129 | headers = utils.json_content_headers(self.auth.access_token) 130 | 131 | request_url = utils.get_request_url(url, self.auth.instance_url, self.url_resources.get_resource_url()) 132 | 133 | return utils.send_request(method, 134 | self.httplib, 135 | request_url, 136 | headers, 137 | **kwargs) 138 | 139 | 140 | class RestSObject(SObject): 141 | def __init__(self, name, httplib, auth, url_resources): 142 | super(RestSObject, self).__init__(httplib, auth, url_resources) 143 | 144 | self.__name = name 145 | 146 | @utils.authenticate 147 | def describe(self): 148 | return self.get('/describe') 149 | 150 | @utils.authenticate 151 | def create(self, data): 152 | return self.post(data) 153 | 154 | @utils.authenticate 155 | def update(self, data): 156 | if not isinstance(data, list): 157 | raise TypeError("'update' require a parameter type 'list'") 158 | 159 | record_id = data[0] 160 | records = data[1] 161 | 162 | update_url = '{0}/{1}'.format( 163 | self.url_resources.get_resource_sobject_url( 164 | self.auth.instance_url, 165 | ResourcesName.get_resource_name("sobject"), 166 | self.__name), 167 | record_id) 168 | 169 | return self.__send_request('PATCH', 170 | update_url, 171 | data=json.dumps(records)) 172 | 173 | @utils.authenticate 174 | def delete(self, record_id): 175 | delete_url = '{0}/{1}'.format( 176 | self.url_resources.get_resource_sobject_url( 177 | self.auth.instance_url, 178 | ResourcesName.get_resource_name("sobject"), 179 | self.__name), 180 | record_id) 181 | 182 | return self.__send_request('DELETE', 183 | delete_url) 184 | 185 | @utils.authenticate 186 | def post(self, data, record_id=None): 187 | post_url = self.url_resources.get_resource_sobject_url( 188 | self.auth.instance_url, 189 | ResourcesName.get_resource_name("sobject"), 190 | self.__name) 191 | 192 | if record_id is not None: 193 | post_url += '/' + record_id 194 | 195 | return self.__send_request('POST', 196 | post_url, 197 | data=json.dumps(data)) 198 | 199 | @utils.authenticate 200 | def get(self, url=None, params=None): 201 | get_url = self.url_resources.get_resource_sobject_url( 202 | self.auth.instance_url, 203 | ResourcesName.get_resource_name("sobject"), 204 | self.__name) 205 | 206 | if url is not None: 207 | get_url += url 208 | 209 | return self.__send_request('GET', 210 | get_url, 211 | params=params) 212 | 213 | def __send_request(self, method, url, **kwargs): 214 | headers = utils.json_content_headers(self.auth.access_token) 215 | 216 | request_url = utils.get_request_url(url, self.auth.instance_url, self.url_resources.get_resource_url()) 217 | 218 | return utils.send_request(method, 219 | self.httplib, 220 | request_url, 221 | headers, 222 | **kwargs) 223 | -------------------------------------------------------------------------------- /salesforce/salesforceSoapApi.py: -------------------------------------------------------------------------------- 1 | from salesforceApi import SalesforceAPI 2 | from login import LoginWithSoapAPI 3 | from sObject import SObject 4 | import utils 5 | import xml.dom.minidom 6 | 7 | 8 | class SalesforceSoapAPI(SalesforceAPI): 9 | def __init__(self, 10 | httplib, 11 | url_resources, 12 | auth=None): 13 | super(SalesforceSoapAPI, self).__init__(url_resources, httplib, auth) 14 | 15 | self.__login_api = None 16 | 17 | def authenticate(self, **kwargs): 18 | self.__login_api = LoginWithSoapAPI( 19 | self.httplib, 20 | self.url_resources, 21 | **kwargs) 22 | 23 | return self.__login_api.authenticate() 24 | 25 | @utils.authenticate 26 | def query(self, query_string): 27 | return self.post(query_string, SalesforceSoapAPI.Action.QUERY) 28 | 29 | @utils.authenticate 30 | def query_all(self, query_string): 31 | response = self.post(query_string, SalesforceSoapAPI.Action.QUERYALL) 32 | xml_resp_value = xml.dom.minidom.parseString(response.text) 33 | 34 | def do_query_all(xml_response_value): 35 | done = utils.get_element_by_name(xml_response_value, 'done') 36 | 37 | if done: 38 | return response 39 | else: 40 | query_locator = utils.get_element_by_name(xml_response_value, 'queryLocator') 41 | 42 | result = self.query_more(query_locator) 43 | xml_result_value = xml.dom.minidom.parseString(result.text) 44 | 45 | done = utils.get_element_by_name(xml_result_value, 'done') 46 | total_size = utils.get_element_by_name(xml_result_value, 'totalSize') 47 | records = utils.get_element_by_name(xml_result_value, 'records') 48 | 49 | xml_response_value.getElementsByTagName('done').item(0).firstChild.nodeValue = done 50 | xml_response_value.getElementsByTagName('totalSize').item(0).firstChild.nodeValue += total_size 51 | xml_response_value.getElementsByTagName('records').appendChild(records) 52 | 53 | return do_query_all(xml_response_value) 54 | 55 | return do_query_all(xml_resp_value) 56 | 57 | @utils.authenticate 58 | def query_more(self, query_string): 59 | return self.post(query_string, SalesforceSoapAPI.Action.QUERYMORE) 60 | 61 | @utils.authenticate 62 | def search(self, search_string): 63 | return self.post(search_string, SalesforceSoapAPI.Action.SEARCH) 64 | 65 | @utils.authenticate 66 | def quick_search(self, search_string): 67 | return self.search('FIND {%s}' % search_string) 68 | 69 | @utils.authenticate 70 | def post(self, data, action): 71 | body = '' 72 | 73 | if action == SalesforceSoapAPI.Action.QUERY: 74 | body = query_body = utils.get_soap_query_body(data) 75 | 76 | elif action == SalesforceSoapAPI.Action.QUERYMORE: 77 | body = utils.get_soap_query_more_body(data) 78 | 79 | elif action == SalesforceSoapAPI.Action.QUERYALL: 80 | body = utils.get_soap_query_body(data) 81 | 82 | elif action == SalesforceSoapAPI.Action.SEARCH: 83 | body = utils.get_soap_search_body(data) 84 | 85 | else: 86 | raise ValueError("'action' " + action + " is not supported!") 87 | 88 | request_body = utils.soap_request_header().format( 89 | access_token=self.auth.access_token, 90 | method=action, 91 | request=body) 92 | 93 | post_url = self.url_resources.get_full_resource_url( 94 | self.auth.instance_url) 95 | 96 | return self.__send_request('POST', 97 | post_url, 98 | action, 99 | data=request_body) 100 | 101 | @utils.authenticate 102 | def get(self, get_url, params=None): 103 | pass 104 | 105 | @utils.authenticate 106 | def __getattr__(self, name): 107 | if not name[0].isalpha(): 108 | return object.__getattribute__(self, name) 109 | 110 | return SoapSObject(name, 111 | self.httplib, 112 | self.auth, 113 | self.url_resources) 114 | 115 | def __getstate__(self): 116 | return self.__dict__ 117 | 118 | def __setstate__(self, d): 119 | self.__dict__.update(d) 120 | 121 | def __send_request(self, method, url, action, **kwargs): 122 | headers = utils.xml_content_headers(len(kwargs['data']), action) 123 | 124 | request_url = utils.get_request_url(url, self.auth.instance_url, self.url_resources.get_resource_url()) 125 | 126 | return utils.send_request(method, 127 | self.httplib, 128 | request_url, 129 | headers, 130 | **kwargs) 131 | 132 | class Action(object): 133 | QUERY = 'query' 134 | QUERYALL = 'queryAll' 135 | QUERYMORE = 'queryMore' 136 | SEARCH = 'search' 137 | 138 | 139 | class SoapSObject(SObject): 140 | def __init__(self, name, httplib, auth, url_resources): 141 | super(SoapSObject, self).__init__(httplib, auth, url_resources) 142 | 143 | self.__name = name 144 | 145 | @utils.authenticate 146 | def describe(self): 147 | return self.post(None, SoapSObject.Action.DESCRIBE) 148 | 149 | @utils.authenticate 150 | def create(self, data): 151 | if not isinstance(data, list): 152 | raise TypeError("'create' require a parameter type 'list'") 153 | 154 | return self.post(data, SoapSObject.Action.CREATE) 155 | 156 | @utils.authenticate 157 | def update(self, data): 158 | if not isinstance(data, list): 159 | raise TypeError("'update' require a parameter type 'list of lists'") 160 | 161 | return self.post(data, SoapSObject.Action.UPDATE) 162 | 163 | @utils.authenticate 164 | def delete(self, record_ids): 165 | if not isinstance(record_ids, list): 166 | raise TypeError("'update' require a parameter type 'list of lists'") 167 | 168 | return self.post(record_ids, SoapSObject.Action.DELETE) 169 | 170 | @utils.authenticate 171 | def post(self, data, action=None): 172 | if action is None: 173 | raise ValueError("'action' is required") 174 | 175 | if action != SoapSObject.Action.DESCRIBE and not isinstance(data, list): 176 | raise TypeError("'create' require a parameter type 'list'") 177 | 178 | body = '' 179 | if action == SoapSObject.Action.DESCRIBE: 180 | body = utils.get_soap_describe_body(self.__name) 181 | 182 | elif action == SoapSObject.Action.CREATE: 183 | body = utils.get_soap_create_body(self.__name, data) 184 | 185 | elif action == SoapSObject.Action.UPDATE: 186 | body = utils.get_soap_update_body(self.__name, data) 187 | 188 | elif action == SoapSObject.Action.DELETE: 189 | body = utils.get_soap_delete_body(data) 190 | 191 | else: 192 | raise ValueError("'action' " + action + " is not supported!") 193 | 194 | request_body = utils.soap_request_header().format( 195 | access_token=self.auth.access_token, 196 | method=action, 197 | request=body) 198 | 199 | post_url = self.url_resources.get_full_resource_url( 200 | self.auth.instance_url) 201 | 202 | return self.__send_request('POST', 203 | post_url, 204 | action, 205 | data=request_body) 206 | 207 | @utils.authenticate 208 | def get(self, record_id=None, params=None): 209 | pass 210 | 211 | def __send_request(self, method, url, action, **kwargs): 212 | headers = utils.xml_content_headers(len(kwargs['data']), action) 213 | 214 | request_url = utils.get_request_url(url, self.auth.instance_url, self.url_resources.get_resource_url()) 215 | 216 | return utils.send_request(method, 217 | self.httplib, 218 | request_url, 219 | headers, 220 | **kwargs) 221 | 222 | class Action(object): 223 | DESCRIBE = 'describeSObject' 224 | CREATE = 'create' 225 | DELETE = 'delete' 226 | UPDATE = 'update' 227 | -------------------------------------------------------------------------------- /salesforce/urlResources.py: -------------------------------------------------------------------------------- 1 | class UrlResources(object): 2 | def __init__(self, domain, sandbox, version): 3 | super(UrlResources, self).__init__() 4 | 5 | self.domain = domain 6 | self.sandbox = sandbox 7 | self.version = version 8 | 9 | def get_resource_url(self): 10 | return self.get_resource_path().format(version=self.version) 11 | 12 | def get_full_resource_url(self, **kwargs): 13 | raise NotImplementedError 14 | 15 | def get_resource_path(self): 16 | raise NotImplementedError 17 | 18 | 19 | class RestUrlResources(UrlResources): 20 | RESOURCE_PATH = "/services/data/v{version}" 21 | 22 | def __init__(self, domain, sandbox, version): 23 | super(RestUrlResources, self).__init__(domain, sandbox, version) 24 | 25 | def get_resource_path(self): 26 | return RestUrlResources.RESOURCE_PATH 27 | 28 | def get_full_resource_url(self, instance_url, resource_name): 29 | return '{0}{1}{2}'.format( 30 | instance_url, 31 | RestUrlResources.RESOURCE_PATH.format(version=self.version), 32 | resource_name) 33 | 34 | def get_resource_sobject_url(self, 35 | instance_url, 36 | resource_name, 37 | sobject_name): 38 | return '{0}{1}'.format( 39 | self.get_full_resource_url(instance_url, resource_name), 40 | sobject_name) 41 | 42 | 43 | class SoapUrlResources(UrlResources): 44 | RESOURCE_PATH = "/services/Soap/u/{version}" 45 | 46 | def __init__(self, domain, sandbox, version): 47 | super(SoapUrlResources, self).__init__(domain, sandbox, version) 48 | 49 | def get_resource_path(self): 50 | return SoapUrlResources.RESOURCE_PATH 51 | 52 | def get_full_resource_url(self, instance_url): 53 | return '{0}{1}'.format( 54 | instance_url, 55 | SoapUrlResources.RESOURCE_PATH.format(version=self.version)) 56 | 57 | 58 | class ResourcesName(object): 59 | __RESOURCES_NAME = { 60 | 'query': '/query/', 61 | 'queryAll': '/queryAll/', 62 | 'sobject': '/sobjects/', 63 | 'search': '/search/', 64 | } 65 | 66 | @staticmethod 67 | def get_resource_name(name): 68 | if name in ResourcesName.__RESOURCES_NAME: 69 | return ResourcesName.__RESOURCES_NAME[name] 70 | else: 71 | raise ValueError('Not a valid name %s' % name) 72 | -------------------------------------------------------------------------------- /salesforce/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from exception import RequestFailed, AuthenticationFailed 3 | 4 | 5 | def json_content_headers(access_token): 6 | return { 7 | 'Content-Type': 'application/json', 8 | 'Authorization': 'Bearer ' + access_token, 9 | 'X-PrettyPrint': '1', 10 | 'Accept': 'application/json' 11 | } 12 | 13 | 14 | def xml_content_headers(length, action): 15 | return { 16 | 'Content-Type': 'text/xml', 17 | 'charset': 'utf-8', 18 | 'Content-length': '%d' % length, 19 | 'SOAPAction': action, 20 | } 21 | 22 | 23 | def get_soap_env(): 24 | return """ 25 | 30 | {header} 31 | {body} 32 | """ 33 | 34 | 35 | def get_soap_header(): 36 | return ''' 37 | 38 | {access_token} 39 | 40 | ''' 41 | 42 | 43 | def get_soap_body(): 44 | return ''' 45 | 46 | {request} 47 | 48 | ''' 49 | 50 | 51 | def get_login_soap_body(): 52 | return ''' 53 | {request} 54 | ''' 55 | 56 | 57 | def get_soap_login_body(username, password): 58 | return ''' 59 | 60 | {0} 61 | {1} 62 | '''.format(username, password) 63 | 64 | 65 | def soap_login_header(): 66 | return get_soap_env().format( 67 | header='', 68 | body=get_login_soap_body()) 69 | 70 | 71 | def soap_request_header(): 72 | return get_soap_env().format( 73 | header=get_soap_header(), 74 | body=get_soap_body()) 75 | 76 | 77 | def get_soap_query_body(query_string): 78 | return '{0}'.format(query_string) 79 | 80 | 81 | def get_soap_query_more_body(query_string): 82 | return '{0}'.format(query_string) 83 | 84 | 85 | def get_soap_search_body(search_string): 86 | return '{0}'.format(search_string) 87 | 88 | 89 | def get_soap_describe_body(sobject): 90 | return '{0}'.format(sobject) 91 | 92 | 93 | def get_soap_create_body(sobject, data): 94 | create_body = '' 95 | 96 | for item in data: 97 | create_body += ' \n'.format(sobject) 98 | 99 | for key, value in item.iteritems(): 100 | create_body += '<{0}>{1} \n'.format(key, value) 101 | 102 | create_body += ' \n' 103 | 104 | return create_body 105 | 106 | 107 | def get_soap_delete_body(ids): 108 | delete_body = '' 109 | 110 | for sf_id in ids: 111 | delete_body += '{0}'.format(sf_id) 112 | 113 | return delete_body 114 | 115 | 116 | def get_soap_update_body(sobject, data): 117 | update_body = '' 118 | 119 | for item in data: 120 | if not isinstance(item, list): 121 | raise TypeError("'update' require a parameter type 'list of lists'") 122 | 123 | update_body += ' \n'.format(sobject) 124 | update_body += '{0}'.format(item[0]) 125 | 126 | for key, value in item[1].iteritems(): 127 | update_body += '{1} \n'.format(key, value) 128 | 129 | update_body += ' \n' 130 | 131 | return update_body 132 | 133 | 134 | def verify_response(response): 135 | if response.status_code >= requests.codes.multiple_choices: 136 | error_code = response.status_code 137 | message = response.text 138 | 139 | raise RequestFailed(error_code, message) 140 | 141 | 142 | def send_request(method, httplib, url, headers, **kwargs): 143 | print method + ": Sending request to " + url + "\n" 144 | 145 | response = httplib(method, 146 | url, 147 | headers=headers, 148 | **kwargs) 149 | try: 150 | verify_response(response) 151 | except RequestFailed: 152 | raise 153 | 154 | if headers and 'SOAPAction' in headers: 155 | return response 156 | else: 157 | return response.json() 158 | 159 | 160 | def get_request_url(url, instance_url, resource_url): 161 | if url.startswith(instance_url + resource_url): 162 | return url 163 | 164 | elif url.startswith(resource_url): 165 | return '{0}{1}'.format(instance_url, url) 166 | 167 | return '{0}{1}{2}'.format(instance_url, resource_url, url) 168 | 169 | 170 | def get_element_by_name(xml_string, element_name): 171 | elements = xml_string.getElementsByTagName(element_name) 172 | 173 | if len(elements) > 0: 174 | return elements.item(0).firstChild.nodeValue 175 | 176 | return None 177 | 178 | 179 | def authenticate(func): 180 | def authenticate_and_call(self, *args, **kwargs): 181 | if self.auth is None or not self.auth.is_authenticated(): 182 | raise AuthenticationFailed("You need to first authentificate!") 183 | 184 | return func(self, *args, **kwargs) 185 | 186 | return authenticate_and_call 187 | 188 | 189 | def validate_boolean_input(value, name): 190 | if value not in (True, False): 191 | raise TypeError(name + ' should be True or False') -------------------------------------------------------------------------------- /salesforce/version.py: -------------------------------------------------------------------------------- 1 | import utils 2 | 3 | 4 | class Version(object): 5 | VERSION_PATH = "http://na1.salesforce.com/services/data/" 6 | 7 | def __init__(self): 8 | super(Version, self).__init__() 9 | 10 | @staticmethod 11 | def get_latest_version(httplib): 12 | version_api_url = Version.VERSION_PATH 13 | latest_version = 0 14 | 15 | response = utils.send_request('GET', 16 | httplib, 17 | version_api_url, 18 | None) 19 | 20 | for value in response: 21 | if float(value['version']) > latest_version: 22 | latest_version = float(value['version']) 23 | 24 | return latest_version 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup 3 | 4 | INSTALL_REQUIRES = ['requests'] 5 | assert sys.version_info >= (2, 6), "We only support Python 2.6+" 6 | 7 | with open('LICENSE.rst') as f: 8 | license_text = f.read() 9 | 10 | with open('README.rst') as f: 11 | readme_text = f.read() 12 | 13 | setup( 14 | name='salesforce-python-sdk', 15 | version='0.1.3', 16 | description='This is Salesforce Python SDK for REST and SOAP APIs', 17 | long_description=readme_text, 18 | author='Hormoz Tarevern', 19 | author_email='htarevern@yahoo.com', 20 | url='https://github.com/htarevern/salesforce-python-sdk', 21 | packages=['salesforce'], 22 | package_data={'salesforce': ['*rst']}, 23 | install_requires=INSTALL_REQUIRES, 24 | keywords="Salesforce python sdk salesforce.com salesforce-python-sdk SalesforcePythonSDK", 25 | license=license_text 26 | ) --------------------------------------------------------------------------------