Skip to content

Commit 6134d11

Browse files
committed
updated version to 0.3.7; added support for "push_lead"; added retry for timeout (604) and hitting the concurrent connection limit (615); fixed error handling message
1 parent 577d316 commit 6134d11

File tree

4 files changed

+264
-18
lines changed

4 files changed

+264
-18
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ API Ref: http://developers.marketo.com/documentation/rest/associate-lead/
139139
lead = mc.execute(method='associate_lead', id=2234, cookie='id:287-GTJ-838%26token:_mch-marketo.com-1396310362214-46169')
140140
```
141141

142+
Push Lead
143+
---------
144+
API Ref: http://developers.marketo.com/rest-api/endpoint-reference/lead-database-endpoint-reference/#!/Leads/pushToMarketoUsingPOST
145+
```python
146+
leads = [
147+
{"email":"lead1@example.com","firstName":"Joe", "cookies":"id:662-XAB-092&token:_mch-castelein.net-1487035251303-23757"},
148+
{"email":"lead2@example.com","firstName":"Jillian"}
149+
]
150+
lead = mc.execute(method='push_lead', leads=leads, lookupField='email', programName='Big Launch Webinar',
151+
programStatus='Registered', source='example source', reason='example reason')
152+
153+
# leads, lookupField and programName are required
154+
# all others are optional
155+
# to associate Cookie ID, put it in a field called 'cookies' (see example above)
156+
# to associate mkt_tok, put it in a field called 'mktToken' (see http://developers.marketo.com/rest-api/lead-database/leads/#push_lead_to_marketo)
157+
```
158+
142159
Merge Lead
143160
----------
144161
API Ref: http://developers.marketo.com/documentation/rest/merge-lead/

marketorestpython/client.py

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def execute(self, method, *args, **kargs):
5858
'change_lead_program_status': self.change_lead_program_status,
5959
'create_update_leads': self.create_update_leads,
6060
'associate_lead': self.associate_lead,
61+
'push_lead': self.push_lead,
6162
'merge_lead': self.merge_lead,
6263
'get_lead_partitions': self.get_lead_partitions,
6364
'get_list_by_id': self.get_list_by_id,
@@ -222,7 +223,17 @@ def execute(self, method, *args, **kargs):
222223
'describe_sales_person': self.describe_sales_person,
223224
'create_update_sales_persons': self.create_update_sales_persons,
224225
'delete_sales_persons': self.delete_sales_persons,
225-
'get_sales_persons': self.get_sales_persons
226+
'get_sales_persons': self.get_sales_persons,
227+
'get_custom_activity_types': self.get_custom_activity_types,
228+
'describe_custom_activity_type': self.describe_custom_activity_type,
229+
'create_custom_activity_type': self.create_custom_activity_type,
230+
'update_custom_activity_type': self.update_custom_activity_type,
231+
'approve_custom_activity_type': self.approve_custom_activity_type,
232+
'create_custom_activity_type_attribute': self.create_custom_activity_type_attribute,
233+
'discard_custom_activity_type_draft': self.discard_custom_activity_type_draft,
234+
'delete_custom_activity_type': self.delete_custom_activity_type,
235+
'update_custom_activity_type_attribute': self.update_custom_activity_type_attribute,
236+
'delete_custom_activity_type_attribute': self.delete_custom_activity_type_attribute
226237
}
227238
result = method_map[method](*args,**kargs)
228239
except MarketoException as e:
@@ -453,8 +464,35 @@ def associate_lead(self, id, cookie):
453464
}
454465
result = self._api_call('post', self.host + "/rest/v1/leads/" + str(id) + "/associate.json", args)
455466
if result is None: raise Exception("Empty Response")
467+
if not result['success']: raise MarketoException(result['errors'][0])
468+
return result['success'] # there is no 'result' node returned in this call
469+
470+
def push_lead(self, leads, lookupField, programName, programStatus=None, partitionName=None, source=None,
471+
reason=None):
472+
self.authenticate()
473+
if leads is None: raise ValueError("Invalid argument: required argument 'leads' is None.")
474+
if lookupField is None: raise ValueError("Invalid argument: required argument 'lookupField' is None.")
475+
if programName is None: raise ValueError("Invalid argument: required argument 'programName' is None.")
476+
args = {
477+
'access_token': self.token
478+
}
479+
data = {
480+
'input': leads,
481+
'lookupField': lookupField,
482+
'programName': programName
483+
}
484+
if programStatus is not None:
485+
data['programStatus'] = programStatus
486+
if partitionName is not None:
487+
data['partitionName'] = partitionName
488+
if source is not None:
489+
data['source'] = source
490+
if reason is not None:
491+
data['reason'] = reason
492+
result = self._api_call('post', self.host + "/rest/v1/leads/push.json", args, data)
493+
if result is None: raise Exception("Empty Response")
456494
if not result['success'] : raise MarketoException(result['errors'][0])
457-
return result['success'] # there is no 'result' node returned in this call
495+
return result['result']
458496

459497
def merge_lead(self, id, leadIds, mergeInCRM=False):
460498
self.authenticate()
@@ -3561,3 +3599,176 @@ def get_sales_persons(self, filterType, filterValues, fields=None, batchSize=Non
35613599
break
35623600
args['nextPageToken'] = result['nextPageToken']
35633601
return result_list
3602+
3603+
def get_custom_activity_types(self):
3604+
self.authenticate()
3605+
args = {
3606+
'access_token' : self.token
3607+
}
3608+
result = self._api_call('get', self.host + "/rest/v1/activities/external/types.json", args)
3609+
if result is None: raise Exception("Empty Response")
3610+
if not result['success'] : raise MarketoException(result['errors'][0])
3611+
return result['result']
3612+
3613+
def describe_custom_activity_type(self, apiName, draft=None):
3614+
if apiName is None: raise ValueError("Required argument apiName is none.")
3615+
self.authenticate()
3616+
args = {
3617+
'access_token' : self.token
3618+
}
3619+
if draft:
3620+
args['draft'] = draft
3621+
result = self._api_call('get', self.host + "/rest/v1/activities/external/type/" + apiName + "/describe.json",
3622+
args)
3623+
if result is None: raise Exception("Empty Response")
3624+
if not result['success'] : raise MarketoException(result['errors'][0])
3625+
return result['result']
3626+
3627+
def create_custom_activity_type(self, apiName, name, triggerName, filterName, primaryAttributeApiName,
3628+
primaryAttributeName, primaryAttributeDescription=None, description=None):
3629+
self.authenticate()
3630+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3631+
if name is None: raise ValueError("Required argument 'name' is none.")
3632+
if triggerName is None: raise ValueError("Required argument 'triggerName' is none")
3633+
if filterName is None: raise ValueError("Required argument 'filterName' is none.")
3634+
if primaryAttributeApiName is None: raise ValueError("Required argument 'primaryAttributeApiName' is none.")
3635+
if primaryAttributeName is None: raise ValueError("Required argument 'primaryAttributeName' is none.")
3636+
#if primaryAttributeDescription is None: raise ValueError("Required argument 'primaryAttributeDescription' is none.")
3637+
args = {
3638+
'access_token': self.token
3639+
}
3640+
data = {
3641+
'apiName': apiName,
3642+
'name': name,
3643+
'triggerName': triggerName,
3644+
'filterName': filterName,
3645+
'primaryAttribute': {
3646+
'apiName': primaryAttributeApiName,
3647+
'name': primaryAttributeName
3648+
}
3649+
}
3650+
if description is not None:
3651+
data['description'] = description
3652+
if primaryAttributeDescription is not None:
3653+
data['primaryAttribute']['description'] = primaryAttributeDescription
3654+
result = self._api_call('post', self.host + "/rest/v1/activities/external/type.json", args, data)
3655+
if result is None: raise Exception("Empty Response")
3656+
if not result['success'] : raise MarketoException(result['errors'][0])
3657+
return result['result']
3658+
3659+
def update_custom_activity_type(self, apiName, name=None, triggerName=None, filterName=None,
3660+
primaryAttributeApiName=None, primaryAttributeName=None,
3661+
primaryAttributeDescription=None, description=None):
3662+
self.authenticate()
3663+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3664+
args = {
3665+
'access_token': self.token
3666+
}
3667+
data = {}
3668+
if name is not None:
3669+
data['name'] = name
3670+
if triggerName is not None:
3671+
data['triggerName'] = triggerName
3672+
if filterName is not None:
3673+
data['filterName'] = filterName
3674+
if description is not None:
3675+
data['description'] = description
3676+
if primaryAttributeApiName or primaryAttributeName or primaryAttributeDescription:
3677+
data['primaryAttribute'] = {}
3678+
if primaryAttributeApiName:
3679+
data['primaryAttribute']['apiName'] = primaryAttributeApiName
3680+
if primaryAttributeName:
3681+
data['primaryAttribute']['name'] = primaryAttributeName
3682+
if primaryAttributeDescription:
3683+
data['primaryAttribute']['description'] = primaryAttributeDescription
3684+
result = self._api_call('post', self.host + "/rest/v1/activities/external/type/" + apiName + ".json", args, data)
3685+
if result is None: raise Exception("Empty Response")
3686+
if not result['success'] : raise MarketoException(result['errors'][0])
3687+
return result['result']
3688+
3689+
def approve_custom_activity_type(self, apiName):
3690+
self.authenticate()
3691+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3692+
args = {
3693+
'access_token': self.token
3694+
}
3695+
result = self._api_call('post',
3696+
self.host + "/rest/v1/activities/external/type/" + apiName + "/approve.json", args)
3697+
if result is None: raise Exception("Empty Response")
3698+
if not result['success'] : raise MarketoException(result['errors'][0])
3699+
return result['result']
3700+
3701+
def discard_custom_activity_type_draft(self, apiName):
3702+
self.authenticate()
3703+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3704+
args = {
3705+
'access_token': self.token
3706+
}
3707+
result = self._api_call('post',
3708+
self.host + "/rest/v1/activities/external/type/" + apiName + "/discardDraft.json", args)
3709+
if result is None: raise Exception("Empty Response")
3710+
if not result['success'] : raise MarketoException(result['errors'][0])
3711+
return result['result']
3712+
3713+
def delete_custom_activity_type(self, apiName):
3714+
self.authenticate()
3715+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3716+
args = {
3717+
'access_token': self.token
3718+
}
3719+
result = self._api_call('post',
3720+
self.host + "/rest/v1/activities/external/type/" + apiName + "/delete.json", args)
3721+
if result is None: raise Exception("Empty Response")
3722+
if not result['success'] : raise MarketoException(result['errors'][0])
3723+
return result['result']
3724+
3725+
def create_custom_activity_type_attribute(self, apiName, attributes):
3726+
self.authenticate()
3727+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3728+
if attributes is None: raise ValueError("Required argument 'attributes' is none.")
3729+
args = {
3730+
'access_token': self.token
3731+
}
3732+
data = {
3733+
"attributes": attributes
3734+
}
3735+
result = self._api_call('post',
3736+
self.host + "/rest/v1/activities/external/type/" + apiName + "/attributes/create.json",
3737+
args, data)
3738+
if result is None: raise Exception("Empty Response")
3739+
if not result['success'] : raise MarketoException(result['errors'][0])
3740+
return result['result']
3741+
3742+
def update_custom_activity_type_attribute(self, apiName, attributes):
3743+
self.authenticate()
3744+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3745+
if attributes is None: raise ValueError("Required argument 'attributes' is none.")
3746+
args = {
3747+
'access_token': self.token
3748+
}
3749+
data = {
3750+
"attributes": attributes
3751+
}
3752+
result = self._api_call('post',
3753+
self.host + "/rest/v1/activities/external/type/" + apiName + "/attributes/update.json",
3754+
args, data)
3755+
if result is None: raise Exception("Empty Response")
3756+
if not result['success'] : raise MarketoException(result['errors'][0])
3757+
return result['result']
3758+
3759+
def delete_custom_activity_type_attribute(self, apiName, attributes):
3760+
self.authenticate()
3761+
if apiName is None: raise ValueError("Required argument 'apiName' is none.")
3762+
if attributes is None: raise ValueError("Required argument 'attributes' is none.")
3763+
args = {
3764+
'access_token': self.token
3765+
}
3766+
data = {
3767+
"attributes": attributes
3768+
}
3769+
result = self._api_call('post',
3770+
self.host + "/rest/v1/activities/external/type/" + apiName +
3771+
"/attributes/delete.json", args, data)
3772+
if result is None: raise Exception("Empty Response")
3773+
if not result['success'] : raise MarketoException(result['errors'][0])
3774+
return result['result']

marketorestpython/helper/http_lib.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
class HttpLib:
66
max_retries = 3
77
sleep_duration = 3
8-
num_calls_per_second = 5 # can run five times per second at most (at 100/20 rate limit)
8+
num_calls_per_second = 5 # can run five times per second at most (at 100/20 rate limit)
99

1010
def _rate_limited(maxPerSecond):
1111
minInterval = 1.0 / float(maxPerSecond)
@@ -24,7 +24,7 @@ def rateLimitedFunction(*args,**kargs):
2424

2525
@_rate_limited(num_calls_per_second)
2626
def get(self, endpoint, args=None, mode=None):
27-
retries = 0
27+
retries = 1
2828
while True:
2929
if retries > self.max_retries:
3030
return None
@@ -39,13 +39,21 @@ def get(self, endpoint, args=None, mode=None):
3939
if 'success' in r_json: # this is for all normal API calls (but not the access token call)
4040
if r_json['success'] == False:
4141
print('error from http_lib.py: ' + str(r_json['errors'][0]))
42-
if r_json['errors'][0]['code'] == '606':
43-
print('error 606, rate limiter. Pausing, then trying again')
44-
time.sleep(5)
45-
elif r_json['errors'][0]['code'] == '615':
46-
print('error 615, concurrent call limit. Pausing, then trying again')
47-
time.sleep(2)
42+
if r_json['errors'][0]['code'] in ('606', '615', '604'):
43+
# this handles Marketo exceptions; HTTP response is still 200, but error is in the JSON
44+
error_code = r_json['errors'][0]['code']
45+
error_description = {
46+
'606': 'rate limiter',
47+
'615': 'concurrent call limit',
48+
'604': 'timeout'}
49+
if retries < self.max_retries:
50+
print('Attempt %s. Error %s, %s. Pausing, then trying again.' % (retries, error_code, error_description[error_code]))
51+
else:
52+
print('Attempt %s. Error %s, %s. This was the final attempt.' % (retries, error_code, error_description[error_code]))
53+
time.sleep(self.sleep_duration)
54+
retries += 1
4855
else:
56+
# fatal exceptions will still error out; exceptions caught above may be recoverable
4957
return r_json
5058
else:
5159
return r_json
@@ -58,7 +66,7 @@ def get(self, endpoint, args=None, mode=None):
5866

5967
@_rate_limited(num_calls_per_second)
6068
def post(self, endpoint, args, data=None, files=None, filename=None, mode=None):
61-
retries = 0
69+
retries = 1
6270
while True:
6371
if retries > self.max_retries:
6472
return None
@@ -77,13 +85,23 @@ def post(self, endpoint, args, data=None, files=None, filename=None, mode=None):
7785
if 'success' in r_json: # this is for all normal API calls (but not the access token call)
7886
if r_json['success'] == False:
7987
print('error from http_lib.py: ' + str(r_json['errors'][0]))
80-
if r_json['errors'][0]['code'] == '606':
81-
print('error 606, rate limiter. Pausing, then trying again')
82-
time.sleep(5)
83-
elif r_json['errors'][0]['code'] == '615':
84-
print('error 615, concurrent call limit. Pausing, then trying again')
85-
time.sleep(2)
88+
if r_json['errors'][0]['code'] in ('606', '615', '604'):
89+
# this handles Marketo exceptions; HTTP response is still 200, but error is in the JSON
90+
error_code = r_json['errors'][0]['code']
91+
error_description = {
92+
'606': 'rate limiter',
93+
'615': 'concurrent call limit',
94+
'604': 'timeout'}
95+
if retries < self.max_retries:
96+
print('Attempt %s. Error %s, %s. Pausing, then trying again.' % (
97+
retries, error_code, error_description[error_code]))
98+
else:
99+
print('Attempt %s. Error %s, %s. This was the final attempt.' % (
100+
retries, error_code, error_description[error_code]))
101+
time.sleep(self.sleep_duration)
102+
retries += 1
86103
else:
104+
# fatal exceptions will still error out; exceptions caught above may be recoverable
87105
return r_json
88106
else:
89107
return r_json

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
setup(
2121
name='marketorestpython',
22-
version= '0.3.6',
22+
version= '0.3.7',
2323
url='https://github.com/jepcastelein/marketo-rest-python',
2424
author='Jep Castelein',
2525
author_email='jep@castelein.net',

0 commit comments

Comments
 (0)