metabase_tarot 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|