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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/README.md +17 -22
  4. data/bin/mode +1 -1
  5. data/lib/mode.rb +34 -6
  6. data/lib/mode/api/form.rb +53 -0
  7. data/lib/mode/api/link.rb +31 -0
  8. data/lib/mode/api/request.rb +181 -0
  9. data/lib/mode/api/resource.rb +67 -0
  10. data/lib/mode/auth/access_token.rb +23 -0
  11. data/lib/mode/cli.rb +3 -3
  12. data/lib/mode/cli/analyze.rb +1 -1
  13. data/lib/mode/cli/base.rb +5 -0
  14. data/lib/mode/cli/connect.rb +18 -0
  15. data/lib/mode/cli/helpers.rb +0 -9
  16. data/lib/mode/cli/import.rb +9 -38
  17. data/lib/mode/cli/login.rb +13 -0
  18. data/lib/mode/cli/package.rb +2 -5
  19. data/lib/mode/commands/analyze_field.rb +20 -21
  20. data/lib/mode/commands/analyze_schema.rb +69 -48
  21. data/lib/mode/commands/connect.rb +78 -0
  22. data/lib/mode/commands/helpers.rb +54 -0
  23. data/lib/mode/commands/import.rb +209 -20
  24. data/lib/mode/commands/login.rb +111 -0
  25. data/lib/mode/config.rb +13 -33
  26. data/lib/mode/configurable.rb +46 -0
  27. data/lib/mode/connector/config.rb +31 -0
  28. data/lib/mode/connector/daemon.rb +27 -0
  29. data/lib/mode/connector/data_source.rb +75 -0
  30. data/lib/mode/connector/dataset.rb +13 -0
  31. data/lib/mode/connector/message.rb +31 -0
  32. data/lib/mode/connector/poller.rb +27 -0
  33. data/lib/mode/connector/processor.rb +58 -0
  34. data/lib/mode/connector/registrar.rb +36 -0
  35. data/lib/mode/connector/scheduler.rb +62 -0
  36. data/lib/mode/connector/selector.rb +47 -0
  37. data/lib/mode/connector/type_map.rb +45 -0
  38. data/lib/mode/connector/uploader.rb +50 -0
  39. data/lib/mode/logger.rb +202 -0
  40. data/lib/mode/version.rb +1 -1
  41. data/mode.gemspec +13 -2
  42. data/spec/api/form_spec.rb +51 -0
  43. data/spec/api/link_spec.rb +23 -0
  44. data/spec/api/request_spec.rb +111 -0
  45. data/spec/api/resource_spec.rb +70 -0
  46. data/spec/auth/access_token_spec.rb +22 -0
  47. data/spec/commands/analyze_field_spec.rb +26 -0
  48. data/spec/commands/analyze_schema_spec.rb +7 -5
  49. data/spec/commands/connect_spec.rb +80 -0
  50. data/spec/commands/helpers_spec.rb +69 -0
  51. data/spec/commands/import_spec.rb +155 -0
  52. data/spec/commands/login_spec.rb +178 -0
  53. data/spec/config_spec.rb +9 -7
  54. data/spec/connector/config_spec.rb +46 -0
  55. data/spec/connector/daemon_spec.rb +30 -0
  56. data/spec/connector/data_source_spec.rb +73 -0
  57. data/spec/connector/message_spec.rb +22 -0
  58. data/spec/connector/poller_spec.rb +26 -0
  59. data/spec/connector/processor_spec.rb +93 -0
  60. data/spec/connector/registrar_spec.rb +53 -0
  61. data/spec/connector/scheduler_spec.rb +93 -0
  62. data/spec/connector/selector_spec.rb +54 -0
  63. data/spec/connector/type_map_spec.rb +45 -0
  64. data/spec/connector/uploader_spec.rb +55 -0
  65. data/spec/fixtures/country-codes/README.md +71 -0
  66. data/spec/fixtures/country-codes/data/country-codes.csv +250 -0
  67. data/spec/fixtures/country-codes/datapackage.json +142 -0
  68. data/spec/fixtures/country-codes/scripts/get_countries_of_earth.py +370 -0
  69. data/spec/fixtures/country-codes/scripts/reorder_columns.py +8 -0
  70. data/spec/fixtures/country-codes/scripts/requirements.pip +2 -0
  71. data/spec/fixtures/espn_draft.csv +473 -1
  72. data/spec/fixtures/espn_draft/data.csv +473 -0
  73. data/spec/fixtures/espn_draft/datapackage.json +43 -0
  74. data/spec/logger_spec.rb +79 -0
  75. data/spec/spec_helper.rb +6 -1
  76. metadata +156 -19
  77. data/lib/mode/cli/setup.rb +0 -12
  78. data/lib/mode/commands/package.rb +0 -56
  79. data/lib/mode/commands/setup.rb +0 -36
  80. data/lib/mode/package_builder.rb +0 -57
  81. data/spec/commands/setup_spec.rb +0 -62
  82. data/spec/fixtures/MOCK_DATA.csv +0 -100001
  83. data/spec/fixtures/cb_clean_small.csv +0 -100000
  84. data/spec/fixtures/duplicate_keys.csv +0 -3
  85. data/spec/fixtures/format_examples.csv.txt +0 -6
  86. 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
@@ -1,36 +1,225 @@
1
1
  module Mode
2
2
  module Commands
3
- class Import < Thor
4
- attr_accessor :account
5
- attr_accessor :table_name
6
- attr_accessor :package
7
- attr_accessor :resource_name
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, package, resource_name)
10
+ def initialize(source, account, table_name)
11
+ @source = source
10
12
  @account = account
11
13
  @table_name = table_name
12
- @package = package
13
- @resource_name = resource_name
14
+
15
+ require_config!
16
+ require_credentials!
17
+ configure_api_requests!
14
18
  end
15
19
 
16
- no_commands do
17
- def execute
20
+ def execute
21
+ package = make_package
22
+ uploaded = upload_package(package)
23
+ imported = import_package(uploaded)
24
+ end
18
25
 
19
- puts "Importing #{package.name}/#{resource_name} to #{account}/#{table_name}"
20
- # We need to check the datapackage.md5
21
- # We need to check the md5 of the source
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
- # 1. Compress to temporary dir
25
- # 2. POST /imports with name and zipfile
26
- # 3. Poll for execution status until finished or timeout (what's the timeout?)
27
- end
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 valid_package?(package)
33
- # package.valid?
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
@@ -2,52 +2,32 @@ require 'yaml'
2
2
 
3
3
  module Mode
4
4
  class Config
5
- FILENAME = '.mode.yml'
5
+ include Mode::Configurable
6
6
 
7
- attr_accessor :path
8
-
9
- attr_accessor :username, :access_token
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 init(path, filename = nil)
29
- File.open(full_path(path, filename), 'w+') do |file|
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
- private
42
- def configure(config)
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