├── .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}{0}> \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 | )
--------------------------------------------------------------------------------