├── .gitignore ├── README.md ├── marketo ├── __init__.py ├── auth.py ├── rfc3339.py ├── version.py └── wrapper │ ├── __init__.py │ ├── get_lead.py │ ├── get_lead_activity.py │ ├── lead_activity.py │ ├── lead_record.py │ ├── request_campaign.py │ └── sync_lead.py ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | **sublime** 2 | *.pyc 3 | dist 4 | *.egg-info 5 | dist 6 | MANIFEST 7 | build 8 | 9 | hidden-test.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | marketo-python 2 | ============== 3 | 4 | marketo-python is a python query client that wraps the Marketo SOAP API. For sending data to Marketo, check out [Segment.io](https://segment.io). 5 | 6 | ## Get Started 7 | 8 | ``` 9 | pip install marketo 10 | ``` 11 | 12 | ```python 13 | import marketo 14 | client = marketo.Client(soap_endpoint='https://na-q.marketo.com/soap/mktows/2_0', 15 | user_id='bigcorp1_461839624B16E06BA2D663', 16 | encryption_key='899756834129871744AAEE88DDCC77CDEEDEC1AAAD66') 17 | ``` 18 | 19 | ## Get Lead 20 | 21 | This function retrieves a single lead record from Marketo. 22 | 23 | ```python 24 | > lead = client.get_lead(email='ilya@segment.io') 25 | 26 | > lead.id 27 | '384563' 28 | > lead.email 29 | 'ilya@segment.io' 30 | > lead.attributes 31 | {'Website': 'segment.io', 'Lead_Round_Robin_ID__c': 1, 'LeadStatus': 'Open', 'InferredCountry': 'United States', 'LeadScore': 20, 'FirstName': 'Ilya', 'AnonymousIP': '64.181.3.19', 'LastName': 'Volodarsky', 'Company': 'mkto', 'Created_Time__c': '2012-10-15 19:01:26Z', 'InferredCompany': 'Comcast Cable', 'Phone': '222-222-2222', 'Created_Timestamp__c': '2012-10-15T14:01:26-05:00', 'Lead_Assignment_Number__c': '3114', 'Referrer__c': 'drc'} 32 | ``` 33 | 34 | XML unwrapped [here](https://github.com/segmentio/marketo-python/blob/master/marketo/wrapper/lead_record.py). 35 | 36 | ## Get Lead Activity 37 | 38 | ```python 39 | > activities = client.get_lead_activity(email='user@gmail.com') 40 | [Activity (16095520 - Visit Webpage), Activity (16095507 - Click Link), Activity (16095506 - Click Link)] 41 | > activity = activities[0] 42 | > activity.id 43 | '16095520' 44 | > activity.type 45 | 'Visit Webpage' 46 | > activity.attributes 47 | {'Webpage ID': '20499', 'Message Id': '19122416', 'Webpage URL': '/pricing', 'Lead ID': '1474562', 'Query Parameters': None, 'Referrer URL': 'https://company.com/appointments/', 'Client IP Address': '61.183.85.141', 'User Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17', 'Created At': '2013-02-11 16:19:48'} 48 | ``` 49 | 50 | XML unwrapped [here](https://github.com/segmentio/marketo-python/blob/master/marketo/wrapper/get_lead_activity.py). 51 | 52 | ### Error 53 | 54 | An Exception is raised if the lead is not found, or if a Marketo error occurs. 55 | 56 | ```python 57 | try: 58 | lead = client.get_lead(email='ilyaaaaaaa@segment.io') 59 | except Exception as error: 60 | print error 61 | 62 | ''' 63 | SOAP-ENV:Client20103 - Lead not foundmktServiceExceptionNo lead found with EMAIL = ilyaaaaaaa@segment.io (20103)20103 64 | ''' 65 | ``` 66 | 67 | ## Sync Lead 68 | 69 | This function updates a single lead record from Marketo. If a lead without a matching email isn't found in the database, a new one is created. If the request is successful, the lead record is returned. 70 | 71 | ```python 72 | lead = client.sync_lead( 73 | email='user@gmail.com', 74 | attributes=( 75 | ('City', 'string', 'Toronto'), 76 | ('Country', 'string', 'Canada'), 77 | ('Title', 'string', 'Web Developer'), 78 | ) 79 | ) 80 | ``` 81 | 82 | ## Request Campaign 83 | 84 | This function triggers a Marketo campaign request (typically used to activate a campaign after a user has filled out a form). This requires the numeric ID of both a campaign and the lead that is to be associated with the campaign. Returns True on success. 85 | 86 | ```python 87 | > campaign = client.request_campaign('1190', '384563') 88 | True 89 | ``` 90 | 91 | ## License 92 | 93 | ``` 94 | WWWWWW||WWWWWW 95 | W W W||W W W 96 | || 97 | ( OO )__________ 98 | / | \ 99 | /o o| MIT \ 100 | \___/||_||__||_|| * 101 | || || || || 102 | _||_|| _||_|| 103 | (__|__|(__|__| 104 | ``` 105 | 106 | (The MIT License) 107 | 108 | Copyright (c) 2012 Segment.io Inc. 109 | 110 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 111 | 112 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /marketo/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import version 3 | 4 | VERSION = version.VERSION 5 | __version__ = VERSION 6 | 7 | import requests 8 | import auth 9 | 10 | from marketo.wrapper import get_lead, get_lead_activity, request_campaign, sync_lead 11 | 12 | 13 | class Client: 14 | 15 | def __init__(self, soap_endpoint=None, user_id=None, encryption_key=None): 16 | 17 | if not soap_endpoint or not isinstance(soap_endpoint, str): 18 | raise ValueError('Must supply a soap_endpoint as a non empty string.') 19 | 20 | if not user_id or not isinstance(user_id, (str, unicode)): 21 | raise ValueError('Must supply a user_id as a non empty string.') 22 | 23 | if not encryption_key or not isinstance(encryption_key, str): 24 | raise ValueError('Must supply a encryption_key as a non empty string.') 25 | 26 | self.soap_endpoint = soap_endpoint 27 | self.user_id = user_id 28 | self.encryption_key = encryption_key 29 | 30 | def wrap(self, body): 31 | return ( 32 | '' + 33 | '' + 40 | auth.header(self.user_id, self.encryption_key) + 41 | '' + 42 | body + 43 | '' + 44 | '') 45 | 46 | def request(self, body): 47 | envelope = self.wrap(body) 48 | response = requests.post(self.soap_endpoint, data=envelope, 49 | headers={ 50 | 'Connection': 'Keep-Alive', 51 | 'Soapaction': '', 52 | 'Content-Type': 'text/xml;charset=UTF-8', 53 | 'Accept': '*/*'}) 54 | return response 55 | 56 | def get_lead(self, email=None): 57 | 58 | if not email or not isinstance(email, (str, unicode)): 59 | raise ValueError('Must supply an email as a non empty string.') 60 | 61 | body = get_lead.wrap(email) 62 | response = self.request(body) 63 | if response.status_code == 200: 64 | return get_lead.unwrap(response) 65 | else: 66 | raise Exception(response.text) 67 | 68 | def get_lead_activity(self, email=None): 69 | 70 | if not email or not isinstance(email, (str, unicode)): 71 | raise ValueError('Must supply an email as a non empty string.') 72 | 73 | body = get_lead_activity.wrap(email) 74 | response = self.request(body) 75 | if response.status_code == 200: 76 | return get_lead_activity.unwrap(response) 77 | else: 78 | raise Exception(response.text) 79 | 80 | def request_campaign(self, campaign=None, lead=None): 81 | 82 | if not campaign or not isinstance(campaign, (str, unicode)): 83 | raise ValueError('Must supply campaign id as a non empty string.') 84 | 85 | if not lead or not isinstance(lead, (str, unicode)): 86 | raise ValueError('Must supply lead id as a non empty string.') 87 | 88 | body = request_campaign.wrap(campaign, lead) 89 | 90 | response = self.request(body) 91 | if response.status_code == 200: 92 | return True 93 | else: 94 | raise Exception(response.text) 95 | 96 | def sync_lead(self, email=None, attributes=None): 97 | 98 | if not email or not isinstance(email, (str, unicode)): 99 | raise ValueError('Must supply lead id as a non empty string.') 100 | 101 | if not attributes or not isinstance(attributes, tuple): 102 | raise ValueError('Must supply attributes as a non empty tuple.') 103 | 104 | body = sync_lead.wrap(email, attributes) 105 | 106 | response = self.request(body) 107 | if response.status_code == 200: 108 | return sync_lead.unwrap(response) 109 | else: 110 | raise Exception(response.text) 111 | -------------------------------------------------------------------------------- /marketo/auth.py: -------------------------------------------------------------------------------- 1 | from rfc3339 import rfc3339 2 | import hmac 3 | import hashlib 4 | import datetime 5 | 6 | 7 | def sign(message, encryption_key): 8 | digest = hmac.new(encryption_key, message, hashlib.sha1) 9 | return digest.hexdigest().lower() 10 | 11 | 12 | def header(user_id, encryption_key): 13 | timestamp = rfc3339(datetime.datetime.now()) 14 | signature = sign(timestamp + user_id, encryption_key) 15 | return ( 16 | '' + 17 | '' + user_id + '' + 18 | '' + signature + '' + 19 | '' + timestamp + '' + 20 | '') 21 | -------------------------------------------------------------------------------- /marketo/rfc3339.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2009, 2010, Henry Precheur 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | # PERFORMANCE OF THIS SOFTWARE. 16 | # 17 | '''Formats dates according to the :RFC:`3339`. 18 | 19 | Report bugs & problems on BitBucket_ 20 | 21 | .. _BitBucket: https://bitbucket.org/henry/clan.cx/issues 22 | ''' 23 | 24 | __author__ = 'Henry Precheur ' 25 | __license__ = 'ISCL' 26 | __version__ = '5.1' 27 | __all__ = ('rfc3339', ) 28 | 29 | import datetime 30 | import time 31 | import unittest 32 | 33 | def _timezone(utc_offset): 34 | ''' 35 | Return a string representing the timezone offset. 36 | 37 | >>> _timezone(0) 38 | '+00:00' 39 | >>> _timezone(3600) 40 | '+01:00' 41 | >>> _timezone(-28800) 42 | '-08:00' 43 | >>> _timezone(-1800) 44 | '-00:30' 45 | ''' 46 | # Python's division uses floor(), not round() like in other languages: 47 | # -1 / 2 == -1 and not -1 / 2 == 0 48 | # That's why we use abs(utc_offset). 49 | hours = abs(utc_offset) // 3600 50 | minutes = abs(utc_offset) % 3600 // 60 51 | return '%c%02d:%02d' % ('-' if utc_offset < 0 else '+', hours, minutes) 52 | 53 | def _timedelta_to_seconds(timedelta): 54 | ''' 55 | >>> _timedelta_to_seconds(datetime.timedelta(hours=3)) 56 | 10800 57 | >>> _timedelta_to_seconds(datetime.timedelta(hours=3, minutes=15)) 58 | 11700 59 | ''' 60 | return (timedelta.days * 86400 + timedelta.seconds + 61 | timedelta.microseconds // 1000) 62 | 63 | def _utc_offset(date, use_system_timezone): 64 | ''' 65 | Return the UTC offset of `date`. If `date` does not have any `tzinfo`, use 66 | the timezone informations stored locally on the system. 67 | 68 | >>> if time.localtime().tm_isdst: 69 | ... system_timezone = -time.altzone 70 | ... else: 71 | ... system_timezone = -time.timezone 72 | >>> _utc_offset(datetime.datetime.now(), True) == system_timezone 73 | True 74 | >>> _utc_offset(datetime.datetime.now(), False) 75 | 0 76 | ''' 77 | if isinstance(date, datetime.datetime) and date.tzinfo is not None: 78 | return _timedelta_to_seconds(date.dst() or date.utcoffset()) 79 | elif use_system_timezone: 80 | if date.year < 1970: 81 | # We use 1972 because 1970 doesn't have a leap day (feb 29) 82 | t = time.mktime(date.replace(year=1972).timetuple()) 83 | else: 84 | t = time.mktime(date.timetuple()) 85 | if time.localtime(t).tm_isdst: # pragma: no cover 86 | return -time.altzone 87 | else: 88 | return -time.timezone 89 | else: 90 | return 0 91 | 92 | def _string(d, timezone): 93 | return ('%04d-%02d-%02dT%02d:%02d:%02d%s' % 94 | (d.year, d.month, d.day, d.hour, d.minute, d.second, timezone)) 95 | 96 | def rfc3339(date, utc=False, use_system_timezone=True): 97 | ''' 98 | Return a string formatted according to the :RFC:`3339`. If called with 99 | `utc=True`, it normalizes `date` to the UTC date. If `date` does not have 100 | any timezone information, uses the local timezone:: 101 | 102 | >>> d = datetime.datetime(2008, 4, 2, 20) 103 | >>> rfc3339(d, utc=True, use_system_timezone=False) 104 | '2008-04-02T20:00:00Z' 105 | >>> rfc3339(d) # doctest: +ELLIPSIS 106 | '2008-04-02T20:00:00...' 107 | 108 | If called with `user_system_timezone=False` don't use the local timezone if 109 | `date` does not have timezone informations and consider the offset to UTC 110 | to be zero:: 111 | 112 | >>> rfc3339(d, use_system_timezone=False) 113 | '2008-04-02T20:00:00+00:00' 114 | 115 | `date` must be a `datetime.datetime`, `datetime.date` or a timestamp as 116 | returned by `time.time()`:: 117 | 118 | >>> rfc3339(0, utc=True, use_system_timezone=False) 119 | '1970-01-01T00:00:00Z' 120 | >>> rfc3339(datetime.date(2008, 9, 6), utc=True, 121 | ... use_system_timezone=False) 122 | '2008-09-06T00:00:00Z' 123 | >>> rfc3339(datetime.date(2008, 9, 6), 124 | ... use_system_timezone=False) 125 | '2008-09-06T00:00:00+00:00' 126 | >>> rfc3339('foo bar') 127 | Traceback (most recent call last): 128 | ... 129 | TypeError: Expected timestamp or date object. Got . 130 | 131 | For dates before January 1st 1970, the timezones will be the ones used in 132 | 1970. It might not be accurate, but on most sytem there is no timezone 133 | information before 1970. 134 | ''' 135 | # Try to convert timestamp to datetime 136 | try: 137 | if use_system_timezone: 138 | date = datetime.datetime.fromtimestamp(date) 139 | else: 140 | date = datetime.datetime.utcfromtimestamp(date) 141 | except TypeError: 142 | pass 143 | 144 | if not isinstance(date, datetime.date): 145 | raise TypeError('Expected timestamp or date object. Got %r.' % 146 | type(date)) 147 | 148 | if not isinstance(date, datetime.datetime): 149 | date = datetime.datetime(*date.timetuple()[:3]) 150 | utc_offset = _utc_offset(date, use_system_timezone) 151 | if utc: 152 | return _string(date + datetime.timedelta(seconds=utc_offset), 'Z') 153 | else: 154 | return _string(date, _timezone(utc_offset)) 155 | 156 | 157 | class LocalTimeTestCase(unittest.TestCase): 158 | ''' 159 | Test the use of the timezone saved locally. Since it is hard to test using 160 | doctest. 161 | ''' 162 | 163 | def setUp(self): 164 | local_utcoffset = _utc_offset(datetime.datetime.now(), True) 165 | self.local_utcoffset = datetime.timedelta(seconds=local_utcoffset) 166 | self.local_timezone = _timezone(local_utcoffset) 167 | 168 | def test_datetime(self): 169 | d = datetime.datetime.now() 170 | self.assertEqual(rfc3339(d), 171 | d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) 172 | 173 | def test_datetime_timezone(self): 174 | 175 | class FixedNoDst(datetime.tzinfo): 176 | 'A timezone info with fixed offset, not DST' 177 | 178 | def utcoffset(self, dt): 179 | return datetime.timedelta(hours=2, minutes=30) 180 | 181 | def dst(self, dt): 182 | return None 183 | 184 | fixed_no_dst = FixedNoDst() 185 | 186 | class Fixed(FixedNoDst): 187 | 'A timezone info with DST' 188 | 189 | def dst(self, dt): 190 | return datetime.timedelta(hours=3, minutes=15) 191 | 192 | fixed = Fixed() 193 | 194 | d = datetime.datetime.now().replace(tzinfo=fixed_no_dst) 195 | timezone = _timezone(_timedelta_to_seconds(fixed_no_dst.\ 196 | utcoffset(None))) 197 | self.assertEqual(rfc3339(d), 198 | d.strftime('%Y-%m-%dT%H:%M:%S') + timezone) 199 | 200 | d = datetime.datetime.now().replace(tzinfo=fixed) 201 | timezone = _timezone(_timedelta_to_seconds(fixed.dst(None))) 202 | self.assertEqual(rfc3339(d), 203 | d.strftime('%Y-%m-%dT%H:%M:%S') + timezone) 204 | 205 | def test_datetime_utc(self): 206 | d = datetime.datetime.now() 207 | d_utc = d + self.local_utcoffset 208 | self.assertEqual(rfc3339(d, utc=True), 209 | d_utc.strftime('%Y-%m-%dT%H:%M:%SZ')) 210 | 211 | def test_date(self): 212 | d = datetime.date.today() 213 | self.assertEqual(rfc3339(d), 214 | d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) 215 | 216 | def test_date_utc(self): 217 | d = datetime.date.today() 218 | # Convert `date` to `datetime`, since `date` ignores seconds and hours 219 | # in timedeltas: 220 | # >>> datetime.date(2008, 9, 7) + datetime.timedelta(hours=23) 221 | # datetime.date(2008, 9, 7) 222 | d_utc = datetime.datetime(*d.timetuple()[:3]) + self.local_utcoffset 223 | self.assertEqual(rfc3339(d, utc=True), 224 | d_utc.strftime('%Y-%m-%dT%H:%M:%SZ')) 225 | 226 | def test_timestamp(self): 227 | d = time.time() 228 | self.assertEqual(rfc3339(d), 229 | datetime.datetime.fromtimestamp(d).\ 230 | strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) 231 | 232 | def test_timestamp_utc(self): 233 | d = time.time() 234 | d_utc = datetime.datetime.utcfromtimestamp(d) + self.local_utcoffset 235 | self.assertEqual(rfc3339(d), 236 | (d_utc.strftime('%Y-%m-%dT%H:%M:%S') + 237 | self.local_timezone)) 238 | 239 | def test_before_1970(self): 240 | d = datetime.date(1885, 01, 04) 241 | self.assertEqual(rfc3339(d), 242 | '1885-01-04T00:00:00' + self.local_timezone) 243 | self.assertEqual(rfc3339(d, utc=True, use_system_timezone=False), 244 | '1885-01-04T00:00:00Z') 245 | 246 | def test_1920(self): 247 | d = datetime.date(1920, 02, 29) 248 | self.assertEqual(rfc3339(d, utc=False, use_system_timezone=True), 249 | '1920-02-29T00:00:00' + self.local_timezone) 250 | 251 | # If these tests start failing it probably means there was a policy change 252 | # for the Pacific time zone. 253 | # See http://en.wikipedia.org/wiki/Pacific_Time_Zone. 254 | if 'PST' in time.tzname: 255 | def testPDTChange(self): 256 | '''Test Daylight saving change''' 257 | # PDT switch happens at 2AM on March 14, 2010 258 | 259 | # 1:59AM PST 260 | self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 1, 59)), 261 | '2010-03-14T01:59:00-08:00') 262 | # 3AM PDT 263 | self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 3, 0)), 264 | '2010-03-14T03:00:00-07:00') 265 | 266 | def testPSTChange(self): 267 | '''Test Standard time change''' 268 | # PST switch happens at 2AM on November 6, 2010 269 | 270 | # 0:59AM PDT 271 | self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 0, 59)), 272 | '2010-11-07T00:59:00-07:00') 273 | 274 | # 1:00AM PST 275 | # There's no way to have 1:00AM PST without a proper tzinfo 276 | self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 1, 0)), 277 | '2010-11-07T01:00:00-07:00') 278 | 279 | 280 | if __name__ == '__main__': # pragma: no cover 281 | import doctest 282 | doctest.testmod() 283 | unittest.main() 284 | -------------------------------------------------------------------------------- /marketo/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.1.0' 2 | -------------------------------------------------------------------------------- /marketo/wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = '0.0.5' 3 | -------------------------------------------------------------------------------- /marketo/wrapper/get_lead.py: -------------------------------------------------------------------------------- 1 | 2 | import xml.etree.ElementTree as ET 3 | import lead_record 4 | 5 | 6 | def wrap(email=None): 7 | return ( 8 | '' + 9 | '' + 10 | 'EMAIL' + 11 | '' + email + '' + 12 | '' + 13 | '') 14 | 15 | 16 | def unwrap(response): 17 | root = ET.fromstring(response.text) 18 | lead_record_xml = root.find('.//leadRecord') 19 | return lead_record.unwrap(lead_record_xml) 20 | -------------------------------------------------------------------------------- /marketo/wrapper/get_lead_activity.py: -------------------------------------------------------------------------------- 1 | 2 | import xml.etree.ElementTree as ET 3 | import lead_activity 4 | 5 | 6 | def wrap(email=None): 7 | return ( 8 | '' + 9 | '' + 10 | 'EMAIL' + 11 | '' + email + '' + 12 | '' + 13 | '') 14 | 15 | 16 | def unwrap(response): 17 | root = ET.fromstring(response.text) 18 | activities = [] 19 | for activity_el in root.findall('.//activityRecord'): 20 | activity = lead_activity.unwrap(activity_el) 21 | activities.append(activity) 22 | return activities 23 | -------------------------------------------------------------------------------- /marketo/wrapper/lead_activity.py: -------------------------------------------------------------------------------- 1 | import iso8601 2 | 3 | 4 | class LeadActivity: 5 | 6 | def __init__(self): 7 | self.id = 'unknown' 8 | self.type = 'unknown' 9 | self.attributes = {} 10 | 11 | def __str__(self): 12 | return "Activity (%s - %s)" % (self.id, self.type) 13 | 14 | def __repr__(self): 15 | return self.__str__() 16 | 17 | 18 | def unwrap(xml): 19 | activity = LeadActivity() 20 | activity.id = xml.find('id').text 21 | activity.timestamp = iso8601.parse_date(xml.find('activityDateTime').text) 22 | activity.type = xml.find('activityType').text 23 | 24 | for attribute in xml.findall('.//attribute'): 25 | name = attribute.find('attrName').text 26 | attr_type = attribute.find('attrType').text 27 | val = attribute.find('attrValue').text 28 | 29 | if attr_type == 'integer': 30 | val = int(val) 31 | 32 | activity.attributes[name] = val 33 | 34 | return activity 35 | -------------------------------------------------------------------------------- /marketo/wrapper/lead_record.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class LeadRecord: 4 | 5 | def __init__(self): 6 | self.attributes = {} 7 | 8 | def __str__(self): 9 | return "Lead (%s - %s)" % (self.id, self.email) 10 | 11 | def __repr__(self): 12 | return self.__str__() 13 | 14 | 15 | def unwrap(xml): 16 | lead = LeadRecord() 17 | lead.id = xml.find('Id').text 18 | lead.email = xml.find('Email').text 19 | 20 | for attribute in xml.findall('.//attribute'): 21 | name = attribute.find('attrName').text 22 | attr_type = attribute.find('attrType').text 23 | val = attribute.find('attrValue').text 24 | 25 | if attr_type == 'integer': 26 | val = int(val) 27 | 28 | lead.attributes[name] = val 29 | 30 | return lead 31 | -------------------------------------------------------------------------------- /marketo/wrapper/request_campaign.py: -------------------------------------------------------------------------------- 1 | 2 | def wrap(campaign=None, lead=None): 3 | return ( 4 | '' + 5 | 'MKTOWS' + 6 | '' + campaign + '' + 7 | '' + 8 | '' + 9 | 'IDNUM' + 10 | '' + lead + '' + 11 | '' + 12 | '' + 13 | '') 14 | -------------------------------------------------------------------------------- /marketo/wrapper/sync_lead.py: -------------------------------------------------------------------------------- 1 | 2 | import xml.etree.ElementTree as ET 3 | from xml.sax.saxutils import escape 4 | import lead_record 5 | 6 | 7 | def wrap(email=None, attributes=None): 8 | attr = '' 9 | for i in attributes: 10 | attr += '' \ 11 | '' + i[0] + '' \ 12 | '' + i[1] + '' \ 13 | '' + escape(i[2]) + '' \ 14 | '' 15 | 16 | return( 17 | '' + 18 | '' + 19 | '' + email + '' + 20 | '' + attr + '' + 21 | '' + 22 | 'true' + 23 | '' + 24 | '' 25 | ) 26 | 27 | 28 | def unwrap(response): 29 | root = ET.fromstring(response.text) 30 | lead_record_xml = root.find('.//leadRecord') 31 | return lead_record.unwrap(lead_record_xml) 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | # Don't import analytics-python module here, since deps may not be installed 11 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'marketo')) 12 | from version import VERSION 13 | 14 | long_description = ''' 15 | marketo-python is a python query client that wraps the Marketo SOAP API. 16 | For sending data to Marketo, check out https://segment.io. 17 | ''' 18 | 19 | setup( 20 | name='marketo', 21 | version=VERSION, 22 | url='https://github.com/segmentio/marketo-python', 23 | author='Ilya Volodarsky', 24 | author_email='ilya@segment.io', 25 | maintainer='Segment.io', 26 | maintainer_email='friends@segment.io', 27 | packages=['marketo', 'marketo.wrapper'], 28 | license='MIT License', 29 | install_requires=[ 30 | 'requests', 31 | 'iso8601' 32 | ], 33 | description='marketo-python is a python query client that wraps the Marketo SOAP API.', 34 | long_description=long_description 35 | ) 36 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import marketo 5 | import marketo.auth 6 | 7 | 8 | class MarketoBasicTests(unittest.TestCase): 9 | 10 | def test_auth(self): 11 | # From Marketo example" 12 | user_id = "bigcorp1_461839624B16E06BA2D663" 13 | encryption_key = "899756834129871744AAEE88DDCC77CDEEDEC1AAAD66" 14 | timestamp = "2010-04-09T14:04:54-07:00" 15 | signature = "ffbff4d4bef354807481e66dc7540f7890523a87" 16 | self.assertTrue(marketo.auth.sign(timestamp + user_id, encryption_key) == signature) 17 | 18 | if __name__ == '__main__': 19 | unittest.main() 20 | --------------------------------------------------------------------------------