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.
@@ -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