embulk-input-marketo 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +25 -6
- data/Rakefile +53 -1
- data/embulk-input-marketo.gemspec +1 -1
- data/lib/embulk/input/marketo/activity_log.rb +55 -0
- data/lib/embulk/input/marketo/base.rb +89 -0
- data/lib/embulk/input/marketo/lead.rb +73 -0
- data/lib/embulk/input/marketo_api.rb +14 -3
- data/lib/embulk/input/marketo_api/soap/activity_log.rb +90 -0
- data/lib/embulk/input/marketo_api/soap/base.rb +55 -0
- data/lib/embulk/input/marketo_api/soap/lead.rb +75 -0
- data/test/activity_log_fixtures.rb +155 -0
- data/test/embulk/input/marketo/test_activity_log.rb +201 -0
- data/test/embulk/input/marketo/test_base.rb +63 -0
- data/test/embulk/input/marketo/test_lead.rb +172 -0
- data/test/embulk/input/marketo_api/soap/test_activity_log.rb +109 -0
- data/test/embulk/input/marketo_api/soap/test_base.rb +43 -0
- data/test/embulk/input/marketo_api/soap/test_lead.rb +110 -0
- data/test/embulk/input/test_marketo_api.rb +28 -0
- metadata +24 -8
- data/lib/embulk/input/marketo.rb +0 -138
- data/lib/embulk/input/marketo_api/soap.rb +0 -112
- data/test/embulk/input/marketo_api/test_soap.rb +0 -123
- data/test/embulk/input/test_marketo.rb +0 -176
@@ -0,0 +1,55 @@
|
|
1
|
+
require "savon"
|
2
|
+
|
3
|
+
module Embulk
|
4
|
+
module Input
|
5
|
+
module MarketoApi
|
6
|
+
module Soap
|
7
|
+
class Base
|
8
|
+
attr_reader :endpoint, :wsdl, :user_id, :encryption_key
|
9
|
+
|
10
|
+
def initialize(endpoint, wsdl, user_id, encryption_key)
|
11
|
+
@endpoint = endpoint
|
12
|
+
@wsdl = wsdl
|
13
|
+
@user_id = user_id
|
14
|
+
@encryption_key = encryption_key
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def savon
|
20
|
+
headers = {
|
21
|
+
'ns1:AuthenticationHeader' => {
|
22
|
+
"mktowsUserId" => user_id,
|
23
|
+
}.merge(signature)
|
24
|
+
}
|
25
|
+
# NOTE: Do not memoize this to use always fresh signature (avoid 20016 error)
|
26
|
+
# ref. https://jira.talendforge.org/secure/attachmentzip/unzip/167201/49761%5B1%5D/Marketo%20Enterprise%20API%202%200.pdf (41 page)
|
27
|
+
Savon.client(
|
28
|
+
log: true,
|
29
|
+
logger: Embulk.logger,
|
30
|
+
wsdl: wsdl,
|
31
|
+
soap_header: headers,
|
32
|
+
endpoint: endpoint,
|
33
|
+
open_timeout: 90,
|
34
|
+
read_timeout: 300,
|
35
|
+
raise_errors: true,
|
36
|
+
namespace_identifier: :ns1,
|
37
|
+
env_namespace: 'SOAP-ENV'
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def signature
|
42
|
+
timestamp = Time.now.to_s
|
43
|
+
encryption_string = timestamp + user_id
|
44
|
+
digest = OpenSSL::Digest.new('sha1')
|
45
|
+
hashed_signature = OpenSSL::HMAC.hexdigest(digest, encryption_key, encryption_string)
|
46
|
+
{
|
47
|
+
'requestTimestamp' => timestamp,
|
48
|
+
'requestSignature' => hashed_signature.to_s
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "embulk/input/marketo_api/soap/base"
|
2
|
+
|
3
|
+
module Embulk
|
4
|
+
module Input
|
5
|
+
module MarketoApi
|
6
|
+
module Soap
|
7
|
+
class Lead < Base
|
8
|
+
def metadata
|
9
|
+
# http://developers.marketo.com/documentation/soap/describemobject/
|
10
|
+
response = savon.call(:describe_m_object, message: {object_name: "LeadRecord"})
|
11
|
+
response.body[:success_describe_m_object][:result][:metadata][:field_list][:field]
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(last_updated_at, &block)
|
15
|
+
# http://developers.marketo.com/documentation/soap/getmultipleleads/
|
16
|
+
|
17
|
+
last_updated_at = Time.parse(last_updated_at).iso8601
|
18
|
+
|
19
|
+
# TODO: generate request in #fetch
|
20
|
+
# TODO: use PREVIEW_COUNT as batch_size in preview
|
21
|
+
request = {
|
22
|
+
lead_selector: {
|
23
|
+
oldest_updated_at: last_updated_at,
|
24
|
+
},
|
25
|
+
attributes!: {
|
26
|
+
lead_selector: {"xsi:type" => "ns1:LastUpdateAtSelector"}
|
27
|
+
},
|
28
|
+
batch_size: 1000,
|
29
|
+
}
|
30
|
+
|
31
|
+
stream_position = fetch(request, &block)
|
32
|
+
|
33
|
+
while stream_position
|
34
|
+
stream_position = fetch(request.merge(stream_position: stream_position), &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def fetch(request = {}, &block)
|
41
|
+
response = savon.call(:get_multiple_leads, message: request)
|
42
|
+
|
43
|
+
remaining = response.xpath('//remainingCount').text.to_i
|
44
|
+
Embulk.logger.info "Remaining records: #{remaining}"
|
45
|
+
response.xpath('//leadRecordList/leadRecord').each do |lead|
|
46
|
+
record = {
|
47
|
+
"id" => {type: :integer, value: lead.xpath('Id').text.to_i},
|
48
|
+
"email" => {type: :string, value: lead.xpath('Email').text}
|
49
|
+
}
|
50
|
+
lead.xpath('leadAttributeList/attribute').each do |attr|
|
51
|
+
name = attr.xpath('attrName').text
|
52
|
+
type = attr.xpath('attrType').text
|
53
|
+
value = attr.xpath('attrValue').text
|
54
|
+
record = record.merge(
|
55
|
+
name => {
|
56
|
+
type: type,
|
57
|
+
value: value
|
58
|
+
}
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
block.call(record)
|
63
|
+
end
|
64
|
+
|
65
|
+
if remaining > 0
|
66
|
+
response.xpath('//newStreamPosition').text
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module ActivityLogFixtures
|
2
|
+
private
|
3
|
+
|
4
|
+
def activity_logs_response
|
5
|
+
activity_logs(response)
|
6
|
+
end
|
7
|
+
|
8
|
+
def next_stream_activity_logs_response
|
9
|
+
activity_logs(next_stream_response)
|
10
|
+
end
|
11
|
+
|
12
|
+
def preview_activity_logs_response
|
13
|
+
activity_logs(preview_response)
|
14
|
+
end
|
15
|
+
|
16
|
+
def activity_logs(body)
|
17
|
+
Struct.new(:body).new({
|
18
|
+
success_get_lead_changes: {
|
19
|
+
result: body
|
20
|
+
}
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
def response
|
25
|
+
{
|
26
|
+
return_count: "2",
|
27
|
+
remaining_count: "1",
|
28
|
+
new_start_position: {
|
29
|
+
latest_created_at: true,
|
30
|
+
oldest_created_at: DateTime.parse("2015-07-14T00:13:13+00:00"),
|
31
|
+
activity_created_at: true,
|
32
|
+
offset: offset,
|
33
|
+
},
|
34
|
+
lead_change_record_list: {
|
35
|
+
lead_change_record: [
|
36
|
+
{
|
37
|
+
id: "1",
|
38
|
+
activity_date_time: DateTime.parse("2015-07-14T00:00:09+00:00"),
|
39
|
+
activity_type: "at1",
|
40
|
+
mktg_asset_name: "score1",
|
41
|
+
activity_attributes: {
|
42
|
+
attribute: [
|
43
|
+
{
|
44
|
+
attr_name: "Attribute Name",
|
45
|
+
attr_type: nil,
|
46
|
+
attr_value: "Attribute1",
|
47
|
+
},
|
48
|
+
{
|
49
|
+
attr_name: "Old Value",
|
50
|
+
attr_type: nil,
|
51
|
+
attr_value: "402",
|
52
|
+
},
|
53
|
+
],
|
54
|
+
},
|
55
|
+
mkt_person_id: "100",
|
56
|
+
},
|
57
|
+
{
|
58
|
+
id: "2",
|
59
|
+
activity_date_time: DateTime.parse("2015-07-14T00:00:10+00:00"),
|
60
|
+
activity_type: "at2",
|
61
|
+
mktg_asset_name: "score2",
|
62
|
+
activity_attributes: {
|
63
|
+
attribute: [
|
64
|
+
{
|
65
|
+
attr_name: "Attribute Name",
|
66
|
+
attr_type: nil,
|
67
|
+
attr_value: "Attribute2",
|
68
|
+
},
|
69
|
+
{
|
70
|
+
attr_name: "Old Value",
|
71
|
+
attr_type: nil,
|
72
|
+
attr_value: "403",
|
73
|
+
},
|
74
|
+
],
|
75
|
+
},
|
76
|
+
mkt_person_id: "90",
|
77
|
+
},
|
78
|
+
]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def offset
|
84
|
+
"offset"
|
85
|
+
end
|
86
|
+
|
87
|
+
def next_stream_response
|
88
|
+
{
|
89
|
+
return_count: 1,
|
90
|
+
remaining_count: 0,
|
91
|
+
new_start_position: {
|
92
|
+
},
|
93
|
+
lead_change_record_list: {
|
94
|
+
lead_change_record: [
|
95
|
+
{
|
96
|
+
id: "3",
|
97
|
+
activity_date_time: DateTime.parse("2015-07-14T00:00:11+00:00"),
|
98
|
+
activity_type: "at3",
|
99
|
+
mktg_asset_name: "score3",
|
100
|
+
activity_attributes: {
|
101
|
+
attribute: [
|
102
|
+
{
|
103
|
+
attr_name: "Attribute Name",
|
104
|
+
attr_type: nil,
|
105
|
+
attr_value: "Attribute3",
|
106
|
+
},
|
107
|
+
{
|
108
|
+
attr_name: "Old Value",
|
109
|
+
attr_type: nil,
|
110
|
+
attr_value: "404",
|
111
|
+
},
|
112
|
+
],
|
113
|
+
},
|
114
|
+
mkt_person_id: "100",
|
115
|
+
},
|
116
|
+
]
|
117
|
+
}
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
def preview_response
|
122
|
+
records = (1..15).map do |i|
|
123
|
+
{
|
124
|
+
id: i,
|
125
|
+
activity_date_time: DateTime.parse("2015-07-14T00:00:11+00:00"),
|
126
|
+
activity_type: "at#{i}",
|
127
|
+
mktg_asset_name: "score#{i}",
|
128
|
+
activity_attributes: {
|
129
|
+
attribute: [
|
130
|
+
{
|
131
|
+
attr_name: "Attribute Name",
|
132
|
+
attr_type: nil,
|
133
|
+
attr_value: "Attribute#{i}",
|
134
|
+
},
|
135
|
+
{
|
136
|
+
attr_name: "Old Value",
|
137
|
+
attr_type: nil,
|
138
|
+
attr_value: "404",
|
139
|
+
},
|
140
|
+
],
|
141
|
+
},
|
142
|
+
mkt_person_id: "100",
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
{
|
147
|
+
return_count: 15,
|
148
|
+
remaining_count: 0,
|
149
|
+
new_start_position: {},
|
150
|
+
lead_change_record_list: {
|
151
|
+
lead_change_record: records
|
152
|
+
}
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require "prepare_embulk"
|
2
|
+
require "embulk/input/marketo/activity_log"
|
3
|
+
require "activity_log_fixtures"
|
4
|
+
|
5
|
+
module Embulk
|
6
|
+
module Input
|
7
|
+
module Marketo
|
8
|
+
class ActivityLogTest < Test::Unit::TestCase
|
9
|
+
include ActivityLogFixtures
|
10
|
+
|
11
|
+
def test_target
|
12
|
+
assert_equal(:activity_log, ActivityLog.target)
|
13
|
+
end
|
14
|
+
|
15
|
+
class GuessTest < self
|
16
|
+
setup :setup_soap
|
17
|
+
|
18
|
+
def setup_soap
|
19
|
+
@soap = MarketoApi::Soap::ActivityLog.new(settings[:endpoint], settings[:wsdl], settings[:user_id], settings[:encryption_key])
|
20
|
+
|
21
|
+
stub(ActivityLog).soap_client(config) { @soap }
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_include_metadata
|
25
|
+
stub(@soap).metadata(last_updated_at, batch_size: ActivityLog::PREVIEW_COUNT) { Guess::SchemaGuess.from_hash_records(records) }
|
26
|
+
|
27
|
+
assert_equal(
|
28
|
+
{"columns" => expected_guessed_columns},
|
29
|
+
Marketo::ActivityLog.guess(config)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def records
|
36
|
+
[
|
37
|
+
{
|
38
|
+
id: "12",
|
39
|
+
activity_date_time: "2015-06-25T00:12:00+00:00",
|
40
|
+
activity_type: "Visit Webpage",
|
41
|
+
mktg_asset_name: "webpage.example.com/person/1/edit",
|
42
|
+
mkt_person_id: "34",
|
43
|
+
"Webpage ID" => "56",
|
44
|
+
"Webpage URL" => "/person/1/edit",
|
45
|
+
"Referrer URL" => "https://webpage.example.com",
|
46
|
+
"Client IP Address" => "127.0.0.1",
|
47
|
+
"User Agent" => "UserAgent",
|
48
|
+
"Message Id" => "78",
|
49
|
+
"Created At" => "2015-07-06 19:00:02",
|
50
|
+
"Lead ID" => "90"
|
51
|
+
}
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
def expected_guessed_columns
|
56
|
+
[
|
57
|
+
{name: :id, type: :long},
|
58
|
+
{name: :activity_date_time, type: :timestamp, format: "%Y-%m-%dT%H:%M:%S%z"},
|
59
|
+
{name: :activity_type, type: :string},
|
60
|
+
{name: :mktg_asset_name, type: :string},
|
61
|
+
{name: :mkt_person_id, type: :long},
|
62
|
+
{name: "Webpage ID", type: :long},
|
63
|
+
{name: "Webpage URL", type: :string},
|
64
|
+
{name: "Referrer URL", type: :string},
|
65
|
+
{name: "Client IP Address", type: :string},
|
66
|
+
{name: "User Agent", type: :string},
|
67
|
+
{name: "Message Id", type: :long},
|
68
|
+
{name: "Created At", type: :timestamp, format: "%Y-%m-%d %H:%M:%S"},
|
69
|
+
{name: "Lead ID", type: :long}
|
70
|
+
]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class RunTest < self
|
75
|
+
def setup_soap
|
76
|
+
@soap = MarketoApi::Soap::ActivityLog.new(settings[:endpoint], settings[:wsdl], settings[:user_id], settings[:encryption_key])
|
77
|
+
|
78
|
+
stub(ActivityLog).soap_client(task) { @soap }
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup_plugin
|
82
|
+
@page_builder = Object.new
|
83
|
+
@plugin = ActivityLog.new(task, nil, nil, @page_builder)
|
84
|
+
stub(Embulk).logger { ::Logger.new(IO::NULL) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def setup
|
88
|
+
setup_soap
|
89
|
+
setup_plugin
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_run_through
|
93
|
+
stub(@plugin).preview? { false }
|
94
|
+
|
95
|
+
any_instance_of(Savon::Client) do |klass|
|
96
|
+
mock(klass).call(:get_lead_changes, message: request) do
|
97
|
+
activity_logs_response
|
98
|
+
end
|
99
|
+
|
100
|
+
mock(klass).call(:get_lead_changes, message: offset_request) do
|
101
|
+
next_stream_activity_logs_response
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
mock(@page_builder).add(["1", Time.parse("2015-07-14 09:00:09 +0900"), "at1", "score1", "100", "Attribute1", "402"])
|
106
|
+
mock(@page_builder).add(["2", Time.parse("2015-07-14 09:00:10 +0900"), "at2", "score2", "90", "Attribute2", "403"])
|
107
|
+
mock(@page_builder).add(["3", Time.parse("2015-07-14 09:00:11 +0900"), "at3", "score3", "100", "Attribute3", "404"])
|
108
|
+
mock(@page_builder).finish
|
109
|
+
|
110
|
+
@plugin.run
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_preview_through
|
114
|
+
stub(@plugin).preview? { true }
|
115
|
+
|
116
|
+
any_instance_of(Savon::Client) do |klass|
|
117
|
+
mock(klass).call(:get_lead_changes, message: preview_request) do
|
118
|
+
preview_activity_logs_response
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
1.upto(ActivityLog::PREVIEW_COUNT) do |count|
|
123
|
+
mock(@page_builder).add([count, Time.parse("2015-07-14 09:00:11 +0900"), "at#{count}", "score#{count}", "100", "Attribute#{count}", "404"])
|
124
|
+
end
|
125
|
+
mock(@page_builder).finish
|
126
|
+
|
127
|
+
@plugin.run
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def request
|
133
|
+
{
|
134
|
+
start_position: {
|
135
|
+
oldest_created_at: Time.parse(last_updated_at).iso8601,
|
136
|
+
},
|
137
|
+
batch_size: 100
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def offset_request
|
142
|
+
{
|
143
|
+
start_position: {
|
144
|
+
offset: "offset"
|
145
|
+
},
|
146
|
+
batch_size: 100
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def preview_request
|
152
|
+
{
|
153
|
+
start_position: {
|
154
|
+
oldest_created_at: Time.parse(last_updated_at).iso8601,
|
155
|
+
},
|
156
|
+
batch_size: ActivityLog::PREVIEW_COUNT
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def settings
|
163
|
+
{
|
164
|
+
endpoint: "https://marketo.example.com",
|
165
|
+
wsdl: "https://marketo.example.com/?wsdl",
|
166
|
+
user_id: "user_id",
|
167
|
+
encryption_key: "TOPSECRET",
|
168
|
+
last_updated_at: last_updated_at,
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
def config
|
173
|
+
DataSource[settings.to_a]
|
174
|
+
end
|
175
|
+
|
176
|
+
def task
|
177
|
+
{
|
178
|
+
endpoint_url: "https://marketo.example.com",
|
179
|
+
wsdl_url: "https://marketo.example.com/?wsdl",
|
180
|
+
user_id: "user_id",
|
181
|
+
encryption_key: "TOPSECRET",
|
182
|
+
last_updated_at: last_updated_at,
|
183
|
+
columns: [
|
184
|
+
{"name" => :id, "type" => :long},
|
185
|
+
{"name" => :activity_date_time, "type" => :timestamp, "format" => "%Y-%m-%dT%H:%M:%S%z"},
|
186
|
+
{"name" => :activity_type, "type" => :string},
|
187
|
+
{"name" => :mktg_asset_name, "type" => :string},
|
188
|
+
{"name" => :mkt_person_id, "type" => :long},
|
189
|
+
{"name" => "Attribute Name", "type" => :string},
|
190
|
+
{"name" => "Old Value", "type" => :string},
|
191
|
+
]
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
def last_updated_at
|
196
|
+
"2015-07-01 00:00:00+00:00"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|