mode 0.0.5 → 0.0.7
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 +4 -4
- data/.gitignore +2 -1
- data/README.md +17 -22
- data/bin/mode +1 -1
- data/lib/mode.rb +34 -6
- data/lib/mode/api/form.rb +53 -0
- data/lib/mode/api/link.rb +31 -0
- data/lib/mode/api/request.rb +181 -0
- data/lib/mode/api/resource.rb +67 -0
- data/lib/mode/auth/access_token.rb +23 -0
- data/lib/mode/cli.rb +3 -3
- data/lib/mode/cli/analyze.rb +1 -1
- data/lib/mode/cli/base.rb +5 -0
- data/lib/mode/cli/connect.rb +18 -0
- data/lib/mode/cli/helpers.rb +0 -9
- data/lib/mode/cli/import.rb +9 -38
- data/lib/mode/cli/login.rb +13 -0
- data/lib/mode/cli/package.rb +2 -5
- data/lib/mode/commands/analyze_field.rb +20 -21
- data/lib/mode/commands/analyze_schema.rb +69 -48
- data/lib/mode/commands/connect.rb +78 -0
- data/lib/mode/commands/helpers.rb +54 -0
- data/lib/mode/commands/import.rb +209 -20
- data/lib/mode/commands/login.rb +111 -0
- data/lib/mode/config.rb +13 -33
- data/lib/mode/configurable.rb +46 -0
- data/lib/mode/connector/config.rb +31 -0
- data/lib/mode/connector/daemon.rb +27 -0
- data/lib/mode/connector/data_source.rb +75 -0
- data/lib/mode/connector/dataset.rb +13 -0
- data/lib/mode/connector/message.rb +31 -0
- data/lib/mode/connector/poller.rb +27 -0
- data/lib/mode/connector/processor.rb +58 -0
- data/lib/mode/connector/registrar.rb +36 -0
- data/lib/mode/connector/scheduler.rb +62 -0
- data/lib/mode/connector/selector.rb +47 -0
- data/lib/mode/connector/type_map.rb +45 -0
- data/lib/mode/connector/uploader.rb +50 -0
- data/lib/mode/logger.rb +202 -0
- data/lib/mode/version.rb +1 -1
- data/mode.gemspec +13 -2
- data/spec/api/form_spec.rb +51 -0
- data/spec/api/link_spec.rb +23 -0
- data/spec/api/request_spec.rb +111 -0
- data/spec/api/resource_spec.rb +70 -0
- data/spec/auth/access_token_spec.rb +22 -0
- data/spec/commands/analyze_field_spec.rb +26 -0
- data/spec/commands/analyze_schema_spec.rb +7 -5
- data/spec/commands/connect_spec.rb +80 -0
- data/spec/commands/helpers_spec.rb +69 -0
- data/spec/commands/import_spec.rb +155 -0
- data/spec/commands/login_spec.rb +178 -0
- data/spec/config_spec.rb +9 -7
- data/spec/connector/config_spec.rb +46 -0
- data/spec/connector/daemon_spec.rb +30 -0
- data/spec/connector/data_source_spec.rb +73 -0
- data/spec/connector/message_spec.rb +22 -0
- data/spec/connector/poller_spec.rb +26 -0
- data/spec/connector/processor_spec.rb +93 -0
- data/spec/connector/registrar_spec.rb +53 -0
- data/spec/connector/scheduler_spec.rb +93 -0
- data/spec/connector/selector_spec.rb +54 -0
- data/spec/connector/type_map_spec.rb +45 -0
- data/spec/connector/uploader_spec.rb +55 -0
- data/spec/fixtures/country-codes/README.md +71 -0
- data/spec/fixtures/country-codes/data/country-codes.csv +250 -0
- data/spec/fixtures/country-codes/datapackage.json +142 -0
- data/spec/fixtures/country-codes/scripts/get_countries_of_earth.py +370 -0
- data/spec/fixtures/country-codes/scripts/reorder_columns.py +8 -0
- data/spec/fixtures/country-codes/scripts/requirements.pip +2 -0
- data/spec/fixtures/espn_draft.csv +473 -1
- data/spec/fixtures/espn_draft/data.csv +473 -0
- data/spec/fixtures/espn_draft/datapackage.json +43 -0
- data/spec/logger_spec.rb +79 -0
- data/spec/spec_helper.rb +6 -1
- metadata +156 -19
- data/lib/mode/cli/setup.rb +0 -12
- data/lib/mode/commands/package.rb +0 -56
- data/lib/mode/commands/setup.rb +0 -36
- data/lib/mode/package_builder.rb +0 -57
- data/spec/commands/setup_spec.rb +0 -62
- data/spec/fixtures/MOCK_DATA.csv +0 -100001
- data/spec/fixtures/cb_clean_small.csv +0 -100000
- data/spec/fixtures/duplicate_keys.csv +0 -3
- data/spec/fixtures/format_examples.csv.txt +0 -6
- data/spec/fixtures/format_examples_after_excel.csv.txt +0 -1
@@ -5,6 +5,60 @@ module Mode
|
|
5
5
|
started_at = Time.now
|
6
6
|
return yield, (Time.now.to_f - started_at.to_f)
|
7
7
|
end
|
8
|
+
|
9
|
+
def config_dir
|
10
|
+
Mode::Config.default_dir
|
11
|
+
end
|
12
|
+
|
13
|
+
def config_path
|
14
|
+
Mode::Config.full_path(config_dir)
|
15
|
+
end
|
16
|
+
|
17
|
+
def config_exists?
|
18
|
+
Mode::Config.exists?(config_dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect_config_exists?
|
22
|
+
Mode::Connector::Config.exists?(config_dir)
|
23
|
+
end
|
24
|
+
|
25
|
+
def require_config!
|
26
|
+
unless config_exists?
|
27
|
+
raise "Couldn't find Mode configuration. Please use `mode login` before continuing."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def require_connect_config!
|
32
|
+
unless connect_config_exists?
|
33
|
+
raise "No connect configuration file found."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def require_credentials!
|
38
|
+
message = "Couldn't find Mode login credentials. Please use `mode login` before continuing."
|
39
|
+
|
40
|
+
if config_exists?
|
41
|
+
config = Mode::Config.new(config_dir)
|
42
|
+
unless config.username && config.access_token
|
43
|
+
raise message
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise message
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def configure_api_requests!
|
51
|
+
config = Mode::Config.new(config_dir)
|
52
|
+
|
53
|
+
Mode::API::Request.configure(
|
54
|
+
config.environment, {
|
55
|
+
:credentials => {
|
56
|
+
:username => config.username,
|
57
|
+
:access_token => config.access_token
|
58
|
+
}
|
59
|
+
}
|
60
|
+
)
|
61
|
+
end
|
8
62
|
end
|
9
63
|
end
|
10
64
|
end
|
data/lib/mode/commands/import.rb
CHANGED
@@ -1,36 +1,225 @@
|
|
1
1
|
module Mode
|
2
2
|
module Commands
|
3
|
-
class Import
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
class Import
|
4
|
+
include Mode::Commands::Helpers
|
5
|
+
|
6
|
+
attr_reader :source
|
7
|
+
attr_reader :account
|
8
|
+
attr_reader :table_name
|
8
9
|
|
9
|
-
def initialize(account, table_name
|
10
|
+
def initialize(source, account, table_name)
|
11
|
+
@source = source
|
10
12
|
@account = account
|
11
13
|
@table_name = table_name
|
12
|
-
|
13
|
-
|
14
|
+
|
15
|
+
require_config!
|
16
|
+
require_credentials!
|
17
|
+
configure_api_requests!
|
14
18
|
end
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
def execute
|
21
|
+
package = make_package
|
22
|
+
uploaded = upload_package(package)
|
23
|
+
imported = import_package(uploaded)
|
24
|
+
end
|
18
25
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
# If either of these have changed the package needs to be reverified
|
26
|
+
def make_package
|
27
|
+
find_or_build_package(source, :name => table_name)
|
28
|
+
end
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
30
|
+
def upload_package(package)
|
31
|
+
resource = package.resources.first
|
32
|
+
file_path = File.join(resource.base_path, resource.path)
|
33
|
+
|
34
|
+
replace_resource_path!(resource)
|
35
|
+
|
36
|
+
Mode::API::Request.post(upload_path, :package => {
|
37
|
+
:contents => {
|
38
|
+
'datapackage.json' => package.to_json,
|
39
|
+
resource.path => Faraday::UploadIO.new(file_path, 'application/octet-stream')
|
40
|
+
}, :name => table_name
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
def import_package(uploaded)
|
45
|
+
resource = parse_uploaded_response(uploaded)
|
46
|
+
submit_import_form(resource, { 'table_name' => table_name })
|
47
|
+
end
|
48
|
+
|
49
|
+
def target_resource_path(path)
|
50
|
+
self.class.target_resource_path(path)
|
28
51
|
end
|
29
52
|
|
30
53
|
private
|
31
54
|
|
32
|
-
def
|
33
|
-
|
55
|
+
def upload_path
|
56
|
+
Mode::API::Request.packages_path
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_or_build_package(source, options = {})
|
60
|
+
self.class.find_or_build_package(source, options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# We replace the resource path with the desired path on the remote end
|
64
|
+
def replace_resource_path!(resource)
|
65
|
+
resource.path = target_resource_path(resource.path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def submit_import_form(resource, params)
|
69
|
+
resource.forms('import').submit!('import' => params)
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_uploaded_response(content)
|
73
|
+
resource = Mode::API::Resource.new(content)
|
74
|
+
end
|
75
|
+
|
76
|
+
class << self
|
77
|
+
#
|
78
|
+
# Utilities that can be useful elsewhere
|
79
|
+
#
|
80
|
+
|
81
|
+
def find_package(source, path = nil)
|
82
|
+
path = path || search_path(source)
|
83
|
+
|
84
|
+
if path =~ /\/data$/
|
85
|
+
parent = parent_path(path)
|
86
|
+
find_package(source, parent)
|
87
|
+
elsif package_exists?(path)
|
88
|
+
DataPackage::Package.open(path)
|
89
|
+
else
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_package(source, options = {})
|
95
|
+
name = options[:name] || 'data'
|
96
|
+
dest = options[:dest] || tmpdir
|
97
|
+
|
98
|
+
if File.exist?(source)
|
99
|
+
converter = convert(source)
|
100
|
+
DataPackage::Package.new(dest).tap do |package|
|
101
|
+
package.name = name
|
102
|
+
package.version = '0.0.1'
|
103
|
+
package.resources << build_resource(converter)
|
104
|
+
end
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_or_build_package(source, options = {})
|
111
|
+
package = find_package(source)
|
112
|
+
|
113
|
+
if package.nil?
|
114
|
+
build_opts = {
|
115
|
+
:name => options[:name],
|
116
|
+
:dest => options[:dest]
|
117
|
+
}
|
118
|
+
package = build_package(source, build_opts)
|
119
|
+
else
|
120
|
+
package = prune_package(source, package)
|
121
|
+
end
|
122
|
+
|
123
|
+
package
|
124
|
+
end
|
125
|
+
|
126
|
+
def target_resource_path(path = nil)
|
127
|
+
"data/index#{path.nil? ? '.csv' : File.extname(path)}"
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
#
|
133
|
+
# Package Building
|
134
|
+
#
|
135
|
+
|
136
|
+
def build_resource(converter)
|
137
|
+
DataPackage::Resource.new(tmpdir, {
|
138
|
+
'name' => 'index.csv',
|
139
|
+
'path' => target_resource_path,
|
140
|
+
'size' => File.size?(target),
|
141
|
+
'schema' => build_schema(converter)
|
142
|
+
})
|
143
|
+
end
|
144
|
+
|
145
|
+
def build_schema(converter)
|
146
|
+
{
|
147
|
+
'fields' => build_fields(converter),
|
148
|
+
'dialect' => DataPackage::Dialect.new
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
def build_fields(converter)
|
153
|
+
converter.field_types.inject([]) do |fields, (field_name, field_type)|
|
154
|
+
fields << {'name' => field_name, 'type' => field_type.to_s}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Data Conversion
|
160
|
+
#
|
161
|
+
|
162
|
+
def convert(source)
|
163
|
+
csv = DataKit::CSV::Parser.new(source)
|
164
|
+
analysis = DataKit::CSV::SchemaAnalyzer.analyze(csv, :sampling_rate => 1.0)
|
165
|
+
DataKit::CSV::Converter.convert(csv, analysis, target)
|
166
|
+
end
|
167
|
+
|
168
|
+
def tmpdir
|
169
|
+
@tmpdir ||= Dir.mktmpdir
|
170
|
+
end
|
171
|
+
|
172
|
+
def tmppath(path)
|
173
|
+
File.join(tmpdir, path)
|
174
|
+
end
|
175
|
+
|
176
|
+
def target(path = 'data', name = 'index.csv')
|
177
|
+
full_path = tmppath(path)
|
178
|
+
FileUtils.mkdir_p(full_path)
|
179
|
+
@target ||= File.join(full_path, name)
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Checks and Validations
|
184
|
+
#
|
185
|
+
|
186
|
+
def search_path(source)
|
187
|
+
File.file?(source) ? File.dirname(source) : source
|
188
|
+
end
|
189
|
+
|
190
|
+
def package_exists?(path)
|
191
|
+
DataPackage::Package.exist?(path)
|
192
|
+
end
|
193
|
+
|
194
|
+
def parent_path(path)
|
195
|
+
File.expand_path(File.join(path, '..'))
|
196
|
+
end
|
197
|
+
|
198
|
+
def resource_path(path)
|
199
|
+
matched = path.match(/((data\/)?[^\/]+)$/)
|
200
|
+
matched.nil? ? matched : matched.to_a.first
|
201
|
+
end
|
202
|
+
|
203
|
+
def first_resource(package)
|
204
|
+
package.resources.find{ |r| not r.path.nil? }
|
205
|
+
end
|
206
|
+
|
207
|
+
def find_resource(package, path)
|
208
|
+
package.resources.find{ |r| r.path == path }
|
209
|
+
end
|
210
|
+
|
211
|
+
def prune_package(source, package)
|
212
|
+
if File.directory?(source)
|
213
|
+
resource = first_resource(package)
|
214
|
+
elsif File.file?(source)
|
215
|
+
resource_path = resource_path(source)
|
216
|
+
resource = find_resource(package, resource_path)
|
217
|
+
end
|
218
|
+
|
219
|
+
package.resources = []
|
220
|
+
package.resources << resource
|
221
|
+
package
|
222
|
+
end
|
34
223
|
end
|
35
224
|
end
|
36
225
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Mode
|
2
|
+
module Commands
|
3
|
+
class Login < Thor
|
4
|
+
attr_reader :options
|
5
|
+
attr_reader :environment
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options
|
9
|
+
|
10
|
+
if options[:test]
|
11
|
+
@environment = :test
|
12
|
+
elsif options[:staging]
|
13
|
+
@environment = :staging
|
14
|
+
elsif options[:development]
|
15
|
+
@environment = :development
|
16
|
+
else
|
17
|
+
@environment = :production
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
no_commands do
|
22
|
+
include Mode::Commands::Helpers
|
23
|
+
|
24
|
+
def execute
|
25
|
+
username, password = configure_credentials
|
26
|
+
chosen_token = choose_access_token.token
|
27
|
+
update_configuration(environment, username, chosen_token)
|
28
|
+
rescue => err
|
29
|
+
say err.message
|
30
|
+
Mode::Logger.instance.error(
|
31
|
+
"Commands::Login", err.message, err.backtrace)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def configure_api(username, password)
|
38
|
+
Mode::API::Request.configure(environment, {
|
39
|
+
:credentials => {
|
40
|
+
:username => username,
|
41
|
+
:access_token => password
|
42
|
+
}
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
def configure_credentials
|
47
|
+
say "Enter your Mode credentials:"
|
48
|
+
username = ask "Username:"
|
49
|
+
password = ask "Password:", :echo => false
|
50
|
+
|
51
|
+
# go ahead and configure API
|
52
|
+
configure_api(username, password)
|
53
|
+
|
54
|
+
return username, password
|
55
|
+
end
|
56
|
+
|
57
|
+
def fetch_access_tokens
|
58
|
+
resource = Mode::API::Request.get(:access_tokens)
|
59
|
+
|
60
|
+
if resource.is_a?(Mode::API::Resource)
|
61
|
+
resource.embedded('access_tokens').map do |access_token|
|
62
|
+
Mode::Auth::AccessToken.new(access_token)
|
63
|
+
end
|
64
|
+
elsif resource.status == 401
|
65
|
+
raise "Login failed, credentials invalid"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def prompt_access_tokens(access_tokens)
|
70
|
+
say "\nPlease choose an access token to use:\n\n"
|
71
|
+
|
72
|
+
choices = []
|
73
|
+
access_tokens.each_with_index do |token, index|
|
74
|
+
choices << (index + 1).to_s
|
75
|
+
say "#{index + 1}. #{token.name}"
|
76
|
+
end
|
77
|
+
choice = ask "\nWhich token would you like to use?:", :limited_to => choices
|
78
|
+
end
|
79
|
+
|
80
|
+
def choose_access_token
|
81
|
+
access_tokens = fetch_access_tokens
|
82
|
+
|
83
|
+
if access_tokens.length == 1
|
84
|
+
access_tokens.first
|
85
|
+
else
|
86
|
+
chosen = prompt_access_tokens(access_tokens)
|
87
|
+
access_tokens[chosen.to_i - 1]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def update_configuration(environment, username, access_token)
|
92
|
+
config = find_or_create_config
|
93
|
+
config.username = username
|
94
|
+
config.access_token = access_token
|
95
|
+
config.environment = environment.to_s
|
96
|
+
config.save
|
97
|
+
|
98
|
+
say "Updated configuration at #{config.path}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_or_create_config
|
102
|
+
if config_exists?
|
103
|
+
Mode::Config.new(config_dir)
|
104
|
+
else
|
105
|
+
FileUtils.mkdir_p(config_dir)
|
106
|
+
Mode::Config.init(config_dir)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/mode/config.rb
CHANGED
@@ -2,52 +2,32 @@ require 'yaml'
|
|
2
2
|
|
3
3
|
module Mode
|
4
4
|
class Config
|
5
|
-
|
5
|
+
include Mode::Configurable
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
attr_accessor :
|
10
|
-
|
11
|
-
def initialize(path, filename = nil)
|
12
|
-
@path = self.class.full_path(path, filename)
|
13
|
-
|
14
|
-
if File.exist?(@path)
|
15
|
-
configure YAML.load_file(@path)
|
16
|
-
else
|
17
|
-
raise "Could not load configuration file from #{@path}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def save
|
22
|
-
File.open(path, 'w+') do |file|
|
23
|
-
file.write(to_yaml)
|
24
|
-
end
|
25
|
-
end
|
7
|
+
# Config Variables
|
8
|
+
attr_accessor :username
|
9
|
+
attr_accessor :access_token
|
10
|
+
attr_accessor :environment
|
26
11
|
|
27
12
|
class << self
|
28
|
-
def
|
29
|
-
|
30
|
-
file.write({}.to_yaml)
|
31
|
-
end
|
32
|
-
|
33
|
-
new(path, filename)
|
34
|
-
end
|
35
|
-
|
36
|
-
def full_path(path, filename = nil)
|
37
|
-
File.expand_path(File.join(path, filename || Mode::Config::FILENAME))
|
13
|
+
def default_filename
|
14
|
+
'config.yml'
|
38
15
|
end
|
39
16
|
end
|
40
17
|
|
41
|
-
|
42
|
-
|
18
|
+
private
|
19
|
+
|
20
|
+
def configure(config = {})
|
43
21
|
@username = config['username']
|
44
22
|
@access_token = config['access_token']
|
23
|
+
@environment = config['environment'].to_s
|
45
24
|
end
|
46
25
|
|
47
26
|
def to_yaml
|
48
27
|
{
|
49
28
|
'username' => username,
|
50
|
-
'access_token' => access_token
|
29
|
+
'access_token' => access_token,
|
30
|
+
'environment' => environment
|
51
31
|
}.to_yaml
|
52
32
|
end
|
53
33
|
end
|