embulk-input-marketo 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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