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,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
|