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,83 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Mode
|
6
|
+
module Sdk
|
7
|
+
class Client
|
8
|
+
# The Response class wraps a Mode API response
|
9
|
+
#
|
10
|
+
class Response
|
11
|
+
attr_reader :http_response, :expected_codes
|
12
|
+
|
13
|
+
# Construct a new Response instance
|
14
|
+
#
|
15
|
+
# @param http_response [Net::HTTPResponse] the HTTP response
|
16
|
+
# @param expected_codes [optional, Array<Integer>] array of expected
|
17
|
+
# HTTP response codes
|
18
|
+
#
|
19
|
+
# @return [Mode::Sdk::Client::Response] the instance
|
20
|
+
#
|
21
|
+
def initialize(http_response, expected_codes = [])
|
22
|
+
@http_response = http_response
|
23
|
+
@expected_codes = expected_codes
|
24
|
+
end
|
25
|
+
|
26
|
+
# The HTTP response code
|
27
|
+
#
|
28
|
+
# @return [Integer] the response code
|
29
|
+
#
|
30
|
+
def code
|
31
|
+
http_response.code.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
# The parsed HTTP response body
|
35
|
+
#
|
36
|
+
# @return [Hash] the JSON response hash
|
37
|
+
#
|
38
|
+
def body
|
39
|
+
JSON.parse(http_response.body)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Validate the HTTP response code
|
43
|
+
#
|
44
|
+
# @return [true]
|
45
|
+
#
|
46
|
+
def validate!
|
47
|
+
raise_unexpected_code if unexpected_code?
|
48
|
+
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
# Raise exception due to an unexpected response code
|
53
|
+
#
|
54
|
+
# @raise [Mode::Sdk::Client::AuthenticationError if the response is 401
|
55
|
+
# @raise [Mode::Sdk::Client::AuthorizationError if the response is 403
|
56
|
+
# @raise [Mode::Sdk::Client::ResponseError] for other unexpected
|
57
|
+
# responses
|
58
|
+
#
|
59
|
+
def raise_unexpected_code
|
60
|
+
case code
|
61
|
+
when 401
|
62
|
+
fail Mode::Sdk::Client::AuthenticationError
|
63
|
+
when 403
|
64
|
+
fail Mode::Sdk::Client::AuthorizationError
|
65
|
+
else
|
66
|
+
fail Mode::Sdk::Client::ResponseError,
|
67
|
+
"Unexpected response code: #{code}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def unexpected_code?
|
74
|
+
expected_codes.any? && !expected_codes.include?(code)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class ResponseError < StandardError; end
|
79
|
+
class AuthenticationError < StandardError; end
|
80
|
+
class AuthorizationError < StandardError; end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'mode/sdk/hash_util'
|
4
|
+
require 'mode/sdk/warehouse_util'
|
5
|
+
|
6
|
+
module Mode
|
7
|
+
module Sdk
|
8
|
+
# Represents a single column of a table stored in the Mode public data
|
9
|
+
# warehouse
|
10
|
+
#
|
11
|
+
# @attr [Hash] attributes hash of column attributes
|
12
|
+
#
|
13
|
+
class Column
|
14
|
+
extend Mode::Sdk::WarehouseUtil
|
15
|
+
include Mode::Sdk::HashUtil
|
16
|
+
|
17
|
+
attr_reader :attributes
|
18
|
+
|
19
|
+
# Construct a new Column instance
|
20
|
+
#
|
21
|
+
# @param attributes [Hash] hash of column attributes
|
22
|
+
#
|
23
|
+
# @return [Mode::Sdk::Column] the instance
|
24
|
+
#
|
25
|
+
def initialize(attributes)
|
26
|
+
@attributes = stringify_keys(attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Validate the provided column attributes
|
30
|
+
#
|
31
|
+
# @return [true]
|
32
|
+
#
|
33
|
+
def validate!
|
34
|
+
validate_keys!
|
35
|
+
validate_name!
|
36
|
+
validate_type!
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
KEYS = %w(name type)
|
44
|
+
TYPES = %w(serial string integer number date time datetime boolean)
|
45
|
+
|
46
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_]{0,62}[a-z0-9]\z/
|
47
|
+
|
48
|
+
def name
|
49
|
+
attributes.fetch('name').to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def type
|
53
|
+
attributes.fetch('type').to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def keys
|
57
|
+
attributes.keys
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_keys!
|
61
|
+
KEYS.each do |key|
|
62
|
+
next if keys.include?(key)
|
63
|
+
|
64
|
+
invalid! "Column missing required key '#{key}': #{attributes.inspect}"
|
65
|
+
end
|
66
|
+
|
67
|
+
extra = keys - KEYS
|
68
|
+
|
69
|
+
invalid! "Column has unexpected keys: #{extra.inspect}" if extra.any?
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_name!
|
75
|
+
invalid! "Column name is invalid: #{name}" unless name =~ NAME_PATTERN
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_type!
|
81
|
+
unless TYPES.include?(type)
|
82
|
+
invalid! "Column type is invalid: #{type} (valid: #{TYPES.inspect})"
|
83
|
+
end
|
84
|
+
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def invalid!(message)
|
89
|
+
fail InvalidError, message
|
90
|
+
end
|
91
|
+
|
92
|
+
class InvalidError < StandardError; end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Represents a set of Mode::Column instances
|
6
|
+
#
|
7
|
+
class ColumnSet
|
8
|
+
attr_reader :original_columns
|
9
|
+
|
10
|
+
# Construct a new ColumnSet instance
|
11
|
+
#
|
12
|
+
# @param original_columns [Array<Hash>] an array of hashes defining
|
13
|
+
# column names and types
|
14
|
+
#
|
15
|
+
# @return [Mode::Sdk::ColumnSet] the instance
|
16
|
+
#
|
17
|
+
def initialize(original_columns)
|
18
|
+
@original_columns = original_columns
|
19
|
+
end
|
20
|
+
|
21
|
+
# Build an array of Mode::Sdk::Column instances
|
22
|
+
#
|
23
|
+
# @return [Array<Mode::Sdk::Column>] array of columns
|
24
|
+
#
|
25
|
+
def columns
|
26
|
+
@columns ||= (original_columns || []).map do |column|
|
27
|
+
Mode::Sdk::Column.new(column)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Validate the provided columns
|
32
|
+
#
|
33
|
+
# @raise [Mode::Sdk::ColumnSet::InvalidError] if no columns are provided
|
34
|
+
#
|
35
|
+
# @return [true]
|
36
|
+
#
|
37
|
+
def validate!
|
38
|
+
fail InvalidError, 'No columns provided' unless columns.any?
|
39
|
+
|
40
|
+
columns.each(&:validate!)
|
41
|
+
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert columns to array of hashes
|
46
|
+
#
|
47
|
+
# @return [Array<Hash>] array of column attribute hashes
|
48
|
+
#
|
49
|
+
def to_array
|
50
|
+
columns.map(&:attributes)
|
51
|
+
end
|
52
|
+
|
53
|
+
class InvalidError < StandardError; end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Manage Mode API tokens:
|
6
|
+
# [https://modeanalytics.com/settings/access_tokens]
|
7
|
+
# (https://modeanalytics.com/settings/access_tokens)
|
8
|
+
#
|
9
|
+
# API authentication documentation:
|
10
|
+
# [http://developer.modeanalytics.com/#page:authentication]
|
11
|
+
# (http://developer.modeanalytics.com/#page:authentication)
|
12
|
+
#
|
13
|
+
# @attr [String] token Mode API token
|
14
|
+
# @attr [String] secret Mode API secret
|
15
|
+
# @attr [String] host Mode API host
|
16
|
+
#
|
17
|
+
class Configuration
|
18
|
+
attr_accessor :token, :secret, :host
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
module Mode
|
4
|
+
module Sdk
|
5
|
+
# Provides simple utility methods for manipulating a Hash
|
6
|
+
#
|
7
|
+
module HashUtil
|
8
|
+
# Stringify hash keys
|
9
|
+
#
|
10
|
+
# @param hash [Hash] the original hash
|
11
|
+
#
|
12
|
+
# @return [Hash] a new hash with string keys
|
13
|
+
#
|
14
|
+
def stringify_keys(hash)
|
15
|
+
hash.each_with_object({}) do |(key, value), result|
|
16
|
+
result[key.to_s] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'mode/sdk/warehouse_util'
|
4
|
+
|
5
|
+
module Mode
|
6
|
+
module Sdk
|
7
|
+
# Represents a table stored in the Mode public data warehouse
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# table = Mode::Sdk::Table.new('sf_film_locations')
|
11
|
+
# table.columns = [
|
12
|
+
# { name: 'movie_title', type: 'string' },
|
13
|
+
# { name: 'release_year', type: 'integer' },
|
14
|
+
# { name: 'location', type: 'string' }
|
15
|
+
# ]
|
16
|
+
# table.create
|
17
|
+
#
|
18
|
+
# @attr_reader [Array<Hash>] columns an array of hashes defining column
|
19
|
+
# names and types
|
20
|
+
#
|
21
|
+
# @attr [String] description a brief table description
|
22
|
+
# @attr [String] upload_token the token of the Mode::Sdk::Upload
|
23
|
+
# containing the CSV with which to populate the table
|
24
|
+
#
|
25
|
+
class Table
|
26
|
+
extend Mode::Sdk::WarehouseUtil
|
27
|
+
|
28
|
+
# Pattern to determine whether a String is a valid table name
|
29
|
+
#
|
30
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_]{2,62}[a-z0-9]\z/
|
31
|
+
|
32
|
+
attr_reader :columns
|
33
|
+
|
34
|
+
attr_accessor :description, :upload_token
|
35
|
+
|
36
|
+
# Construct a new Table instance
|
37
|
+
#
|
38
|
+
# @param name [String] valid table name
|
39
|
+
# @param options [optional, Hash] hash of options
|
40
|
+
#
|
41
|
+
# @option options [String] :owner the username or alias of the Mode
|
42
|
+
# account associated with the table (defaults to currently
|
43
|
+
# authenticated account)
|
44
|
+
#
|
45
|
+
# @return [Mode::Sdk::Table] the instance
|
46
|
+
#
|
47
|
+
def initialize(name, options = {})
|
48
|
+
@name = name
|
49
|
+
@options = options
|
50
|
+
end
|
51
|
+
|
52
|
+
# Determine whether a table with this name already exists for the given
|
53
|
+
# Mode account
|
54
|
+
#
|
55
|
+
# @return [true, false] whether the table exists
|
56
|
+
#
|
57
|
+
def exists?
|
58
|
+
Mode::Sdk::Client.head(resource_path, expect: [200, 404]).code == 200
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create the table in Mode
|
62
|
+
#
|
63
|
+
# @return [Mode::Sdk::Client::Response] the response
|
64
|
+
#
|
65
|
+
def create
|
66
|
+
upsert(:create)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Replace an existing table in Mode
|
70
|
+
#
|
71
|
+
# @return [Mode::Sdk::Client::Response] the response
|
72
|
+
#
|
73
|
+
def replace
|
74
|
+
upsert(:replace)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Assign columns to the table and unmemoize column set, if any
|
78
|
+
#
|
79
|
+
# @param columns [Array<Hash>] an array of hashes defining column names
|
80
|
+
# and types
|
81
|
+
#
|
82
|
+
# @return [Array<Hash>] the array of columns
|
83
|
+
#
|
84
|
+
def columns=(columns)
|
85
|
+
if instance_variable_defined?(:@column_set)
|
86
|
+
remove_instance_variable(:@column_set)
|
87
|
+
end
|
88
|
+
|
89
|
+
@columns = columns
|
90
|
+
end
|
91
|
+
|
92
|
+
# The full name of the table, including owner schema
|
93
|
+
#
|
94
|
+
# @return [String] the full name
|
95
|
+
#
|
96
|
+
def full_name
|
97
|
+
[owner, name].join('.')
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
attr_reader :name, :options
|
103
|
+
|
104
|
+
def owner
|
105
|
+
options.fetch(:owner, nil) || Mode::Sdk.username
|
106
|
+
end
|
107
|
+
|
108
|
+
def collection_path
|
109
|
+
"/api/#{owner}/tables"
|
110
|
+
end
|
111
|
+
|
112
|
+
def resource_path
|
113
|
+
[collection_path, name].join('/')
|
114
|
+
end
|
115
|
+
|
116
|
+
def column_set
|
117
|
+
@column_set ||= Mode::Sdk::ColumnSet.new(columns)
|
118
|
+
end
|
119
|
+
|
120
|
+
def validate!
|
121
|
+
unless upload_token
|
122
|
+
fail InvalidError, 'Missing required attribute: upload_token'
|
123
|
+
end
|
124
|
+
|
125
|
+
unless name =~ NAME_PATTERN
|
126
|
+
fail InvalidError, "Invalid name: #{name.inspect}"
|
127
|
+
end
|
128
|
+
|
129
|
+
column_set.validate!
|
130
|
+
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
def upsert(action)
|
135
|
+
validate!
|
136
|
+
|
137
|
+
path = action == :replace ? resource_path : collection_path
|
138
|
+
verb = action == :replace ? :put : :post
|
139
|
+
|
140
|
+
response = Mode::Sdk::Client.send(
|
141
|
+
verb, path, upsert_json, expect: [202, 400])
|
142
|
+
|
143
|
+
if response.code == 400
|
144
|
+
fail InvalidError, "Could not #{action} table: #{response.body}"
|
145
|
+
end
|
146
|
+
|
147
|
+
response
|
148
|
+
end
|
149
|
+
|
150
|
+
def upsert_json
|
151
|
+
upsert_data.to_json
|
152
|
+
end
|
153
|
+
|
154
|
+
def upsert_data
|
155
|
+
{
|
156
|
+
upload_token: upload_token,
|
157
|
+
table: {
|
158
|
+
name: name,
|
159
|
+
description: description,
|
160
|
+
columns: column_set.to_array
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
class InvalidError < StandardError; end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|