mode-sdk 0.0.1

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