embulk-input-mixpanel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d553d9b694f3b3150c2c634f1f4b334e7c2162e9
4
+ data.tar.gz: 89480ccc7dc13ace59cb88769af0263b0c078c72
5
+ SHA512:
6
+ metadata.gz: 60063447a33ad85b84d33bb01d85ad757c3bcc0494908f30a9c8c78e8fb5df17049c27833b1ce6acccf52358b6575b036ebdbd770fbff4e38558ecdd90734120
7
+ data.tar.gz: d98177f66557bec6a845749bf93ac69a4b4b8bbbbf63695222f945d37b1d6832432eb58c8448b354018c819e676d5fcf48409b723a32fd488291fd0b724f6bd6
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *~
2
+ /pkg/
3
+ /tmp/
4
+ /.bundle/
5
+ /Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ rvm:
3
+ - jruby-19mode
4
+ addons:
5
+ code_climate:
6
+ repo_token:
7
+ secure: "opE/ZhRzsEU2Fn6YEnItMD/rMs3O2OHVYQQ7Ly0dAkIc9ZrVMa//ogt4k7h0QGZUettRVV7kawCtRzde+QbOmezTRwWqQJ7Mi6D6qfwlYMz+D9FcufHaCJyLy+dYeBuQqr1d6n/3gVqVGh8MAMGHipYodudCub38DQ1sVWcCMDNNo4PMRFLS0pS839SC80HAS7tutOeaeohRc+Ct5yoY2ZDoTuGtNeJhwqmDAo13RwvVu9aZw97EZPvt8UcaW5oYDnx47kDpKi4XGrTPaWpSm/IitwW11FF+Kevt0RpUS0uVWqg4/6xTjDg++ETG+94ePYrOZGF4ne/CPtC0AtaWF1jgSlL9iu4IR/Awt+2BqawKzCnoSRgCGIHRZApErC3KacadJBaPCrKwf4xxxqRXex4lXcptKRygkG4ic2r+MblyPPwIsc1Wb1QYCeVjIEGWzOgKVEUpT8qN0DAj6KQe/HuyuXKE/FiPIRfkJkRY5oGCNZtzeCzXlC+IqhZYCg6HwcZuk3CbcxRrg5VFKDlL0VWacD/FQoGgfTp3SUmaL6NMcCKSrL0vjEgx98/yM9rsGZlZOU9ioN93PQVgqpI5dl+nm9vKkFyuzW07nNM+/6PNdHbbZBY1OLNd3RSpWVC9TDtZ3Q2gAx25+31TDzD2/3yjJjfdtwIH3bdyrO44MK8="
8
+ notifications:
9
+ slack:
10
+ secure: cAAHcb/HdJK6elr6gfxlotUJdZys8nA/BUb1rKVt7T7EShOjqNOwHo0vZBG5fXQEbUJjHCFAl8tH1N8wz+KLoVErlMmD2OgBKGgvgm3fddkIgGRRvxe8yB0yEFy5yuvfgqJDlwPHZxbwj2Ddb8uTaLd8yzmRzGWNwLyRQTDaQbaKBaB+emP2t/1AilQHyItN02NuW3qcEGr7Hzg+IsKabDRzhMi89RV0xk2eP5iWTYW76ULbbfP3NqBpcVLskAqVQd1YOFUSEb2fdzRj7UuLUUp3C0Kb5gKfXPHb97XuMqwyviakTr02omdaM82NvB8GKn7QKBbcwA1Iz1euinxQQ7MTzJe8wLMH8q205lTpl96CANKwIxrk/HestgEboq97xSp5Wc1Qn1ynnrLOvBYW7EG/e6exguojQmkLkV9P6LW9g4nYsR8UEjyuRd252xch4+b4M3lxtlAXtpWguPfm5ZKhICBsJMlYm6//q1cBlNUOzR0cufCHDTNdlB/YLynvdg1ykFISYARP+5sV2YM9rD/1Rs/396qYr0mNbCh83P9GlZ5NZtWLQePJroHe3iQ18X3YHEcb50OTfRmKNoXTldQBD9iKNkwlFpWfruVdBWT+JRM77+yIV9ORjY7UE4ZtH8HxAg4bX+ey+C5axgxQk569h3CdLmVapFt6wovCSVk=
11
+ jdk:
12
+ - oraclejdk8
13
+ gemfile:
14
+ - gemfiles/embulk-0.6.16
15
+ - gemfiles/embulk-0.6.17
16
+ - gemfiles/embulk-latest
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 - 2015-07-28
2
+
3
+ The first release!!
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Everyleaf Corporation
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ [![Build Status](https://travis-ci.org/treasure-data/embulk-input-mixpanel.svg?branch=master)](https://travis-ci.org/treasure-data/embulk-input-mixpanel)
2
+ [![Code Climate](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/badges/gpa.svg)](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel)
3
+ [![Test Coverage](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/badges/coverage.svg)](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/coverage)
4
+
5
+ # Mixpanel input plugin for Embulk
6
+
7
+ embulk-input-mixpanel is the Embulk input plugin for [Mixpanel](https://mixpanel.com).
8
+
9
+ ## Overview
10
+
11
+ Required Embulk version >= 0.6.16.
12
+
13
+ * **Plugin type**: input
14
+ * **Resume supported**: no
15
+ * **Cleanup supported**: no
16
+ * **Guess supported**: yes
17
+
18
+ ## Setup
19
+
20
+ ### How to get API configuration
21
+
22
+ This plugin uses API key and API secret for target project. Before you make your config.yml, you should get API key and API secret in mixpanel website.
23
+
24
+ For API configuration, you should log in mixpanel website, and click "Account" at the header. When you select "Projects" panel, you can get "API Key" and "API Secret" for each project.
25
+
26
+ ### Configuration
27
+
28
+ - **api_key**: project API Key (string, required)
29
+ - **api_secret**: project API Secret (string, required)
30
+ - **timezone**: project timezone(string, required)
31
+ - **from_date**: From date to export (string, required)
32
+ - **to_date**: To date to export (string, required)
33
+ - **event**: The event or events to filter data (array, optional, default: nil)
34
+ - **where**: Expression to filter data (c.f. https://mixpanel.com/docs/api-documentation/data-export-api#segmentation-expressions) (string, optional, default: nil)
35
+ - **bucket**:The data backet to filter data (string, optional, default: nil)
36
+
37
+ ## Example
38
+
39
+ ```yaml
40
+ in:
41
+ type: mixpanel
42
+ api_key: "API_KEY"
43
+ api_secret: "API_SECRET"
44
+ timezone: "US/Pacific"
45
+ from_date: "2015-07-19"
46
+ to_date: "2015-07-20"
47
+ ```
48
+
49
+ ## Run test
50
+
51
+ ```
52
+ $ rake
53
+ ```
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task default: :test
4
+
5
+ desc "Run tests"
6
+ task :test do
7
+ ruby("test/run-test.rb", "--use-color=yes", "--collector=dir")
8
+ end
9
+
10
+ namespace :release do
11
+ desc "Add header of now version release to ChangeLog and bump up version"
12
+ task :prepare do
13
+ root_dir = Pathname.new(File.expand_path("../", __FILE__))
14
+ changelog_file = root_dir.join("CHANGELOG.md")
15
+ gemspec_file = root_dir.join("embulk-input-mixpanel.gemspec")
16
+
17
+ system("git fetch origin")
18
+
19
+ # detect merged PR
20
+ old_version = gemspec_file.read[/spec\.version += *"([0-9]+\.[0-9]+\.[0-9]+)"/, 1]
21
+ pr_numbers = `git log v#{old_version}..origin/master --oneline`.scan(/#[0-9]+/)
22
+
23
+ if !$?.success? || pr_numbers.empty?
24
+ puts "Detecting PR failed. Please confirm if any PR were merged after the latest release."
25
+ exit(false)
26
+ end
27
+
28
+ # Generate new version
29
+ major, minor, patch = old_version.split(".").map(&:to_i)
30
+ new_version = "#{major}.#{minor}.#{patch + 1}"
31
+
32
+ # Update ChangeLog
33
+ pr_descriptions = pr_numbers.map do |number|
34
+ body = open("https://api.github.com/repos/treasure-data/embulk-input-mixpanel/issues/#{number.gsub("#", "")}").read
35
+ payload = JSON.parse(body)
36
+ "* [] #{payload["title"]} [#{number}](https://github.com/treasure-data/embulk-input-mixpanel/pull/#{number.gsub('#', '')})"
37
+ end.join("\n")
38
+
39
+ new_changelog = <<-HEADER
40
+ ## #{new_version} - #{Time.now.strftime("%Y-%m-%d")}
41
+ #{pr_descriptions}
42
+
43
+ #{changelog_file.read.chomp}
44
+ HEADER
45
+
46
+ File.open(changelog_file, "w") {|f| f.write(new_changelog) }
47
+
48
+ # Update version.rb
49
+ old_content = gemspec_file.read
50
+ File.open(gemspec_file, "w") do |f|
51
+ f.write old_content.gsub(/(spec\.version += *)".*?"/, %Q!\\1"#{new_version}"!)
52
+ end
53
+
54
+ # Update Gemfile.lock
55
+ system("bundle install")
56
+
57
+ puts "ChangeLog, version and Gemfile.lock were updated. New version is #{new_version}."
58
+ end
59
+ end
60
+
@@ -0,0 +1,25 @@
1
+
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "embulk-input-mixpanel"
4
+ spec.version = "0.1.0"
5
+ spec.authors = ["yoshihara", "uu59"]
6
+ spec.summary = "Mixpanel input plugin for Embulk"
7
+ spec.description = "Loads records from Mixpanel."
8
+ spec.email = ["h.yoshihara@everyleaf.com", "k@uu59.org"]
9
+ spec.licenses = ["Apache2"]
10
+ spec.homepage = "https://github.com/treasure-data/embulk-input-mixpanel"
11
+
12
+ spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"]
13
+ spec.test_files = spec.files.grep(%r{^(test|spec)/})
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.add_dependency 'httpclient'
17
+ spec.add_dependency 'tzinfo'
18
+ spec.add_development_dependency 'bundler', ['~> 1.0']
19
+ spec.add_development_dependency 'rake', ['>= 10.0']
20
+ spec.add_development_dependency 'embulk', ['>= 0.6.16', '< 1.0']
21
+ spec.add_development_dependency 'pry'
22
+ spec.add_development_dependency 'test-unit'
23
+ spec.add_development_dependency 'test-unit-rr'
24
+ spec.add_development_dependency 'codeclimate-test-reporter'
25
+ end
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "0.6.16"
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "0.6.17"
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
@@ -0,0 +1,165 @@
1
+ require "tzinfo"
2
+ require "embulk/input/mixpanel_api/client"
3
+
4
+ module Embulk
5
+ module Input
6
+
7
+ class Mixpanel < InputPlugin
8
+ Plugin.register_input("mixpanel", self)
9
+
10
+ GUESS_RECORDS_COUNT = 10
11
+
12
+ # NOTE: It takes long time to fetch data between from_date to
13
+ # to_date by one API request. So this plugin fetches data
14
+ # between each 7 (SLICE_DAYS_COUNT) days.
15
+ SLICE_DAYS_COUNT = 7
16
+
17
+ def self.transaction(config, &control)
18
+ task = {}
19
+
20
+ task[:params] = export_params(config)
21
+
22
+ from_date = config.param(:from_date, :string)
23
+ to_date = config.param(:to_date, :string)
24
+ dates = Date.parse(from_date)..Date.parse(to_date)
25
+ task[:dates] = dates.map {|date| date.to_s}
26
+
27
+ task[:api_key] = config.param(:api_key, :string)
28
+ task[:api_secret] = config.param(:api_secret, :string)
29
+ task[:timezone] = config.param(:timezone, :string)
30
+
31
+ begin
32
+ # raises exception if timezone is invalid string
33
+ TZInfo::Timezone.get(task[:timezone])
34
+ rescue => e
35
+ Embulk.logger.error "'#{task[:timezone]}' is invalid timezone"
36
+ raise e
37
+ end
38
+
39
+ columns = []
40
+ task[:schema] = config.param(:columns, :array)
41
+ task[:schema].each do |column|
42
+ name = column["name"]
43
+ type = column["type"].to_sym
44
+
45
+ columns << Column.new(nil, name, type, column["format"])
46
+ end
47
+
48
+ resume(task, columns, 1, &control)
49
+ end
50
+
51
+ def self.resume(task, columns, count, &control)
52
+ commit_reports = yield(task, columns, count)
53
+
54
+ next_config_diff = {}
55
+ return next_config_diff
56
+ end
57
+
58
+ def self.guess(config)
59
+ client = MixpanelApi::Client.new(config.param(:api_key, :string), config.param(:api_secret, :string))
60
+
61
+ from_date = config.param(:from_date, :string)
62
+ # NOTE: It should have 7 days beteen from_date and to_date
63
+ to_date = (Date.parse(from_date) + SLICE_DAYS_COUNT - 1).to_s
64
+
65
+ params = export_params(config)
66
+ params = params.merge(
67
+ from_date: from_date,
68
+ to_date: to_date,
69
+ )
70
+
71
+ records = client.export(params)
72
+ sample_records = records.first(GUESS_RECORDS_COUNT)
73
+ properties = Guess::SchemaGuess.from_hash_records(sample_records.map{|r| r["properties"]})
74
+ columns = properties.map do |col|
75
+ result = {
76
+ name: col.name,
77
+ type: col.type,
78
+ }
79
+ result[:format] = col.format if col.format
80
+ result
81
+ end
82
+ columns.unshift(name: "event", type: :string)
83
+ return {"columns" => columns}
84
+ end
85
+
86
+ def init
87
+ @api_key = task[:api_key]
88
+ @api_secret = task[:api_secret]
89
+ @params = task[:params]
90
+ @timezone = task[:timezone]
91
+ @schema = task[:schema]
92
+ @dates = task[:dates]
93
+ end
94
+
95
+ def run
96
+ client = MixpanelApi::Client.new(@api_key, @api_secret)
97
+ @dates.each_slice(SLICE_DAYS_COUNT) do |dates|
98
+ from_date = dates.first
99
+ to_date = dates.last
100
+ Embulk.logger.info "Fetching data from #{from_date} to #{to_date} ..."
101
+
102
+ params = @params.merge(
103
+ "from_date" => from_date,
104
+ "to_date" => to_date
105
+ )
106
+
107
+ records = client.export(params)
108
+
109
+ records.each do |record|
110
+ values = @schema.map do |column|
111
+ case column["name"]
112
+ when "event"
113
+ record["event"]
114
+ when "time"
115
+ time = record["properties"]["time"]
116
+ adjust_timezone(time)
117
+ else
118
+ record["properties"][column["name"]]
119
+ end
120
+ end
121
+ page_builder.add(values)
122
+ end
123
+
124
+ break if preview?
125
+ end
126
+
127
+ page_builder.finish
128
+
129
+ commit_report = {}
130
+ return commit_report
131
+ end
132
+
133
+ private
134
+
135
+ def adjust_timezone(epoch)
136
+ # Adjust timezone offset to get UTC time
137
+ # c.f. https://mixpanel.com/docs/api-documentation/exporting-raw-data-you-inserted-into-mixpanel#export
138
+ tz = TZInfo::Timezone.get(@timezone)
139
+ offset = tz.period_for_local(epoch, true).offset.utc_offset
140
+ epoch - offset
141
+ end
142
+
143
+ def preview?
144
+ begin
145
+ org.embulk.spi.Exec.isPreview()
146
+ rescue java.lang.NullPointerException => e
147
+ false
148
+ end
149
+ end
150
+
151
+ def self.export_params(config)
152
+ event = config.param(:event, :array, default: nil)
153
+ event = event.nil? ? nil : event.to_json
154
+
155
+ {
156
+ api_key: config.param(:api_key, :string),
157
+ event: event,
158
+ where: config.param(:where, :string, default: nil),
159
+ bucket: config.param(:bucket, :string, default: nil),
160
+ }
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,62 @@
1
+ require "uri"
2
+ require "digest/md5"
3
+ require "json"
4
+ require "httpclient"
5
+
6
+ module Embulk
7
+ module Input
8
+ module MixpanelApi
9
+ class Client
10
+ ENDPOINT_EXPORT = "https://data.mixpanel.com/api/2.0/export/".freeze
11
+ TIMEOUT_SECONDS = 3600
12
+
13
+ def initialize(api_key, api_secret)
14
+ @api_key = api_key
15
+ @api_secret = api_secret
16
+ end
17
+
18
+ def export(params = {})
19
+ # https://mixpanel.com/docs/api-documentation/exporting-raw-data-you-inserted-into-mixpanel
20
+ params[:expire] ||= Time.now.to_i + TIMEOUT_SECONDS
21
+ params[:sig] = signature(params)
22
+ response = httpclient.get(ENDPOINT_EXPORT, params)
23
+
24
+ if response.code >= 400
25
+ Embulk.logger.error response.body
26
+ return Enumerator.new{ }
27
+ end
28
+
29
+ Enumerator.new do |y|
30
+ response.body.lines.each do |json|
31
+ y << JSON.parse(json)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def signature(params)
39
+ # https://mixpanel.com/docs/api-documentation/data-export-api#auth-implementation
40
+ sorted_keys = params.keys.map(&:to_s).sort
41
+ signature = sorted_keys.inject("") do |sig, key|
42
+ value = params[key] || params[key.to_sym]
43
+ next sig unless value
44
+ sig << "#{key}=#{value}"
45
+ end
46
+
47
+ Digest::MD5.hexdigest(signature + @api_secret)
48
+ end
49
+
50
+ def httpclient
51
+ @client ||=
52
+ begin
53
+ client = HTTPClient.new
54
+ client.receive_timeout = TIMEOUT_SECONDS
55
+ client.default_header = {Accept: "application/json; charset=UTF-8"}
56
+ client
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,130 @@
1
+ require "embulk/input/mixpanel_api/client"
2
+
3
+ module Embulk
4
+ module Input
5
+ module MixpanelApi
6
+ class ClientTest < Test::Unit::TestCase
7
+ API_KEY = "api_key".freeze
8
+ API_SECRET = "api_secret".freeze
9
+
10
+ def setup
11
+ @client = Client.new(API_KEY, API_SECRET)
12
+ end
13
+
14
+ # NOTE: Client#signature is private method but this value
15
+ # can't be checked via other methods.
16
+ def test_signature
17
+ now = Time.parse("2015-07-22 00:00:00")
18
+ stub(Time).now { now }
19
+
20
+ params = {
21
+ string: "string",
22
+ array: ["elem1", "elem2"],
23
+ }
24
+ expected = "4be4a4f92f57e12b543a2a5f2f5897b6"
25
+
26
+ assert_equal(expected, @client.__send__(:signature, params))
27
+ end
28
+
29
+ class ExportTest < self
30
+ def setup
31
+ super
32
+
33
+ @httpclient = HTTPClient.new
34
+ stub(@client).httpclient { @httpclient }
35
+ end
36
+
37
+ def test_response_class
38
+ stub_response(success_response)
39
+
40
+ actual = @client.export(params)
41
+
42
+ assert_equal(Enumerator, actual.class)
43
+ end
44
+
45
+ def test_http_request
46
+ mock(@httpclient).get(Client::ENDPOINT_EXPORT, params) do
47
+ success_response
48
+ end
49
+
50
+ @client.export(params)
51
+ end
52
+
53
+ def test_success
54
+ stub_response(success_response)
55
+
56
+ actual = @client.export(params)
57
+
58
+ assert_equal(dummy_responses, actual.to_a)
59
+ end
60
+
61
+ def test_failure
62
+ stub_response(failure_response)
63
+
64
+ stub(Embulk.logger).error(failure_response.body) {}
65
+ actual = @client.export(params)
66
+
67
+ assert_equal([], actual.to_a)
68
+ end
69
+
70
+ def test_failure_logging
71
+ stub_response(failure_response)
72
+
73
+ mock(Embulk.logger).error(failure_response.body) {}
74
+ @client.export(params)
75
+ end
76
+
77
+ private
78
+
79
+ def stub_response(response)
80
+ stub(@httpclient).get(Client::ENDPOINT_EXPORT, params) do
81
+ response
82
+ end
83
+ end
84
+
85
+ def success_response
86
+ Struct.new(:code, :body).new(200, jsonl_dummy_responses)
87
+ end
88
+
89
+ def failure_response
90
+ Struct.new(:code, :body).new(400, "{'error': 'invalid'}")
91
+ end
92
+
93
+ def params
94
+ {
95
+ api_key: API_KEY,
96
+ api_secret: API_SECRET,
97
+ from_date: "2015-01-01",
98
+ to_date: "2015-03-02",
99
+ }
100
+ end
101
+
102
+ def dummy_responses
103
+ [
104
+ {
105
+ "event" => "event",
106
+ "properties" => {
107
+ "foo" => "FOO",
108
+ "bar" => "2000-01-01 11:11:11",
109
+ "int" => 42,
110
+ }
111
+ },
112
+ {
113
+ "event" => "event2",
114
+ "properties" => {
115
+ "foo" => "fooooooooo",
116
+ "bar" => "1988-12-01 12:11:11",
117
+ "int" => 1,
118
+ }
119
+ },
120
+ ]
121
+ end
122
+
123
+ def jsonl_dummy_responses
124
+ dummy_responses.map{|res| JSON.dump(res)}.join("\n")
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,175 @@
1
+ require "prepare_embulk"
2
+ require "embulk/input/mixpanel"
3
+ require "json"
4
+
5
+ module Embulk
6
+ module Input
7
+ class MixpanelTest < Test::Unit::TestCase
8
+ API_KEY = "api_key".freeze
9
+ API_SECRET = "api_secret".freeze
10
+ FROM_DATE = "2015-02-22".freeze
11
+ TO_DATE = "2015-03-02".freeze
12
+
13
+ DURATIONS = [
14
+ {from_date: FROM_DATE, to_date: "2015-02-28"}, # It has 7 days between 2015-02-22 and 2015-02-28
15
+ {from_date: "2015-03-01", to_date: TO_DATE},
16
+ ]
17
+
18
+ def setup
19
+ setup_client
20
+ setup_logger
21
+ end
22
+
23
+ def setup_client
24
+ params = {
25
+ api_key: API_KEY,
26
+ event: nil,
27
+ where: nil,
28
+ bucket: nil,
29
+ }
30
+
31
+ any_instance_of(MixpanelApi::Client) do |klass|
32
+ DURATIONS.each do |duration|
33
+ from_date = duration[:from_date]
34
+ to_date = duration[:to_date]
35
+
36
+ stub(klass).export(params) { records }
37
+ end
38
+ end
39
+ end
40
+
41
+ def setup_logger
42
+ stub(Embulk).logger { ::Logger.new(IO::NULL) }
43
+ end
44
+
45
+ def test_guess
46
+ expected = {
47
+ "columns" => [
48
+ {name: "event", type: :string},
49
+ {name: "foo", type: :string},
50
+ {name: "time", type: :long},
51
+ {name: "int", type: :long},
52
+ ]
53
+ }
54
+
55
+ actual = Mixpanel.guess(embulk_config)
56
+ assert_equal(expected, actual)
57
+ end
58
+
59
+ def test_export_params
60
+ config_params = [
61
+ :type, "mixpanel",
62
+ :api_key, API_KEY,
63
+ :api_secret, API_SECRET,
64
+ :from_date, FROM_DATE,
65
+ :to_date, TO_DATE,
66
+ :event, ["ViewHoge", "ViewFuga"],
67
+ :where, 'properties["$os"] == "Windows"',
68
+ :bucket, "987",
69
+ ]
70
+
71
+ config = DataSource[*config_params]
72
+
73
+ expected = {
74
+ api_key: API_KEY,
75
+ event: "[\"ViewHoge\",\"ViewFuga\"]",
76
+ where: 'properties["$os"] == "Windows"',
77
+ bucket: "987",
78
+ }
79
+ actual = Mixpanel.export_params(config)
80
+
81
+ assert_equal(expected, actual)
82
+ end
83
+
84
+ class RunTest < self
85
+ def setup
86
+ super
87
+
88
+ @page_builder = Object.new
89
+ @plugin = Mixpanel.new(task, nil, nil, @page_builder)
90
+ end
91
+
92
+ def test_preview
93
+ stub(@plugin).preview? { true }
94
+ mock(@page_builder).add(anything).times(records.length)
95
+ mock(@page_builder).finish
96
+
97
+ @plugin.run
98
+ end
99
+
100
+ def test_run
101
+ stub(@plugin).preview? { false }
102
+ mock(@page_builder).add(anything).times(records.length * 2)
103
+ mock(@page_builder).finish
104
+
105
+ @plugin.run
106
+ end
107
+
108
+ def test_timezone
109
+ stub(@plugin).preview? { false }
110
+ adjusted = record_epoch - timezone_offset_seconds
111
+ mock(@page_builder).add(["FOO", adjusted]).times(records.length * 2)
112
+ mock(@page_builder).finish
113
+
114
+ @plugin.run
115
+ end
116
+
117
+ def test_invalid_timezone
118
+ assert_raise(TZInfo::InvalidTimezoneIdentifier) do
119
+ Mixpanel.new(task.merge(timezone: "Asia/Tokyooooooooo"), nil, nil, @page_builder).run
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def task
126
+ {
127
+ api_key: API_KEY,
128
+ api_secret: API_SECRET,
129
+ timezone: "Asia/Tokyo",
130
+ schema: [
131
+ {"name" => "foo", "type" => "long"},
132
+ {"name" => "time", "type" => "long"},
133
+ ],
134
+ dates: (Date.parse(FROM_DATE)..Date.parse(TO_DATE)).to_a,
135
+ params: Mixpanel.export_params(embulk_config),
136
+ }
137
+ end
138
+
139
+ def timezone_offset_seconds
140
+ 60 * 60 * 9 # Asia/Tokyo
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def records
147
+ [
148
+ {
149
+ "event" => "event",
150
+ "properties" => {
151
+ "foo" => "FOO",
152
+ "time" => record_epoch,
153
+ "int" => 42,
154
+ }
155
+ },
156
+ ] * 30
157
+ end
158
+
159
+ def record_epoch
160
+ 1234567890
161
+ end
162
+
163
+ def embulk_config
164
+ config = {
165
+ type: "mixpanel",
166
+ api_key: API_KEY,
167
+ api_secret: API_SECRET,
168
+ from_date: FROM_DATE,
169
+ to_date: TO_DATE,
170
+ }
171
+ DataSource[*config.to_a.flatten(1)]
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,10 @@
1
+ require "embulk/command/embulk_run"
2
+
3
+ classpath_dir = Embulk.home("classpath")
4
+ jars = Dir.entries(classpath_dir).select{|f| f =~ /\.jar$/ }.sort
5
+ jars.each do |jar|
6
+ require File.join(classpath_dir, jar)
7
+ end
8
+ require "embulk/java/bootstrap"
9
+
10
+ require "embulk"
data/test/run-test.rb ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
4
+ lib_dir = File.join(base_dir, "lib")
5
+ test_dir = File.join(base_dir, "test")
6
+
7
+ require "test-unit"
8
+ require "test/unit/rr"
9
+ require "codeclimate-test-reporter"
10
+
11
+ $LOAD_PATH.unshift(lib_dir)
12
+ $LOAD_PATH.unshift(test_dir)
13
+
14
+ ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "5000"
15
+
16
+ CodeClimate::TestReporter.start
17
+
18
+ exit Test::Unit::AutoRunner.run(true, test_dir)
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: embulk-input-mixpanel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - yoshihara
8
+ - uu59
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-07-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ name: httpclient
21
+ prerelease: false
22
+ type: :runtime
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ name: tzinfo
35
+ prerelease: false
36
+ type: :runtime
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ name: bundler
49
+ prerelease: false
50
+ type: :development
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
56
+ - !ruby/object:Gem::Dependency
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ name: rake
63
+ prerelease: false
64
+ type: :development
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '10.0'
70
+ - !ruby/object:Gem::Dependency
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.6.16
76
+ - - <
77
+ - !ruby/object:Gem::Version
78
+ version: '1.0'
79
+ name: embulk
80
+ prerelease: false
81
+ type: :development
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '>='
85
+ - !ruby/object:Gem::Version
86
+ version: 0.6.16
87
+ - - <
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ - !ruby/object:Gem::Dependency
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ name: pry
97
+ prerelease: false
98
+ type: :development
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ - !ruby/object:Gem::Dependency
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ name: test-unit
111
+ prerelease: false
112
+ type: :development
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ - !ruby/object:Gem::Dependency
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ name: test-unit-rr
125
+ prerelease: false
126
+ type: :development
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ - !ruby/object:Gem::Dependency
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ name: codeclimate-test-reporter
139
+ prerelease: false
140
+ type: :development
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ description: Loads records from Mixpanel.
147
+ email:
148
+ - h.yoshihara@everyleaf.com
149
+ - k@uu59.org
150
+ executables: []
151
+ extensions: []
152
+ extra_rdoc_files: []
153
+ files:
154
+ - .gitignore
155
+ - .travis.yml
156
+ - CHANGELOG.md
157
+ - Gemfile
158
+ - LICENSE
159
+ - README.md
160
+ - Rakefile
161
+ - embulk-input-mixpanel.gemspec
162
+ - gemfiles/embulk-0.6.16
163
+ - gemfiles/embulk-0.6.17
164
+ - gemfiles/embulk-latest
165
+ - lib/embulk/input/mixpanel.rb
166
+ - lib/embulk/input/mixpanel_api/client.rb
167
+ - test/embulk/input/mixpanel_api/test_client.rb
168
+ - test/embulk/input/test_mixpanel.rb
169
+ - test/prepare_embulk.rb
170
+ - test/run-test.rb
171
+ homepage: https://github.com/treasure-data/embulk-input-mixpanel
172
+ licenses:
173
+ - Apache2
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - '>='
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubyforge_project:
191
+ rubygems_version: 2.4.6
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: Mixpanel input plugin for Embulk
195
+ test_files:
196
+ - test/embulk/input/mixpanel_api/test_client.rb
197
+ - test/embulk/input/test_mixpanel.rb
198
+ - test/prepare_embulk.rb
199
+ - test/run-test.rb