Beatbox/beatbox.py

632 строки
23 KiB
Python

"""beatbox: Makes the salesforce.com SOAP API easily accessible."""
__version__ = "0.95"
__author__ = "Simon Fell"
__credits__ = "Mad shouts to the sforce possie"
__copyright__ = "(C) 2006-2013 Simon Fell. GNU GPL 2."
import sys
import httplib
from urlparse import urlparse
from StringIO import StringIO
import gzip
import datetime
import xmltramp
from xmltramp import islst
from xml.sax.saxutils import XMLGenerator
from xml.sax.saxutils import quoteattr
from xml.sax.xmlreader import AttributesNSImpl
# global constants for namespace strings, used during serialization
_partnerNs = "urn:partner.soap.sforce.com"
_sobjectNs = "urn:sobject.partner.soap.sforce.com"
_envNs = "http://schemas.xmlsoap.org/soap/envelope/"
_noAttrs = AttributesNSImpl({}, {})
# global constants for xmltramp namespaces, used to access response data
_tPartnerNS = xmltramp.Namespace(_partnerNs)
_tSObjectNS = xmltramp.Namespace(_sobjectNs)
_tSoapNS = xmltramp.Namespace(_envNs)
# global config
gzipRequest=True # are we going to gzip the request ?
gzipResponse=True # are we going to tell the server to gzip the response ?
forceHttp=False # force all connections to be HTTP, for debugging
def makeConnection(scheme, host, timeout=1200):
kwargs = {} if sys.version_info<(2,6,0) else {'timeout':timeout}
if forceHttp or scheme.upper() == 'HTTP':
return httplib.HTTPConnection(host, **kwargs)
return httplib.HTTPSConnection(host, **kwargs)
# the main sforce client proxy class
class Client:
def __init__(self):
self.batchSize = 500
self.serverUrl = "https://login.salesforce.com/services/Soap/u/28.0"
self.__conn = None
self.timeout = 15
def __del__(self):
if self.__conn != None:
self.__conn.close()
# login, the serverUrl and sessionId are automatically handled, returns the loginResult structure
def login(self, username, password):
lr = LoginRequest(self.serverUrl, username, password).post()
self.useSession(str(lr[_tPartnerNS.sessionId]), str(lr[_tPartnerNS.serverUrl]))
return lr
# perform a portal login, orgId is always needed, portalId is needed for new style portals
# is not required for the old self service portal
# for the self service portal, only the login request will work, self service users don't
# get API access, for new portals, the users should have API acesss, and can call the rest
# of the API.
def portalLogin(self, username, password, orgId, portalId):
lr = PortalLoginRequest(self.serverUrl, username, password, orgId, portalId).post()
self.useSession(str(lr[_tPartnerNS.sessionId]), str(lr[_tPartnerNS.serverUrl]))
return lr
# initialize from an existing sessionId & serverUrl, useful if we're being launched via a custom link
def useSession(self, sessionId, serverUrl):
self.sessionId = sessionId
self.__serverUrl = serverUrl
(scheme, host, path, params, query, frag) = urlparse(self.__serverUrl)
self.__conn = makeConnection(scheme, host)
# calls logout which invalidates the current sessionId, in general its better to not call this and just
# let the sessions expire on their own.
def logout(self):
return LogoutRequest(self.__serverUrl, self.sessionId).post(self.__conn, True)
# set the batchSize property on the Client instance to change the batchsize for query/queryMore
def query(self, soql):
return QueryRequest(self.__serverUrl, self.sessionId, self.batchSize, soql).post(self.__conn)
# query include deleted and archived rows.
def queryAll(self, soql):
return QueryRequest(self.__serverUrl, self.sessionId, self.batchSize, soql, "queryAll").post(self.__conn)
def queryMore(self, queryLocator):
return QueryMoreRequest(self.__serverUrl, self.sessionId, self.batchSize, queryLocator).post(self.__conn)
def search(self, sosl):
return SearchRequest(self.__serverUrl, self.sessionId, sosl).post(self.__conn)
def getUpdated(self, sObjectType, start, end):
return GetUpdatedRequest(self.__serverUrl, self.sessionId, sObjectType, start, end).post(self.__conn)
def getDeleted(self, sObjectType, start, end):
return GetDeletedRequest(self.__serverUrl, self.sessionId, sObjectType, start, end).post(self.__conn)
def retrieve(self, fields, sObjectType, ids):
return RetrieveRequest(self.__serverUrl, self.sessionId, fields, sObjectType, ids).post(self.__conn)
# sObjects can be 1 or a list, returns a single save result or a list
def create(self, sObjects):
return CreateRequest(self.__serverUrl, self.sessionId, sObjects).post(self.__conn)
# sObjects can be 1 or a list, returns a single save result or a list
def update(self, sObjects):
return UpdateRequest(self.__serverUrl, self.sessionId, sObjects).post(self.__conn)
# sObjects can be 1 or a list, returns a single upsert result or a list
def upsert(self, externalIdName, sObjects):
return UpsertRequest(self.__serverUrl, self.sessionId, externalIdName, sObjects).post(self.__conn)
# ids can be 1 or a list, returns a single delete result or a list
def delete(self, ids):
return DeleteRequest(self.__serverUrl, self.sessionId, ids).post(self.__conn)
# ids can be 1 or a list, returns a single delete result or a list
def undelete(self, ids):
return UndeleteRequest(self.__serverUrl, self.sessionId, ids).post(self.__conn)
# leadConverts can be 1 or a list of dictionaries, each dictionary should be filled out as per the LeadConvert type in the WSDL.
# <element name="accountId" type="tns:ID" nillable="true"/>
# <element name="contactId" type="tns:ID" nillable="true"/>
# <element name="convertedStatus" type="xsd:string"/>
# <element name="doNotCreateOpportunity" type="xsd:boolean"/>
# <element name="leadId" type="tns:ID"/>
# <element name="opportunityName" type="xsd:string" nillable="true"/>
# <element name="overwriteLeadSource" type="xsd:boolean"/>
# <element name="ownerId" type="tns:ID" nillable="true"/>
# <element name="sendNotificationEmail" type="xsd:boolean"/>
def convertLead(self, leadConverts):
return ConvertLeadRequest(self.__serverUrl, self.sessionId, leadConverts).post(self.__conn)
# sObjectTypes can be 1 or a list, returns a single describe result or a list of them
def describeSObjects(self, sObjectTypes):
return DescribeSObjectsRequest(self.__serverUrl, self.sessionId, sObjectTypes).post(self.__conn)
def describeGlobal(self):
return AuthenticatedRequest(self.__serverUrl, self.sessionId, "describeGlobal").post(self.__conn)
def describeLayout(self, sObjectType):
return DescribeLayoutRequest(self.__serverUrl, self.sessionId, sObjectType).post(self.__conn)
def describeTabs(self):
return AuthenticatedRequest(self.__serverUrl, self.sessionId, "describeTabs").post(self.__conn, True)
def describeSearchScopeOrder(self):
return AuthenticatedRequest(self.__serverUrl, self.sessionId, "describeSearchScopeOrder").post(self.__conn, True)
def describeQuickActions(self, actions):
return DescribeQuickActionsRequest(self.__serverUrl, self.sessionId, actions).post(self.__conn, True)
def describeAvailableQuickActions(self, parentType = None):
return DescribeAvailableQuickActionsRequest(self.__serverUrl, self.sessionId, parentType).post(self.__conn, True)
def performQuickActions(self, actions):
return PerformQuickActionsRequest(self.__serverUrl, self.sessionId, actions).post(self.__conn, True)
def getServerTimestamp(self):
return str(AuthenticatedRequest(self.__serverUrl, self.sessionId, "getServerTimestamp").post(self.__conn)[_tPartnerNS.timestamp])
def resetPassword(self, userId):
return ResetPasswordRequest(self.__serverUrl, self.sessionId, userId).post(self.__conn)
def setPassword(self, userId, password):
SetPasswordRequest(self.__serverUrl, self.sessionId, userId, password).post(self.__conn)
def getUserInfo(self):
return AuthenticatedRequest(self.__serverUrl, self.sessionId, "getUserInfo").post(self.__conn)
#def convertLead(self, convertLeads):
# fixed version of XmlGenerator, handles unqualified attributes correctly
class BeatBoxXmlGenerator(XMLGenerator):
def __init__(self, destination, encoding):
XMLGenerator.__init__(self, destination, encoding)
def makeName(self, name):
if name[0] is None:
#if the name was not namespace-scoped, use the qualified part
return name[1]
# else try to restore the original prefix from the namespace
return self._current_context[name[0]] + ":" + name[1]
def startElementNS(self, name, qname, attrs):
self._write(unicode('<' + self.makeName(name)))
for pair in self._undeclared_ns_maps:
self._write(unicode(' xmlns:%s="%s"' % pair))
self._undeclared_ns_maps = []
for (name, value) in attrs.items():
self._write(unicode(' %s=%s' % (self.makeName(name), quoteattr(value))))
self._write(unicode('>'))
# general purpose xml writer, does a bunch of useful stuff above & beyond XmlGenerator
class XmlWriter:
def __init__(self, doGzip):
self.__buf = StringIO("")
if doGzip:
self.__gzip = gzip.GzipFile(mode='wb', fileobj=self.__buf)
stm = self.__gzip
else:
stm = self.__buf
self.__gzip = None
self.xg = BeatBoxXmlGenerator(stm, "utf-8")
self.xg.startDocument()
self.__elems = []
def startPrefixMapping(self, prefix, namespace):
self.xg.startPrefixMapping(prefix, namespace)
def endPrefixMapping(self, prefix):
self.xg.endPrefixMapping(prefix)
def startElement(self, namespace, name, attrs = _noAttrs):
self.xg.startElementNS((namespace, name), name, attrs)
self.__elems.append((namespace, name))
# if value is a list, then it writes out repeating elements, one for each value
def writeStringElement(self, namespace, name, value, attrs = _noAttrs):
if islst(value):
for v in value:
self.writeStringElement(namespace, name, v, attrs)
else:
self.startElement(namespace, name, attrs)
self.characters(value)
self.endElement()
def endElement(self):
e = self.__elems[-1];
self.xg.endElementNS(e, e[1])
del self.__elems[-1]
def characters(self, s):
# todo base64 ?
if isinstance(s, datetime.datetime):
# todo, timezones
s = s.isoformat()
elif isinstance(s, datetime.date):
# todo, try isoformat
s = "%04d-%02d-%02d" % (s.year, s.month, s.day)
elif isinstance(s, int):
s = str(s)
elif isinstance(s, float):
s = str(s)
self.xg.characters(s)
def endDocument(self):
self.xg.endDocument()
if (self.__gzip != None):
self.__gzip.close();
return self.__buf.getvalue()
# exception class for soap faults
class SoapFaultError(Exception):
def __init__(self, faultCode, faultString):
self.faultCode = faultCode
self.faultString = faultString
def __str__(self):
return repr(self.faultCode) + " " + repr(self.faultString)
# soap specific stuff ontop of XmlWriter
class SoapWriter(XmlWriter):
__xsiNs = "http://www.w3.org/2001/XMLSchema-instance"
def __init__(self):
XmlWriter.__init__(self, gzipRequest)
self.startPrefixMapping("s", _envNs)
self.startPrefixMapping("p", _partnerNs)
self.startPrefixMapping("o", _sobjectNs)
self.startPrefixMapping("x", SoapWriter.__xsiNs)
self.startElement(_envNs, "Envelope")
def writeStringElement(self, namespace, name, value, attrs = _noAttrs):
if value is None:
if attrs:
attrs[(SoapWriter.__xsiNs, "nil")] = 'true';
else:
attrs = { (SoapWriter.__xsiNs, "nil") : 'true' }
value = ""
XmlWriter.writeStringElement(self, namespace, name, value, attrs)
def endDocument(self):
self.endElement() # envelope
self.endPrefixMapping("o")
self.endPrefixMapping("p")
self.endPrefixMapping("s")
self.endPrefixMapping("x")
return XmlWriter.endDocument(self)
# processing for a single soap request / response
class SoapEnvelope:
def __init__(self, serverUrl, operationName, clientId="BeatBox/" + __version__):
self.serverUrl = serverUrl
self.operationName = operationName
self.clientId = clientId
def writeHeaders(self, writer):
pass
def writeBody(self, writer):
pass
def makeEnvelope(self):
s = SoapWriter()
s.startElement(_envNs, "Header")
s.characters("\n")
s.startElement(_partnerNs, "CallOptions")
s.writeStringElement(_partnerNs, "client", self.clientId)
s.endElement()
s.characters("\n")
self.writeHeaders(s)
s.endElement() # Header
s.startElement(_envNs, "Body")
s.characters("\n")
s.startElement(_partnerNs, self.operationName)
self.writeBody(s)
s.endElement() # operation
s.endElement() # body
return s.endDocument()
# does all the grunt work,
# serializes the request,
# makes a http request,
# passes the response to tramp
# checks for soap fault
# todo: check for mU='1' headers
# returns the relevant result from the body child
def post(self, conn=None, alwaysReturnList=False):
headers = { "User-Agent": "BeatBox/" + __version__,
"SOAPAction": "\"\"",
"Content-Type": "text/xml; charset=utf-8" }
if gzipResponse:
headers['accept-encoding'] = 'gzip'
if gzipRequest:
headers['content-encoding'] = 'gzip'
close = False
(scheme, host, path, params, query, frag) = urlparse(self.serverUrl)
if conn == None:
conn = makeConnection(scheme, host)
close = True
rawRequest = self.makeEnvelope();
# print rawRequest
conn.request("POST", path, rawRequest, headers)
response = conn.getresponse()
rawResponse = response.read()
if response.getheader('content-encoding','') == 'gzip':
rawResponse = gzip.GzipFile(fileobj=StringIO(rawResponse)).read()
if close:
conn.close()
tramp = xmltramp.parse(rawResponse)
try:
faultString = str(tramp[_tSoapNS.Body][_tSoapNS.Fault].faultstring)
faultCode = str(tramp[_tSoapNS.Body][_tSoapNS.Fault].faultcode).split(':')[-1]
raise SoapFaultError(faultCode, faultString)
except KeyError:
pass
# first child of body is XXXXResponse
result = tramp[_tSoapNS.Body][0]
# it contains either a single child, or for a batch call multiple children
if alwaysReturnList or len(result) > 1:
return result[:]
else:
return result[0]
class LoginRequest(SoapEnvelope):
def __init__(self, serverUrl, username, password):
SoapEnvelope.__init__(self, serverUrl, "login")
self.__username = username
self.__password = password
def writeBody(self, s):
s.writeStringElement(_partnerNs, "username", self.__username)
s.writeStringElement(_partnerNs, "password", self.__password)
class PortalLoginRequest(LoginRequest):
def __init__(self, serverUrl, username, password, orgId, portalId):
LoginRequest.__init__(self, serverUrl, username, password)
self.__orgId = orgId
self.__portalId = portalId
def writeHeaders(self, s):
s.startElement(_partnerNs, "LoginScopeHeader")
s.writeStringElement(_partnerNs, "organizationId", self.__orgId)
if (not (self.__portalId is None or self.__portalId == "")):
s.writeStringElement(_partnerNs, "portalId", self.__portalId)
s.endElement()
# base class for all methods that require a sessionId
class AuthenticatedRequest(SoapEnvelope):
def __init__(self, serverUrl, sessionId, operationName):
SoapEnvelope.__init__(self, serverUrl, operationName)
self.sessionId = sessionId
def writeHeaders(self, s):
s.startElement(_partnerNs, "SessionHeader")
s.writeStringElement(_partnerNs, "sessionId", self.sessionId)
s.endElement()
def writeDict(self, s, elemName, d):
if islst(d):
for o in d:
self.writeDict(s, elemName, o)
else:
s.startElement(_partnerNs, elemName)
for fn in d.keys():
if (isinstance(d[fn], dict)):
self.writeDict(s, d[fn], fn)
else:
s.writeStringElement(_sobjectNs, fn, d[fn])
s.endElement()
def writeSObjects(self, s, sObjects, elemName="sObjects"):
if islst(sObjects):
for o in sObjects:
self.writeSObjects(s, o, elemName)
else:
s.startElement(_partnerNs, elemName)
# type has to go first
s.writeStringElement(_sobjectNs, "type", sObjects['type'])
for fn in sObjects.keys():
if (fn != 'type'):
if (isinstance(sObjects[fn],dict)):
self.writeSObjects(s, sObjects[fn], fn)
else:
s.writeStringElement(_sobjectNs, fn, sObjects[fn])
s.endElement()
class LogoutRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "logout")
class QueryOptionsRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, batchSize, operationName):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, operationName)
self.batchSize = batchSize
def writeHeaders(self, s):
AuthenticatedRequest.writeHeaders(self, s)
s.startElement(_partnerNs, "QueryOptions")
s.writeStringElement(_partnerNs, "batchSize", self.batchSize)
s.endElement()
class QueryRequest(QueryOptionsRequest):
def __init__(self, serverUrl, sessionId, batchSize, soql, operationName="query"):
QueryOptionsRequest.__init__(self, serverUrl, sessionId, batchSize, operationName)
self.__query = soql
def writeBody(self, s):
s.writeStringElement(_partnerNs, "queryString", self.__query)
class QueryMoreRequest(QueryOptionsRequest):
def __init__(self, serverUrl, sessionId, batchSize, queryLocator):
QueryOptionsRequest.__init__(self, serverUrl, sessionId, batchSize, "queryMore")
self.__queryLocator = queryLocator
def writeBody(self, s):
s.writeStringElement(_partnerNs, "queryLocator", self.__queryLocator)
class SearchRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, sosl):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "search")
self.__query = sosl
def writeBody(self, s):
s.writeStringElement(_partnerNs, "searchString", self.__query)
class GetUpdatedRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, sObjectType, start, end, operationName="getUpdated"):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, operationName)
self.__sObjectType = sObjectType
self.__start = start;
self.__end = end;
def writeBody(self, s):
s.writeStringElement(_partnerNs, "sObjectType", self.__sObjectType)
s.writeStringElement(_partnerNs, "startDate", self.__start)
s.writeStringElement(_partnerNs, "endDate", self.__end)
class GetDeletedRequest(GetUpdatedRequest):
def __init__(self, serverUrl, sessionId, sObjectType, start, end):
GetUpdatedRequest.__init__(self, serverUrl, sessionId, sObjectType, start, end, "getDeleted")
class UpsertRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, externalIdName, sObjects):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "upsert")
self.__externalIdName = externalIdName
self.__sObjects = sObjects
def writeBody(self, s):
s.writeStringElement(_partnerNs, "externalIDFieldName", self.__externalIdName)
self.writeSObjects(s, self.__sObjects)
class UpdateRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, sObjects, operationName="update"):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, operationName)
self.__sObjects = sObjects
def writeBody(self, s):
self.writeSObjects(s, self.__sObjects)
class CreateRequest(UpdateRequest):
def __init__(self, serverUrl, sessionId, sObjects):
UpdateRequest.__init__(self, serverUrl, sessionId, sObjects, "create")
class DeleteRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, ids, operationName="delete"):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, operationName)
self.__ids = ids;
def writeBody(self, s):
s.writeStringElement(_partnerNs, "id", self.__ids)
class UndeleteRequest(DeleteRequest):
def __init__(self, serverUrl, sessionId, ids):
DeleteRequest.__init__(self, serverUrl, sessionId, ids, "undelete")
class RetrieveRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, fields, sObjectType, ids):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "retrieve")
self.__fields = fields
self.__sObjectType = sObjectType
self.__ids = ids
def writeBody(self, s):
s.writeStringElement(_partnerNs, "fieldList", self.__fields)
s.writeStringElement(_partnerNs, "sObjectType", self.__sObjectType);
s.writeStringElement(_partnerNs, "ids", self.__ids)
class ResetPasswordRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, userId):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "resetPassword")
self.__userId = userId
def writeBody(self, s):
s.writeStringElement(_partnerNs, "userId", self.__userId)
class SetPasswordRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, userId, password):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "setPassword")
self.__userId = userId
self.__password = password
def writeBody(self, s):
s.writeStringElement(_partnerNs, "userId", self.__userId)
s.writeStringElement(_partnerNs, "password", self.__password)
class ConvertLeadRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, leadConverts):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "convertLead")
self.__leads = leadConverts
def writeBody(self, s):
self.writeDict(s, "leadConverts", self.__leads)
class DescribeSObjectsRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, sObjectTypes):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "describeSObjects")
self.__sObjectTypes = sObjectTypes
def writeBody(self, s):
s.writeStringElement(_partnerNs, "sObjectType", self.__sObjectTypes)
class DescribeLayoutRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, sObjectType):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "describeLayout")
self.__sObjectType = sObjectType
def writeBody(self, s):
s.writeStringElement(_partnerNs, "sObjectType", self.__sObjectType)
class DescribeQuickActionsRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, actions):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "describeQuickActions")
self.__actions = actions
def writeBody(self, s):
s.writeStringElement(_partnerNs, "action", self.__actions)
class DescribeAvailableQuickActionsRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, parentType):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "describeAvailableQuickActions")
self.__parentType = parentType
def writeBody(self, s):
s.writeStringElement(_partnerNs, "parentType", self.__parentType)
class PerformQuickActionsRequest(AuthenticatedRequest):
def __init__(self, serverUrl, sessionId, actions):
AuthenticatedRequest.__init__(self, serverUrl, sessionId, "performQuickActions")
self.__actions = actions
def writeBody(self, s):
if (islst(self.__actions)):
for action in self.__actions:
self.writeQuckAction(s, action)
else:
self.writeQuickAction(s, self.__actions)
def writeQuickAction(self, s, action):
s.startElement(_partnerNs, "quickActions")
s.writeStringElement(_partnerNs, "parentId", action.get("parentId"))
s.writeStringElement(_partnerNs, "quickActionName", action["quickActionName"])
self.writeSObjects(s, action["records"], "records")
s.endElement()