Skip to content

Commit f581d75

Browse files
committed
make attribution-window user-configurable
1 parent 2d218b3 commit f581d75

File tree

4 files changed

+59
-28
lines changed

4 files changed

+59
-28
lines changed

tap_marketo/__init__.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env python3
22

33
# Marketo Docs are located at http://developers.marketo.com/rest-api/
4+
5+
from datetime import datetime, timedelta
46
import itertools
57
import re
68

@@ -35,39 +37,40 @@
3537

3638

3739
ATTRIBUTION_WINDOW_README = """
38-
`attribution_window` may be specified by a combination of days, hours and minutes. This parameter is
40+
`attribution_window` may be specified by a combination of days, hours and minutes seconds. This parameter is
3941
quite useful in a moderate frequency incremental bulk extracts (e.g. once an hour)
4042
to allow users a way to avoid extracting all leads updated 1 day prior (i.e. default attribution window)
41-
examples of valid attribution_windows: 1d, 12h, 1h30m, 1d6h55m
43+
examples of valid attribution_windows: `1 day`, `1 days`, `2 day`, `10 days`, `10:00:00`, `1 day 05:00:00`
4244
"""
43-
ATTRIBUTION_WINDOW_SPEC_RE_ATOMS = [r'\d{1,2}d', r'\d{1,2}h', r'\d{1,2}m']
44-
ATTRIBUTION_WINDOW_ALLOWED_RE = [
45-
re.compile(''.join(x))
46-
for i in range(1, 4)
47-
for x in itertools.combinations(ATTRIBUTION_WINDOW_SPEC_RE_ATOMS, i)
48-
]
49-
5045

5146
def parse_attribution_window(attribution_window_string):
5247
f"""
5348
Parse optional config parameter `attribution_window`.
54-
5549
Attribution window is used to set an earlier export_start
5650
for incremental replication of of the leads stream.
5751
5852
{ATTRIBUTION_WINDOW_README}
5953
"""
60-
aw = attribution_window_string.lower().strip()
61-
if not any(p.match(aw) for p in ATTRIBUTION_WINDOW_ALLOWED_RE):
62-
raise ValueError(
63-
f"Invalid attribution window string: {attribution_window_string}."
64-
f"{ATTRIBUTION_WINDOW_README}"
65-
)
66-
window_code_name_map = {'d': 'days', 'h': 'hours', 'm': 'minutes'}
67-
return {
68-
window_code_name_map[y]: int(x)
69-
for x, y in re.findall(r'(\d{1,2})([dhm])', aw)
70-
}
54+
errstr = f"`{attribution_window_string}` is not a valid attribution window."
55+
pat = '^((?P<day>^\d+)\s+days?)?(\s+)?(?P<time>(\d{2}:\d{2}:\d{2}))?$'
56+
match = re.match(pat, attribution_window_string)
57+
if not match:
58+
raise ValueError(errstr)
59+
groups = match.groupdict()
60+
delta_day = groups["day"] or '0'
61+
delta_time = groups["time"] or '00:00:00'
62+
try:
63+
parsed_time = datetime.strptime(delta_time, '%H:%M:%S')
64+
return timedelta(
65+
days=int(delta_day) if delta_day else 0,
66+
hours=parsed_time.hour,
67+
minutes=parsed_time.minute,
68+
seconds=parsed_time.second
69+
)
70+
except ValueError as e:
71+
raise ValueError(errstr)
72+
73+
7174

7275

7376
def validate_state(config, catalog, state):

tap_marketo/sync.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import timedelta
2+
13
import csv
24
import json
35
import pendulum
@@ -307,8 +309,11 @@ def sync_leads(client, state, stream, config):
307309
initial_bookmark = pendulum.parse(bookmarks.get_bookmark(state, "leads", replication_key))
308310
export_start = pendulum.parse(bookmarks.get_bookmark(state, "leads", replication_key))
309311
if client.use_corona:
310-
attribution_window = config.get('attribution_window', {'days': ATTRIBUTION_WINDOW_DAYS})
311-
export_start = export_start.subtract(**attribution_window)
312+
aw = config.get(
313+
'attribution_window',
314+
timedelta(days=ATTRIBUTION_WINDOW_DAYS)
315+
)
316+
export_start = export_start.subtract(days=aw.days, seconds=aw.seconds)
312317

313318
# job_started is truncated to the microsecond
314319
# in get_export_end the field export_end is also truncated to the microsecond

tests/test_startup.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
from datetime import timedelta
12
import unittest
23

34
import pendulum
45
import requests_mock
56

6-
from tap_marketo import validate_state
7+
from tap_marketo import validate_state, parse_attribution_window
78
from tap_marketo.sync import determine_replication_key
89

910
class TestValidateState(unittest.TestCase):
@@ -268,3 +269,29 @@ def test_validate_state(self):
268269

269270
self.assertDictEqual(validate_state(mock_config, mock_catalog, mock_state_2),
270271
expected_state_2)
272+
273+
def test_parse_attribution_window_parses(self):
274+
"""Verify attribution window is successfully parsed for valid patterns."""
275+
276+
aw1 = '3 day 20:00:00'
277+
aw2 = '1 days'
278+
aw3 = '10:10:10'
279+
res1 = parse_attribution_window(aw1)
280+
res2 = parse_attribution_window(aw2)
281+
res3 = parse_attribution_window(aw3)
282+
assert res1 == timedelta(days=3, hours=20)
283+
assert res2 == timedelta(days=1)
284+
assert res3 == timedelta(hours=10, minutes=10, seconds=10)
285+
286+
def test_parse_attribution_window_raises(self):
287+
"""Verify parse_attribution_window raised ValueError for invalid patterns."""
288+
289+
aw1 = 'foobar 3 day 20:00:00'
290+
aw2 = '3 day 1:00:00'
291+
aw3 = '10:00:00 foobar'
292+
with self.assertRaises(ValueError):
293+
parse_attribution_window(aw1)
294+
with self.assertRaises(ValueError):
295+
parse_attribution_window(aw2)
296+
with self.assertRaises(ValueError):
297+
parse_attribution_window(aw3)

tests/test_sync.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
from tap_marketo.sync import *
1515

1616

17-
SAMPLE_LEADS_CSV = """
18-
"""
19-
2017
def parse_params(request):
2118
return dict(urllib.parse.parse_qsl(request.query))
2219

@@ -56,7 +53,6 @@ class TestMarketoExport(unittest.TestCase):
5653
def setUp(self):
5754
self.client = Client("123-ABC-789", "id", "secret")
5855
self.client._use_corona = True
59-
self.sample_leads_csv = SAMPLE_LEADS_CSV
6056
self.mock_status_completed = {
6157
"result": [
6258
{

0 commit comments

Comments
 (0)