metabase_tarot 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 +7 -0
- data/bin/tarot +197 -0
- data/lib/tarot.rb +307 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd886182a092af7e593d482ddea5460b9e7197440a740cb364c09074f29ba7c3
|
4
|
+
data.tar.gz: 5974f750ce4309063b7e30ec19415ad855da958cd3fec0bc9b089ab7878791d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf750b934c4b05fa9d804b3bcfc84c0ebee5f1085c9280c653b668da45febd4fdd2ee0ac52303693d97bf78db84df62d05860c5fb520692f22e58aab18a75fb4
|
7
|
+
data.tar.gz: c6494b6d00ac88a3a9feb9f7020eb190c7b2356f659fbbfab93786ccd7da1254b4d301a670890c74510c223e14e74f05d09fe4b7cd8165b3f43e7ed95e7523e8
|
data/bin/tarot
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'tarot'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
SUBCOMMAND_NAMES = %w[run columns new console]
|
6
|
+
|
7
|
+
if ARGV.length == 0
|
8
|
+
puts "Tarot will give you the answer! But first you have to give tarot the question (#{SUBCOMMAND_NAMES.join(', ')})"
|
9
|
+
exit(1)
|
10
|
+
end
|
11
|
+
|
12
|
+
# https://stackoverflow.com/questions/16323571/measure-the-distance-between-two-strings-with-ruby
|
13
|
+
def levenshtein_distance(s, t)
|
14
|
+
m = s.length
|
15
|
+
n = t.length
|
16
|
+
return m if n == 0
|
17
|
+
return n if m == 0
|
18
|
+
d = Array.new(m+1) {Array.new(n+1)}
|
19
|
+
|
20
|
+
(0..m).each {|i| d[i][0] = i}
|
21
|
+
(0..n).each {|j| d[0][j] = j}
|
22
|
+
(1..n).each do |j|
|
23
|
+
(1..m).each do |i|
|
24
|
+
d[i][j] = if s[i-1] == t[j-1] # adjust index into string
|
25
|
+
d[i-1][j-1] # no operation required
|
26
|
+
else
|
27
|
+
[ d[i-1][j]+1, # deletion
|
28
|
+
d[i][j-1]+1, # insertion
|
29
|
+
d[i-1][j-1]+1, # substitution
|
30
|
+
].min
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
d[m][n]
|
35
|
+
end
|
36
|
+
|
37
|
+
def pretty_print_databases
|
38
|
+
puts <<~TEXT
|
39
|
+
Available databases:
|
40
|
+
#{Tarot.database_names.join("\n")}
|
41
|
+
TEXT
|
42
|
+
end
|
43
|
+
|
44
|
+
def pretty_print_array(array)
|
45
|
+
array.each_slice(3) do |slice|
|
46
|
+
puts slice.map { |item| item.ljust(40) }.join
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def demand_argument(index, name)
|
51
|
+
return ARGV[index] if ARGV.count > index
|
52
|
+
|
53
|
+
puts "Missing argument #{index}: #{name}"
|
54
|
+
exit(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def require_tarot_spec
|
58
|
+
path = "#{Dir.pwd}/tarot_spec.rb"
|
59
|
+
if !File.exist?("#{Dir.pwd}/tarot_spec.rb")
|
60
|
+
puts "No tarot_spec.rb found on #{Dir.pwd}. To create it on this directory, type: tarot new ."
|
61
|
+
exit(1)
|
62
|
+
end
|
63
|
+
require_relative path
|
64
|
+
end
|
65
|
+
|
66
|
+
def retrieve_database(database_name)
|
67
|
+
begin
|
68
|
+
database = Tarot.database(database_name)
|
69
|
+
rescue Tarot::DatabaseNotFound
|
70
|
+
pretty_print_databases
|
71
|
+
puts "\nNot found: #{database_name}"
|
72
|
+
did_you_mean(database_name, Tarot.database_names)
|
73
|
+
exit(1)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def sort_possibilities(input, possibilities)
|
78
|
+
possibilities.sort_by { |possibility| levenshtein_distance(input, possibility) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def did_you_mean(input, possibilities)
|
82
|
+
return if possibilities.empty?
|
83
|
+
|
84
|
+
sorted_possibilities = sort_possibilities(input, possibilities)
|
85
|
+
closest_match = sorted_possibilities.first
|
86
|
+
distance = levenshtein_distance(input, closest_match)
|
87
|
+
|
88
|
+
return if distance > (input.length / 2)
|
89
|
+
|
90
|
+
puts "Did you mean? #{closest_match}"
|
91
|
+
end
|
92
|
+
|
93
|
+
case ARGV[0]
|
94
|
+
when 'run'
|
95
|
+
path = demand_argument(1, 'consultation')
|
96
|
+
require_tarot_spec()
|
97
|
+
require_relative "#{Dir.pwd}/consultations/#{path}"
|
98
|
+
when 'new'
|
99
|
+
path = "#{Dir.pwd}/#{demand_argument(1, 'path')}"
|
100
|
+
|
101
|
+
if File.exist?("#{path}/tarot_spec.rb")
|
102
|
+
puts 'This already is a valid Tarot folder (tarot_spec.rb exists)'
|
103
|
+
exit(1)
|
104
|
+
end
|
105
|
+
|
106
|
+
FileUtils.mkdir_p(path)
|
107
|
+
File.open("#{path}/tarot_spec.rb", "w") do |file|
|
108
|
+
file.write(
|
109
|
+
<<~RUBY
|
110
|
+
require 'tarot'
|
111
|
+
include Tarot
|
112
|
+
|
113
|
+
Config.build do |config|
|
114
|
+
config.session_expire_days = 13
|
115
|
+
config.url = 'You have to configure the url!'
|
116
|
+
config.database_aliases = {
|
117
|
+
# my_database: 'The name they actually use on metabase'
|
118
|
+
}
|
119
|
+
end
|
120
|
+
RUBY
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
File.open("#{path}/.gitignore", "w") do |file|
|
125
|
+
file.write(
|
126
|
+
<<~TEXT
|
127
|
+
.secret_token
|
128
|
+
consultations/
|
129
|
+
TEXT
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
File.open("#{path}/Gemfile", "w") do |file|
|
134
|
+
file.write(
|
135
|
+
<<~TEXT
|
136
|
+
source 'https://rubygems.org'
|
137
|
+
|
138
|
+
ruby '3.0.0'
|
139
|
+
gem 'metabase_tarot'
|
140
|
+
TEXT
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
puts "New deck created! Remember to configure #{path}/tarot_spec.rb"
|
145
|
+
when 'consultation'
|
146
|
+
input = demand_argument(1, 'consultation name')
|
147
|
+
filepath = "consultations/#{input}.rb"
|
148
|
+
FileUtils.mkdir_p(File.dirname(filepath))
|
149
|
+
|
150
|
+
File.open(filepath, "w") do |file|
|
151
|
+
file.write(
|
152
|
+
<<~RUBY
|
153
|
+
consultation(__FILE__) do
|
154
|
+
{
|
155
|
+
query: db('mydatabase').query!(
|
156
|
+
<<~SQL
|
157
|
+
SELECT *
|
158
|
+
FROM yourtablehere
|
159
|
+
SQL
|
160
|
+
)
|
161
|
+
}
|
162
|
+
end
|
163
|
+
RUBY
|
164
|
+
)
|
165
|
+
end
|
166
|
+
when 'dbs'
|
167
|
+
require_tarot_spec
|
168
|
+
pretty_print_databases
|
169
|
+
when 'cols'
|
170
|
+
require_tarot_spec
|
171
|
+
database_name = demand_argument(1, 'database name')
|
172
|
+
table_name = demand_argument(2, 'table name')
|
173
|
+
database = retrieve_database(database_name)
|
174
|
+
begin
|
175
|
+
query = database.query!("SELECT * FROM #{table_name} LIMIT 1")
|
176
|
+
rescue Tarot::QueryError => e
|
177
|
+
puts "Query for retrieving columns went wrong, probably the table '#{table_name}' does not exist:"
|
178
|
+
puts e
|
179
|
+
exit(1)
|
180
|
+
end
|
181
|
+
pretty_print_array(query.first.keys)
|
182
|
+
when 'tables'
|
183
|
+
require_tarot_spec
|
184
|
+
database_name = demand_argument(1, 'database name')
|
185
|
+
database = retrieve_database(database_name)
|
186
|
+
tables = database.query!("SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'")
|
187
|
+
.map { |x| x.values.first }
|
188
|
+
.reject { |x| x.start_with?("pg_") || x.start_with?("sql_") }
|
189
|
+
pretty_print_array(tables)
|
190
|
+
when 'console'
|
191
|
+
require_tarot_spec
|
192
|
+
Tarot.print_cheatsheet
|
193
|
+
binding.irb
|
194
|
+
else
|
195
|
+
puts "Invalid command #{ARGV[0]}"
|
196
|
+
did_you_mean(ARGV[0], SUBCOMMAND_NAMES)
|
197
|
+
end
|
data/lib/tarot.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'io/console'
|
5
|
+
require 'date'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
# Módulo para interagir com a API do Metabase.
|
9
|
+
module Tarot
|
10
|
+
# Exceção base do Tarot para suprimir backtracing.
|
11
|
+
class TarotError < StandardError
|
12
|
+
def backtrace
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Módulo para interagir com a configuração do tarot local do usuário.
|
17
|
+
module Config
|
18
|
+
# Exceção levantada quando a configuração do usuário (localizado na tarot_spec.rb) falha.
|
19
|
+
class ConfigError < TarotError
|
20
|
+
end
|
21
|
+
|
22
|
+
module_function
|
23
|
+
# Estrutura para representar uma configuração de usuário.
|
24
|
+
Data = Struct.new(:session_expire_days, :url, :database_aliases)
|
25
|
+
|
26
|
+
# Usa um bloco, normalmente definido na tarot_spec.rb, para popular as configurações de usuário.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# Config.build do |config|
|
30
|
+
# config.session_expire_days = 13
|
31
|
+
# config.url = 'https://metabase.mycompany.com'
|
32
|
+
# config.database_aliases = {}
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @yieldreturn [Hash] Bloco que recebe um Tarot::Config::Data e modifica seus valores.
|
36
|
+
# @return [void]
|
37
|
+
# @raise [ConfigError] Se alguma configuração estiver faltando após a execução do bloco.
|
38
|
+
def build(&block)
|
39
|
+
@data = Data.new()
|
40
|
+
block.call(@data)
|
41
|
+
|
42
|
+
if @data.session_expire_days.nil?
|
43
|
+
raise ConfigError, "Tarot config is missing 'session_expire_days'"
|
44
|
+
end
|
45
|
+
|
46
|
+
if @data.url.nil?
|
47
|
+
raise ConfigError, "Tarot config is missing 'url'"
|
48
|
+
end
|
49
|
+
|
50
|
+
if @data.database_aliases.nil?
|
51
|
+
raise ConfigError, "Tarot config is missing 'database_aliases'"
|
52
|
+
end
|
53
|
+
|
54
|
+
@data.session_expire_days.freeze
|
55
|
+
@data.url.freeze
|
56
|
+
@data.database_aliases.freeze
|
57
|
+
|
58
|
+
@built = true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Retorna as configurações de usuário
|
62
|
+
# @return [Config::Data] Resultado da consulta.
|
63
|
+
# @raise [ConfigError] Se as configurações ainda não foram definidas via Config.build.
|
64
|
+
def data
|
65
|
+
return @data unless @built.nil?
|
66
|
+
|
67
|
+
raise ConfigError, 'Tarot is not properly configured, check tarot_spec.rb'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Caminho para o arquivo que carrega o token de autenticação do usuário + a data de expiração do token
|
72
|
+
SECRET_TOKEN_PATH = "#{Dir.pwd}/.secret_token".freeze
|
73
|
+
|
74
|
+
# Exceção levantada quando a autenticação falha.
|
75
|
+
class LoginError < TarotError
|
76
|
+
end
|
77
|
+
|
78
|
+
# Exceção levantada quando uma database não é encontrada na API. Imprime na tela quais são as database disponíveis.
|
79
|
+
class DatabaseNotFound < TarotError
|
80
|
+
end
|
81
|
+
|
82
|
+
# Exceção levantada quando o endpoint de query retornou erros. Vem com informações sobre o erro que ocorreu, caso a API disponibilize.
|
83
|
+
class QueryError < TarotError
|
84
|
+
end
|
85
|
+
|
86
|
+
# Estrutura para representar uma database na API.
|
87
|
+
Database = Struct.new(:id, :name) do
|
88
|
+
def inspect
|
89
|
+
"<Metabase::Database id=#{id.inspect}, name=#{name.inspect}>"
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
"'#{name}'(id #{id})"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Mesmo que Tarot.query!(esse_banco, sql)
|
97
|
+
# Executa uma consulta SQL e retorna o resultado. Levanta erro conforme resposta da API.
|
98
|
+
# @param sql [String] Consulta SQL a ser executada.
|
99
|
+
# @return [Object] Resultado da consulta.
|
100
|
+
# @raise [QueryError] Se ocorrer um erro na consulta.
|
101
|
+
def query!(sql)
|
102
|
+
Tarot.query!(self, sql)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Mesmo que Tarot.query(esse_banco, sql)
|
106
|
+
# Executa uma consulta SQL e retorna o resultado.
|
107
|
+
# @param sql [String] Consulta SQL a ser executada.
|
108
|
+
# @return [Object] Resultado da API contendo a resposta da consulta ou um JSON com os erros que ocorreram.
|
109
|
+
def query(sql)
|
110
|
+
Tarot.query(self, sql)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module_function
|
115
|
+
|
116
|
+
# Grava os resultados de um bloco em um arquivo YAML com o mesmo nome passado pelo argumento filepath (porém terminando em .yaml), incluindo metadados de quando foi gerado, e do script que gerou o arquivo.
|
117
|
+
#
|
118
|
+
# @example
|
119
|
+
# exemplos/test.rb
|
120
|
+
# consultation(__FILE__) do
|
121
|
+
# db('minha db').query!('SELECT * FROM plans WHERE id = 3;')
|
122
|
+
# end
|
123
|
+
# # Isto irá criar um arquivo 'exemplos/test.yaml' com os resultados do bloco e metadados.
|
124
|
+
#
|
125
|
+
# @param filepath [String] Caminho do arquivo de origem, cujo conteúdo será usado nos metadados. É esperado que se preencha com __FILE__.
|
126
|
+
# @yieldreturn [Hash] Bloco que retorna um hash com os dados a serem gravados.
|
127
|
+
# @return [void]
|
128
|
+
def consultation(filepath, &block)
|
129
|
+
target_file = filepath.gsub(/\.rb\z/, '.yaml')
|
130
|
+
|
131
|
+
File.write(target_file, YAML.dump({
|
132
|
+
metadata: {
|
133
|
+
generated_at: DateTime.now.iso8601,
|
134
|
+
generated_by: File.read(filepath)
|
135
|
+
},
|
136
|
+
results: block.call()
|
137
|
+
}))
|
138
|
+
puts "Recorded #{target_file}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Encontra uma database pelo nome/alias.
|
142
|
+
# @param name [String] Nome ou alias da database.
|
143
|
+
# @return [Database] A database encontrada.
|
144
|
+
# @raise [DatabaseNotFound] Se a database não for encontrada.
|
145
|
+
def database(name)
|
146
|
+
if Config.data.database_aliases.keys.include?(name.to_sym)
|
147
|
+
name = Config.data.database_aliases[name.to_sym]
|
148
|
+
end
|
149
|
+
|
150
|
+
result = databases.find { |d| d.name == name }
|
151
|
+
|
152
|
+
unless result
|
153
|
+
message = <<~TEXT
|
154
|
+
|
155
|
+
Database not found: #{name}
|
156
|
+
To see available databases, run: tarot dbs
|
157
|
+
TEXT
|
158
|
+
|
159
|
+
raise DatabaseNotFound, message
|
160
|
+
end
|
161
|
+
|
162
|
+
result
|
163
|
+
end
|
164
|
+
|
165
|
+
alias db database
|
166
|
+
|
167
|
+
# Retorna uma lista do nome de todos os nomes de databases disponíveis. Caso exista um alias para um nome, ele substituirá o nome.
|
168
|
+
# @return [String] String com o nomes das databases.
|
169
|
+
def database_names
|
170
|
+
names = Tarot.databases.map(&:name)
|
171
|
+
aliases = Tarot::Config.data.database_aliases.keys.map(&:to_s)
|
172
|
+
|
173
|
+
names.filter! do |name|
|
174
|
+
!Tarot::Config.data.database_aliases.values.include?(name)
|
175
|
+
end
|
176
|
+
|
177
|
+
(names + aliases)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Retorna uma lista de todas as databases disponíveis.
|
181
|
+
# @return [Array<Database>] Array de objetos Database.
|
182
|
+
def databases
|
183
|
+
unless @databases
|
184
|
+
uri = URI.parse("#{Config.data.url}/api/database")
|
185
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
186
|
+
http.use_ssl = true
|
187
|
+
|
188
|
+
request = Net::HTTP::Get.new(uri)
|
189
|
+
request['Accept'] = 'application/json'
|
190
|
+
request['X-Metabase-Session'] = fetch_token
|
191
|
+
|
192
|
+
response = http.request(request)
|
193
|
+
@databases = JSON.parse(response.body).map do |payload|
|
194
|
+
Database.new(payload['id'], payload['name'])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
@databases
|
198
|
+
end
|
199
|
+
|
200
|
+
# Executa uma consulta SQL e retorna o resultado. Levanta erro conforme resposta da API.
|
201
|
+
# @param database [Database] Base de dados onde executar a consulta.
|
202
|
+
# @param sql [String] Consulta SQL a ser executada.
|
203
|
+
# @return [Object] Resultado da consulta.
|
204
|
+
# @raise [QueryError] Se ocorrer um erro na consulta.
|
205
|
+
def query!(database, sql)
|
206
|
+
result = query(database, sql)
|
207
|
+
raise QueryError, result['error'] if result.is_a?(Hash) && result['error']
|
208
|
+
|
209
|
+
result
|
210
|
+
end
|
211
|
+
|
212
|
+
# Executa uma consulta SQL e retorna o resultado.
|
213
|
+
# @param database [Database] Base de dados onde executar a consulta. Use Tarot.database('minah db') para conseguir esse objeto.
|
214
|
+
# @param sql [String] Consulta SQL a ser executada.
|
215
|
+
# @return [Object] Resultado da API contendo a resposta da consulta ou um JSON com os erros que ocorreram.
|
216
|
+
def query(database, sql)
|
217
|
+
uri = URI.parse("#{Config.data.url}/api/dataset/json")
|
218
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
219
|
+
http.use_ssl = true
|
220
|
+
|
221
|
+
request = Net::HTTP::Post.new(uri,
|
222
|
+
'Accept' => 'application/json',
|
223
|
+
'X-Metabase-Session' => fetch_token
|
224
|
+
)
|
225
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
226
|
+
|
227
|
+
params = {
|
228
|
+
'query': JSON.dump({
|
229
|
+
'type' => 'native',
|
230
|
+
'database' => database.id,
|
231
|
+
'parameters' => [],
|
232
|
+
'native' => {
|
233
|
+
'query' => sql,
|
234
|
+
'template-tags' => {}
|
235
|
+
},
|
236
|
+
})
|
237
|
+
}
|
238
|
+
request.body = URI.encode_www_form(params)
|
239
|
+
response = http.request(request)
|
240
|
+
JSON.parse(response.body) rescue response.body
|
241
|
+
end
|
242
|
+
|
243
|
+
# Limpa o token de sessão, armazenado localmente.
|
244
|
+
def clear_token
|
245
|
+
File.write(SECRET_TOKEN_PATH, '{}')
|
246
|
+
end
|
247
|
+
|
248
|
+
# Imprime a cheatsheet na tela
|
249
|
+
def print_cheatsheet
|
250
|
+
puts <<~TEXT
|
251
|
+
Tarot Cheatsheet
|
252
|
+
|
253
|
+
┌─────────────────────────┬────────────────────────────────────────────────────┐
|
254
|
+
│ Ver databases │ puts database_names │
|
255
|
+
│ Puxar database por nome | db('nome') │
|
256
|
+
│ Fazer query │ minha_db.query!('SELECT * FROM mytable') │
|
257
|
+
│ Limpar dados de sessão │ clear_token │
|
258
|
+
│ Ver essa mensagem │ print_cheatsheet │
|
259
|
+
└─────────────────────────┴────────────────────────────────────────────────────┘
|
260
|
+
TEXT
|
261
|
+
end
|
262
|
+
|
263
|
+
# Obtém o token de sessão do Metabase, solicitando ao usuário se necessário.
|
264
|
+
# @return [String] Token de sessão.
|
265
|
+
def fetch_token
|
266
|
+
token_json = File.exist?(SECRET_TOKEN_PATH) ? JSON.load(File.read(SECRET_TOKEN_PATH)) : {}
|
267
|
+
token = token_json['token']
|
268
|
+
|
269
|
+
if token.nil?
|
270
|
+
uri = URI("#{Config.data.url}/api/session")
|
271
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
272
|
+
http.use_ssl = true
|
273
|
+
|
274
|
+
request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
|
275
|
+
login = {}
|
276
|
+
puts 'Login to Metabase'
|
277
|
+
print 'Username: '
|
278
|
+
login['username'] = STDIN.gets.chomp
|
279
|
+
print 'Password: '
|
280
|
+
login['password'] = STDIN.noecho(&:gets).chomp
|
281
|
+
puts "\n\n"
|
282
|
+
|
283
|
+
request.body = JSON.dump(login)
|
284
|
+
begin
|
285
|
+
response = http.request(request)
|
286
|
+
rescue OpenSSL::SSL::SSLError
|
287
|
+
puts "#{Config.data.url} seems to be unreachable at the moment"
|
288
|
+
end
|
289
|
+
|
290
|
+
token = JSON.parse(response.body)['id']
|
291
|
+
|
292
|
+
raise(LoginError, 'Wrong credentials') if token.nil?
|
293
|
+
|
294
|
+
File.write(SECRET_TOKEN_PATH, JSON.dump(token: token, expire_at: DateTime.now + Config.data.session_expire_days))
|
295
|
+
puts 'Authorized! :>'
|
296
|
+
else
|
297
|
+
expire_at = DateTime.parse(token_json['expire_at'])
|
298
|
+
if DateTime.now >= expire_at
|
299
|
+
puts 'Your token expired'
|
300
|
+
clear_token
|
301
|
+
fetch_token
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
token
|
306
|
+
end
|
307
|
+
end
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: metabase_tarot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hikari Luz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Utility tool for connecting directly to Metabase via its API
|
14
|
+
email: hikaridesuyoo@gmail.com
|
15
|
+
executables:
|
16
|
+
- tarot
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/tarot
|
21
|
+
- lib/tarot.rb
|
22
|
+
homepage:
|
23
|
+
licenses:
|
24
|
+
- GPL-3.0
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 3.0.0
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubygems_version: 3.2.3
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: Want to know something from Metabase? Tarot will give you the answer!
|
45
|
+
test_files: []
|