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
@@ -0,0 +1,45 @@
1
+ module Mode
2
+ module Connector
3
+ class TypeMap
4
+ include Enumerable
5
+
6
+ attr_reader :types
7
+
8
+ def initialize
9
+ @types = {}
10
+ end
11
+
12
+ def each(&block)
13
+ types.each do |name, value|
14
+ yield name, type?(name)
15
+ end
16
+ end
17
+
18
+ def insert(row)
19
+ row.each do |name, value|
20
+ types[name] ||= {}
21
+ types[name][type(value)] ||= 1
22
+ end
23
+ end
24
+
25
+ def type(value)
26
+ if value.kind_of?(Integer)
27
+ :integer
28
+ elsif value.kind_of?(Float)
29
+ :number
30
+ elsif value.kind_of?(Date) || value.kind_of?(Time)
31
+ :datetime
32
+ elsif value.kind_of?(TrueClass) || value.kind_of?(FalseClass)
33
+ :boolean
34
+ else
35
+ :string
36
+ end
37
+ end
38
+
39
+ def type?(name)
40
+ keys = types[name].keys
41
+ keys.compact.length == 1 ? keys.first : :string
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ module Mode
2
+ module Connector
3
+ class Uploader
4
+ attr_reader :path
5
+ attr_reader :dataset
6
+
7
+ def initialize(path, dataset)
8
+ @path = path
9
+ @dataset = dataset
10
+ end
11
+
12
+ def perform!
13
+ Mode::API::Request.put(
14
+ path,
15
+ :execution => {
16
+ :dataset => {
17
+ :content => content,
18
+ :columns => JSON.generate(columns)
19
+ }
20
+ }
21
+ )
22
+ end
23
+
24
+ class << self
25
+ def error!(path, message, detail = nil)
26
+ Mode::API::Request.put(path,
27
+ :execution => {
28
+ :error => {
29
+ :detail => detail,
30
+ :message => message
31
+ }
32
+ }
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def columns
40
+ dataset.column_types.map do |name, type|
41
+ {:name => name, :type => type}
42
+ end
43
+ end
44
+
45
+ def content
46
+ Faraday::UploadIO.new(dataset.path, 'applicaton/octet-stream')
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,202 @@
1
+ require 'logger'
2
+
3
+ #
4
+ # LEVEL [TIME] COMPONENT: MESSAGE
5
+ # ! Detail Line 1
6
+ # ! Detail Line 2
7
+ #
8
+ # All timestamps are in UTC and ISO 8601 format.
9
+ #
10
+ # You can grep for messages of a specific level really easily:
11
+ #
12
+ # tail -f mode.log | grep '^WARN'
13
+ # You can grep for messages from a specific component really easily:
14
+ #
15
+ # tail -f mode.log | grep 'Connector::Config'
16
+ # You can even pull out full exception stack traces, plus the accompanying log message:
17
+ #
18
+ # tail -f mode.log | grep -B 1 '^\!'
19
+ #
20
+
21
+ #
22
+ # Modified singleton that allows replacement which is very useful for testing
23
+ #
24
+
25
+ module Mode
26
+ class Logger
27
+ # For Convenience
28
+ DEBUG = ::Logger::DEBUG
29
+ WARN = ::Logger::WARN
30
+ INFO = ::Logger::INFO
31
+ ERROR = ::Logger::ERROR
32
+ FATAL = ::Logger::FATAL
33
+
34
+ attr_reader :logger
35
+ attr_reader :mutex
36
+
37
+ attr_reader :enabled
38
+
39
+ def initialize(options = {})
40
+ @enabled = true
41
+ @mutex = Mutex.new # Keep it secret, keep it safe
42
+
43
+ path = options[:path] || self.class.default_path
44
+ level = options[:level] || self.class.default_level
45
+ formatter = options[:formatter] || self.class.default_formatter
46
+
47
+ @logger = ::Logger.new(path, rotate_freq)
48
+
49
+ set_log_level(level)
50
+ set_log_formatter(formatter)
51
+ end
52
+
53
+ def disable!
54
+ mutex.synchronize { @enabled = false }
55
+ end
56
+
57
+ def enable!
58
+ mutex.synchronize { @enabled = true }
59
+ end
60
+
61
+ def debug(component, message, detail = [])
62
+ log_with_formatting(Logger::DEBUG, component, message, detail)
63
+ end
64
+
65
+ def debug!
66
+ set_log_level(Logger::DEBUG)
67
+ end
68
+
69
+ def info(component, message, detail = [])
70
+ log_with_formatting(Logger::INFO, component, message, detail)
71
+ end
72
+
73
+ def info!
74
+ set_log_level(Logger::INFO)
75
+ end
76
+
77
+ def warn(component, message, detail = [])
78
+ log_with_formatting(Logger::WARN, component, message, detail)
79
+ end
80
+
81
+ def warn!
82
+ set_log_level(Logger::WARN)
83
+ end
84
+
85
+ def error(component, message, detail = [])
86
+ log_with_formatting(Logger::ERROR, component, message, detail)
87
+ end
88
+
89
+ def error!
90
+ set_log_level(Logger::ERROR)
91
+ end
92
+
93
+ def fatal(component, message, detail = [])
94
+ log_with_formatting(Logger::FATAL, component, message, detail)
95
+ end
96
+
97
+ def fatal!
98
+ set_log_level(Logger::FATAL)
99
+ end
100
+
101
+ class << self
102
+ attr_reader :mutex
103
+ attr_reader :instance
104
+
105
+ def instance(logger = nil)
106
+ @mutex ||= Mutex.new
107
+
108
+ if logger
109
+ mutex.synchronize do
110
+ @instance = logger
111
+ end
112
+ else
113
+ @instance ||= new
114
+ end
115
+ end
116
+
117
+ def default_level
118
+ Logger::INFO
119
+ end
120
+
121
+ def default_path
122
+ File.join(mode_path, 'mode.log') # This should come from config
123
+ end
124
+
125
+ def default_rotate_freq
126
+ 'weekly'
127
+ end
128
+
129
+ def default_formatter
130
+ @default_formatter ||= Proc.new do |severity, datetime, progname, message|
131
+ "#{severity} [#{format_time(datetime)}]: #{message}\n"
132
+ end
133
+ end
134
+
135
+ def format_time(time)
136
+ time_to_datetime(time).new_offset(0).iso8601
137
+ end
138
+
139
+ def time_to_datetime(time)
140
+ #
141
+ # http://stackoverflow.com/questions/279769/convert-to-from-datetime-and-time-in-ruby
142
+ #
143
+ seconds = time.sec + Rational(time.usec, 10**6)
144
+ offset = Rational(time.utc_offset, 60 * 60 * 24)
145
+ DateTime.new(time.year, time.month, time.day, time.hour, time.min, seconds, offset)
146
+ end
147
+
148
+ private
149
+
150
+ def mode_path
151
+ FileUtils.mkdir_p(File.expand_path("~/.mode"))
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def rotate_freq
158
+ self.class.default_rotate_freq
159
+ end
160
+
161
+ def set_log_level(level)
162
+ @logger.level = level
163
+ end
164
+
165
+ def set_log_formatter(formatter)
166
+ @logger.formatter = formatter
167
+ end
168
+
169
+ def log_with_formatting(severity, component, message, detail = [])
170
+ mutex.synchronize do
171
+ return unless enabled
172
+
173
+ set_log_formatter(component_formatter(component))
174
+
175
+ logger.add(severity, message)
176
+
177
+ if detail.length > 0
178
+ set_log_formatter(detail_formatter)
179
+
180
+ detail.each do |message|
181
+ logger.add(severity, message)
182
+ end
183
+ end
184
+
185
+ set_log_formatter(self.class.default_formatter)
186
+ end
187
+ end
188
+
189
+ def component_formatter(component)
190
+ @component_formatter ||= Proc.new do |severity, datetime, progname, message|
191
+ datetime = self.class.format_time(datetime)
192
+ "#{severity} [#{datetime}] #{component}: #{message}\n"
193
+ end
194
+ end
195
+
196
+ def detail_formatter
197
+ @detail_formatter ||= Proc.new do |severity, datetime, progname, message|
198
+ "! #{message}\n"
199
+ end
200
+ end
201
+ end
202
+ end
@@ -1,3 +1,3 @@
1
1
  module Mode
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -18,14 +18,25 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- # Runtime Dependencies
21
+ # CLI
22
22
  spec.add_runtime_dependency "thor"
23
23
  spec.add_runtime_dependency "terminal-table"
24
24
 
25
- # Our Libraries
25
+ # Data Packaging
26
26
  spec.add_runtime_dependency "data_kit"
27
27
  spec.add_runtime_dependency "data_package"
28
28
 
29
+ # HTTP
30
+ spec.add_runtime_dependency "faraday"
31
+ spec.add_runtime_dependency "uri_template"
32
+
33
+ # Connectivity
34
+ spec.add_runtime_dependency "sequel"
35
+
36
+ # Concurrency & Processes
37
+ spec.add_runtime_dependency "daemon-spawn"
38
+ spec.add_runtime_dependency "rufus-scheduler"
39
+
29
40
  # Development Dependencies
30
41
  spec.add_development_dependency "bundler", "~> 1.3"
31
42
  spec.add_development_dependency "rake"
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mode::API::Link do
4
+ let(:content) {
5
+ {
6
+ 'method' => 'post',
7
+ 'action' => '/resources',
8
+ 'input' => {
9
+ 'resource' => {
10
+ 'name' => {
11
+ 'type' => 'text',
12
+ 'value' => ''
13
+ },
14
+ 'widgets' => {
15
+ 'type' => 'text',
16
+ 'value' => '500'
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ before do
24
+ initialize_logger
25
+ end
26
+
27
+ it "initializes with content" do
28
+ form = Mode::API::Form.new(content)
29
+ form.method.should == content['method']
30
+ form.action.should == content['action']
31
+ form.input('resource').should == content['input']['resource']
32
+ end
33
+
34
+ it "submits with params" do
35
+ params = {
36
+ 'resource' => {
37
+ 'name' => 'test'
38
+ }
39
+ }
40
+
41
+ Mode::API::Request.should_receive(:post).with('/resources', {
42
+ 'resource' => {
43
+ 'name' => 'test',
44
+ 'widgets' => '500'
45
+ }
46
+ }).and_return(:response)
47
+
48
+ form = Mode::API::Form.new(content)
49
+ form.submit!(params).should == :response
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mode::API::Link do
4
+ let(:content) {
5
+ {
6
+ 'templated' => true,
7
+ 'href' => '/path/to{/thing}{?q}',
8
+ }
9
+ }
10
+
11
+ before do
12
+ initialize_logger
13
+ end
14
+
15
+ it "initializes with content" do
16
+ link = Mode::API::Link.new(content)
17
+ link.href.should == content['href']
18
+ link.templated.should == content['templated']
19
+
20
+ link.expand(:thing => 't').should == "/path/to/t"
21
+ link.expand(:thing => 't', :q => 'query').should == "/path/to/t?q=query"
22
+ end
23
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mode::API::Request do
4
+ let(:username) { 'besquared' }
5
+ let(:access_token) { 'access_token '}
6
+
7
+ before do
8
+ Mode::API::Request.configure(:test, {
9
+ :credentials => {
10
+ :username => username,
11
+ :access_token => access_token
12
+ }
13
+ })
14
+ end
15
+
16
+ before do
17
+ initialize_logger
18
+ end
19
+
20
+ it 'initializes with default values' do
21
+ request = Mode::API::Request.new(:get, '/packages')
22
+ request.method.should == :get
23
+ request.path.should == '/packages'
24
+ request.params.should == {}
25
+
26
+ http = double(:http)
27
+ request.should_receive(:http).and_return(http)
28
+
29
+ response = double(:response,
30
+ :body => '{}', :success? => true, :status => 200)
31
+ http.should_receive(:get).and_return(response)
32
+
33
+ request.perform.should be_instance_of(Mode::API::Resource)
34
+ end
35
+
36
+ it 'validates configuration' do
37
+ Mode::API::Request.should_receive(:valid?).and_return(false)
38
+ expect { Mode::API::Request.validate! }.to raise_error
39
+ end
40
+
41
+ it 'has environment host' do
42
+ hosts = {
43
+ 'test' => 'test',
44
+ 'development' => 'localhost',
45
+ 'staging' => 'staging.modeanalytics.com',
46
+ 'production' => 'www.modeanalytics.com'
47
+ }
48
+
49
+ hosts.each do |env, host|
50
+ Mode::API::Request.configure(env, {})
51
+ Mode::API::Request.send(:env_host).should == host
52
+ end
53
+ end
54
+
55
+ it "has bookmarked paths" do
56
+ Mode::API::Request.access_tokens_path.should == \
57
+ '/api/besquared/access_tokens'
58
+
59
+ Mode::API::Request.packages_path.should == \
60
+ '/api/besquared/packages'
61
+
62
+ Mode::API::Request.data_source_connections_path.should == \
63
+ '/api/besquared/data_source_connections'
64
+
65
+ Mode::API::Request.data_source_connection_path.should == \
66
+ '/api/besquared/data_source_connections/access_token'
67
+
68
+ Mode::API::Request.data_source_connection_messages_path.should == \
69
+ '/api/besquared/data_source_connections/access_token/messages'
70
+ end
71
+
72
+ it 'has http object' do
73
+ # make this more robust
74
+ Mode::API::Request.send(:http).should_not == nil
75
+ end
76
+
77
+ it 'has shortcut methods' do
78
+ response = { :body => '{}', :success? => true, :status => 200 }
79
+
80
+ http = double(:http, {
81
+ :head => double(:head, response),
82
+ :get => double(:get, response),
83
+ :post => double(:post, response),
84
+ :put => double(:put, response),
85
+ :delete => double(:delete, response)
86
+ })
87
+
88
+ Mode::API::Request.should_receive(:http).exactly(5).times.and_return(http)
89
+
90
+ Mode::API::Request.head('/packages/1')
91
+ Mode::API::Request.get('/packages/1')
92
+ Mode::API::Request.post('/packages')
93
+ Mode::API::Request.put('/packages/1')
94
+ Mode::API::Request.delete('/packages/1')
95
+ end
96
+
97
+ it 'return log error and return raw response on request errors' do
98
+ response = double(:get, {
99
+ :status => 500,
100
+ :success? => false,
101
+ :body => '{"error": "failed"}'
102
+ })
103
+
104
+ http = double(:http, { :get => response })
105
+ Mode::API::Request.should_receive(:http).and_return(http)
106
+
107
+ request = Mode::API::Request.new(:get, '/packages/1')
108
+ Mode::Logger.instance.should_receive(:error)
109
+ request.perform.should == response
110
+ end
111
+ end