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