lexicon-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Lexicon::Cli
2
+
3
+ This repository provides a CLI to download/upload/load/enable compiled lexicon packages into a Postgres (PostGIS) database.
4
+
5
+ You can read the [documentation in the `doc/` folder](doc/usage.md)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "lexicon-cli"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/lexicon ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'lexicon-cli'
5
+
6
+ require 'dotenv/load'
7
+
8
+ application = Lexicon::Cli::Application.new(ARGV, extensions: [
9
+ Lexicon::Cli::Extension::CommonExtension.new(data_root: Pathname.new(__dir__).join('..')),
10
+ Lexicon::Cli::Extension::RemoteExtension.new,
11
+ Lexicon::Cli::Extension::ProductionExtension.new,
12
+ ])
13
+
14
+ application.start
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/lexicon/cli/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lexicon-cli'
7
+ spec.version = Lexicon::Cli::VERSION
8
+ spec.authors = ['Ekylibre developers']
9
+ spec.email = ['dev@ekylibre.com']
10
+
11
+ spec.summary = 'Basic Cli for the Lexicon'
12
+ spec.required_ruby_version = '>= 2.6.0'
13
+ spec.homepage = 'https://www.ekylibre.com'
14
+ spec.license = 'AGPL-3.0-only'
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.glob(%w[lib/**/*.rb bin/**/* *.gemspec Gemfile Rakefile *.md])
19
+
20
+ spec.bindir = 'bin'
21
+ spec.executables << 'lexicon'
22
+
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'corindon', '~> 0.7.0'
26
+ spec.add_dependency 'dotenv', '~> 2.7'
27
+ spec.add_dependency 'lexicon-common', '~> 0.1.0'
28
+ spec.add_dependency 'thor', '~> 1.0'
29
+ spec.add_dependency 'zeitwerk', '~> 2.4'
30
+
31
+ spec.add_development_dependency 'bundler', '~> 2.0'
32
+ spec.add_development_dependency 'minitest', '~> 5.14'
33
+ spec.add_development_dependency 'rake', '~> 13.0'
34
+ spec.add_development_dependency 'rubocop', '~> 1.3.1'
35
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'corindon'
4
+ require 'lexicon-common'
5
+ require 'thor'
6
+ require 'zeitwerk'
7
+
8
+ # Make sure the Lexicon module already exists so that Zeitwerk does not manage it
9
+ module Lexicon
10
+ end
11
+
12
+ loader = Zeitwerk::Loader.for_gem
13
+ loader.ignore(__FILE__)
14
+ loader.setup
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lexicon
4
+ module Cli
5
+ class Application
6
+ attr_reader :args
7
+
8
+ def initialize(args, extensions: [])
9
+ @args = args
10
+ @extensions = extensions
11
+ end
12
+
13
+ def start
14
+ container = Corindon::DependencyInjection::Container.new
15
+ extensions.each do |extension|
16
+ extension.boot(container)
17
+ end
18
+
19
+ make_app(extensions).start(args, container: container)
20
+ end
21
+
22
+ private
23
+
24
+ # @return [Array<Lexicon::Cli::ExtensionBase>]
25
+ attr_reader :extensions
26
+
27
+ # @param [Array<Lexicon::Cli::ExtensionBase>]
28
+ # @return [Class]
29
+ def make_app(extensions)
30
+ Class.new(CliBase) do
31
+ extensions.each do |extension|
32
+ if (commands = extension.commands).is_a?(Proc)
33
+ instance_eval(&commands)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lexicon
4
+ module Cli
5
+ class CliBase < Command::ContainerAwareCommand
6
+ def initialize(args = [], local_options = {}, config = {})
7
+ super(args, local_options, config)
8
+
9
+ register_config(container, options)
10
+ end
11
+
12
+ default_command :help
13
+ class_option :verbose, type: :boolean, default: false, aliases: ['v']
14
+ class_option :parallel, type: :boolean, default: false, aliases: ['P']
15
+
16
+ private
17
+
18
+ def register_config(container, options)
19
+ verbose = options['verbose']
20
+ parallel = options['parallel']
21
+ jobs = options.fetch('jobs', parallel ? 4 : 1)
22
+
23
+ container.set_parameter('config.verbose', verbose)
24
+ container.set_parameter('config.parallel', parallel)
25
+ container.set_parameter('config.jobs', jobs)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lexicon
4
+ module Cli
5
+ module Command
6
+ class ConsoleCommand < ContainerAwareCommand
7
+ default_command :exec_command
8
+
9
+ desc 'run console', ''
10
+
11
+ def exec_command
12
+ # rubocop:disable Lint/Debugger
13
+ binding.pry
14
+ # rubocop:enable Lint/Debugger
15
+ end
16
+
17
+ private
18
+
19
+ %i[collect load normalize].each do |meth|
20
+ define_method meth do |*names|
21
+ get('datasource.name_runner').run(names, action: meth)
22
+ end
23
+ end
24
+
25
+ def version_bump(part)
26
+ get('version.bumper').bump(part)
27
+ end
28
+
29
+ def release(*names)
30
+ get('database.dumper').dump(get('version'), datasource_names: names)
31
+ end
32
+
33
+ def load_package(version = nil)
34
+ version ||= get('version')
35
+
36
+ get('production.package.loader').load_package(version)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lexicon
4
+ module Cli
5
+ module Command
6
+ class ContainerAwareCommand < Thor
7
+ include Lexicon::Common::Mixin::ContainerAware
8
+
9
+ def initialize(args = [], local_options = {}, config = {})
10
+ super(args, local_options, config)
11
+
12
+ self.container = config[:container]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Lexicon
6
+ module Cli
7
+ module Command
8
+ class ProductionCommand < ContainerAwareCommand
9
+ include Lexicon::Common::Mixin::SchemaNamer
10
+
11
+ desc 'loadable', 'List all available loadable versions'
12
+
13
+ def loadable
14
+ available_packages.each do |package|
15
+ puts package.version.to_s.green
16
+ package.file_sets
17
+ .sort_by(&:name)
18
+ .each do |fs|
19
+ puts ' -> ' + fs.name.send(fs.data_path.nil? ? :yellow : :green)
20
+ end
21
+ end
22
+ end
23
+
24
+ desc 'config', 'Display production config information'
25
+
26
+ def config
27
+ puts <<~TEXT
28
+ Version dir: #{container.parameter('lexicon.common.package_dir')}
29
+ Prod database URL: #{container.parameter('lexicon.common.production.database.url')}
30
+ TEXT
31
+ end
32
+
33
+ desc 'load <VERSION>', 'Load a package into the production database'
34
+ option :validate, type: :boolean, default: true
35
+ option :datasources, type: :array, default: []
36
+ option :without, type: :array, default: []
37
+
38
+ def load(pkg_name)
39
+ # @type [Production::DatasourceLoader] datasource_loader
40
+ datasource_loader = get(Lexicon::Common::Production::DatasourceLoader)
41
+ # @type [Package::PackageIntegrityValidator] integrity_validator
42
+ integrity_validator = get(Lexicon::Common::Package::PackageIntegrityValidator)
43
+ # @type [Package::DirectoryPackageLoader] package_loader
44
+ package_loader = get(Lexicon::Common::Package::DirectoryPackageLoader)
45
+
46
+ validate = options.fetch(:validate)
47
+ names = options.fetch(:datasources, [])
48
+ without = options.fetch(:without, [])
49
+ package = package_loader.load_package(pkg_name)
50
+
51
+ if package.nil?
52
+ puts '[ NOK ] Did not find any package to load'.red
53
+ elsif package.nil?
54
+ puts "[ NOK ] No Package found for version #{version}".red
55
+ elsif !validate || integrity_validator.valid?(package)
56
+ datasource_loader.load_package(package, only: (names.empty? ? nil : names), without: without)
57
+ else
58
+ puts "[ NOK ] Lexicon package #{package.version} is corrupted".red
59
+ end
60
+ end
61
+
62
+ desc 'versions', 'List versions available on the server'
63
+
64
+ def versions
65
+ puts "Lexicon is #{enabled? ? 'ENABLED'.green + " (#{enabled_version.to_s.yellow})" : 'DISABLED'.red}"
66
+
67
+ available = loaded_versions
68
+ if available.empty?
69
+ puts 'No other versions are loaded'
70
+ else
71
+ puts 'Available loaded versions are:'
72
+ loaded_versions
73
+ .each { |e| puts " - #{e}" }
74
+ end
75
+ end
76
+
77
+ desc 'disable', 'Disable the lexicon'
78
+
79
+ def disable
80
+ if enabled? && !(version = enabled_version).nil?
81
+ puts "Disabling version #{version.to_s.yellow}"
82
+ do_disable
83
+ puts '[ OK ] Done'.green
84
+ else
85
+ puts 'Lexicon is not enabled'.red
86
+ end
87
+ end
88
+
89
+ desc 'enable [version]', 'Enable the lexicon'
90
+
91
+ def enable(version = nil)
92
+ if enabled?
93
+ puts 'Disabling current version'
94
+ do_disable
95
+ end
96
+
97
+ semver = if version.nil?
98
+ loaded_versions.max
99
+ else
100
+ Semantic::Version.new(version)
101
+ end
102
+
103
+ puts "Enabling version #{semver.to_s.yellow}"
104
+
105
+ do_enable(semver)
106
+
107
+ puts '[ OK ] Done'.green
108
+ end
109
+
110
+ desc 'delete', 'Deletes a loaded version of the lexicon'
111
+
112
+ def delete(version)
113
+ semver = Semantic::Version.new(version)
114
+
115
+ if loaded_versions.include?(semver)
116
+ production_database
117
+ .query("DROP SCHEMA \"#{version_to_schema(semver)}\" CASCADE")
118
+
119
+ puts '[ OK ] '.green + "The version #{semver} has been deleted."
120
+ else
121
+ puts '[ NOK ] '.red + "The version #{semver.to_s.yellow} is not loaded or is enabled."
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def production_database
128
+ get(Lexicon::Cli::Extension::ProductionExtension::DATABASE)
129
+ end
130
+
131
+ # @return [Array<Package::Package>]
132
+ def available_packages
133
+ # @type [Package::DirectoryPackageLoader] package_loader
134
+ package_loader = get(Lexicon::Common::Package::DirectoryPackageLoader)
135
+
136
+ if package_loader.root_dir.exist?
137
+ package_loader.root_dir
138
+ .children
139
+ .select(&:directory?)
140
+ .map { |dir| package_loader.load_package(dir.basename.to_s) }
141
+ .compact
142
+ .sort { |a, b| a.version <=> b.version }
143
+ else
144
+ []
145
+ end
146
+ end
147
+
148
+ # @param [Semantic::Version] version
149
+ def do_enable(version)
150
+ production_database.query <<~SQL
151
+ BEGIN;
152
+ ALTER SCHEMA "lexicon__#{version.to_s.gsub('.', '_')}" RENAME TO "lexicon";
153
+ CREATE TABLE "lexicon"."version" ("version" VARCHAR);
154
+ INSERT INTO "lexicon"."version" VALUES ('#{version}');
155
+ COMMIT;
156
+ SQL
157
+ end
158
+
159
+ def do_disable
160
+ version = enabled_version
161
+ raise StandardError.new('No version table present, cannot continue automatically') if version.nil?
162
+
163
+ production_database.query <<~SQL
164
+ BEGIN;
165
+ DROP TABLE "lexicon"."version";
166
+ ALTER SCHEMA "lexicon" RENAME TO "#{version_to_schema(version)}";
167
+ COMMIT;
168
+ SQL
169
+ end
170
+
171
+ # @return [Array<Semantic::Version>]
172
+ def loaded_versions
173
+ disabled = production_database
174
+ .query("SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname LIKE 'lexicon__%';")
175
+ .to_a
176
+ .map { |name| schema_to_version(name.fetch('nspname')) }
177
+
178
+ enabled = enabled_version
179
+
180
+ if enabled.nil?
181
+ disabled
182
+ else
183
+ [*disabled, enabled].sort
184
+ end
185
+ end
186
+
187
+ # @return [Semantic::Version, nil]
188
+ def enabled_version
189
+ if version_table_present?
190
+ Semantic::Version.new(
191
+ production_database
192
+ .query('SELECT version FROM lexicon.version LIMIT 1')
193
+ .to_a.first
194
+ .fetch('version')
195
+ )
196
+ else
197
+ nil
198
+ end
199
+ end
200
+
201
+ # @param [String, nil] version
202
+ # @return [Semantic::Version]
203
+ def as_version(version, &block)
204
+ if version.nil?
205
+ block.call
206
+ else
207
+ Semantic::Version.new(version)
208
+ end
209
+ end
210
+
211
+ def version_table_present?
212
+ production_database
213
+ .query("SELECT count(*) AS presence FROM information_schema.tables WHERE table_schema = 'lexicon' AND table_name = 'version'")
214
+ .to_a.first
215
+ .fetch('presence').to_i.positive?
216
+ end
217
+
218
+ def enabled?
219
+ production_database
220
+ .query("SELECT count(nspname) AS presence FROM pg_catalog.pg_namespace WHERE nspname = 'lexicon'")
221
+ .to_a.first
222
+ .fetch('presence').to_i.positive?
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end