ckan_cli 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0d5686c7f8bd8d3fe87ac9fc4de600667ecd455fa773e383f8af10410d237dab
4
+ data.tar.gz: 0203fd33391a22b27eefcabc8f663b75080df194673800383fe2e563f448096f
5
+ SHA512:
6
+ metadata.gz: 9f2b823b83f3a9c9570fe45db2b797012e9b32d77f2db22b52d63db1bc1f7dfc1d2ffd3aa89ca6d019995b07f25f8dfaf7f43f3ffdf0b814cc409cfb018b584c
7
+ data.tar.gz: 9597fcf8f12824676a93c23b92496e8e282ff847ac3b41657915873e024b969f8b276d3aa17cfa2d5a4a91682e5a8eeb27f4e20064beb64b7a29db8500072005
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 datagovlvlv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # [CKAN CLI](https://github.com/datagovlv/ckan_cli)
2
+
3
+ > CKAN on your command line. Use your terminal to validate and publish resources to CKAN. Works with CKAN API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ckan_cli'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ckan_cli
20
+
21
+ ## Usage
22
+
23
+ Run it:
24
+
25
+ ```shell
26
+ $ ckancli
27
+ ```
28
+
29
+ For instance, to publish to CKAN all CSV files in folder do
30
+
31
+ ```shell
32
+ $ ckancli upload -d C:\my_csv_collection\ -c config.json -r resource.json
33
+ ```
34
+
35
+ To see help do
36
+
37
+ ```shell
38
+ $ ckancli help
39
+ ```
40
+
41
+ ## Development
42
+
43
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
44
+
45
+ To install this gem onto your local machine, run `bundle exec rake install`.
46
+
47
+ ## Contributing
48
+
49
+ Bug reports and pull requests are welcome on GitHub at https://github.com/datagovlv/ckan_cli. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
50
+
51
+ ## Code of Conduct
52
+
53
+ Everyone interacting in the CKAN CLI project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/datagovlv/ckan_cli/blob/master/CODE_OF_CONDUCT.md).
54
+
55
+ ## Copyright
56
+
57
+ Copyright (c) 2019 datagovlv. See [MIT License](LICENSE.txt) for further details.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/ckan_cli.gemspec ADDED
@@ -0,0 +1,41 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ckancli/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ckan_cli"
8
+ spec.license = "MIT"
9
+ spec.version = Ckancli::VERSION
10
+ spec.authors = ["datagovlv"]
11
+ spec.email = ["dati@varam.gov.lv"]
12
+ spec.summary = "CKAN command line interface."
13
+ spec.description = "CKAN CLI with built-in CSV file validation and CKAN API integration."
14
+ spec.homepage = "https://github.com/datagovlv/ckan_cli"
15
+
16
+ spec.files = Dir['lib/**/*']
17
+ spec.files += Dir['bin/*']
18
+ spec.files += Dir['exe/*']
19
+ spec.files += Dir['ckan_cli.gemspec', 'README.md', 'LICENSE.txt', 'Rakefile']
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = '>= 2.0.0'
25
+
26
+ spec.add_dependency "tty-color", "~> 0.4"
27
+ spec.add_dependency "tty-logger", "~> 0.1.0"
28
+ spec.add_dependency "tty-platform", "~> 0.2.0"
29
+ spec.add_dependency "tty-progressbar", "~> 0.16.0"
30
+ spec.add_dependency "tty-spinner", "~> 0.9.0"
31
+ spec.add_dependency "tty-which", "~> 0.4"
32
+ spec.add_dependency "pastel", "~> 0.7.2"
33
+ spec.add_dependency "thor", "~> 0.20.0"
34
+ spec.add_dependency "csvlint", "~> 0.4.0"
35
+ spec.add_dependency "rest-client", "~> 2.0.2"
36
+ spec.add_dependency "mail", "~> 2.7.1"
37
+
38
+ spec.add_development_dependency "bundler", "~> 1.17"
39
+ spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ end
data/exe/ckancli.rb ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib_path = File.expand_path('../lib', __dir__)
5
+ $:.unshift(lib_path) if !$:.include?(lib_path)
6
+ require 'ckancli/cli'
7
+
8
+ Signal.trap('INT') do
9
+ warn("\n#{caller.join("\n")}: interrupted")
10
+ exit(1)
11
+ end
12
+
13
+ begin
14
+ Ckancli::CLI.start
15
+ rescue Ckancli::CLI::Error => err
16
+ puts "ERROR: #{err.message}"
17
+ exit 1
18
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Ckancli
6
+ # Handle the application command line parsing
7
+ # and the dispatch to various command objects
8
+ #
9
+ # @api public
10
+ class CLI < Thor
11
+ class_option :dir, type: :string, required: true, desc: 'Path to CSV files. Full path to directory or URL.', aliases: '-d'
12
+ class_option :resourceconfig, type: :string, required: true, desc: 'CKAN resource metadata file (JSON). Absolute or relative path.', aliases: '-r'
13
+ class_option :configuration, type: :string, required: true, desc: 'Global configuration file (JSON). Absolute or relative path.', aliases: '-c'
14
+ class_option :validationschema, type: :string, required: false, desc: 'Validation schema (JSON). Absolute or relative path.', aliases: '-v'
15
+ class_option :datasetconfig, type: :string, required: false, desc: 'CKAN dataset metadata file (JSON). Absolute or relative path.', aliases: '-p'
16
+ class_option :updatemodified, type: :boolean, required: false, desc: 'Update modified date of resource.', aliases: '-m'
17
+ class_option :ignoreextension, type: :boolean, require: false, desc: 'Ignore file extensions (process all files not only CSV).', aliases: '-i'
18
+ class_option :overwrite, type: :boolean, require: false, desc: 'Overwrite resource if existing is found by identifier.', aliases: '-w'
19
+
20
+ # Error raised by this runner
21
+ Error = Class.new(StandardError)
22
+
23
+ # desc 'version', 'CKAN CLI version'
24
+ # def version
25
+ # require_relative 'version'
26
+ # puts "v#{Ckancli::VERSION}"
27
+ # end
28
+ # map %w(--version -v) => :version
29
+
30
+ desc 'upload', 'Processes CSV files and uploads to CKAN API. '
31
+ method_option :help, aliases: '-h', type: :boolean,
32
+ desc: 'Display usage information'
33
+ def upload(*)
34
+ if options[:help]
35
+ invoke :help, ['upload']
36
+ else
37
+ require_relative 'commands/upload'
38
+ Ckancli::Commands::Upload.new(options).execute
39
+ end
40
+ end
41
+ default_task :help
42
+ end
43
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Ckancli
6
+ class Command
7
+ extend Forwardable
8
+
9
+ def_delegators :command, :run
10
+
11
+ # Execute this command
12
+ #
13
+ # @api public
14
+ def execute(*)
15
+ raise(
16
+ NotImplementedError,
17
+ "#{self.class}##{__method__} must be implemented"
18
+ )
19
+ end
20
+
21
+ # The external commands runner
22
+ #
23
+ # @see http://www.rubydoc.info/gems/tty-command
24
+ #
25
+ # @api public
26
+ def command(**options)
27
+ require 'tty-command'
28
+ TTY::Command.new(options)
29
+ end
30
+
31
+ # The cursor movement
32
+ #
33
+ # @see http://www.rubydoc.info/gems/tty-cursor
34
+ #
35
+ # @api public
36
+ def cursor
37
+ require 'tty-cursor'
38
+ TTY::Cursor
39
+ end
40
+
41
+ # Open a file or text in the user's preferred editor
42
+ #
43
+ # @see http://www.rubydoc.info/gems/tty-editor
44
+ #
45
+ # @api public
46
+ def editor
47
+ require 'tty-editor'
48
+ TTY::Editor
49
+ end
50
+
51
+ # File manipulation utility methods
52
+ #
53
+ # @see http://www.rubydoc.info/gems/tty-file
54
+ #
55
+ # @api public
56
+ def generator
57
+ require 'tty-file'
58
+ TTY::File
59
+ end
60
+
61
+ # Terminal output paging
62
+ #
63
+ # @see http://www.rubydoc.info/gems/tty-pager
64
+ #
65
+ # @api public
66
+ def pager(**options)
67
+ require 'tty-pager'
68
+ TTY::Pager.new(options)
69
+ end
70
+
71
+ # Terminal platform and OS properties
72
+ #
73
+ # @see http://www.rubydoc.info/gems/tty-pager
74
+ #
75
+ # @api public
76
+ def platform
77
+ require 'tty-platform'
78
+ TTY::Platform.new
79
+ end
80
+
81
+ # The interactive prompt
82
+ #
83
+ # @see http://www.rubydoc.info/gems/tty-prompt
84
+ #
85
+ # @api public
86
+ def prompt(**options)
87
+ require 'tty-prompt'
88
+ TTY::Prompt.new(options)
89
+ end
90
+
91
+ # Get terminal screen properties
92
+ #
93
+ # @see http://www.rubydoc.info/gems/tty-screen
94
+ #
95
+ # @api public
96
+ def screen
97
+ require 'tty-screen'
98
+ TTY::Screen
99
+ end
100
+
101
+ # The unix which utility
102
+ #
103
+ # @see http://www.rubydoc.info/gems/tty-which
104
+ #
105
+ # @api public
106
+ def which(*args)
107
+ require 'tty-which'
108
+ TTY::Which.which(*args)
109
+ end
110
+
111
+ # Check if executable exists
112
+ #
113
+ # @see http://www.rubydoc.info/gems/tty-which
114
+ #
115
+ # @api public
116
+ def exec_exist?(*args)
117
+ require 'tty-which'
118
+ TTY::Which.exist?(*args)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,445 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require 'pathname'
5
+ require 'csvlint'
6
+ require 'json'
7
+ require 'tty-logger'
8
+ require 'tty-spinner'
9
+ require 'net/smtp'
10
+ require 'mail'
11
+ require 'open-uri'
12
+ require 'uri'
13
+
14
+ require_relative '../command'
15
+ require_relative '../../ckanclient/ckanclient'
16
+
17
+ module Ckancli
18
+ module Commands
19
+ class Upload < Ckancli::Command
20
+ def initialize(options)
21
+ @options = options
22
+
23
+ @dir = nil
24
+ @resources = []
25
+ @http_resource = false
26
+ @schema = nil
27
+ @resource_config = nil
28
+ @dataset_config = nil
29
+ @api_config = nil
30
+ @logger = nil
31
+ @ckan_client = nil
32
+ @update_modified = false
33
+ @mail_server = nil
34
+ end
35
+
36
+ def execute(input: $stdin, output: $stdout)
37
+ begin
38
+ prepare_options(@options)
39
+
40
+ if !@schema.nil?
41
+ validate()
42
+ else
43
+ puts ""
44
+ show_info "No schema - skipping validation"
45
+ end
46
+
47
+ upload()
48
+
49
+ if !@dataset_config.nil?
50
+ update_dataset()
51
+ else
52
+ puts ""
53
+ show_info "No dataset configuration - skipping update"
54
+ end
55
+
56
+ @resources.each do |res|
57
+ if !res[:log_file].nil?
58
+ res[:log_file].close
59
+ end
60
+ end
61
+
62
+ if !@mail_server.nil? && !@mail_receivers.nil?
63
+ mail_notify()
64
+ end
65
+ rescue StandardError => e
66
+ return_error "Unexpected error", e
67
+ end
68
+ end
69
+
70
+ private
71
+ def prepare_options(options)
72
+ @http_resource = true if options[:dir] =~ /^http(s)?/
73
+
74
+ if @http_resource
75
+ @dir = Dir.pwd
76
+ else
77
+ @dir = File.directory?(options[:dir]) ? options[:dir] : (File.file?(options[:dir]) ? File.dirname(options[:dir]) : nil)
78
+ end
79
+
80
+ if @dir.nil?
81
+ return_error "Invalid directory - #{options[:dir]}"
82
+ end
83
+
84
+ begin
85
+ @logger = TTY::Logger.new do |config|
86
+ config.metadata = [:date, :time]
87
+ config.handlers = [
88
+ [:stream, output: File.open(File.join(@dir, "output.log"), "a")]
89
+ ]
90
+ end
91
+ rescue StandartError => e
92
+ return_error "Could not initialize logger", e
93
+ end
94
+
95
+ begin
96
+ if @http_resource
97
+ file_path = File.join(@dir, File.basename(URI.parse(options[:dir]).path))
98
+
99
+ File.open(file_path, "wb") do |file|
100
+ file.write open(options[:dir]).read
101
+ end
102
+
103
+ @resources.push(file_path)
104
+ else
105
+ if File.directory?(options[:dir])
106
+ @resources = Dir.entries(options[:dir]).select{ |f| File.file?(File.join(options[:dir], f)) && (options[:ignoreextension] || File.extname(f).downcase == ".csv") }.map { |f| File.join(options[:dir], f) }
107
+ else
108
+ @resources.push(options[:dir])
109
+ end
110
+ end
111
+
112
+ @resources = @resources.map{ |r|
113
+ {
114
+ :path => r,
115
+ :valid => true,
116
+ :summary => nil,
117
+ :log_file => File.open(File.join(@dir, "#{File.basename(r)}.log"), "w"),
118
+ :log => nil
119
+ }
120
+ }
121
+
122
+ @resources.each do |res|
123
+ res[:log] = TTY::Logger.new do |config|
124
+ config.metadata = [:date, :time]
125
+ config.handlers = [
126
+ [:stream, output: res[:log_file]]
127
+ ]
128
+ end
129
+ end
130
+ rescue Errno::ENOENT => e
131
+ return_error "Could not load resources", e
132
+ rescue StandardError => e
133
+ return_error "Could not load resources", e
134
+ end
135
+
136
+ if options[:validationschema]
137
+ @schema = (Pathname.new(options[:validationschema])).absolute? ? options[:validationschema] : File.join(@dir, options[:validationschema])
138
+ end
139
+
140
+ if options[:resourceconfig]
141
+ begin
142
+ @resource_config = JSON.parse(File.read((Pathname.new(options[:resourceconfig])).absolute? ? options[:resourceconfig] : File.join(@dir, options[:resourceconfig])))
143
+ rescue Errno::ENOENT => e
144
+ return_error "Could not load resource configuration", e
145
+ end
146
+ end
147
+
148
+ if options[:datasetconfig]
149
+ begin
150
+ @dataset_config = JSON.parse(File.read((Pathname.new(options[:datasetconfig])).absolute? ? options[:datasetconfig] : File.join(@dir, options[:datasetconfig])))
151
+ rescue Errno::ENOENT => e
152
+ return_error "Could not load dataset configuration", e
153
+ end
154
+ end
155
+
156
+ if options[:configuration]
157
+ begin
158
+ @api_config = JSON.parse(File.read((Pathname.new(options[:configuration])).absolute? ? options[:configuration] : File.join(@dir, options[:configuration])))
159
+ rescue Errno::ENOENT => e
160
+ return_error "Could not load configuration", e
161
+ end
162
+ end
163
+
164
+ if !@api_config.nil?
165
+ @ckan_client = CkanClient::Client.new(@api_config["ckan_api"]["url"], @api_config["ckan_api"]["api_key"])
166
+ @mail_server = @api_config["email_server"]
167
+ @mail_receivers = @api_config["notification_receiver"]
168
+ end
169
+
170
+ @update_modified = options[:updatemodified]
171
+ @overwrite = options[:overwrite]
172
+ end
173
+
174
+ def validate()
175
+ puts ""
176
+ show_info "Starting validations..."
177
+ @resources.each do |res|
178
+ task("Validating resource: #{File.basename(res[:path])}") do
179
+ ok = false
180
+ msg = nil
181
+ exception = nil
182
+
183
+ begin
184
+ res[:log].info "Validating resource..."
185
+
186
+ res[:valid] = false
187
+
188
+ source = read_resource(res[:path])
189
+ schema = read_schema(@schema)
190
+
191
+ if schema.class == Csvlint::Schema && schema.description == "malformed"
192
+ msg = "Invalid schema - malformed JSON"
193
+ else
194
+ validator = Csvlint::Validator.new(source, {}, schema)
195
+
196
+ if validator.errors.length > 0 || validator.warnings.length > 0
197
+ msg = "#{validator.errors.length} errors, #{validator.warnings.length} warnings"
198
+ res[:summary] = {
199
+ :errors => validator.errors.map { |v| format_validation(v, schema) },
200
+ :warnings => validator.warnings.map { |v| format_validation(v, schema) }
201
+ }
202
+
203
+ res[:log].error res[:summary].to_json
204
+ end
205
+
206
+ ok = validator.valid?
207
+ res[:valid] = ok
208
+ end
209
+ rescue Errno::ENOENT, OpenURI::HTTPError => e
210
+ msg = "Could not load file - #{e.message}"
211
+ rescue Csvlint::Csvw::MetadataError => e
212
+ msg = "Invalid schema metadata - #{e.message}"
213
+ rescue StandardError => e
214
+ msg = "Could not validate resource"
215
+ exception = e
216
+ end
217
+
218
+ if !exception.nil?
219
+ res[:log].error msg, exception
220
+ elsif !msg.nil?
221
+ res[:log].info msg
222
+ else
223
+ res[:log].info "OK - Validation successfull"
224
+ end
225
+ res[:log].info "End of validation"
226
+
227
+ [ok, msg, exception]
228
+ end
229
+ end
230
+
231
+ show_info "End of validations"
232
+ end
233
+
234
+ def mail_notify
235
+ puts ""
236
+ show_info "Sending e-mail notifications..."
237
+
238
+ begin
239
+ mail = Mail.new
240
+ mail[:from] = @mail_server["sender"]
241
+ mail[:to] = @mail_receivers["error"]
242
+ mail[:subject] = @mail_server["subject"]
243
+ mail.delivery_method :smtp, {
244
+ :address => @mail_server["address"],
245
+ :port => @mail_server["port"],
246
+ :user_name => @mail_server["smtp_user"],
247
+ :password => @mail_server["smtp_password"],
248
+ :authentication => @mail_server["smtp_user"].nil? ? "none" : "plain",
249
+ :ssl => @mail_server["ssl"],
250
+ :openssl_verify_mode => OpenSSL::SSL::VERIFY_NONE
251
+ }
252
+
253
+ # message
254
+ msg = "CKAN CLI task completed.\r\n\r\n#{@resources.length} files processed"
255
+
256
+ # attachments
257
+ @resources.each do |res|
258
+ msg = "#{msg}\r\n- #{File.basename(res[:path])}"
259
+ if !res[:summary].nil?
260
+ msg = "#{msg} (#{res[:summary][:errors].length} errors, #{res[:summary][:warnings].length} warnings)"
261
+ end
262
+ mail.add_file :filename => File.basename(res[:path] + ".log"), :content => File.read(res[:path] + ".log")
263
+ end
264
+ msg = "#{msg}\r\n\r\nLog files attached."
265
+ mail[:body] = msg
266
+
267
+ mail.deliver!
268
+ rescue StandardError => e
269
+ show_error "Could not send e-mail", e
270
+ end
271
+
272
+ show_info "End of sending"
273
+ end
274
+
275
+ def upload()
276
+ puts ""
277
+ show_info "Starting upload..."
278
+ @resources.each do |res|
279
+ next unless res[:valid]
280
+
281
+ task("Uploading resource: #{File.basename(res[:path])}") do
282
+ res[:log].info "Uploading resource..."
283
+
284
+ ok = true
285
+ msg = nil
286
+ exception = nil
287
+
288
+ params = @resource_config["result"].clone
289
+
290
+ if @update_modified
291
+ params["last_modified"] = JSON.parse(Time.new.to_json)
292
+ end
293
+
294
+ # check if resource exists and if overwriting
295
+ if !params["id"].nil? && !@overwrite
296
+ resource = @ckan_client.get_resource(params["id"])
297
+
298
+ if !resource.nil?
299
+ ok = false
300
+ msg = "Resource already exists - skipping. Use parameter 'overwrite' to overwrite changes."
301
+ end
302
+ end
303
+
304
+ if ok
305
+ @ckan_client.create_or_update_resource(params, File.new(res[:path], 'rb'), true){ |response, status_ok|
306
+ ok = status_ok
307
+
308
+ if !ok
309
+ msg = "HTTP error #{response.code}"
310
+
311
+ log_error response.body
312
+ end
313
+ }
314
+ end
315
+
316
+ if !ok
317
+ res[:log].error msg
318
+ else
319
+ res[:log].info "OK - upload successfull"
320
+ end
321
+ res[:log].info "End of upload"
322
+
323
+ [ok, msg, exception]
324
+ end
325
+ end
326
+
327
+ show_info "End of upload"
328
+ end
329
+
330
+ def update_dataset()
331
+ puts ""
332
+ show_info "Starting dataset update..."
333
+
334
+ task("Updating dataset") do
335
+ ok = false
336
+ msg = nil
337
+ exception = nil
338
+
339
+ @ckan_client.update_package(@dataset_config["result"]["id"], @dataset_config["result"].clone, true){ |response, status_ok|
340
+ ok = status_ok
341
+
342
+ if !ok
343
+ msg = "HTTP error #{response.code}"
344
+
345
+ log_error response.body
346
+ end
347
+ }
348
+
349
+ [ok, msg, exception]
350
+ end
351
+ show_info "End of dataset update"
352
+ end
353
+
354
+ def return_error(message, exception = nil)
355
+ show_error(message, exception)
356
+
357
+ exit 1
358
+ end
359
+
360
+ def show_error(message, exception = nil)
361
+ if !exception.nil?
362
+ message = "#{message} - #{exception.message}"
363
+ end
364
+
365
+ puts " #{message} ".white.on_red
366
+
367
+ log_fatal message, exception
368
+ end
369
+
370
+ def show_info(message)
371
+ puts " #{message} "
372
+
373
+ log_info message
374
+ end
375
+
376
+ def task(message)
377
+ log_info message
378
+
379
+ spinner = TTY::Spinner.new("[:spinner] #{message}...")
380
+ spinner.auto_spin
381
+ ok, message, exception = yield
382
+ spinner.stop(ok ? "OK".green : "ERROR".red)
383
+
384
+ if !message.nil?
385
+ message = " #{message}"
386
+
387
+ if !exception.nil?
388
+ show_error message, exception
389
+ else
390
+ show_info message
391
+ end
392
+ end
393
+ end
394
+
395
+ def read_resource(source)
396
+ # check if URL or file
397
+ unless source =~ /^http(s)?/
398
+ source = File.new( source )
399
+ end
400
+
401
+ source
402
+ end
403
+
404
+ def read_schema(path)
405
+ schema = Csvlint::Schema.load_from_uri(path, false)
406
+
407
+ schema
408
+ end
409
+
410
+ def format_validation(error, schema = nil)
411
+ h = {
412
+ type: error.type,
413
+ category: error.category,
414
+ row: error.row,
415
+ col: error.column
416
+ }
417
+
418
+ if error.column && !schema.nil? && schema.class == Csvlint::Schema && schema.fields[error.column - 1] != nil
419
+ field = schema.fields[error.column - 1]
420
+ h[:header] = field.name
421
+ end
422
+
423
+ h
424
+ end
425
+
426
+ def log_fatal(message, exception = nil)
427
+ if !@logger.nil?
428
+ @logger.fatal message, exception
429
+ end
430
+ end
431
+
432
+ def log_info(message)
433
+ if !@logger.nil?
434
+ @logger.info message
435
+ end
436
+ end
437
+
438
+ def log_error(message)
439
+ if !@logger.nil?
440
+ @logger.error message
441
+ end
442
+ end
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,3 @@
1
+ module Ckancli
2
+ VERSION = "1.0.0"
3
+ end
data/lib/ckancli.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "ckancli/version"
2
+
3
+ module Ckancli
4
+ class Error < StandardError; end
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'rest-client'
5
+
6
+ module CkanClient
7
+ class Client
8
+ def initialize(url, key)
9
+ raise ArgumentError, "No URL provided" unless !url.nil?
10
+ raise ArgumentError, "No API KEY provided" unless !key.nil?
11
+
12
+ # remove trailig slashes
13
+ @url = url.sub(/(\/)+$/,"")
14
+ @key = key
15
+ @headers = {}
16
+ @headers[:Authorization] = @key
17
+ end
18
+
19
+ def get_resource(id, params = {}, &block)
20
+ raise ArgumentError, "No ID provided for resource" unless !id.nil?
21
+
22
+ result = nil
23
+ params["id"] = id
24
+
25
+ post("resource_show", params, nil){ |response, status_ok|
26
+ if status_ok
27
+ result = JSON.parse(response.body)["result"]
28
+ end
29
+ }
30
+
31
+ return result
32
+ end
33
+
34
+ def create_resource(params = {}, file = nil, &block)
35
+ if params["name"].nil? && !file.nil?
36
+ params["name"] = File.basename(file.path)
37
+ end
38
+
39
+ post("resource_create", params, file, &block)
40
+ end
41
+
42
+ def update_resource(id, params = {}, file = nil, resolve_delta = false, &block)
43
+ raise ArgumentError, "No ID provided for resource update" unless !id.nil?
44
+
45
+ params["id"] = id
46
+
47
+ if resolve_delta
48
+ metadata = get_resource(id)
49
+ if !metadata.nil?
50
+ params = metadata.merge(params)
51
+ end
52
+ end
53
+
54
+ post("resource_update", params, file, &block)
55
+ end
56
+
57
+ def create_or_update_resource(params = {}, file = nil, resolve_delta = false, &block)
58
+ if params["id"].nil?
59
+ create_resource(params, file, &block)
60
+ else
61
+ update_resource(params["id"], params, file, resolve_delta, &block)
62
+ end
63
+ end
64
+
65
+ def get_package(id, params = {}, &block)
66
+ raise ArgumentError, "No ID provided for package" unless !id.nil?
67
+
68
+ result = nil
69
+ params["id"] = id
70
+
71
+ post("package_show", params, nil){ |response, status_ok|
72
+ if status_ok
73
+ result = JSON.parse(response.body)["result"]
74
+ end
75
+ }
76
+
77
+ return result
78
+ end
79
+
80
+ def create_package(params = {}, &block)
81
+ post("package_create", params, nil, &block)
82
+ end
83
+
84
+ def update_package(id, params = {}, resolve_delta = false, &block)
85
+ raise ArgumentError, "No ID provided for package update" unless !id.nil?
86
+
87
+ params["id"] = id
88
+
89
+ if resolve_delta
90
+ metadata = get_package(id)
91
+ if !metadata.nil?
92
+ params = metadata.merge(params)
93
+ end
94
+ end
95
+
96
+ post("package_update", params, nil, &block)
97
+ end
98
+
99
+ def create_or_update_package(params = {}, resolve_delta = false, &block)
100
+ if params["id"].nil?
101
+ create_package(params, &block)
102
+ else
103
+ update_package(params["id"], params, resolve_delta, &block)
104
+ end
105
+ end
106
+
107
+ private
108
+ def post(action, params = {}, file = nil, &block)
109
+ raise ArgumentError, "No action provided" unless !action.nil?
110
+
111
+ payload = {}
112
+ if !file.nil?
113
+ payload[:upload] = file
114
+ end
115
+ payload = payload.merge(params)
116
+
117
+ if file.nil?
118
+ payload = payload.to_json
119
+ @headers[:content_type] = :json
120
+ else
121
+ @headers.delete(:content_type)
122
+ end
123
+
124
+ RestClient.post("#{@url}/action/#{action}", payload, @headers){ |response, request, result|
125
+ block.call(response, response.code == 200)
126
+ }
127
+ end
128
+ end
129
+ end
metadata ADDED
@@ -0,0 +1,252 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ckan_cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - datagovlv
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tty-color
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tty-logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-platform
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.16.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.16.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-spinner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-which
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pastel
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.7.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.7.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: thor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.20.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.20.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: csvlint
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.4.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.4.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: rest-client
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 2.0.2
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 2.0.2
153
+ - !ruby/object:Gem::Dependency
154
+ name: mail
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 2.7.1
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 2.7.1
167
+ - !ruby/object:Gem::Dependency
168
+ name: bundler
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.17'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.17'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rake
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '10.0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '10.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: rspec
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '3.0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '3.0'
209
+ description: CKAN CLI with built-in CSV file validation and CKAN API integration.
210
+ email:
211
+ - dati@varam.gov.lv
212
+ executables:
213
+ - ckancli.rb
214
+ extensions: []
215
+ extra_rdoc_files: []
216
+ files:
217
+ - LICENSE.txt
218
+ - README.md
219
+ - Rakefile
220
+ - bin/setup
221
+ - ckan_cli.gemspec
222
+ - exe/ckancli.rb
223
+ - lib/ckancli.rb
224
+ - lib/ckancli/cli.rb
225
+ - lib/ckancli/command.rb
226
+ - lib/ckancli/commands/upload.rb
227
+ - lib/ckancli/version.rb
228
+ - lib/ckanclient/ckanclient.rb
229
+ homepage: https://github.com/datagovlv/ckan_cli
230
+ licenses:
231
+ - MIT
232
+ metadata: {}
233
+ post_install_message:
234
+ rdoc_options: []
235
+ require_paths:
236
+ - lib
237
+ required_ruby_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: 2.0.0
242
+ required_rubygems_version: !ruby/object:Gem::Requirement
243
+ requirements:
244
+ - - ">="
245
+ - !ruby/object:Gem::Version
246
+ version: '0'
247
+ requirements: []
248
+ rubygems_version: 3.0.3
249
+ signing_key:
250
+ specification_version: 4
251
+ summary: CKAN command line interface.
252
+ test_files: []