├── tests
├── test_config.py
├── test_partner.py
├── test_enterprise.py
└── test_base.py
├── setup.py
├── FUTURE
├── sforce
├── __init__.py
├── enterprise.py
├── partner.py
└── base.py
├── TODO
├── HACKS
├── LICENSE
├── README
└── EXAMPLES
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | USERNAME = ''
2 | PASSWORD = ''
3 | TOKEN = ''
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='salesforce-python-toolkit',
5 | version='0.1.3',
6 | description='A fork of http://code.google.com/p/salesforce-python-toolkit/',
7 | url='http://github.com/BayCitizen/salesforce-python-toolkit',
8 | packages=[
9 | 'sforce',
10 | ],
11 |
12 | install_requires=[
13 | 'suds==0.3.9',
14 | ],
15 | )
16 |
--------------------------------------------------------------------------------
/FUTURE:
--------------------------------------------------------------------------------
1 | - Support for client certificates
2 |
3 | - Support for Metadata client
4 | - solution at http://www.threepillarsoftware.com/soap_client_auth
5 |
6 | - Support for WSDL caching
7 | - The default location (directory) is /tmp/suds so Windows users will need
8 | to set the location to something that makes sense on Windows.
9 | - Is this still the case?
10 | - Suds may be caching remote WSDLs automatically - check on this
11 |
12 | - SOAP compression (decompression not yet implemented by Suds)
13 | - Accept-Encoding: gzip, deflate
14 |
15 | - persistent connections
16 | - not supported as of urllib2 version in Python 3.1
17 | - see comment in do_open() in Lib/urllib/request.py (or Lib/urllib2.py for Python 2.x)
18 |
19 |
--------------------------------------------------------------------------------
/sforce/__init__.py:
--------------------------------------------------------------------------------
1 | # This program is free software; you can redistribute it and/or modify
2 | # it under the terms of the (LGPL) GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This program is distributed in the hope that it will be useful,
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | # GNU Library Lesser General Public License for more details at
10 | # ( http://www.gnu.org/licenses/lgpl.html ).
11 | #
12 | # You should have received a copy of the GNU Lesser General Public License
13 | # along with this program; if not, write to the Free Software
14 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 | # Written by: David Lanstein ( lanstein yahoo com )
16 |
17 | import string
18 | import sys
19 |
20 | #
21 | # Exceptions
22 | #
23 |
24 | class NotImplementedError(Exception):
25 | def __init__(self, name):
26 | Exception.__init__(self, name)
27 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | ------------------------
2 |
3 | POST-PUBLIC ALPHA
4 |
5 | - add tests for strict return types
6 |
7 | - test proxy with HTTP auth
8 | - https://fedorahosted.org/suds/wiki/Documentation#HTTPAUTHENTICATION
9 | - Check to see if we can use RFC-2617-compliant solution
10 |
11 | - remove SforcePartnerClient's dependency _marshallSObjects()
12 | - need to instantiate LeadConvert object, move generateObject into E/P
13 | - Conditionally instantiate tns: instead of ens:
14 |
15 | - Convert suds.sax.text.Text to string
16 |
17 | ------------------------
18 |
19 | DONE/FIXED
20 |
21 | - query() needs to give the user a way to get at the sObjects -
22 | if they queried enough info (Id, LastName, Company, etc...)
23 | should be able to pass that directly to update(), merge(), etc.
24 | - works fine for Enterprise
25 | - doesn't work for Partner
26 | - query() in Partner returns lists instead of strings
27 |
28 | - SSL
29 |
30 | - Accept local files
31 |
32 | - retrieve() in Enterprise returns sf:
33 | - query() emulating retrieve() is the answer until Suds gets patched
34 |
35 | - Add examples for every call with documentation (EXAMPLES file)
36 |
37 | - check fieldsToNull
38 |
39 | - create/update results return list of SaveResult object
40 | - implemented SFORCE_STRICT_METHOD_TYPING
41 | - just need to implement the var now
42 |
43 | - move to LGPL
44 |
45 | - Once unit tests are written, see if we can remove obj: and sf: and replace with ens:
46 | - Add unit test for fieldsToNull, P & E
47 |
48 | - Make logging configurable
49 | - Just added README pointing to Suds docs
50 |
51 | - Finish separating out unit tests
52 | - Commented tests
53 | - DoNotCall bool return
54 |
55 | - Fix docstrings
56 |
57 | - Test CallOptions
58 |
59 | - Ensure both P&E pass with 0/1/2 fieldsToNull on update call, query record to test correctly
60 |
61 | - move _marshallSObjects() into SforceBaseClient
62 |
63 | - test proxy without HTTP auth
64 |
65 | - ensure namespaced add-ons (SFGA, etc.) work
66 |
67 | - test unicode
68 |
--------------------------------------------------------------------------------
/HACKS:
--------------------------------------------------------------------------------
1 | Sacrifices we had to make when implementing the Toolkit (none of these impact the end-user), and
2 | what needs to happen in Suds to remove each hack:
3 |
4 | - Partner WSDL calls return lists containing strings instead of strings for elements
5 | - This affects query(), queryAll(), queryMore(), retrieve(), and search()
6 | - Hack is to recursively iterate through the QueryResult object and convert lists to strings
7 | - Solution is for Suds to make the default type for anyType elements configurable
8 |
9 | - Suds with Partner and Enterprise WSDLs return an empty string when no search() results found
10 | - gets unmarshalled as '' instead of an empty SearchResult
11 | - Hack is to return an empty SearchResult instead
12 | - Solution is for Suds to check the XML and return SearchResult
13 |
14 | - Enterprise retrieve() call is unable to recognize sf: prefixes in SOAP response
15 | - Probably the same issue as https://fedorahosted.org/suds/ticket/12
16 | - Hack is to emulate retrieve() using query()
17 | - Solution is likely similar to the one in the issue link above
18 |
19 | - Enterprise objects cannot be instantiated like self._sforce.factory.create('ens:Lead')
20 | because entire WSDL gets parsed, taking minutes to hours depending on WSDL size
21 | - Hack is to instantiate an sObject, and manually marshall the data into XML, passing the
22 | SAX element object to the SOAP method call
23 | - The object returned by generateObject() behaves identically, and when the underlying SOAP
24 | layer can resolve the dependcies quickly, there will be no code changes required in order
25 | to instantiate and use a 'Lead' object instead of an 'sObject' object
26 | - Solution is to optimize this somehow - maybe the referenced types (e.g. ens:Contact in Lead)
27 | aren't being cached?
28 | - Instantiating an ens:Lead up front would eliminate the need to marshall the data into XML
29 | - The XML below with circular references to Lead and Contact types resolves quickly,
30 | indicating that this isn't an infinite loop
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/sforce/enterprise.py:
--------------------------------------------------------------------------------
1 | # This program is free software; you can redistribute it and/or modify
2 | # it under the terms of the (LGPL) GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This program is distributed in the hope that it will be useful,
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | # GNU Library Lesser General Public License for more details at
10 | # ( http://www.gnu.org/licenses/lgpl.html ).
11 | #
12 | # You should have received a copy of the GNU Lesser General Public License
13 | # along with this program; if not, write to the Free Software
14 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 | # Written by: David Lanstein ( lanstein yahoo com )
16 |
17 | from base import SforceBaseClient
18 |
19 | import suds.sudsobject
20 |
21 | class SforceEnterpriseClient(SforceBaseClient):
22 | def __init__(self, wsdl, **kwargs):
23 | super(SforceEnterpriseClient, self).__init__(wsdl, **kwargs)
24 |
25 | # Core calls
26 |
27 | def convertLead(self, leadConverts):
28 | xml = self._marshallSObjects(leadConverts)
29 | return super(SforceEnterpriseClient, self).convertLead(xml)
30 |
31 | def create(self, sObjects):
32 | xml = self._marshallSObjects(sObjects)
33 | return super(SforceEnterpriseClient, self).create(xml)
34 |
35 | def merge(self, sObjects):
36 | xml = self._marshallSObjects(sObjects)
37 | return super(SforceEnterpriseClient, self).merge(xml)
38 |
39 | def process(self, sObjects):
40 | xml = self._marshallSObjects(sObjects)
41 | return super(SforceEnterpriseClient, self).process(xml)
42 |
43 | def retrieve(self, fieldList, sObjectType, ids):
44 | '''
45 | Currently, this uses query() to emulate the retrieve() functionality, as suds' unmarshaller
46 | borks on the sf: prefix that Salesforce prepends to all fields other than Id and type (any
47 | fields not defined in the 'sObject' section of the Enterprise WSDL)
48 | '''
49 | # HACK HACK HACKITY HACK
50 |
51 | if not isinstance(ids, (list, tuple)):
52 | ids = (ids, )
53 |
54 | # The only way to make sure we return objects in the correct order, and return None where an
55 | # object can't be retrieved by Id, is to query each ID individually
56 | sObjects = []
57 | for id in ids:
58 | queryString = 'SELECT Id, ' + fieldList + ' FROM ' + sObjectType + ' WHERE Id = \'' + id + '\' LIMIT 1'
59 | queryResult = self.query(queryString)
60 |
61 | if queryResult.size == 0:
62 | sObjects.append(None)
63 | continue
64 |
65 | # There will exactly one record in queryResult.records[] at this point
66 | record = queryResult.records[0]
67 | sObject = self.generateObject(sObjectType)
68 | for (k, v) in record:
69 | setattr(sObject, k, v)
70 | sObjects.append(sObject)
71 |
72 | return self._handleResultTyping(sObjects)
73 |
74 | def search(self, searchString):
75 | searchResult = super(SforceEnterpriseClient, self).search(searchString)
76 |
77 | # HACK gets unmarshalled as '' instead of an empty SearchResult
78 | # return an empty SearchResult instead
79 | if searchResult == '':
80 | return self._sforce.factory.create('SearchResult')
81 |
82 | return searchResult
83 |
84 | def update(self, sObjects):
85 | xml = self._marshallSObjects(sObjects)
86 | return super(SforceEnterpriseClient, self).update(xml)
87 |
88 | def upsert(self, externalIdFieldName, sObjects):
89 | xml = self._marshallSObjects(sObjects)
90 | return super(SforceEnterpriseClient, self).upsert(externalIdFieldName, xml)
91 |
92 | # Utility calls
93 |
94 | def sendEmail(self, sObjects):
95 | xml = self._marshallSObjects(sObjects)
96 | return super(SforceEnterpriseClient, self).sendEmail(xml)
97 |
--------------------------------------------------------------------------------
/tests/test_partner.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the (LGPL) GNU Lesser General Public License as
5 | # published by the Free Software Foundation; either version 3 of the
6 | # License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU Library Lesser General Public License for more details at
12 | # ( http://www.gnu.org/licenses/lgpl.html ).
13 | #
14 | # You should have received a copy of the GNU Lesser General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 | # Written by: David Lanstein ( lanstein yahoo com )
18 |
19 | import datetime
20 | import re
21 | import string
22 | import sys
23 | import unittest
24 |
25 | sys.path.append('../')
26 |
27 | import test_base
28 | import test_config
29 | from sforce.partner import SforcePartnerClient
30 |
31 | from suds import WebFault
32 |
33 | class SforcePartnerClientTest(test_base.SforceBaseClientTest):
34 | wsdlFormat = 'Partner'
35 | h = None
36 |
37 | def setUp(self):
38 | if self.h is None:
39 | self.h = SforcePartnerClient('../partner.wsdl.xml')
40 | self.h.login(test_config.USERNAME, test_config.PASSWORD, test_config.TOKEN)
41 |
42 | def testSearchOneResult(self):
43 | result = self.h.search('FIND {Single User} IN Name Fields RETURNING Lead(Name, Phone, Fax, Description, DoNotCall)')
44 |
45 | self.assertEqual(len(result.searchRecords), 1)
46 | self.assertEqual(result.searchRecords[0].record.Name, 'Single User')
47 | # it's not a string, it's a SAX Text object, but it can be cast to a string
48 | # just need to make sure it's not a bool
49 | self.assertTrue(result.searchRecords[0].record.DoNotCall in ('false', 'true'))
50 | # make sure we get None and not ''
51 | self.assertEqual(result.searchRecords[0].record.Description, None)
52 |
53 |
54 | def testSearchManyResults(self):
55 | result = self.h.search(u'FIND {Joë Möke} IN Name Fields RETURNING Lead(Name, Phone, DoNotCall, Company)')
56 |
57 | self.assertTrue(len(result.searchRecords) > 1)
58 | for searchRecord in result.searchRecords:
59 | self.assertEqual(searchRecord.record.Name, u'Joë Möke')
60 | self.assertEqual(searchRecord.record.Company, u'你好公司')
61 | self.assertTrue(searchRecord.record.DoNotCall in ('false', 'true'))
62 |
63 | def testUpdateOneFieldToNull(self):
64 | self.setHeaders('update')
65 |
66 | (result, lead) = self.createLead(True)
67 |
68 | lead.fieldsToNull = ('Email')
69 | lead.Email = None
70 |
71 | result = self.h.update(lead)
72 | self.assertTrue(result.success)
73 | self.assertEqual(result.id, lead.Id)
74 |
75 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', (lead.Id))
76 | self.assertEqual(result.FirstName, u'Joë')
77 | self.assertEqual(result.LastName, u'Möke')
78 | self.assertEqual(result.Company, u'你好公司')
79 | self.assertEqual(result.Email, None)
80 |
81 | def testUpdateTwoFieldsToNull(self):
82 | self.setHeaders('update')
83 |
84 | (result, lead) = self.createLead(True)
85 |
86 | lead.fieldsToNull = ('FirstName', 'Email')
87 | lead.Email = None
88 | lead.FirstName = None
89 |
90 | result = self.h.update(lead)
91 | self.assertTrue(result.success)
92 | self.assertEqual(result.id, lead.Id)
93 |
94 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', (lead.Id))
95 |
96 | self.assertEqual(result.FirstName, None)
97 | self.assertEqual(result.LastName, u'Möke')
98 | self.assertEqual(result.Email, None)
99 |
100 | if __name__ == '__main__':
101 | unittest.main('test_partner')
102 |
--------------------------------------------------------------------------------
/tests/test_enterprise.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the (LGPL) GNU Lesser General Public License as
5 | # published by the Free Software Foundation; either version 3 of the
6 | # License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU Library Lesser General Public License for more details at
12 | # ( http://www.gnu.org/licenses/lgpl.html ).
13 | #
14 | # You should have received a copy of the GNU Lesser General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 | # Written by: David Lanstein ( lanstein yahoo com )
18 |
19 | import datetime
20 | import re
21 | import string
22 | import sys
23 | import unittest
24 | import logging
25 |
26 | sys.path.append('../')
27 |
28 | import test_base
29 | import test_config
30 | from sforce.enterprise import SforceEnterpriseClient
31 |
32 | from suds import WebFault
33 |
34 | #logging.basicConfig(level=logging.INFO)
35 |
36 | #logging.getLogger('suds.client').setLevel(logging.DEBUG)
37 |
38 | class SforceEnterpriseClientTest(test_base.SforceBaseClientTest):
39 | wsdlFormat = 'Enterprise'
40 | h = None
41 |
42 | def setUp(self):
43 | if self.h is None:
44 | self.h = SforceEnterpriseClient('../enterprise.wsdl.xml')
45 | self.h.login(test_config.USERNAME, test_config.PASSWORD, test_config.TOKEN)
46 |
47 | def testSearchOneResult(self):
48 | result = self.h.search('FIND {Single User} IN Name Fields RETURNING Lead(Name, Phone, Fax, Description, DoNotCall)')
49 |
50 | self.assertEqual(len(result.searchRecords), 1)
51 | self.assertEqual(result.searchRecords[0].record.Name, 'Single User')
52 | self.assertTrue(isinstance(result.searchRecords[0].record.DoNotCall, bool))
53 | # make sure we get None and not ''
54 | self.assertFalse(hasattr(result.searchRecords[0].record, 'Description'))
55 |
56 |
57 | def testSearchManyResults(self):
58 | result = self.h.search(u'FIND {Joë Möke} IN Name Fields RETURNING Lead(Name, Phone, Company, DoNotCall)')
59 |
60 | self.assertTrue(len(result.searchRecords) > 1)
61 | for searchRecord in result.searchRecords:
62 | self.assertEqual(searchRecord.record.Name, u'Joë Möke')
63 | self.assertEqual(searchRecord.record.Company, u'你好公司')
64 | self.assertTrue(isinstance(result.searchRecords[0].record.DoNotCall, bool))
65 |
66 | def testUpdateOneFieldToNull(self):
67 | self.setHeaders('update')
68 |
69 | (result, lead) = self.createLead(True)
70 |
71 | lead.fieldsToNull = ('Email')
72 | lead.Email = None
73 |
74 | result = self.h.update(lead)
75 | self.assertTrue(result.success)
76 | self.assertEqual(result.id, lead.Id)
77 |
78 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', (lead.Id))
79 | self.assertEqual(result.FirstName, u'Joë')
80 | self.assertEqual(result.LastName, u'Möke')
81 | self.assertEqual(result.Company, u'你好公司')
82 | self.assertFalse(hasattr(result, 'Email'))
83 |
84 | def testUpdateTwoFieldsToNull(self):
85 | self.setHeaders('update')
86 |
87 | (result, lead) = self.createLead(True)
88 |
89 | lead.fieldsToNull = ('FirstName', 'Email')
90 | lead.Email = None
91 | lead.FirstName = None
92 |
93 | result = self.h.update(lead)
94 | self.assertTrue(result.success)
95 | self.assertEqual(result.id, lead.Id)
96 |
97 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', (lead.Id))
98 |
99 | self.assertFalse(hasattr(result, 'FirstName'))
100 | self.assertEqual(result.LastName, u'Möke')
101 | self.assertFalse(hasattr(result, 'Email'))
102 |
103 | if __name__ == '__main__':
104 | unittest.main('test_enterprise')
105 |
--------------------------------------------------------------------------------
/sforce/partner.py:
--------------------------------------------------------------------------------
1 | # This program is free software; you can redistribute it and/or modify
2 | # it under the terms of the (LGPL) GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This program is distributed in the hope that it will be useful,
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | # GNU Library Lesser General Public License for more details at
10 | # ( http://www.gnu.org/licenses/lgpl.html ).
11 | #
12 | # You should have received a copy of the GNU Lesser General Public License
13 | # along with this program; if not, write to the Free Software
14 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 | # Written by: David Lanstein ( lanstein yahoo com )
16 |
17 |
18 | from base import SforceBaseClient
19 |
20 | import string
21 | import suds.sudsobject
22 |
23 | class SforcePartnerClient(SforceBaseClient):
24 | def __init__(self, wsdl, *args, **kwargs):
25 | super(SforcePartnerClient, self).__init__(wsdl, *args, **kwargs)
26 |
27 | # Toolkit-specific calls
28 |
29 | def _stringifyResultRecords(self, struct):
30 | '''
31 | The Partner WSDL defines result element not defined in the "SObject"
32 | section of the Partner WSDL as elements, which get unmarshalled by
33 | suds into single-element lists. We prefer that they are strings, so we'll
34 | convert structures like
35 |
36 | [(records){
37 | type = "Contact"
38 | Id = "003000000000000000"
39 | Account[] =
40 | (Account){
41 | type = "Account"
42 | Id = "001000000000000000"
43 | Name[] =
44 | "Acme",
45 | },
46 | FirstName[] =
47 | "Wile E.",
48 | LastName[] =
49 | "Coyote",
50 | }]
51 |
52 | to
53 |
54 | [(records){
55 | type = "Contact"
56 | Id = "003000000000000000"
57 | Account =
58 | (Account){
59 | type = "Account"
60 | Id = "001000000000000000"
61 | Name = "Acme"
62 | }
63 | FirstName = "Wile E."
64 | LastName = "Coyote"
65 | }]
66 |
67 | and
68 |
69 | searchRecords[] =
70 | (searchRecords){
71 | record =
72 | (record){
73 | type = "Lead"
74 | Id = None
75 | Name[] =
76 | "Single User",
77 | Phone[] =
78 | "(617) 555-1212",
79 | }
80 | },
81 |
82 | to
83 |
84 | searchRecords[] =
85 | (searchRecords){
86 | record =
87 | (record){
88 | type = "Lead"
89 | Id = None
90 | Name = "Single User",
91 | Phone = "(617) 555-1212",
92 | }
93 | },
94 | '''
95 | if not isinstance(struct, list):
96 | struct = [struct]
97 | originallyList = False
98 | else:
99 | originallyList = True
100 |
101 | for record in struct:
102 | for k, v in record:
103 | if isinstance(v, list):
104 | # At this point, we don't know whether a value of [] corresponds to '' or None
105 | # However, anecdotally I've been unable to find a field type whose 'empty' value
106 | # returns anything other that
107 | # so, for now, we'll set it to None
108 | if v == []:
109 | setattr(record, k, None)
110 | else:
111 | # Note that without strong typing there's no way to tell the difference between the
112 | # string 'false' and the bool false. We get false.
113 | # We have to assume strings for everything other than 'Id' and 'type', which are
114 | # defined types in the Partner WSDL.
115 |
116 | # values that are objects may (query()) or may not (search()) be wrapped in a list
117 | # so, remove from nested list first before calling ourselves recursively (if necessary)
118 | setattr(record, k, v[0])
119 |
120 | # refresh v
121 | v = getattr(record, k)
122 |
123 | if isinstance(v, suds.sudsobject.Object):
124 | v = self._stringifyResultRecords(v)
125 | setattr(record, k, v)
126 | if originallyList:
127 | return struct
128 | else:
129 | return struct[0]
130 |
131 | # Core calls
132 |
133 | def convertLead(self, leadConverts):
134 | xml = self._marshallSObjects(leadConverts)
135 | return super(SforcePartnerClient, self).convertLead(xml)
136 |
137 | def merge(self, sObjects):
138 | xml = self._marshallSObjects(sObjects)
139 | return super(SforcePartnerClient, self).merge(xml)
140 |
141 | def process(self, sObjects):
142 | xml = self._marshallSObjects(sObjects)
143 | return super(SforcePartnerClient, self).process(xml)
144 |
145 | def query(self, queryString):
146 | queryResult = super(SforcePartnerClient, self).query(queryString)
147 | if queryResult.size > 0:
148 | queryResult.records = self._stringifyResultRecords(queryResult.records)
149 | return queryResult
150 |
151 | def queryAll(self, queryString):
152 | queryResult = super(SforcePartnerClient, self).queryAll(queryString)
153 | if queryResult.size > 0:
154 | queryResult.records = self._stringifyResultRecords(queryResult.records)
155 | return queryResult
156 |
157 | def queryMore(self, queryLocator):
158 | queryResult = super(SforcePartnerClient, self).queryMore(queryLocator)
159 | if queryResult.size > 0:
160 | queryResult.records = self._stringifyResultRecords(queryResult.records)
161 | return queryResult
162 |
163 | def retrieve(self, fieldList, sObjectType, ids):
164 | sObjects = super(SforcePartnerClient, self).retrieve(fieldList, sObjectType, ids)
165 | sObjects = self._stringifyResultRecords(sObjects)
166 | return sObjects
167 |
168 | def search(self, searchString):
169 | searchResult = super(SforcePartnerClient, self).search(searchString)
170 |
171 | # HACK gets unmarshalled as '' instead of an empty SearchResult
172 | # return an empty SearchResult instead
173 | if searchResult == '':
174 | return self._sforce.factory.create('SearchResult')
175 |
176 | searchResult.searchRecords = self._stringifyResultRecords(searchResult.searchRecords)
177 | return searchResult
178 |
179 | # Utility calls
180 |
181 | def sendEmail(self, sObjects):
182 | xml = self._marshallSObjects(sObjects)
183 | return super(SforcePartnerClient, self).sendEmail(xml)
184 |
185 | # SOAP header-related calls
186 |
187 | def setCallOptions(self, header):
188 | '''
189 | This header is only applicable to the Partner WSDL
190 | '''
191 | self._callOptions = header
192 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | Mission: To provide a thin layer around the raw SOAP interaction that consumes Salesforce's
2 | Enterprise and Partner WSDLs and handles the nitty-gritty details of the SOAP interaction.
3 |
4 |
5 | The Salesforce Python Toolkit features the following:
6 | - Supports both the Enterprise and Partner WSDL
7 | - Unifies the interface to Enterprise and Partner objects. There should be no such thing as an
8 | 'Enterprise' example or a 'Partner' example, outside of specifying the correct WSDL and
9 | instantiating SforceEnterpriseClient or SforcePartnerClient
10 | - Handles rewriting the SOAP endpoint after the connection is established
11 | - Stores the session ID, and attaches it in a SOAP header in each subsequent call
12 | - Manages SOAP headers for you, particularly which headers get attached when
13 | - For example, CallOptions only applies to the Partner WSDL
14 | - AssignmentRuleHeader only applies to create(), merge(), update(), and upsert()
15 | - Check out _setHeaders() in SforceBaseClient for more details :)
16 | - Manages object/field ens:sobject.{partner,enterprise}.soap... namespace vs.
17 | {partner,enterprise}.soap namespace
18 | - Suds doesn't natively do this, in either the Partner WSDL or the Enterprise WSDL.
19 | - Allows you to pass a relative path, absolute page, or local or remote URL pointing to your WSDL
20 |
21 |
22 | But what about those other libraries? And why Suds and not SOAPpy or ZSI? And why not just Suds?
23 | - First and foremost, SOAPpy and ZSI dead projects.
24 | - Why not SOAPpy? http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=523083
25 | - You can't use the Enterprise WSDL because SOAPpy doesn't support manually setting namespace
26 | prefixes for fields.
27 | - Why not Beatbox? No WSDL support.
28 | - Honestly? Suds is pretty damn wonderful, and it has an active community supporting active
29 | development.
30 | - If there weren't so many Salesforce-specific implementation details, building directly on top
31 | of Suds would make perfect sense. Unfortunately, this just isn't the case.
32 |
33 |
34 | Dependencies:
35 | - Suds 0.3.6+
36 | - You can install suds by issuing `easy_install suds --install-dir=`
37 | - This method requires setuptools.
38 | - If you have a previous version of Suds installed, or don't want to install setuptools, you
39 | can simply unpack 0.3.6+ and add the path to PYTHONPATH.
40 |
41 |
42 | Tested with:
43 | - OS X 10.5
44 | - Python 2.5.1
45 | - suds 0.3.6
46 |
47 |
48 | Examples:
49 | - You can find examples for every method call in the EXAMPLES file.
50 |
51 |
52 | Caching:
53 | - The Toolkit supports caching of remote WSDL files for a defineable amount of time (in seconds):
54 |
55 | h = SforceEnterpriseClient('https://example.com/enterprise.wsdl.xml', cacheDuration = 90)
56 |
57 | h = SforcePartnerClient('https://example.com/partner.wsdl.xml', cacheDuration = 86400) # 1 day
58 |
59 |
60 | Proxies:
61 | - The toolkit supports HTTP proxies, but not HTTPS. This is due to a limitation in the underlying
62 | urllib2 library that ships with Python. Details can be found at the bottom of this document.
63 |
64 | It should be noted that all traffic between the proxy and Salesforce will be sent over HTTPS.
65 |
66 | The constructors take an argument 'proxy', e.g.
67 |
68 | h = SforceEnterpriseClient('enterprise.wsdl.xml', proxy = {'http': 'proxy.example.com:8888'})
69 |
70 | h = SforcePartnerClient('partner.wsdl.xml', proxy = {'http': 'proxy.example.com:8888'})
71 |
72 | Any attempt to pass an https proxy will raise a NotImplementedError.
73 |
74 |
75 | Inspecting your data:
76 | - It's quite simple to see the structure of your objects. For instance:
77 |
78 | lead = h.generateObject('Lead')
79 | lead.FirstName = 'Joe'
80 | lead.LastName = 'Moke'
81 | lead.Company = 'Jamoke, Inc.'
82 |
83 | print lead
84 |
85 | outputs this:
86 |
87 | (sObject){
88 | fieldsToNull[] =
89 | Id = None
90 | type = "Lead"
91 | FirstName = "Joe"
92 | LastName = "Moke"
93 | Company = "Acme, Inc."
94 | }
95 |
96 |
97 | Strict- and non-strict modes:
98 | - Set with the method call setStrictResultTyping() (takes a bool)
99 | - If any of the following calls return a single result, the result object will not be wrapped in a
100 | list (i.e. returns sObject instead of [sObject]):
101 | convertLead()
102 | create()
103 | delete()
104 | emptyRecycleBin()
105 | invalidateSessions()
106 | merge()
107 | process()
108 | undelete()
109 | update()
110 | upsert()
111 | describeSObjects()
112 | sendEmail()
113 | - This is to facilitate things for folks who nearly always create/update/delete a single record at
114 | a time.
115 | - Note that calls that return an indeterminate number of results (e.g. query()) will always return
116 | a list of results, even if a single record is returned.
117 | - We accept in strict- and non-strict mode, for parameters, any of
118 | - '00Q.....' (value)
119 | - ['00Q.....'] (value(s) wrapped in list)
120 | - ('00Q.....') (value(s) wrapped in tuple)
121 | - so, simply call crmHandle.emptyRecycleBin('001x00000000JerAAE')
122 | instead of emptyRecycleBin(['001x00000000JerAAE'])
123 |
124 |
125 | Logging:
126 | - If you're curious what's happening at the SOAP level, Suds offers you a great deal of
127 | information. Have a look at https://fedorahosted.org/suds/wiki/Documentation#LOGGING for more
128 | information.
129 |
130 |
131 | Conventions:
132 | - Fields are named internally exactly as they are specified in the documentation and the XML,
133 | except where they differ (userID vs. userId is one example), in which case the XML spec wins.
134 |
135 |
136 | Caveats (short form):
137 | - gzip/deflate not implemented
138 | - HTTP 1.1 persistent connections not supported
139 | - Supports HTTP proxies, not HTTPS (traffic between the proxy and Salesforce still sent HTTPS)
140 | - na0.salesforce.com (a.k.a ssl.salesforce.com) probably doesn't work
141 |
142 |
143 | Caveats (long form):
144 | - gzip/deflate not implemented
145 | - Suds doesn't handle HTTP headers in SOAP response such as
146 | Content-Encoding: gzip
147 | - HTTP 1.1 persistent connections are not supported because of a limitation in the urllib2, which
148 | Suds sits on top of
149 | - urllib2 secretly sets a header in do_open():
150 | headers["Connection"] = "close"
151 | which does bubble up to Suds, and consequently is not part of our header dict. This means
152 | that logging suds.transport will not show the Connection: close header, even though it's sent
153 | - This is still an issue in Python 3.1, though now there is the following comment:
154 | # TODO(jhylton): Should this be redesigned to handle
155 | # persistent connections?
156 | - Connections to a proxy must use HTTP and not HTTPS due to a limitation in urllib2, where it
157 | does not implement the HTTP CONNECT method. All traffic between the proxy and Salesforce will
158 | of course be sent over HTTPS.
159 | - http://code.activestate.com/recipes/456195/ illustrates a possible workaround
160 | - na0.salesforce.com (a.k.a ssl.salesforce.com) probably doesn't work
161 | - This affects customers that signed up from the US web site prior to roughly June 2002
162 | - http://forums.sforce.com/sforce/board/message?board.id=PerlDevelopment&message.id=401
163 | - According to the Salesforce docs, this particular instance requires ISO-8859-1 instead of
164 | UTF-8, and UTF-8 is hard-coded into Suds
165 | - http://www.salesforce.com/us/developer/docs/api/Content/implementation_considerations.htm
166 | - I don't have access to an account on this cluster, so it may or may not take the UTF-8 data
167 | and cram it into ISO-8859-1 (it may also throw a SoapFault). I really have no idea.
168 | - If anyone has access to it and wants to get it working, one possibility _might_ be to add
169 | the HTTP header 'Content-Type: text/xml; charset=iso-8859-1'. You still wouldn't be able
170 | to use any non-8859-1 characters, but Salesforce _might_ be willing to interact using that
171 | header. Needless to say, this would be a horrible hack at best.
172 | - For caveats that don't affect the end-user, but do concern implementation details, see the
173 | HACKS file
174 |
--------------------------------------------------------------------------------
/EXAMPLES:
--------------------------------------------------------------------------------
1 | All of the examples work against either the Enterprise WSDL or the Partner WSDL.
2 |
3 | The examples will be in the same order as they are found in the Salesforce API Reference at
4 | http://www.salesforce.com/us/developer/docs/api/index.htm
5 |
6 | NOTE: These examples are not intended to teach the Salesforce API; rather, they are here to
7 | illustrate how the Python Toolkit implements the Salesforce API.
8 |
9 | Prerequisites
10 | -------------
11 | All examples below assume the Toolkit has been instantiated and the connection has been made.
12 |
13 | Partner WSDL
14 | ------------
15 | from sforce.partner import SforcePartnerClient
16 | h = SforcePartnerClient('/path/to/partner.wsdl.xml')
17 |
18 | Enterprise WSDL
19 | ---------------
20 | from sforce.enterprise import SforceEnterpriseClient
21 | h = SforceEnterpriseClient('/path/to/enterprise.wsdl.xml')
22 |
23 |
24 | Additionally, all examples except login() assume you are logged in, using a call such as
25 | h.login('joe@example.com.sbox1', '*passwordhere*', '*securitytokenhere*')
26 |
27 |
28 | Conventions
29 | -----------
30 | - implies that you copy and paste the code from the 'create lead' section of create(),
31 | so you have a Lead in the variable 'lead' and a SaveResult in the variable 'result'
32 |
33 | - same as create lead, except copy and past the code from 'create 2 leads' in
34 | create()
35 |
36 |
37 | Variable Names:
38 | ---------------
39 | h - reference to an instance of the Toolkit object
40 | result - the result of a Salesforce method call
41 | lead - a lead object
42 |
43 |
44 | Return Typing
45 | -------------
46 | All of the examples assume loose return typing, where a create() call that creates a single object,
47 | for instance, returns a single SaveResult, and a create() call that creates multiple objects returns
48 | a list of SaveResult objects.
49 |
50 |
51 | ----------------------------------------------------------------------------------------------------
52 |
53 | Core Calls:
54 |
55 | convertLead()
56 | -------------
57 |
58 | leadConvert = h.generateObject('LeadConvert')
59 | leadConvert.leadId = result.id
60 | # the possible values for convertedStatus depend on what's in the picklist for your org
61 | # you can take a look at the 'Convert Lead' screen in the UI for your API user to see what's there
62 | leadConvert.convertedStatus = 'Qualified'
63 | result = h.convertLead(leadConvert)
64 |
65 |
66 | create()
67 | --------
68 | # create lead
69 | lead = h.generateObject('Lead')
70 | lead.FirstName = 'Joe'
71 | lead.LastName = 'Moke'
72 | lead.Company = 'Jamoke, Inc.'
73 | lead.Email = 'joe@example.com'
74 | result = h.create(lead)
75 |
76 | or
77 |
78 | # create 2 leads
79 | lead = h.generateObject('Lead')
80 | lead.FirstName = 'Joe'
81 | lead.LastName = 'Moke'
82 | lead.Company = 'Jamoke, Inc.'
83 | lead.Email = 'joe@example.com'
84 |
85 | lead2 = h.generateObject('Lead')
86 | lead2.FirstName = 'Bob'
87 | lead2.LastName = 'Moke'
88 | lead2.Company = 'Jamoke, Inc.'
89 | lead2.Email = 'bob@example.com'
90 |
91 | result = h.create((lead, lead2))
92 |
93 | delete()
94 | --------
95 |
96 | result = h.delete(result.id)
97 |
98 | or
99 |
100 | ids = []
101 | for SaveResult in result:
102 | ids.append(SaveResult.id)
103 | result = h.delete(ids)
104 |
105 |
106 | emptyRecycleBin()
107 | -----------------
108 | result = h.emptyRecycleBin('*ID HERE*')
109 |
110 | or
111 |
112 | result = h.emptyRecycleBin(('*ID HERE*', '*ANOTHER ID HERE*'))
113 |
114 |
115 | getDeleted()
116 | ------------
117 | result = h.getDeleted('Lead', '2009-06-01T23:01:01Z', '2019-01-01T23:01:01Z')
118 |
119 |
120 | getUpdated()
121 | ------------
122 | result = h.getUpdated('Lead', '2009-06-01T23:01:01Z', '2019-01-01T23:01:01Z')
123 |
124 |
125 | invalidateSessions()
126 | --------------------
127 | result = h.invalidateSessions(h.getSessionId())
128 |
129 | or
130 |
131 | result = h.invalidateSessions((h.getSessionId(), '*ANOTHER SESSION ID HERE*'))
132 |
133 |
134 | login()
135 | -------
136 | h.login('joe@example.com.sbox1', '*passwordhere*', '*securitytokenhere*')
137 |
138 |
139 | logout()
140 | --------
141 | result = h.logout()
142 |
143 |
144 | merge()
145 | -------
146 |
147 |
148 | # set the corresponding resulting id in 'lead'
149 | lead.Id = result[0].id
150 |
151 | mergeRequest = h.generateObject('MergeRequest')
152 | mergeRequest.masterRecord = lead
153 | mergeRequest.recordToMergeIds = result[1].id
154 | result = h.merge(mergeRequest)
155 |
156 |
157 | process()
158 | ---------
159 | NOTE: The documentation for this call is currently incorrect, the Process*Request objects take a
160 | property 'comments', not 'comment' as stated here:
161 | http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_process.htm
162 |
163 | However, judging from the fact that it's still incorrect in the docs, it's a similar issue
164 | to protected inheritance in C++ - it's in there 'for completeness' :-)
165 |
166 | By the way, doesn't that just sum up half of C++?
167 |
168 | processRequest = h.generateObject('ProcessSubmitRequest')
169 | processRequest.objectId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*'
170 | processRequest.comments = 'This is what I think.'
171 | result = h.process(processRequest)
172 |
173 | or
174 |
175 | processRequest = h.generateObject('ProcessWorkitemRequest')
176 | processRequest.action = 'Approve'
177 | processRequest.workitemId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*'
178 | processRequest.comments = 'I approved this request.'
179 | result = h.process(processRequest)
180 |
181 |
182 | query()
183 | -------
184 | result = h.query('SELECT FirstName, LastName FROM Lead')
185 |
186 |
187 | queryAll()
188 | ----------
189 | result = h.queryAll('SELECT Account.Name, FirstName, LastName FROM Contact LIMIT 2')
190 | for record in result.records:
191 | print record.FirstName, record.LastName
192 | print record.Account.Name
193 |
194 |
195 | queryMore()
196 | -----------
197 | queryOptions = h.generateHeader('QueryOptions')
198 | queryOptions.batchSize = 200
199 | h.setQueryOptions(queryOptions)
200 |
201 | result = h.queryAll('SELECT FirstName, LastName FROM Lead')
202 | result = h.queryMore(result.queryLocator)
203 | # result.done = True indicates the final batch of results has been sent
204 |
205 | retrieve()
206 | ----------
207 | result = h.retrieve('FirstName, LastName, Company, Email', 'Lead', '00QR0000002yyVs')
208 |
209 | or
210 |
211 | result = h.retrieve('FirstName, LastName, Company, Email', 'Lead', ('00QR0000002yyVs', '00QR0000003HWMPLA4'))
212 |
213 |
214 | search()
215 | --------
216 | result = h.search('FIND {Joe Moke} IN Name Fields RETURNING Lead(Name, Phone)')
217 |
218 |
219 | undelete()
220 | ----------
221 | result = h.undelete('*ID HERE*')
222 |
223 | or
224 |
225 | result = h.undelete(('*ID HERE*', '*ANOTHER ID HERE*'))
226 |
227 |
228 | update()
229 | --------
230 |
231 | lead.Id = result.id
232 |
233 | # As a single value, 'Email' would not _have_ to be wrapped in a tuple/list
234 | lead.fieldsToNull = ('Email')
235 | # to set a value to NULL in Salesforce, must also remove the value from the lead variable or it will
236 | # give you a 'Duplicate Values...' exception message
237 | lead.Email = None
238 | h.update(lead)
239 |
240 |
241 | upsert()
242 | --------
243 |
244 | lead.Id = result.id
245 | lead.FirstName = 'Bob'
246 | h.upsert('Id', lead)
247 |
248 |
249 | ----------------------------------------------------------------------------------------------------
250 |
251 | Describe Calls:
252 |
253 | describeGlobal()
254 | ----------------
255 | result = h.describeGlobal()
256 |
257 |
258 | describeLayout()
259 | ----------------
260 | result = h.describeLayout('Lead', '012000000000000AAA') # Master Record Type
261 |
262 |
263 | describeSObject()
264 | -----------------
265 | result = h.describeSObject('Lead')
266 |
267 |
268 | describeSObjects()
269 | ------------------
270 | result = h.describeSObjects(('Lead', 'Contact'))
271 |
272 |
273 | describeTabs()
274 | --------------
275 | result = h.describeTabs()
276 |
277 |
278 | ----------------------------------------------------------------------------------------------------
279 |
280 | Utility Calls:
281 |
282 | getServerTimestamp()
283 | --------------------
284 | result = h.getServerTimestamp()
285 |
286 |
287 | getUserInfo()
288 | -------------
289 | result = h.getUserInfo()
290 |
291 |
292 | resetPassword()
293 | ---------------
294 | result = h.resetPassword('*USER ID HERE*')
295 |
296 |
297 | sendEmail()
298 | -----------
299 | # Email a single person
300 | email = h.generateObject('SingleEmailMessage')
301 | email.toAddresses = 'joe@example.com'
302 | email.subject = 'This is my subject.'
303 | email.plainTextBody = 'This is the plain-text body of my email.'
304 | result = h.sendEmail([email])
305 |
306 | # MassEmailMessage
307 | email = h.generateObject('MassEmailMessage')
308 | email.targetObjectIds = (('*LEAD OR CONTACT ID TO EMAIL*', '*ANOTHER LEAD OR CONTACT TO EMAIL*'))
309 | email.templateId = '*EMAIL TEMPLATE ID TO USE*'
310 | result = h.sendEmail([email])
311 |
312 |
313 | setPassword()
314 | -------------
315 | result = h.setPassword('*USER ID HERE*', '*NEW PASSWORD HERE*')
316 |
317 |
318 | ----------------------------------------------------------------------------------------------------
319 |
320 | Toolkit-Specific Utility Calls:
321 |
322 | generateHeader()
323 | ----------------
324 | header = h.generateHeader('AllowFieldTruncationHeader');
325 |
326 |
327 | generateObject()
328 | ----------------
329 | lead = h.generateObject('Lead')
330 |
331 |
332 | getLastRequest()
333 | ----------------
334 | result = h.getLastRequest()
335 |
336 |
337 | getLastResponse()
338 | -----------------
339 | result = h.getLastResponse()
340 |
341 | ----------------------------------------------------------------------------------------------------
342 |
343 | SOAP Headers
344 | Note that you need only to call the appropriate set*() call once per header, and the header will
345 | automatically be attached to the SOAP envelope for the calls that it pertains to. The SessionHeader
346 | header is set for you after a successful login() call, but it's a public method for the edge case
347 | where you need to piggyback on another user's session.
348 |
349 |
350 | AllowFieldTruncationHeader
351 | --------------------------
352 | header = h.generateHeader('AllowFieldTruncationHeader');
353 | header.allowFieldTruncation = False
354 | h.setAllowFieldTruncationHeader(header)
355 |
356 |
357 | AssignmentRuleHeader
358 | --------------------------
359 | header = h.generateHeader('AssignmentRuleHeader');
360 | header.useDefaultRule = True
361 | h.setAssignmentRuleHeader(header)
362 |
363 | or
364 |
365 | header = h.generateHeader('AssignmentRuleHeader');
366 | header.assignmentRuleId = '*ASSIGNMENT RULE ID HERE*'
367 | h.setAssignmentRuleHeader(header)
368 |
369 |
370 | CallOptions
371 | --------------------------
372 | Note that this header only applies to the Partner WSDL.
373 |
374 | header = h.generateHeader('CallOptions');
375 | header.client = '*MY CLIENT STRING*'
376 | header.defaultNamespace = '*DEVELOPER NAMESPACE PREFIX*'
377 | h.setCallOptions(header)
378 |
379 |
380 | EmailHeader
381 | --------------------------
382 | header = h.generateHeader('EmailHeader');
383 | header.triggerAutoResponseEmail = True
384 | header.triggerOtherEmail = True
385 | header.triggerUserEmail = True
386 | h.setEmailHeader(header)
387 |
388 |
389 | LocaleOptions
390 | --------------------------
391 | header = h.generateHeader('LocaleOptions');
392 | header.language = 'en_US'
393 | h.setLocaleOptions(header)
394 |
395 |
396 | LoginScopeHeader
397 | --------------------------
398 | header = h.generateHeader('LoginScopeHeader');
399 | header.organizationId = '*YOUR ORGANIZATION ID HERE*'
400 | header.portalId = '*YOUR ORGANIZATION\'S PORTAL ID HERE*'
401 | h.setLoginScopeHeader(header)
402 |
403 |
404 | MruHeader
405 | --------------------------
406 | header = h.generateHeader('MruHeader');
407 | header.updateMru = True
408 | h.setMruHeader(header)
409 |
410 |
411 | PackageVersionHeader
412 | --------------------------
413 | NOTE: I believe this example works. Salesforce accepts the header, and the call in which the
414 | header is embedded succeeds, but as there is no available documentation on what the XML
415 | request should look like, and since I don't have any APEX code available that deals with
416 | package versions, I can't be 100% sure. It is new in version 16.0 of the API, so
417 | documentation should materialize soon :)
418 |
419 | Note also that this is the only header that takes something other than a series of key-value
420 | pairs.
421 |
422 | header = h.generateHeader('PackageVersionHeader');
423 | pkg = {}
424 | pkg['majorNumber'] = 3
425 | pkg['minorNumber'] = 0
426 | pkg['namespace'] = 'SFGA'
427 | header.packageVersions = [pkg, pkg]
428 | h.setPackageVersionHeader(header)
429 |
430 |
431 | QueryOptions
432 | --------------------------
433 | header = h.generateHeader('QueryOptions');
434 | header.batchSize = 200
435 | h.setQueryOptions(header)
436 |
437 |
438 | SessionHeader
439 | --------------------------
440 | header = h.generateHeader('SessionHeader');
441 | header.sessionId = '*PIGGYBACK SESSION ID HERE*'
442 | h.setSessionHeader(header)
443 |
444 |
445 | UserTerritoryDeleteHeader
446 | --------------------------
447 | header = h.generateHeader('UserTerritoryDeleteHeader');
448 | header.transferToUserId = '*USER ID HERE*'
449 | h.setUserTerritoryDeleteHeader(header)
450 |
451 |
452 |
--------------------------------------------------------------------------------
/sforce/base.py:
--------------------------------------------------------------------------------
1 | # This program is free software; you can redistribute it and/or modify
2 | # it under the terms of the (LGPL) GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This program is distributed in the hope that it will be useful,
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | # GNU Library Lesser General Public License for more details at
10 | # ( http://www.gnu.org/licenses/lgpl.html ).
11 | #
12 | # You should have received a copy of the GNU Lesser General Public License
13 | # along with this program; if not, write to the Free Software
14 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 | # Written by: David Lanstein ( lanstein yahoo com )
16 |
17 | import string
18 | import sys
19 | import os.path
20 |
21 | from suds.client import Client
22 |
23 | try:
24 | # suds 0.3.8 and prior
25 | from suds.transport.cache import FileCache
26 | except:
27 | # suds 0.3.9+
28 | from suds.cache import FileCache
29 |
30 | import suds.sudsobject
31 | from suds.sax.element import Element
32 |
33 | class SforceBaseClient(object):
34 | _sforce = None
35 | _sessionId = None
36 | _location = None
37 | _product = 'Python Toolkit'
38 | _version = (0, 1, 3)
39 | _objectNamespace = None
40 | _strictResultTyping = False
41 |
42 | _allowFieldTruncationHeader = None
43 | _assignmentRuleHeader = None
44 | _callOptions = None
45 | _assignmentRuleHeader = None
46 | _emailHeader = None
47 | _localeOptions = None
48 | _loginScopeHeader = None
49 | _mruHeader = None
50 | _packageVersionHeader = None
51 | _queryOptions = None
52 | _sessionHeader = None
53 | _userTerritoryDeleteHeader = None
54 |
55 | def __init__(self, wsdl, cacheDuration = 0, **kwargs):
56 | '''
57 | Connect to Salesforce
58 |
59 | 'wsdl' : Location of WSDL
60 | 'cacheDuration' : Duration of HTTP GET cache in seconds, or 0 for no cache
61 | 'proxy' : Dict of pair of 'protocol' and 'location'
62 | e.g. {'http': 'my.insecure.proxy.example.com:80'}
63 | 'username' : Username for HTTP auth when using a proxy ONLY
64 | 'password' : Password for HTTP auth when using a proxy ONLY
65 | '''
66 | # Suds can only accept WSDL locations with a protocol prepended
67 | if '://' not in wsdl:
68 | # TODO windows users???
69 | # check if file exists, else let bubble up to suds as is
70 | # definitely don't want to assume http or https
71 | if os.path.isfile(wsdl):
72 | wsdl = 'file://' + os.path.abspath(wsdl)
73 |
74 | if cacheDuration > 0:
75 | cache = FileCache()
76 | cache.setduration(seconds = cacheDuration)
77 | else:
78 | cache = None
79 |
80 | self._sforce = Client(wsdl, cache = cache)
81 |
82 | # Set HTTP headers
83 | headers = {'User-Agent': 'Salesforce/' + self._product + '/' + '.'.join(str(x) for x in self._version)}
84 |
85 | # This HTTP header will not work until Suds gunzips/inflates the content
86 | # 'Accept-Encoding': 'gzip, deflate'
87 |
88 | self._sforce.set_options(headers = headers)
89 |
90 | if kwargs.has_key('proxy'):
91 | # urllib2 cannot handle HTTPS proxies yet (see bottom of README)
92 | if kwargs['proxy'].has_key('https'):
93 | raise NotImplementedError('Connecting to a proxy over HTTPS not yet implemented due to a \
94 | limitation in the underlying urllib2 proxy implementation. However, traffic from a proxy to \
95 | Salesforce will use HTTPS.')
96 | self._sforce.set_options(proxy = kwargs['proxy'])
97 |
98 | if kwargs.has_key('username'):
99 | self._sforce.set_options(username = kwargs['username'])
100 |
101 | if kwargs.has_key('password'):
102 | self._sforce.set_options(password = kwargs['password'])
103 |
104 | # Toolkit-specific methods
105 |
106 | def generateHeader(self, sObjectType):
107 | '''
108 | Generate a SOAP header as defined in:
109 | http://www.salesforce.com/us/developer/docs/api/Content/soap_headers.htm
110 | '''
111 | try:
112 | return self._sforce.factory.create(sObjectType)
113 | except:
114 | print 'There is not a SOAP header of type %s' % sObjectType
115 |
116 | def generateObject(self, sObjectType):
117 | '''
118 | Generate a Salesforce object, such as a Lead or Contact
119 | '''
120 | obj = self._sforce.factory.create('ens:sObject')
121 | obj.type = sObjectType
122 | return obj
123 |
124 | def _handleResultTyping(self, result):
125 | '''
126 | If any of the following calls return a single result, and self._strictResultTyping is true,
127 | return the single result, rather than [(SaveResult) {...}]:
128 |
129 | convertLead()
130 | create()
131 | delete()
132 | emptyRecycleBin()
133 | invalidateSessions()
134 | merge()
135 | process()
136 | retrieve()
137 | undelete()
138 | update()
139 | upsert()
140 | describeSObjects()
141 | sendEmail()
142 | '''
143 | if self._strictResultTyping == False and len(result) == 1:
144 | return result[0]
145 | else:
146 | return result
147 |
148 | def _marshallSObjects(self, sObjects, tag = 'sObjects'):
149 | '''
150 | Marshall generic sObjects into a list of SAX elements
151 |
152 | This code is going away ASAP
153 |
154 | tag param is for nested objects (e.g. MergeRequest) where
155 | key: object must be in , not
156 | '''
157 | if not isinstance(sObjects, (tuple, list)):
158 | sObjects = (sObjects, )
159 | if sObjects[0].type in ['LeadConvert', 'SingleEmailMessage', 'MassEmailMessage']:
160 | nsPrefix = 'tns:'
161 | else:
162 | nsPrefix = 'ens:'
163 |
164 | li = []
165 | for obj in sObjects:
166 | el = Element(tag)
167 | el.set('xsi:type', nsPrefix + obj.type)
168 | for k, v in obj:
169 | if k == 'type':
170 | continue
171 |
172 | # This is here to avoid 'duplicate values' error when setting a field in fieldsToNull
173 | # Even a tag like will trigger it
174 | if v == None:
175 | # not going to win any awards for variable-naming scheme here
176 | tmp = Element(k)
177 | tmp.set('xsi:nil', 'true')
178 | el.append(tmp)
179 | elif isinstance(v, (list, tuple)):
180 | for value in v:
181 | el.append(Element(k).setText(value))
182 | elif isinstance(v, suds.sudsobject.Object):
183 | el.append(self._marshallSObjects(v, k))
184 | else:
185 | el.append(Element(k).setText(v))
186 |
187 | li.append(el)
188 | return li
189 |
190 | def _setEndpoint(self, location):
191 | '''
192 | Set the endpoint after when Salesforce returns the URL after successful login()
193 | '''
194 | # suds 0.3.7+ supports multiple wsdl services, but breaks setlocation :(
195 | # see https://fedorahosted.org/suds/ticket/261
196 | try:
197 | self._sforce.set_options(location = location)
198 | except:
199 | self._sforce.wsdl.service.setlocation(location)
200 |
201 | self._location = location
202 |
203 | def _setHeaders(self, call = None):
204 | '''
205 | Attach particular SOAP headers to the request depending on the method call made
206 | '''
207 | # All calls, including utility calls, set the session header
208 | headers = {'SessionHeader': self._sessionHeader}
209 |
210 | if call in ('convertLead',
211 | 'create',
212 | 'merge',
213 | 'process',
214 | 'undelete',
215 | 'update',
216 | 'upsert'):
217 | if self._allowFieldTruncationHeader is not None:
218 | headers['AllowFieldTruncationHeader'] = self._allowFieldTruncationHeader
219 |
220 | if call in ('create',
221 | 'merge',
222 | 'update',
223 | 'upsert'):
224 | if self._assignmentRuleHeader is not None:
225 | headers['AssignmentRuleHeader'] = self._assignmentRuleHeader
226 |
227 | # CallOptions will only ever be set by the SforcePartnerClient
228 | if self._callOptions is not None:
229 | if call in ('create',
230 | 'merge',
231 | 'queryAll',
232 | 'query',
233 | 'queryMore',
234 | 'retrieve',
235 | 'search',
236 | 'update',
237 | 'upsert',
238 | 'convertLead',
239 | 'login',
240 | 'delete',
241 | 'describeGlobal',
242 | 'describeLayout',
243 | 'describeTabs',
244 | 'describeSObject',
245 | 'describeSObjects',
246 | 'getDeleted',
247 | 'getUpdated',
248 | 'process',
249 | 'undelete',
250 | 'getServerTimestamp',
251 | 'getUserInfo',
252 | 'setPassword',
253 | 'resetPassword'):
254 | headers['CallOptions'] = self._callOptions
255 |
256 | if call in ('create',
257 | 'delete',
258 | 'resetPassword',
259 | 'update',
260 | 'upsert'):
261 | if self._emailHeader is not None:
262 | headers['EmailHeader'] = self._emailHeader
263 |
264 | if call in ('describeSObject',
265 | 'describeSObjects'):
266 | if self._localeOptions is not None:
267 | headers['LocaleOptions'] = self._localeOptions
268 |
269 | if call == 'login':
270 | if self._loginScopeHeader is not None:
271 | headers['LoginScopeHeader'] = self._loginScopeHeader
272 |
273 | if call in ('create',
274 | 'merge',
275 | 'query',
276 | 'retrieve',
277 | 'update',
278 | 'upsert'):
279 | if self._mruHeader is not None:
280 | headers['MruHeader'] = self._mruHeader
281 |
282 | if call in ('convertLead',
283 | 'create',
284 | 'delete',
285 | 'describeGlobal',
286 | 'describeLayout',
287 | 'describeSObject',
288 | 'describeSObjects',
289 | 'describeTabs',
290 | 'merge',
291 | 'process',
292 | 'query',
293 | 'retrieve',
294 | 'search',
295 | 'undelete',
296 | 'update',
297 | 'upsert'):
298 | if self._packageVersionHeader is not None:
299 | headers['PackageVersionHeader'] = self._packageVersionHeader
300 |
301 | if call in ('query',
302 | 'queryAll',
303 | 'queryMore',
304 | 'retrieve'):
305 | if self._queryOptions is not None:
306 | headers['QueryOptions'] = self._queryOptions
307 |
308 | if call == 'delete':
309 | if self._userTerritoryDeleteHeader is not None:
310 | headers['UserTerritoryDeleteHeader'] = self._userTerritoryDeleteHeader
311 |
312 | self._sforce.set_options(soapheaders = headers)
313 |
314 | def setStrictResultTyping(self, strictResultTyping):
315 | '''
316 | Set whether single results from any of the following calls return the result wrapped in a list,
317 | or simply the single result object:
318 |
319 | convertLead()
320 | create()
321 | delete()
322 | emptyRecycleBin()
323 | invalidateSessions()
324 | merge()
325 | process()
326 | retrieve()
327 | undelete()
328 | update()
329 | upsert()
330 | describeSObjects()
331 | sendEmail()
332 | '''
333 | self._strictResultTyping = strictResultTyping
334 |
335 | def getSessionId(self):
336 | return self._sessionId
337 |
338 | def getLocation(self):
339 | return self._location
340 |
341 | def getConnection(self):
342 | return self._sforce
343 |
344 | def getLastRequest(self):
345 | return str(self._sforce.last_sent())
346 |
347 | def getLastResponse(self):
348 | return str(self._sforce.last_received())
349 |
350 | # Core calls
351 |
352 | def convertLead(self, leadConverts):
353 | '''
354 | Converts a Lead into an Account, Contact, or (optionally) an Opportunity.
355 | '''
356 | self._setHeaders('convertLead')
357 | return self._handleResultTyping(self._sforce.service.convertLead(leadConverts))
358 |
359 | def create(self, sObjects):
360 | self._setHeaders('create')
361 | return self._handleResultTyping(self._sforce.service.create(sObjects))
362 |
363 | def delete(self, ids):
364 | '''
365 | Deletes one or more objects
366 | '''
367 | self._setHeaders('delete')
368 | return self._handleResultTyping(self._sforce.service.delete(ids))
369 |
370 | def emptyRecycleBin(self, ids):
371 | '''
372 | Permanently deletes one or more objects
373 | '''
374 | self._setHeaders('emptyRecycleBin')
375 | return self._handleResultTyping(self._sforce.service.emptyRecycleBin(ids))
376 |
377 | def getDeleted(self, sObjectType, startDate, endDate):
378 | '''
379 | Retrieves the list of individual objects that have been deleted within the
380 | given timespan for the specified object.
381 | '''
382 | self._setHeaders('getDeleted')
383 | return self._sforce.service.getDeleted(sObjectType, startDate, endDate)
384 |
385 | def getUpdated(self, sObjectType, startDate, endDate):
386 | '''
387 | Retrieves the list of individual objects that have been updated (added or
388 | changed) within the given timespan for the specified object.
389 | '''
390 | self._setHeaders('getUpdated')
391 | return self._sforce.service.getUpdated(sObjectType, startDate, endDate)
392 |
393 | def invalidateSessions(self, sessionIds):
394 | '''
395 | Invalidate a Salesforce session
396 |
397 | This should be used with extreme caution, for the following (undocumented) reason:
398 | All API connections for a given user share a single session ID
399 | This will call logout() WHICH LOGS OUT THAT USER FROM EVERY CONCURRENT SESSION
400 |
401 | return invalidateSessionsResult
402 | '''
403 | self._setHeaders('invalidateSessions')
404 | return self._handleResultTyping(self._sforce.service.invalidateSessions(sessionIds))
405 |
406 | def login(self, username, password, token):
407 | '''
408 | Login to Salesforce.com and starts a client session.
409 |
410 | Unlike other toolkits, token is a separate parameter, because
411 | Salesforce doesn't explicitly tell you to append it when it gives
412 | you a login error. Folks that are new to the API may not know this.
413 |
414 | 'username' : Username
415 | 'password' : Password
416 | 'token' : Token
417 |
418 | return LoginResult
419 | '''
420 | self._setHeaders('login')
421 | result = self._sforce.service.login(username, password + token)
422 |
423 | # set session header
424 | header = self.generateHeader('SessionHeader')
425 | header.sessionId = result['sessionId']
426 | self.setSessionHeader(header)
427 | self._sessionId = result['sessionId']
428 |
429 | # change URL to point from test.salesforce.com to something like cs2-api.salesforce.com
430 | self._setEndpoint(result['serverUrl'])
431 |
432 | # na0.salesforce.com (a.k.a. ssl.salesforce.com) requires ISO-8859-1 instead of UTF-8
433 | if 'ssl.salesforce.com' in result['serverUrl'] or 'na0.salesforce.com' in result['serverUrl']:
434 | # currently, UTF-8 is hard-coded in Suds, can't implement this yet
435 | pass
436 |
437 | return result
438 |
439 | def logout(self):
440 | '''
441 | Logout from Salesforce.com
442 |
443 | This should be used with extreme caution, for the following (undocumented) reason:
444 | All API connections for a given user share a single session ID
445 | Calling logout() LOGS OUT THAT USER FROM EVERY CONCURRENT SESSION
446 |
447 | return LogoutResult
448 | '''
449 | self._setHeaders('logout')
450 | return self._sforce.service.logout()
451 |
452 | def merge(self, mergeRequests):
453 | self._setHeaders('merge')
454 | return self._handleResultTyping(self._sforce.service.merge(mergeRequests))
455 |
456 | def process(self, processRequests):
457 | self._setHeaders('process')
458 | return self._handleResultTyping(self._sforce.service.process(processRequests))
459 |
460 | def query(self, queryString):
461 | '''
462 | Executes a query against the specified object and returns data that matches
463 | the specified criteria.
464 | '''
465 | self._setHeaders('query')
466 | return self._sforce.service.query(queryString)
467 |
468 | def queryAll(self, queryString):
469 | '''
470 | Retrieves data from specified objects, whether or not they have been deleted.
471 | '''
472 | self._setHeaders('queryAll')
473 | return self._sforce.service.queryAll(queryString)
474 |
475 | def queryMore(self, queryLocator):
476 | '''
477 | Retrieves the next batch of objects from a query.
478 | '''
479 | self._setHeaders('queryMore')
480 | return self._sforce.service.queryMore(queryLocator)
481 |
482 | def retrieve(self, fieldList, sObjectType, ids):
483 | '''
484 | Retrieves one or more objects based on the specified object IDs.
485 | '''
486 | self._setHeaders('retrieve')
487 | return self._handleResultTyping(self._sforce.service.retrieve(fieldList, sObjectType, ids))
488 |
489 | def search(self, searchString):
490 | '''
491 | Executes a text search in your organization's data.
492 | '''
493 | self._setHeaders('search')
494 | return self._sforce.service.search(searchString)
495 |
496 | def undelete(self, ids):
497 | '''
498 | Undeletes one or more objects
499 | '''
500 | self._setHeaders('undelete')
501 | return self._handleResultTyping(self._sforce.service.undelete(ids))
502 |
503 | def update(self, sObjects):
504 | self._setHeaders('update')
505 | return self._handleResultTyping(self._sforce.service.update(sObjects))
506 |
507 | def upsert(self, externalIdFieldName, sObjects):
508 | self._setHeaders('upsert')
509 | return self._handleResultTyping(self._sforce.service.upsert(externalIdFieldName, sObjects))
510 |
511 | # Describe calls
512 |
513 | def describeGlobal(self):
514 | '''
515 | Retrieves a list of available objects in your organization
516 | '''
517 | self._setHeaders('describeGlobal')
518 | return self._sforce.service.describeGlobal()
519 |
520 | def describeLayout(self, sObjectType, recordTypeIds = None):
521 | '''
522 | Use describeLayout to retrieve information about the layout (presentation
523 | of data to users) for a given object type. The describeLayout call returns
524 | metadata about a given page layout, including layouts for edit and
525 | display-only views and record type mappings. Note that field-level security
526 | and layout editability affects which fields appear in a layout.
527 | '''
528 | self._setHeaders('describeLayout')
529 | return self._sforce.service.describeLayout(sObjectType, recordTypeIds)
530 |
531 | def describeSObject(self, sObjectsType):
532 | '''
533 | Describes metadata (field list and object properties) for the specified
534 | object.
535 | '''
536 | self._setHeaders('describeSObject')
537 | return self._sforce.service.describeSObject(sObjectsType)
538 |
539 | def describeSObjects(self, sObjectTypes):
540 | '''
541 | An array-based version of describeSObject; describes metadata (field list
542 | and object properties) for the specified object or array of objects.
543 | '''
544 | self._setHeaders('describeSObjects')
545 | return self._handleResultTyping(self._sforce.service.describeSObjects(sObjectTypes))
546 |
547 | # describeSoftphoneLayout not implemented
548 | # From the docs: "Use this call to obtain information about the layout of a SoftPhone.
549 | # Use only in the context of Salesforce CRM Call Center; do not call directly from client programs."
550 |
551 | def describeTabs(self):
552 | '''
553 | The describeTabs call returns information about the standard apps and
554 | custom apps, if any, available for the user who sends the call, including
555 | the list of tabs defined for each app.
556 | '''
557 | self._setHeaders('describeTabs')
558 | return self._sforce.service.describeTabs()
559 |
560 | # Utility calls
561 |
562 | def getServerTimestamp(self):
563 | '''
564 | Retrieves the current system timestamp (GMT) from the Web service.
565 | '''
566 | self._setHeaders('getServerTimestamp')
567 | return self._sforce.service.getServerTimestamp()
568 |
569 | def getUserInfo(self):
570 | self._setHeaders('getUserInfo')
571 | return self._sforce.service.getUserInfo()
572 |
573 | def resetPassword(self, userId):
574 | '''
575 | Changes a user's password to a system-generated value.
576 | '''
577 | self._setHeaders('resetPassword')
578 | return self._sforce.service.resetPassword(userId)
579 |
580 | def sendEmail(self, emails):
581 | self._setHeaders('sendEmail')
582 | return self._handleResultTyping(self._sforce.service.sendEmail(emails))
583 |
584 | def setPassword(self, userId, password):
585 | '''
586 | Sets the specified user's password to the specified value.
587 | '''
588 | self._setHeaders('setPassword')
589 | return self._sforce.service.setPassword(userId, password)
590 |
591 | # SOAP header-related calls
592 |
593 | def setAllowFieldTruncationHeader(self, header):
594 | self._allowFieldTruncationHeader = header
595 |
596 | def setAssignmentRuleHeader(self, header):
597 | self._assignmentRuleHeader = header
598 |
599 | # setCallOptions() is only implemented in SforcePartnerClient
600 | # http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_header_calloptions.htm
601 |
602 | def setEmailHeader(self, header):
603 | self._emailHeader = header
604 |
605 | def setLocaleOptions(self, header):
606 | self._localeOptions = header
607 |
608 | def setLoginScopeHeader(self, header):
609 | self._loginScopeHeader = header
610 |
611 | def setMruHeader(self, header):
612 | self._mruHeader = header
613 |
614 | def setPackageVersionHeader(self, header):
615 | self._packageVersionHeader = header
616 |
617 | def setQueryOptions(self, header):
618 | self._queryOptions = header
619 |
620 | def setSessionHeader(self, header):
621 | self._sessionHeader = header
622 |
623 | def setUserTerritoryDeleteHeader(self, header):
624 | self._userTerritoryDeleteHeader = header
625 |
--------------------------------------------------------------------------------
/tests/test_base.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | # This program is free software; you can redistribute it and/or modify
4 | # it under the terms of the (LGPL) GNU Lesser General Public License as
5 | # published by the Free Software Foundation; either version 3 of the
6 | # License, or (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU Library Lesser General Public License for more details at
12 | # ( http://www.gnu.org/licenses/lgpl.html ).
13 | #
14 | # You should have received a copy of the GNU Lesser General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 | # Written by: David Lanstein ( lanstein yahoo com )
18 |
19 | import datetime
20 | import re
21 | import string
22 | import sys
23 | import unittest
24 |
25 | sys.path.append('../')
26 |
27 | from sforce.base import SforceBaseClient
28 |
29 | from suds import WebFault
30 |
31 | # strings we can look for to ensure headers sent
32 | ALLOW_FIELD_TRUNCATION_HEADER_STRING = 'false'
33 | ASSIGNMENT_RULE_HEADER_STRING = 'true'
34 | CALL_OPTIONS_STRING = '*DEVELOPER NAMESPACE PREFIX*'
35 | EMAIL_HEADER_STRING = 'true'
36 | LOCALE_OPTIONS_STRING = 'en_US'
37 | # starting in 0.3.7, xsi:type="ns1:ID" is omitted from opening tag
38 | LOGIN_SCOPE_HEADER_STRING = '>00D000xxxxxxxxx'
39 | MRU_HEADER_STRING = 'true'
40 | PACKAGE_VERSION_HEADER_STRING = 'SFGA'
41 | QUERY_OPTIONS_STRING = '200'
42 | SESSION_HEADER_STRING = ''
43 | # starting in 0.3.7, xsi:type="ns1:ID" is omitted from opening tag
44 | USER_TERRITORY_DELETE_HEADER_STRING = '>005000xxxxxxxxx'
45 |
46 | class SforceBaseClientTest(unittest.TestCase):
47 | def setUp(self):
48 | pass
49 |
50 | def checkHeaders(self, call):
51 | result = self.h.getLastRequest()
52 |
53 | if (call != 'login'):
54 | self.assertTrue(result.find(SESSION_HEADER_STRING) != -1)
55 |
56 | if (call == 'convertLead' or
57 | call == 'create' or
58 | call == 'merge' or
59 | call == 'process' or
60 | call == 'undelete' or
61 | call == 'update' or
62 | call == 'upsert'):
63 | self.assertTrue(result.find(ALLOW_FIELD_TRUNCATION_HEADER_STRING) != -1)
64 |
65 | if (call == 'create' or
66 | call == 'merge' or
67 | call == 'update' or
68 | call == 'upsert'):
69 | self.assertTrue(result.find(ASSIGNMENT_RULE_HEADER_STRING) != -1)
70 |
71 | # CallOptions will only ever be set by the SforcePartnerClient
72 | if self.wsdlFormat == 'Partner':
73 | if (call == 'create' or
74 | call == 'merge' or
75 | call == 'queryAll' or
76 | call == 'query' or
77 | call == 'queryMore' or
78 | call == 'retrieve' or
79 | call == 'search' or
80 | call == 'update' or
81 | call == 'upsert' or
82 | call == 'convertLead' or
83 | call == 'login' or
84 | call == 'delete' or
85 | call == 'describeGlobal' or
86 | call == 'describeLayout' or
87 | call == 'describeTabs' or
88 | call == 'describeSObject' or
89 | call == 'describeSObjects' or
90 | call == 'getDeleted' or
91 | call == 'getUpdated' or
92 | call == 'process' or
93 | call == 'undelete' or
94 | call == 'getServerTimestamp' or
95 | call == 'getUserInfo' or
96 | call == 'setPassword' or
97 | call == 'resetPassword'):
98 | self.assertTrue(result.find(CALL_OPTIONS_STRING) != -1)
99 |
100 | if (call == 'create' or
101 | call == 'delete' or
102 | call == 'resetPassword' or
103 | call == 'update' or
104 | call == 'upsert'):
105 | self.assertTrue(result.find(EMAIL_HEADER_STRING) != -1)
106 |
107 | if (call == 'describeSObject' or
108 | call == 'describeSObjects'):
109 | self.assertTrue(result.find(LOCALE_OPTIONS_STRING) != -1)
110 |
111 | if call == 'login':
112 | self.assertTrue(result.find(LOGIN_SCOPE_HEADER_STRING) != -1)
113 |
114 | if (call == 'create' or
115 | call == 'merge' or
116 | call == 'query' or
117 | call == 'retrieve' or
118 | call == 'update' or
119 | call == 'upsert'):
120 | self.assertTrue(result.find(MRU_HEADER_STRING) != -1)
121 |
122 | if (call == 'convertLead' or
123 | call == 'create' or
124 | call == 'delete' or
125 | call == 'describeGlobal' or
126 | call == 'describeLayout' or
127 | call == 'describeSObject' or
128 | call == 'describeSObjects' or
129 | call == 'describeTabs' or
130 | call == 'merge' or
131 | call == 'process' or
132 | call == 'query' or
133 | call == 'retrieve' or
134 | call == 'search' or
135 | call == 'undelete' or
136 | call == 'update' or
137 | call == 'upsert'):
138 | self.assertTrue(result.find(PACKAGE_VERSION_HEADER_STRING) != -1)
139 |
140 | if (call == 'query' or
141 | call == 'queryAll' or
142 | call == 'queryMore' or
143 | call == 'retrieve'):
144 | self.assertTrue(result.find(QUERY_OPTIONS_STRING) != -1)
145 |
146 | if call == 'delete':
147 | self.assertTrue(result.find(USER_TERRITORY_DELETE_HEADER_STRING) != -1)
148 |
149 | def createLead(self, returnLead = False):
150 | lead = self.h.generateObject('Lead')
151 | lead.FirstName = u'Joë'
152 | lead.LastName = u'Möke'
153 | lead.Company = u'你好公司'
154 | lead.Email = 'joe@example.com'
155 |
156 | if returnLead:
157 | result = self.h.create(lead)
158 | lead.Id = result.id
159 | return (result, lead)
160 | else:
161 | return self.h.create(lead)
162 |
163 | def createLeads(self, returnLeads = False):
164 | lead = self.h.generateObject('Lead')
165 | lead.FirstName = u'Joë'
166 | lead.LastName = u'Möke'
167 | lead.Company = u'你好公司'
168 | lead.Email = 'joe@example.com'
169 |
170 | lead2 = self.h.generateObject('Lead')
171 | lead2.FirstName = u'Böb'
172 | lead2.LastName = u'Möke'
173 | lead2.Company = u'你好公司'
174 | lead2.Email = 'bob@example.com'
175 |
176 | if returnLeads:
177 | result = self.h.create((lead, lead2))
178 | lead.Id = result[0].id
179 | lead2.Id = result[1].id
180 | return (result, (lead, lead2))
181 | else:
182 | return self.h.create((lead, lead2))
183 |
184 | # Set SOAP headers
185 | def setHeaders(self, call):
186 | # no need to manually attach session ID, will happen after login automatically
187 |
188 | if (call == 'convertLead' or
189 | call == 'create' or
190 | call == 'merge' or
191 | call == 'process' or
192 | call == 'undelete' or
193 | call == 'update' or
194 | call == 'upsert'):
195 | self.setAllowFieldTruncationHeader()
196 |
197 | if (call == 'create' or
198 | call == 'merge' or
199 | call == 'update' or
200 | call == 'upsert'):
201 | self.setAssignmentRuleHeader()
202 |
203 | # CallOptions will only ever be set by the SforcePartnerClient
204 | if self.wsdlFormat == 'Partner':
205 | if (call == 'create' or
206 | call == 'merge' or
207 | call == 'queryAll' or
208 | call == 'query' or
209 | call == 'queryMore' or
210 | call == 'retrieve' or
211 | call == 'search' or
212 | call == 'update' or
213 | call == 'upsert' or
214 | call == 'convertLead' or
215 | call == 'login' or
216 | call == 'delete' or
217 | call == 'describeGlobal' or
218 | call == 'describeLayout' or
219 | call == 'describeTabs' or
220 | call == 'describeSObject' or
221 | call == 'describeSObjects' or
222 | call == 'getDeleted' or
223 | call == 'getUpdated' or
224 | call == 'process' or
225 | call == 'undelete' or
226 | call == 'getServerTimestamp' or
227 | call == 'getUserInfo' or
228 | call == 'setPassword' or
229 | call == 'resetPassword'):
230 | self.setCallOptions()
231 |
232 | if (call == 'create' or
233 | call == 'delete' or
234 | call == 'resetPassword' or
235 | call == 'update' or
236 | call == 'upsert'):
237 | self.setEmailHeader()
238 |
239 | if (call == 'describeSObject' or
240 | call == 'describeSObjects'):
241 | self.setLocaleOptions()
242 |
243 | if call == 'login':
244 | self.setLoginScopeHeader()
245 |
246 | if (call == 'create' or
247 | call == 'merge' or
248 | call == 'query' or
249 | call == 'retrieve' or
250 | call == 'update' or
251 | call == 'upsert'):
252 | self.setMruHeader()
253 |
254 | if (call == 'convertLead' or
255 | call == 'create' or
256 | call == 'delete' or
257 | call == 'describeGlobal' or
258 | call == 'describeLayout' or
259 | call == 'describeSObject' or
260 | call == 'describeSObjects' or
261 | call == 'describeTabs' or
262 | call == 'merge' or
263 | call == 'process' or
264 | call == 'query' or
265 | call == 'retrieve' or
266 | call == 'search' or
267 | call == 'undelete' or
268 | call == 'update' or
269 | call == 'upsert'):
270 | self.setPackageVersionHeader()
271 |
272 | if (call == 'query' or
273 | call == 'queryAll' or
274 | call == 'queryMore' or
275 | call == 'retrieve'):
276 | self.setQueryOptions()
277 |
278 | if call == 'delete':
279 | self.setUserTerritoryDeleteHeader()
280 |
281 | def setAllowFieldTruncationHeader(self):
282 | header = self.h.generateHeader('AllowFieldTruncationHeader');
283 | header.allowFieldTruncation = False
284 | self.h.setAllowFieldTruncationHeader(header)
285 |
286 | def setAssignmentRuleHeader(self):
287 | header = self.h.generateHeader('AssignmentRuleHeader');
288 | header.useDefaultRule = True
289 | self.h.setAssignmentRuleHeader(header)
290 |
291 | def setCallOptions(self):
292 | '''
293 | Note that this header only applies to the Partner WSDL.
294 | '''
295 | if self.wsdlFormat == 'Partner':
296 | header = self.h.generateHeader('CallOptions');
297 | header.client = '*MY CLIENT STRING*'
298 | header.defaultNamespace = '*DEVELOPER NAMESPACE PREFIX*'
299 | self.h.setCallOptions(header)
300 | else:
301 | pass
302 |
303 | def setEmailHeader(self):
304 | header = self.h.generateHeader('EmailHeader');
305 | header.triggerAutoResponseEmail = True
306 | header.triggerOtherEmail = True
307 | header.triggerUserEmail = True
308 | self.h.setEmailHeader(header)
309 |
310 | def setLocaleOptions(self):
311 | header = self.h.generateHeader('LocaleOptions');
312 | header.language = 'en_US'
313 | self.h.setLocaleOptions(header)
314 |
315 | def setLoginScopeHeader(self):
316 | header = self.h.generateHeader('LoginScopeHeader');
317 | header.organizationId = '00D000xxxxxxxxx'
318 | #header.portalId = '00D000xxxxxxxxx'
319 | self.h.setLoginScopeHeader(header)
320 |
321 | def setMruHeader(self):
322 | header = self.h.generateHeader('MruHeader');
323 | header.updateMru = True
324 | self.h.setMruHeader(header)
325 |
326 | def setPackageVersionHeader(self):
327 | header = self.h.generateHeader('PackageVersionHeader');
328 | pkg = {}
329 | pkg['majorNumber'] = 1
330 | pkg['minorNumber'] = 2
331 | pkg['namespace'] = 'SFGA'
332 | header.packageVersions = pkg
333 | self.h.setPackageVersionHeader(header)
334 |
335 | def setQueryOptions(self):
336 | header = self.h.generateHeader('QueryOptions');
337 | header.batchSize = 200
338 | self.h.setQueryOptions(header)
339 |
340 | def setSessionHeader(self):
341 | header = self.h.generateHeader('SessionHeader');
342 | header.sessionId = '*PIGGYBACK SESSION ID HERE*'
343 | self.h.setSessionHeader(header)
344 |
345 | def setUserTerritoryDeleteHeader(self):
346 | header = self.h.generateHeader('UserTerritoryDeleteHeader');
347 | header.transferToUserId = '005000xxxxxxxxx'
348 | self.h.setUserTerritoryDeleteHeader(header)
349 |
350 | # Core calls
351 |
352 | def testConvertLead(self):
353 | result = self.createLead()
354 |
355 | self.setHeaders('convertLead')
356 |
357 | leadConvert = self.h.generateObject('LeadConvert')
358 | leadConvert.leadId = result.id
359 | leadConvert.convertedStatus = 'Qualified'
360 | result = self.h.convertLead(leadConvert)
361 |
362 | self.assertTrue(result.success)
363 | self.assertTrue(result.accountId[0:3] == '001')
364 | self.assertTrue(result.contactId[0:3] == '003')
365 | self.assertTrue(result.leadId[0:3] == '00Q')
366 | self.assertTrue(result.opportunityId[0:3] == '006')
367 |
368 | self.checkHeaders('convertLead')
369 |
370 | def testCreateCustomObject(self):
371 | case = self.h.generateObject('Case')
372 | result = self.h.create(case)
373 |
374 | self.assertTrue(result.success)
375 | self.assertTrue(result.id[0:3] == '500')
376 |
377 | caseNote = self.h.generateObject('Case_Note__c')
378 | caseNote.case__c = result.id
379 | caseNote.subject__c = 'my subject'
380 | caseNote.description__c = 'description here'
381 | result = self.h.create(caseNote)
382 |
383 | self.assertTrue(result.success)
384 | self.assertTrue(result.id[0:3] == 'a0E')
385 |
386 | def testCreateLead(self):
387 | self.setHeaders('create')
388 |
389 | result = self.createLead()
390 |
391 | self.assertTrue(result.success)
392 | self.assertTrue(result.id[0:3] == '00Q')
393 |
394 | self.checkHeaders('create')
395 |
396 | def testCreateLeads(self):
397 | result = self.createLeads()
398 |
399 | self.assertTrue(result[0].success)
400 | self.assertTrue(result[0].id[0:3] == '00Q')
401 | self.assertTrue(result[1].success)
402 | self.assertTrue(result[1].id[0:3] == '00Q')
403 |
404 | def testDeleteLead(self):
405 | self.setHeaders('delete')
406 |
407 | (result, lead) = self.createLead(True)
408 | result = self.h.delete(result.id)
409 |
410 | self.assertTrue(result.success)
411 | self.assertEqual(result.id, lead.Id)
412 |
413 | self.checkHeaders('delete')
414 |
415 | def testDeleteLeads(self):
416 | (result, (lead, lead2)) = self.createLeads(True)
417 | result = self.h.delete((result[0].id, result[1].id))
418 |
419 | self.assertTrue(result[0].success)
420 | self.assertEqual(result[0].id, lead.Id)
421 | self.assertTrue(result[1].success)
422 | self.assertEqual(result[1].id, lead2.Id)
423 |
424 | def testEmptyRecycleBinOneObject(self):
425 | (result, lead) = self.createLead(True)
426 | result = self.h.delete(result.id)
427 | result = self.h.emptyRecycleBin(result.id)
428 |
429 | self.assertTrue(result.success)
430 | self.assertEqual(result.id, lead.Id)
431 |
432 | def testEmptyRecycleBinTwoObjects(self):
433 | (result, (lead, lead2)) = self.createLeads(True)
434 | result = self.h.delete((result[0].id, result[1].id))
435 | result = self.h.emptyRecycleBin((result[0].id, result[1].id))
436 |
437 | self.assertTrue(result[0].success)
438 | self.assertEqual(result[0].id, lead.Id)
439 | self.assertTrue(result[1].success)
440 | self.assertEqual(result[1].id, lead2.Id)
441 |
442 | def testGetDeleted(self):
443 | self.setHeaders('getDeleted')
444 |
445 | now = datetime.datetime.utcnow()
446 | result = self.createLead()
447 | result = self.h.delete(result.id)
448 | result = self.h.getDeleted('Lead', now.isoformat(), '2019-01-01T23:01:01Z')
449 |
450 | # This will nearly always be one single result
451 | self.assertTrue(len(result.deletedRecords) > 0)
452 |
453 | for record in result.deletedRecords:
454 | self.assertTrue(isinstance(record.deletedDate, datetime.datetime))
455 | self.assertEqual(len(record.id), 18)
456 |
457 | self.checkHeaders('getDeleted')
458 |
459 | def testGetUpdated(self):
460 | self.setHeaders('getUpdated')
461 |
462 | now = datetime.datetime.utcnow()
463 | (result, lead) = self.createLead(True)
464 | result = self.h.update(lead)
465 | result = self.h.getUpdated('Lead', now.isoformat(), '2019-01-01T23:01:01Z')
466 |
467 | # This will nearly always be one single result
468 | self.assertTrue(len(result.ids) > 0)
469 |
470 | for id in result.ids:
471 | self.assertEqual(len(id), 18)
472 |
473 | self.checkHeaders('getUpdated')
474 |
475 | def testInvalidateSession(self):
476 | result = self.h.invalidateSessions(self.h.getSessionId())
477 |
478 | self.assertTrue(result.success)
479 |
480 | def testInvalidateSessions(self):
481 | result = self.h.invalidateSessions((self.h.getSessionId(), 'foo'))
482 |
483 | self.assertTrue(result[0].success)
484 | self.assertFalse(result[1].success)
485 |
486 | def testLogin(self):
487 | # This is really only here to test the login() SOAP headers
488 | self.setHeaders('login')
489 |
490 | try:
491 | self.h.login('foo', 'bar', 'baz')
492 | except WebFault:
493 | pass
494 |
495 | self.checkHeaders('login')
496 |
497 | def testLogout(self):
498 | result = self.h.logout()
499 |
500 | self.assertEqual(result, None)
501 |
502 | def testMerge(self):
503 | self.setHeaders('merge')
504 |
505 | (result, (lead, lead2)) = self.createLeads(True)
506 |
507 | mergeRequest = self.h.generateObject('MergeRequest')
508 | mergeRequest.masterRecord = lead
509 | mergeRequest.recordToMergeIds = result[1].id
510 | result = self.h.merge(mergeRequest)
511 |
512 | self.assertTrue(result.success)
513 | self.assertEqual(result.id, lead.Id)
514 | self.assertEqual(result.mergedRecordIds[0], lead2.Id)
515 |
516 | self.checkHeaders('merge')
517 |
518 | def testProcessSubmitRequestMalformedId(self):
519 | self.setHeaders('process')
520 |
521 | processRequest = self.h.generateObject('ProcessSubmitRequest')
522 | processRequest.objectId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*'
523 | processRequest.comments = 'This is what I think.'
524 | result = self.h.process(processRequest)
525 |
526 | self.assertFalse(result.success)
527 | self.assertEqual(result.errors[0].statusCode, 'MALFORMED_ID')
528 |
529 | self.checkHeaders('process')
530 |
531 | def testProcessSubmitRequestInvalidId(self):
532 | processRequest = self.h.generateObject('ProcessSubmitRequest')
533 | processRequest.objectId = '00Q000xxxxxxxxx'
534 | processRequest.comments = 'This is what I think.'
535 | result = self.h.process(processRequest)
536 |
537 | self.assertFalse(result.success)
538 | self.assertEqual(result.errors[0].statusCode, 'INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY')
539 |
540 | def testProcessWorkitemRequestMalformedId(self):
541 | processRequest = self.h.generateObject('ProcessWorkitemRequest')
542 | processRequest.action = 'Approve'
543 | processRequest.workitemId = '*ID OF OBJECT PROCESS REQUEST AFFECTS*'
544 | processRequest.comments = 'I approved this request.'
545 | result = self.h.process(processRequest)
546 |
547 | self.assertFalse(result.success)
548 | self.assertEqual(result.errors[0].statusCode, 'MALFORMED_ID')
549 |
550 | def testProcessWorkitemRequestInvalidId(self):
551 | processRequest = self.h.generateObject('ProcessWorkitemRequest')
552 | processRequest.action = 'Approve'
553 | processRequest.workitemId = '00Q000xxxxxxxxx'
554 | processRequest.comments = 'I approved this request.'
555 | result = self.h.process(processRequest)
556 |
557 | self.assertFalse(result.success)
558 | self.assertEqual(result.errors[0].statusCode, 'INVALID_CROSS_REFERENCE_KEY')
559 |
560 | # Note that Lead.LastName, Lead.Company, Account.Name can never equal NULL, they are required both
561 | # via API and UI
562 | #
563 | # Also, SOQL does not return fields that are NULL
564 | def testQueryNoResults(self):
565 | self.setHeaders('query')
566 |
567 | result = self.h.query('SELECT FirstName, LastName FROM Lead LIMIT 0')
568 |
569 | self.assertFalse(hasattr(result, 'records'))
570 | self.assertEqual(result.size, 0)
571 |
572 | self.checkHeaders('query')
573 |
574 | def testQueryOneResultWithFirstName(self):
575 | result = self.h.query('SELECT FirstName, LastName FROM Lead WHERE FirstName != NULL LIMIT 1')
576 |
577 | self.assertEqual(len(result.records), 1)
578 | self.assertEqual(result.size, 1)
579 | self.assertTrue(hasattr(result.records[0], 'FirstName'))
580 | self.assertTrue(hasattr(result.records[0], 'LastName'))
581 | self.assertFalse(isinstance(result.records[0].FirstName, list))
582 | self.assertFalse(isinstance(result.records[0].LastName, list))
583 |
584 | '''
585 | See explanation below.
586 |
587 | def testQueryOneResultWithoutFirstName(self):
588 | result = self.h.query('SELECT FirstName, LastName FROM Lead WHERE FirstName = NULL LIMIT 1')
589 |
590 | self.assertEqual(len(result.records), 1)
591 | self.assertEqual(result.size, 1)
592 | self.assertFalse(hasattr(result.records[0], 'FirstName'))
593 | self.assertTrue(hasattr(result.records[0], 'LastName'))
594 | self.assertFalse(isinstance(result.records[0].FirstName, list))
595 | self.assertFalse(isinstance(result.records[0].LastName, list))
596 | '''
597 |
598 | def testQueryTwoResults(self):
599 | result = self.h.query('SELECT FirstName, LastName FROM Lead WHERE FirstName != NULL LIMIT 2')
600 |
601 | self.assertTrue(len(result.records) > 1)
602 | self.assertTrue(result.size > 1)
603 | for record in result.records:
604 | self.assertTrue(hasattr(record, 'FirstName'))
605 | self.assertTrue(hasattr(record, 'LastName'))
606 | self.assertFalse(isinstance(record.FirstName, list))
607 | self.assertFalse(isinstance(record.LastName, list))
608 |
609 | def testQueryAllNoResults(self):
610 | self.setHeaders('queryAll')
611 |
612 | result = self.h.queryAll('SELECT Account.Name, FirstName, LastName FROM Contact LIMIT 0')
613 |
614 | self.assertFalse(hasattr(result, 'records'))
615 | self.assertEqual(result.size, 0)
616 |
617 | self.checkHeaders('queryAll')
618 |
619 | def testQueryAllOneResultWithFirstName(self):
620 | result = self.h.queryAll('SELECT Account.Name, FirstName, LastName FROM Contact WHERE FirstName != NULL LIMIT 1')
621 |
622 | self.assertEqual(len(result.records), 1)
623 | self.assertEqual(result.size, 1)
624 | self.assertTrue(hasattr(result.records[0], 'FirstName'))
625 | self.assertTrue(hasattr(result.records[0], 'LastName'))
626 | self.assertTrue(hasattr(result.records[0].Account, 'Name'))
627 | self.assertFalse(isinstance(result.records[0].FirstName, list))
628 | self.assertFalse(isinstance(result.records[0].LastName, list))
629 | self.assertFalse(isinstance(result.records[0].Account.Name, list))
630 |
631 | '''
632 | There's a bug with Salesforce where the query in this test where the Partner WSDL includes
633 | FirstName in the SOAP response, but the Enterprise WSDL does not.
634 |
635 | Will report a bug once self-service portal is back up.
636 |
637 | Partner:
638 | "trueContactAccountUnknownAdministrator1"
639 |
640 | Enterprise:
641 | "trueUnknownAdministrator1"
642 |
643 | def testQueryAllOneResultWithoutFirstName(self):
644 | result = self.h.queryAll('SELECT Account.Name, FirstName, LastName FROM Contact WHERE FirstName = NULL LIMIT 1')
645 | print result
646 |
647 | self.assertEqual(len(result.records), 1)
648 | self.assertEqual(result.size, 1)
649 | self.assertFalse(hasattr(result.records[0], 'FirstName'))
650 | self.assertTrue(hasattr(result.records[0], 'LastName'))
651 | self.assertTrue(hasattr(result.records[0].Account, 'Name'))
652 | self.assertFalse(isinstance(result.records[0].FirstName, list))
653 | self.assertFalse(isinstance(result.records[0].LastName, list))
654 | self.assertFalse(isinstance(result.records[0].Account.Name, list))
655 | '''
656 |
657 | def testQueryAllTwoResults(self):
658 | result = self.h.queryAll('SELECT Account.Name, FirstName, LastName FROM Contact WHERE FirstName != NULL LIMIT 2')
659 |
660 | self.assertTrue(len(result.records) > 1)
661 | self.assertTrue(result.size > 1)
662 | for record in result.records:
663 | self.assertTrue(hasattr(record, 'FirstName'))
664 | self.assertTrue(hasattr(record, 'LastName'))
665 | self.assertTrue(hasattr(record.Account, 'Name'))
666 | self.assertFalse(isinstance(record.FirstName, list))
667 | self.assertFalse(isinstance(record.LastName, list))
668 | self.assertFalse(isinstance(record.Account.Name, list))
669 |
670 | def testQueryMore(self):
671 | self.setHeaders('queryMore')
672 |
673 | result = self.h.queryAll('SELECT FirstName, LastName FROM Lead')
674 |
675 | while (result.done == False):
676 | self.assertTrue(result.queryLocator != None)
677 | self.assertEqual(len(result.records), 200)
678 | result = self.h.queryMore(result.queryLocator)
679 |
680 | self.assertTrue(len(result.records) > 1)
681 | self.assertTrue(len(result.records) <= 200)
682 | self.assertTrue(result.done)
683 | self.assertEqual(result.queryLocator, None)
684 |
685 | self.checkHeaders('queryMore')
686 |
687 | def testRetrievePassingList(self):
688 | self.setHeaders('retrieve')
689 |
690 | (result, lead) = self.createLead(True)
691 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', [result.id])
692 |
693 | self.assertEqual(result.Id, lead.Id)
694 | self.assertEqual(result.type, 'Lead')
695 | self.assertEqual(result.FirstName, lead.FirstName)
696 | self.assertEqual(result.LastName, lead.LastName)
697 | self.assertEqual(result.Company, lead.Company)
698 | self.assertEqual(result.Email, lead.Email)
699 |
700 | self.checkHeaders('retrieve')
701 |
702 | def testRetrievePassingString(self):
703 | (result, lead) = self.createLead(True)
704 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', result.id)
705 |
706 | self.assertEqual(result.Id, lead.Id)
707 | self.assertEqual(result.type, 'Lead')
708 | self.assertEqual(result.FirstName, lead.FirstName)
709 | self.assertEqual(result.LastName, lead.LastName)
710 | self.assertEqual(result.Company, lead.Company)
711 | self.assertEqual(result.Email, lead.Email)
712 |
713 | def testRetrievePassingTuple(self):
714 | (result, lead) = self.createLead(True)
715 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', (result.id))
716 |
717 | self.assertEqual(result.Id, lead.Id)
718 | self.assertEqual(result.type, 'Lead')
719 | self.assertEqual(result.FirstName, lead.FirstName)
720 | self.assertEqual(result.LastName, lead.LastName)
721 | self.assertEqual(result.Company, lead.Company)
722 | self.assertEqual(result.Email, lead.Email)
723 |
724 | def testRetrievePassingListOfTwoIds(self):
725 | self.setHeaders('retrieve')
726 |
727 | (result, lead) = self.createLead(True)
728 | result = self.h.retrieve('FirstName, LastName, Company, Email', 'Lead', [result.id, result.id])
729 |
730 | self.assertEqual(result[0].Id, lead.Id)
731 | self.assertEqual(result[0].type, 'Lead')
732 | self.assertEqual(result[0].FirstName, lead.FirstName)
733 | self.assertEqual(result[0].LastName, lead.LastName)
734 | self.assertEqual(result[0].Company, lead.Company)
735 | self.assertEqual(result[0].Email, lead.Email)
736 | self.assertEqual(result[1].Id, lead.Id)
737 | self.assertEqual(result[1].type, 'Lead')
738 | self.assertEqual(result[1].FirstName, lead.FirstName)
739 | self.assertEqual(result[1].LastName, lead.LastName)
740 | self.assertEqual(result[1].Company, lead.Company)
741 | self.assertEqual(result[1].Email, lead.Email)
742 |
743 | self.checkHeaders('retrieve')
744 |
745 | def testSearchNoResults(self):
746 | self.setHeaders('search')
747 |
748 | result = self.h.search('FIND {asdfasdffdsaasdl;fjkwelhnfd} IN Name Fields RETURNING Lead(Name, Phone)')
749 |
750 | self.assertEqual(len(result.searchRecords), 0)
751 |
752 | self.checkHeaders('search')
753 |
754 | def testUndeleteLead(self):
755 | self.setHeaders('undelete')
756 |
757 | (result, lead) = self.createLead(True)
758 | result = self.h.delete(result.id)
759 | result = self.h.undelete(result.id)
760 |
761 | self.assertTrue(result.success)
762 | self.assertEqual(result.id, lead.Id)
763 |
764 | self.checkHeaders('undelete')
765 |
766 | def testUndeleteLeads(self):
767 | (result, (lead, lead2)) = self.createLeads(True)
768 | result = self.h.delete((result[0].id, result[1].id))
769 | result = self.h.undelete((result[0].id, result[1].id))
770 |
771 | self.assertTrue(result[0].success)
772 | self.assertEqual(result[0].id, lead.Id)
773 | self.assertTrue(result[1].success)
774 | self.assertEqual(result[1].id, lead2.Id)
775 |
776 | def testUpdateNoFieldsToNull(self):
777 | self.setHeaders('update')
778 |
779 | (result, lead) = self.createLead(True)
780 |
781 | lead.fieldsToNull = ()
782 |
783 | result = self.h.update(lead)
784 | self.assertTrue(result.success)
785 | self.assertEqual(result.id, lead.Id)
786 |
787 | self.checkHeaders('update')
788 |
789 | def testUpsertCreate(self):
790 | self.setHeaders('upsert')
791 |
792 | lead = self.h.generateObject('Lead')
793 | lead.FirstName = u'Joë'
794 | lead.LastName = u'Möke'
795 | lead.Company = u'你好公司'
796 | lead.Email = 'joe@example.com'
797 | result = self.h.upsert('Id', lead)
798 |
799 | self.assertTrue(result.created)
800 | self.assertTrue(result.id[0:3] == '00Q')
801 | self.assertTrue(result.success)
802 |
803 | self.checkHeaders('upsert')
804 |
805 | def testUpsertUpdate(self):
806 | (result, lead) = self.createLead(True)
807 | result = self.h.upsert('Id', lead)
808 |
809 | self.assertFalse(result.created)
810 | self.assertEqual(result.id, lead.Id)
811 | self.assertTrue(result.success)
812 |
813 | # Describe calls
814 |
815 | def testDescribeGlobal(self):
816 | self.setHeaders('describeGlobal')
817 |
818 | result = self.h.describeGlobal()
819 |
820 | self.assertTrue(hasattr(result, 'encoding'))
821 | self.assertTrue(hasattr(result, 'maxBatchSize'))
822 |
823 | foundAccount = False
824 | for object in result.sobjects:
825 | if object.name == 'Account':
826 | foundAccount = True
827 |
828 | self.assertTrue(foundAccount)
829 |
830 | self.checkHeaders('describeGlobal')
831 |
832 | def testDescribeLayout(self):
833 | self.setHeaders('describeLayout')
834 |
835 | result = self.h.describeLayout('Lead', '012000000000000AAA') # Master Record Type
836 |
837 | self.assertEqual(result[1][0].recordTypeId, '012000000000000AAA')
838 |
839 | self.checkHeaders('describeLayout')
840 |
841 | def testDescribeSObject(self):
842 | self.setHeaders('describeSObject')
843 |
844 | result = self.h.describeSObject('Lead')
845 |
846 | self.assertTrue(hasattr(result, 'activateable'))
847 | self.assertTrue(hasattr(result, 'childRelationships'))
848 | self.assertEqual(result.keyPrefix, '00Q')
849 | self.assertEqual(result.name, 'Lead')
850 |
851 | self.checkHeaders('describeSObject')
852 |
853 | def testDescribeSObjects(self):
854 | self.setHeaders('describeSObjects')
855 |
856 | result = self.h.describeSObjects(('Contact', 'Account'))
857 |
858 | self.assertTrue(hasattr(result[0], 'activateable'))
859 | self.assertTrue(hasattr(result[0], 'childRelationships'))
860 | self.assertEqual(result[0].keyPrefix, '003')
861 | self.assertEqual(result[0].name, 'Contact')
862 |
863 | self.assertTrue(hasattr(result[1], 'activateable'))
864 | self.assertTrue(hasattr(result[1], 'childRelationships'))
865 | self.assertEqual(result[1].keyPrefix, '001')
866 | self.assertEqual(result[1].name, 'Account')
867 |
868 | self.checkHeaders('describeSObjects')
869 |
870 | def testDescribeTabs(self):
871 | self.setHeaders('describeTabs')
872 |
873 | result = self.h.describeTabs()
874 | self.assertTrue(hasattr(result[0], 'tabs'))
875 |
876 | self.checkHeaders('describeTabs')
877 |
878 | # Utility calls
879 |
880 | def testGetServerTimestamp(self):
881 | self.setHeaders('getServerTimestamp')
882 |
883 | result = self.h.getServerTimestamp()
884 |
885 | self.assertTrue(isinstance(result.timestamp, datetime.datetime))
886 |
887 | self.checkHeaders('getServerTimestamp')
888 |
889 | def testGetUserInfo(self):
890 | self.setHeaders('getUserInfo')
891 |
892 | result = self.h.getUserInfo()
893 |
894 | self.assertTrue(hasattr(result, 'userEmail'))
895 | self.assertTrue(hasattr(result, 'userId'))
896 |
897 | self.checkHeaders('getUserInfo')
898 |
899 | def testResetPassword(self):
900 | self.setHeaders('resetPassword')
901 |
902 | try:
903 | self.h.resetPassword('005000xxxxxxxxx')
904 | self.fail('WebFault not thrown')
905 | except WebFault:
906 | pass
907 |
908 | self.checkHeaders('resetPassword')
909 |
910 | def testSendSingleEmailFail(self):
911 | self.setHeaders('sendEmail')
912 |
913 | email = self.h.generateObject('SingleEmailMessage')
914 | email.toAddresses = 'joeexample.com'
915 | email.subject = 'This is my subject.'
916 | email.plainTextBody = 'This is the plain-text body of my email.'
917 | result = self.h.sendEmail([email])
918 |
919 | self.assertFalse(result.success)
920 | self.assertEqual(result.errors[0].statusCode, 'INVALID_EMAIL_ADDRESS')
921 |
922 | self.checkHeaders('sendEmail')
923 |
924 | def testSendSingleEmailPass(self):
925 | email = self.h.generateObject('SingleEmailMessage')
926 | email.toAddresses = 'joe@example.com'
927 | email.subject = 'This is my subject.'
928 | email.plainTextBody = 'This is the plain-text body of my email.'
929 | result = self.h.sendEmail([email])
930 |
931 | self.assertTrue(result.success)
932 |
933 | def testSendMassEmailFail(self):
934 | email = self.h.generateObject('MassEmailMessage')
935 | email.targetObjectIds = (('*LEAD OR CONTACT ID TO EMAIL*', '*ANOTHER LEAD OR CONTACT TO EMAIL*'))
936 | email.templateId = '*EMAIL TEMPLATE ID TO USE*'
937 | result = self.h.sendEmail([email])
938 |
939 | self.assertFalse(result.success)
940 | self.assertEqual(result.errors[0].statusCode, 'INVALID_ID_FIELD')
941 |
942 | # To make these tests as portable as possible, we won't depend on a particular templateId
943 | # to test to make sure our mass emails succeed. From the failure message we can gather that
944 | # SFDC is successfully receiving our SOAP message, and reasonably infer that our code works.
945 |
946 | def testSetPassword(self):
947 | self.setHeaders('setPassword')
948 |
949 | try:
950 | self.h.setPassword('*USER ID HERE*', '*NEW PASSWORD HERE*')
951 | self.fail('WebFault not thrown')
952 | except WebFault:
953 | pass
954 |
955 | self.checkHeaders('setPassword')
956 |
957 | # Toolkit-Specific Utility Calls:
958 |
959 | def testGenerateHeader(self):
960 | header = self.h.generateHeader('SessionHeader')
961 |
962 | self.assertEqual(header.sessionId, None)
963 |
964 | def testGenerateObject(self):
965 | account = self.h.generateObject('Account')
966 |
967 | self.assertEqual(account.fieldsToNull, [])
968 | self.assertEqual(account.Id, None)
969 | self.assertEqual(account.type, 'Account')
970 |
971 | def testGetLastRequest(self):
972 | self.h.getServerTimestamp()
973 | result = self.h.getLastRequest()
974 |
975 | self.assertTrue(result.find(':getServerTimestamp/>') != -1)
976 |
977 | def testGetLastResponse(self):
978 | self.h.getServerTimestamp()
979 | result = self.h.getLastResponse()
980 |
981 | self.assertTrue(result.find('') != -1)
982 |
983 | # SOAP Headers tested as part of the method calls
984 |
985 | if __name__ == '__main__':
986 | unittest.main()
987 |
988 |
--------------------------------------------------------------------------------