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.
@@ -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