├── EXAMPLES ├── FUTURE ├── HACKS ├── LICENSE ├── README ├── TODO ├── setup.py ├── sforce ├── __init__.py ├── base.py ├── enterprise.py └── partner.py └── tests ├── test_base.py ├── test_config.py ├── test_enterprise.py └── test_partner.py /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | USERNAME = '' 2 | PASSWORD = '' 3 | TOKEN = '' 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------