lexicon-cli 0.1.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.
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