mode-sdk 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/lib/mode/sdk.rb +70 -0
- data/lib/mode/sdk/client.rb +87 -0
- data/lib/mode/sdk/client/request.rb +133 -0
- data/lib/mode/sdk/client/response.rb +83 -0
- data/lib/mode/sdk/column.rb +95 -0
- data/lib/mode/sdk/column_set.rb +56 -0
- data/lib/mode/sdk/configuration.rb +21 -0
- data/lib/mode/sdk/hash_util.rb +21 -0
- data/lib/mode/sdk/table.rb +168 -0
- data/lib/mode/sdk/table_import.rb +52 -0
- data/lib/mode/sdk/upload.rb +57 -0
- data/lib/mode/sdk/version.rb +9 -0
- data/lib/mode/sdk/warehouse_util.rb +35 -0
- data/mode-sdk.gemspec +27 -0
- data/spec/lib/mode/sdk/client/request_spec.rb +105 -0
- data/spec/lib/mode/sdk/client/response_spec.rb +65 -0
- data/spec/lib/mode/sdk/client_spec.rb +71 -0
- data/spec/lib/mode/sdk/column_set_spec.rb +53 -0
- data/spec/lib/mode/sdk/column_spec.rb +101 -0
- data/spec/lib/mode/sdk/hash_util_spec.rb +18 -0
- data/spec/lib/mode/sdk/table_import_spec.rb +33 -0
- data/spec/lib/mode/sdk/table_spec.rb +191 -0
- data/spec/lib/mode/sdk/upload_spec.rb +37 -0
- data/spec/lib/mode/sdk/warehouse_util_spec.rb +40 -0
- data/spec/lib/mode/sdk_spec.rb +50 -0
- data/spec/spec_helper.rb +17 -0
- metadata +145 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Represents the status of a Mode table import job
|
6
|
+
#
|
7
|
+
class TableImport
|
8
|
+
# Construct a new TableImport instance
|
9
|
+
#
|
10
|
+
# @param resource_path [String] Mode API resource path
|
11
|
+
#
|
12
|
+
# @return [Mode::Sdk::TableImport] the instance
|
13
|
+
#
|
14
|
+
def initialize(resource_path)
|
15
|
+
@resource_path = resource_path
|
16
|
+
end
|
17
|
+
|
18
|
+
# Poll the API representation of the table import until the job is no
|
19
|
+
# longer running
|
20
|
+
#
|
21
|
+
# @param interval [optional, Integer, Float] polling interval in seconds
|
22
|
+
#
|
23
|
+
# @yield [Hash] the API representation of the table import job
|
24
|
+
#
|
25
|
+
def poll(interval = 0.5)
|
26
|
+
loop do
|
27
|
+
repr = fetch_repr
|
28
|
+
|
29
|
+
yield repr
|
30
|
+
|
31
|
+
if %w(new enqueued running).include?(repr.fetch('state'))
|
32
|
+
sleep interval
|
33
|
+
else
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :resource_path
|
42
|
+
|
43
|
+
# Get the API representation of the resource from the Mode API
|
44
|
+
#
|
45
|
+
# @return [Hash] the API representation
|
46
|
+
#
|
47
|
+
def fetch_repr
|
48
|
+
Mode::Sdk::Client.get("#{resource_path}?embed[table]=1").body
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Represents a Mode Upload containing raw CSV with no header
|
6
|
+
#
|
7
|
+
class Upload
|
8
|
+
attr_reader :content, :options
|
9
|
+
|
10
|
+
# Construct a new Upload instance
|
11
|
+
#
|
12
|
+
# @param content [String, #read] the content
|
13
|
+
# @param options [optional, Hash] hash of options
|
14
|
+
#
|
15
|
+
# @option options [Integer] :content_size the size of the provided
|
16
|
+
# content
|
17
|
+
#
|
18
|
+
# @return [Mode::Sdk::Upload] the instance
|
19
|
+
#
|
20
|
+
def initialize(content, options = {})
|
21
|
+
@content = content
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create the Upload through the Mode API
|
26
|
+
#
|
27
|
+
# @return [Mode::Sdk::Client::Response] the response
|
28
|
+
#
|
29
|
+
def create
|
30
|
+
@create ||= Mode::Sdk::Client.post(
|
31
|
+
collection_path,
|
32
|
+
content,
|
33
|
+
content_type: 'application/csv',
|
34
|
+
content_length: content_length,
|
35
|
+
expect: [201])
|
36
|
+
end
|
37
|
+
|
38
|
+
# The unique token assigned to the upload
|
39
|
+
#
|
40
|
+
# @return [String] the token
|
41
|
+
#
|
42
|
+
def token
|
43
|
+
create.body.fetch('token')
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def content_length
|
49
|
+
options.fetch(:content_length, nil) || content.size
|
50
|
+
end
|
51
|
+
|
52
|
+
def collection_path
|
53
|
+
'/api/uploads'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Provides simple utility methods for use with the Mode public data
|
6
|
+
# warehouse
|
7
|
+
#
|
8
|
+
module WarehouseUtil
|
9
|
+
# Normalize and truncate a string for use as a Mode warehouse table or
|
10
|
+
# column name
|
11
|
+
#
|
12
|
+
# @param name [String] the original name
|
13
|
+
#
|
14
|
+
# @return [String] the normalized name
|
15
|
+
#
|
16
|
+
def normalize_name(name)
|
17
|
+
normalize_string(name)[0..63]
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def normalize_string(name)
|
23
|
+
name
|
24
|
+
.downcase # no uppercase characters
|
25
|
+
.strip # no leading or trailing whitespace
|
26
|
+
.gsub(/\s+/, ' ') # no multiple consecutive spaces
|
27
|
+
.gsub(/-/, '_') # no hyphens
|
28
|
+
.gsub(/[^\w\s_]/, '') # no unexpected characters
|
29
|
+
.gsub(/\s/, '_') # no spaces
|
30
|
+
.gsub(/_+/, '_') # no multiple consecutive underscores
|
31
|
+
.gsub(/(^_)|(_$)/, '') # no leading or trailing underscores
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/mode-sdk.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'mode/sdk/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'mode-sdk'
|
10
|
+
spec.version = Mode::Sdk::VERSION
|
11
|
+
spec.authors = ['Heather Rivers']
|
12
|
+
spec.email = ['heather@modeanalytics.com']
|
13
|
+
spec.description = 'Mode Ruby SDK'
|
14
|
+
spec.summary = 'Ruby SDK for interacting with the Mode Analytics API'
|
15
|
+
spec.homepage = 'https://github.com/mode/mode-ruby-sdk'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
19
|
+
spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)/)
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'simplecov'
|
27
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mode::Sdk::Client::Request do
|
6
|
+
describe '#response' do
|
7
|
+
let :request do
|
8
|
+
Mode::Sdk::Client::Request.new(Net::HTTP::Head.new('some/path'))
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'performs http request' do
|
12
|
+
expect(Mode::Sdk::Client::Request.http).to receive(:request)
|
13
|
+
|
14
|
+
expect(request.response).to(
|
15
|
+
be_an_instance_of(Mode::Sdk::Client::Response))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#build_http_request' do
|
20
|
+
let :head do
|
21
|
+
Net::HTTP::Head.new('some/path')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'sets default headers' do
|
25
|
+
request = Mode::Sdk::Client::Request.new(head)
|
26
|
+
|
27
|
+
http_request = request.send(:build_http_request)
|
28
|
+
|
29
|
+
expect(http_request['accept']).to match(/application\/json\Z/)
|
30
|
+
expect(http_request['user-agent']).to match(/mode\-sdk/)
|
31
|
+
expect(http_request['authorization']).to match(/Basic/)
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'content' do
|
35
|
+
it 'does not assign nil body' do
|
36
|
+
request = Mode::Sdk::Client::Request.new(head)
|
37
|
+
|
38
|
+
expect(head).to receive(:body=).never
|
39
|
+
expect(head).to receive(:body_stream=).never
|
40
|
+
|
41
|
+
request.send(:build_http_request)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'handles string body' do
|
45
|
+
request = Mode::Sdk::Client::Request.new(head, 'content')
|
46
|
+
|
47
|
+
expect(head).to receive(:body=).once
|
48
|
+
expect(head).to receive(:body_stream=).never
|
49
|
+
|
50
|
+
request.send(:build_http_request)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'handles io body' do
|
54
|
+
request = Mode::Sdk::Client::Request.new(head, StringIO.new)
|
55
|
+
|
56
|
+
expect(head).to receive(:body=).never
|
57
|
+
expect(head).to receive(:body_stream=).once
|
58
|
+
|
59
|
+
request.send(:build_http_request)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'content_type' do
|
64
|
+
it 'has default value' do
|
65
|
+
request = Mode::Sdk::Client::Request.new(head)
|
66
|
+
|
67
|
+
http_request = request.send(:build_http_request)
|
68
|
+
|
69
|
+
expect(http_request.content_type).to eq('application/json')
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'allows override' do
|
73
|
+
request = Mode::Sdk::Client::Request.new(
|
74
|
+
head,
|
75
|
+
nil,
|
76
|
+
content_type: 'application/lol')
|
77
|
+
|
78
|
+
http_request = request.send(:build_http_request)
|
79
|
+
|
80
|
+
expect(http_request.content_type).to eq('application/lol')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'content_length' do
|
85
|
+
it 'is not set by default' do
|
86
|
+
request = Mode::Sdk::Client::Request.new(head)
|
87
|
+
|
88
|
+
http_request = request.send(:build_http_request)
|
89
|
+
|
90
|
+
expect(http_request.content_length).to be_nil
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'is set if provided' do
|
94
|
+
request = Mode::Sdk::Client::Request.new(
|
95
|
+
head,
|
96
|
+
nil,
|
97
|
+
content_length: 42)
|
98
|
+
|
99
|
+
http_request = request.send(:build_http_request)
|
100
|
+
|
101
|
+
expect(http_request.content_length).to eq(42)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mode::Sdk::Client::Response do
|
6
|
+
describe '#code' do
|
7
|
+
it 'returns http response code as an integer' do
|
8
|
+
http_response = double(:http_response, code: '200')
|
9
|
+
|
10
|
+
response = Mode::Sdk::Client::Response.new(http_response)
|
11
|
+
|
12
|
+
expect(response.code).to eq(200)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#body' do
|
17
|
+
it 'parses response body' do
|
18
|
+
http_response = double(:http_response, body: '{"foo":"bar"}')
|
19
|
+
|
20
|
+
response = Mode::Sdk::Client::Response.new(http_response)
|
21
|
+
|
22
|
+
expect(response.body).to eq('foo' => 'bar')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#validate!' do
|
27
|
+
it 'raises exception if response is 401' do
|
28
|
+
http_response = double(:http_response, code: '401')
|
29
|
+
|
30
|
+
response = Mode::Sdk::Client::Response.new(http_response, [200])
|
31
|
+
|
32
|
+
expect {
|
33
|
+
response.validate!
|
34
|
+
}.to raise_error(Mode::Sdk::Client::AuthenticationError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'raises exception if response is 403' do
|
38
|
+
http_response = double(:http_response, code: '403')
|
39
|
+
|
40
|
+
response = Mode::Sdk::Client::Response.new(http_response, [200])
|
41
|
+
|
42
|
+
expect {
|
43
|
+
response.validate!
|
44
|
+
}.to raise_error(Mode::Sdk::Client::AuthorizationError)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'raises exception if response is unexpected' do
|
48
|
+
http_response = double(:http_response, code: '200')
|
49
|
+
|
50
|
+
response = Mode::Sdk::Client::Response.new(http_response, [201])
|
51
|
+
|
52
|
+
expect {
|
53
|
+
response.validate!
|
54
|
+
}.to raise_error(Mode::Sdk::Client::ResponseError)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns true if response is expected' do
|
58
|
+
http_response = double(:http_response, code: '201')
|
59
|
+
|
60
|
+
response = Mode::Sdk::Client::Response.new(http_response, [201])
|
61
|
+
|
62
|
+
expect(response.validate!).to eq(true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mode::Sdk::Client do
|
6
|
+
describe '.head' do
|
7
|
+
it 'performs a head request' do
|
8
|
+
expect(Mode::Sdk::Client).to receive(:request) do |request|
|
9
|
+
expect(request).to be_an_instance_of(Net::HTTP::Head)
|
10
|
+
end
|
11
|
+
|
12
|
+
Mode::Sdk::Client.head('some/path')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.get' do
|
17
|
+
it 'performs a get request' do
|
18
|
+
expect(Mode::Sdk::Client).to receive(:request) do |request|
|
19
|
+
expect(request).to be_an_instance_of(Net::HTTP::Get)
|
20
|
+
end
|
21
|
+
|
22
|
+
Mode::Sdk::Client.get('some/path')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.post' do
|
27
|
+
it 'performs a post request' do
|
28
|
+
expect(Mode::Sdk::Client).to receive(:request) do |request|
|
29
|
+
expect(request).to be_an_instance_of(Net::HTTP::Post)
|
30
|
+
end
|
31
|
+
|
32
|
+
Mode::Sdk::Client.post('some/path', nil)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.put' do
|
37
|
+
it 'performs a put request' do
|
38
|
+
expect(Mode::Sdk::Client).to receive(:request) do |request|
|
39
|
+
expect(request).to be_an_instance_of(Net::HTTP::Put)
|
40
|
+
end
|
41
|
+
|
42
|
+
Mode::Sdk::Client.put('some/path', nil)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '.request' do
|
47
|
+
it 'returns response' do
|
48
|
+
mock_response = Mode::Sdk::Client::Response.new(nil)
|
49
|
+
|
50
|
+
expect_any_instance_of(Mode::Sdk::Client::Request).to(
|
51
|
+
receive(:response).and_return(mock_response))
|
52
|
+
|
53
|
+
response = Mode::Sdk::Client.send(:request, nil, nil)
|
54
|
+
|
55
|
+
expect(response).to be_an_instance_of(Mode::Sdk::Client::Response)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '.account' do
|
60
|
+
it 'returns account representation' do
|
61
|
+
mock_response = double(:response, body: { 'username' => 'someone' })
|
62
|
+
|
63
|
+
expect_any_instance_of(Mode::Sdk::Client::Request).to(
|
64
|
+
receive(:response).and_return(mock_response))
|
65
|
+
|
66
|
+
response = Mode::Sdk::Client.account
|
67
|
+
|
68
|
+
expect(response).to eq('username' => 'someone')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Mode::Sdk::ColumnSet do
|
6
|
+
describe '#columns' do
|
7
|
+
it 'builds an array of columns from hashes' do
|
8
|
+
set = Mode::Sdk::ColumnSet.new([{}, {}])
|
9
|
+
|
10
|
+
expect(set.columns.map(&:class)).to eq([
|
11
|
+
Mode::Sdk::Column, Mode::Sdk::Column])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#validate!' do
|
16
|
+
it 'raises error if no columns are provided' do
|
17
|
+
set = Mode::Sdk::ColumnSet.new(nil)
|
18
|
+
|
19
|
+
expect {
|
20
|
+
set.validate!
|
21
|
+
}.to raise_error(Mode::Sdk::ColumnSet::InvalidError) do |exception|
|
22
|
+
expect(exception.message).to match(/no columns provided/i)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'raises error if column is invalid' do
|
27
|
+
expect_any_instance_of(Mode::Sdk::Column).to receive(
|
28
|
+
:validate!).and_raise(Mode::Sdk::Column::InvalidError)
|
29
|
+
|
30
|
+
set = Mode::Sdk::ColumnSet.new([{}])
|
31
|
+
|
32
|
+
expect {
|
33
|
+
set.validate!
|
34
|
+
}.to raise_error(Mode::Sdk::Column::InvalidError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns true if columns are valid' do
|
38
|
+
expect_any_instance_of(Mode::Sdk::Column).to receive(:validate!)
|
39
|
+
|
40
|
+
set = Mode::Sdk::ColumnSet.new([{}])
|
41
|
+
|
42
|
+
expect(set.validate!).to eq(true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#to_array' do
|
47
|
+
it 'converts columns to attributes' do
|
48
|
+
set = Mode::Sdk::ColumnSet.new([{ foo: 'bar' }])
|
49
|
+
|
50
|
+
expect(set.to_array).to eq([{ 'foo' => 'bar' }])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|