blazer 2.6.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +13 -28
  5. data/app/assets/javascripts/blazer/ace/ace.js +7235 -8906
  6. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +762 -774
  7. data/app/assets/javascripts/blazer/ace/mode-sql.js +177 -72
  8. data/app/assets/javascripts/blazer/ace/snippets/sql.js +5 -29
  9. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -6
  10. data/app/assets/javascripts/blazer/ace/theme-twilight.js +8 -106
  11. data/app/assets/javascripts/blazer/application.js +9 -6
  12. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  13. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  14. data/app/assets/javascripts/blazer/chartkick.js +1020 -914
  15. data/app/assets/javascripts/blazer/highlight.min.js +466 -3
  16. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  17. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +39 -38
  18. data/app/assets/javascripts/blazer/moment.js +105 -88
  19. data/app/assets/javascripts/blazer/queries.js +10 -1
  20. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  21. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  22. data/app/assets/stylesheets/blazer/bootstrap.css +1 -1
  23. data/app/assets/stylesheets/blazer/selectize.css +1 -1
  24. data/app/controllers/blazer/base_controller.rb +85 -84
  25. data/app/controllers/blazer/checks_controller.rb +6 -6
  26. data/app/controllers/blazer/dashboards_controller.rb +24 -24
  27. data/app/controllers/blazer/queries_controller.rb +208 -186
  28. data/app/controllers/blazer/uploads_controller.rb +49 -49
  29. data/app/helpers/blazer/base_helper.rb +0 -4
  30. data/app/models/blazer/query.rb +1 -12
  31. data/app/views/blazer/checks/index.html.erb +1 -1
  32. data/app/views/blazer/dashboards/_form.html.erb +11 -5
  33. data/app/views/blazer/queries/_form.html.erb +19 -14
  34. data/app/views/blazer/queries/docs.html.erb +11 -1
  35. data/app/views/blazer/queries/home.html.erb +9 -6
  36. data/app/views/blazer/queries/run.html.erb +17 -32
  37. data/app/views/blazer/queries/show.html.erb +12 -20
  38. data/app/views/layouts/blazer/application.html.erb +1 -5
  39. data/lib/blazer/adapters/sql_adapter.rb +1 -1
  40. data/lib/blazer/adapters.rb +17 -0
  41. data/lib/blazer/anomaly_detectors.rb +22 -0
  42. data/lib/blazer/data_source.rb +29 -40
  43. data/lib/blazer/engine.rb +11 -9
  44. data/lib/blazer/forecasters.rb +7 -0
  45. data/lib/blazer/result.rb +13 -71
  46. data/lib/blazer/result_cache.rb +71 -0
  47. data/lib/blazer/run_statement.rb +1 -1
  48. data/lib/blazer/run_statement_job.rb +2 -2
  49. data/lib/blazer/statement.rb +3 -1
  50. data/lib/blazer/version.rb +1 -1
  51. data/lib/blazer.rb +51 -53
  52. data/licenses/LICENSE-chart.js.txt +1 -1
  53. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  54. data/licenses/LICENSE-chartkick.js.txt +1 -1
  55. data/licenses/LICENSE-date-fns.txt +21 -0
  56. data/licenses/LICENSE-kurkle-color.txt +9 -0
  57. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  58. data/licenses/{LICENSE-jquery-ujs.txt → LICENSE-rails-ujs.txt} +1 -1
  59. data/licenses/LICENSE-vue.txt +1 -1
  60. metadata +26 -18
  61. data/app/assets/javascripts/blazer/Chart.js +0 -16172
  62. data/app/assets/javascripts/blazer/jquery-ujs.js +0 -555
  63. data/app/assets/javascripts/blazer/vue.js +0 -12014
  64. data/lib/blazer/adapters/mongodb_adapter.rb +0 -43
  65. data/lib/blazer/detect_anomalies.R +0 -19
data/lib/blazer/result.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Blazer
2
2
  class Result
3
- attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached, :forecast_error
3
+ attr_reader :data_source, :columns, :rows, :error, :forecast_error
4
+ attr_accessor :cached_at, :just_cached
4
5
 
5
6
  def initialize(data_source, columns, rows, error, cached_at, just_cached)
6
7
  @data_source = data_source
@@ -19,9 +20,9 @@ module Blazer
19
20
  cached_at.present?
20
21
  end
21
22
 
22
- def boom
23
- @boom ||= begin
24
- boom = {}
23
+ def smart_values
24
+ @smart_values ||= begin
25
+ smart_values = {}
25
26
  columns.each_with_index do |key, i|
26
27
  smart_columns_data_source =
27
28
  ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_columns[key] }
@@ -37,10 +38,10 @@ module Blazer
37
38
  result.rows
38
39
  end
39
40
 
40
- boom[key] = Hash[res.map { |k, v| [k.nil? ? k : k.to_s, v] }]
41
+ smart_values[key] = res.to_h { |k, v| [k.nil? ? k : k.to_s, v] }
41
42
  end
42
43
  end
43
- boom
44
+ smart_values
44
45
  end
45
46
  end
46
47
 
@@ -48,7 +49,7 @@ module Blazer
48
49
  @column_types ||= begin
49
50
  columns.each_with_index.map do |k, i|
50
51
  v = (rows.find { |r| r[i] } || {})[i]
51
- if boom[k]
52
+ if smart_values[k]
52
53
  "string"
53
54
  elsif v.is_a?(Numeric)
54
55
  "numeric"
@@ -93,14 +94,8 @@ module Blazer
93
94
  def forecast
94
95
  count = (@rows.size * 0.25).round.clamp(30, 365)
95
96
 
96
- case Blazer.forecasting
97
- when "prophet"
98
- require "prophet"
99
- forecast = Prophet.forecast(@rows.to_h, count: count)
100
- else
101
- require "trend"
102
- forecast = Trend.forecast(@rows.to_h, count: count)
103
- end
97
+ forecaster = Blazer.forecasters.fetch(Blazer.forecasting)
98
+ forecast = forecaster.call(@rows.to_h, count: count)
104
99
 
105
100
  # round integers
106
101
  if @rows[0][1].is_a?(Integer)
@@ -139,7 +134,7 @@ module Blazer
139
134
  series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
140
135
  end
141
136
  else
142
- rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
137
+ rows.group_by { |r| v = r[1]; (smart_values[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
143
138
  series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
144
139
  end
145
140
  end
@@ -176,61 +171,8 @@ module Blazer
176
171
  def anomaly?(series)
177
172
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
178
173
 
179
- case Blazer.anomaly_checks
180
- when "prophet"
181
- df = Rover::DataFrame.new(series[0..-2].map { |v| {"ds" => v[0], "y" => v[1]} })
182
- m = Prophet.new(interval_width: 0.99)
183
- m.logger.level = ::Logger::FATAL # no logging
184
- m.fit(df)
185
- future = Rover::DataFrame.new(series[-1..-1].map { |v| {"ds" => v[0]} })
186
- forecast = m.predict(future).to_a[0]
187
- lower = forecast["yhat_lower"]
188
- upper = forecast["yhat_upper"]
189
- value = series.last[1]
190
- value < lower || value > upper
191
- when "trend"
192
- anomalies = Trend.anomalies(Hash[series])
193
- anomalies.include?(series.last[0])
194
- when "anomaly_detection"
195
- period = 7 # TODO determine period
196
- anomalies = AnomalyDetection.detect(Hash[series], period: period)
197
- anomalies.include?(series.last[0])
198
- else
199
- csv_str =
200
- CSV.generate do |csv|
201
- csv << ["timestamp", "count"]
202
- series.each do |row|
203
- csv << row
204
- end
205
- end
206
-
207
- r_script = %x[which Rscript].chomp
208
- type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
209
- args = [type, csv_str]
210
- raise "R not found" if r_script.empty?
211
- command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
212
- output = %x[#{command}]
213
- if output.empty?
214
- raise "Unknown R error"
215
- end
216
-
217
- rows = CSV.parse(output, headers: true)
218
- error = rows.first && rows.first["x"]
219
- raise error if error
220
-
221
- timestamps = []
222
- if type == "ts"
223
- rows.each do |row|
224
- timestamps << Time.parse(row["timestamp"])
225
- end
226
- timestamps.include?(series.last[0].to_time)
227
- else
228
- rows.each do |row|
229
- timestamps << row["index"].to_i
230
- end
231
- timestamps.include?(series.length)
232
- end
233
- end
174
+ anomaly_detector = Blazer.anomaly_detectors.fetch(Blazer.anomaly_checks)
175
+ anomaly_detector.call(series)
234
176
  end
235
177
  end
236
178
  end
@@ -0,0 +1,71 @@
1
+ module Blazer
2
+ class ResultCache
3
+ def initialize(data_source)
4
+ @data_source = data_source
5
+ end
6
+
7
+ def write_run(run_id, result)
8
+ write(run_cache_key(run_id), result, expires_in: 30.seconds)
9
+ end
10
+
11
+ def read_run(run_id)
12
+ read(run_cache_key(run_id))
13
+ end
14
+
15
+ def delete_run(run_id)
16
+ delete(run_cache_key(run_id))
17
+ end
18
+
19
+ def write_statement(statement, result, expires_in:)
20
+ write(statement_cache_key(statement), result, expires_in: expires_in) if caching?
21
+ end
22
+
23
+ def read_statement(statement)
24
+ read(statement_cache_key(statement)) if caching?
25
+ end
26
+
27
+ def delete_statement(statement)
28
+ delete(statement_cache_key(statement)) if caching?
29
+ end
30
+
31
+ private
32
+
33
+ def write(key, result, expires_in:)
34
+ raise ArgumentError, "expected Blazer::Result" unless result.is_a?(Blazer::Result)
35
+ value = [result.columns, result.rows, result.error, result.cached_at, result.just_cached]
36
+ cache.write(key, value, expires_in: expires_in)
37
+ end
38
+
39
+ def read(key)
40
+ value = cache.read(key)
41
+ if value
42
+ columns, rows, error, cached_at, just_cached = value
43
+ Blazer::Result.new(@data_source, columns, rows, error, cached_at, just_cached)
44
+ end
45
+ end
46
+
47
+ def delete(key)
48
+ cache.delete(key)
49
+ end
50
+
51
+ def caching?
52
+ @data_source.cache_mode != "off"
53
+ end
54
+
55
+ def cache_key(key)
56
+ (["blazer", "v5", @data_source.id] + key).join("/")
57
+ end
58
+
59
+ def statement_cache_key(statement)
60
+ cache_key(["statement", Digest::SHA256.hexdigest(statement.bind_statement.to_s.gsub("\r\n", "\n") + statement.bind_values.to_json)])
61
+ end
62
+
63
+ def run_cache_key(run_id)
64
+ cache_key(["run", run_id])
65
+ end
66
+
67
+ def cache
68
+ Blazer.cache
69
+ end
70
+ end
71
+ end
@@ -32,7 +32,7 @@ module Blazer
32
32
  audit.save! if audit.changed?
33
33
  end
34
34
 
35
- if query && !result.timed_out? && !query.variables.any?
35
+ if query && !result.timed_out? && !result.cached? && !query.variables.any?
36
36
  query.checks.each do |check|
37
37
  check.update_state(result)
38
38
  end
@@ -11,8 +11,8 @@ module Blazer
11
11
  Blazer::RunStatement.new.perform(statement, options)
12
12
  end
13
13
  rescue Exception => e
14
- Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
15
- Blazer.cache.write(data_source.run_cache_key(options[:run_id]), Marshal.dump([[], [], "Unknown error", nil]), expires_in: 30.seconds)
14
+ result = Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
15
+ data_source.result_cache.write_run(options[:run_id], result)
16
16
  raise e
17
17
  end
18
18
  end
@@ -10,7 +10,9 @@ module Blazer
10
10
  end
11
11
 
12
12
  def variables
13
- @variables ||= Blazer.extract_vars(statement)
13
+ # strip commented out lines
14
+ # and regex {1} or {1,2}
15
+ @variables ||= statement.to_s.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
14
16
  end
15
17
 
16
18
  def add_values(var_params)
@@ -1,3 +1,3 @@
1
1
  module Blazer
2
- VERSION = "2.6.5"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/blazer.rb CHANGED
@@ -4,39 +4,40 @@ require "safely/core"
4
4
 
5
5
  # stdlib
6
6
  require "csv"
7
+ require "digest/sha2"
7
8
  require "json"
8
9
  require "yaml"
9
10
 
10
11
  # modules
11
- require "blazer/version"
12
- require "blazer/data_source"
13
- require "blazer/result"
14
- require "blazer/run_statement"
15
- require "blazer/statement"
12
+ require_relative "blazer/version"
13
+ require_relative "blazer/data_source"
14
+ require_relative "blazer/result"
15
+ require_relative "blazer/result_cache"
16
+ require_relative "blazer/run_statement"
17
+ require_relative "blazer/statement"
16
18
 
17
19
  # adapters
18
- require "blazer/adapters/base_adapter"
19
- require "blazer/adapters/athena_adapter"
20
- require "blazer/adapters/bigquery_adapter"
21
- require "blazer/adapters/cassandra_adapter"
22
- require "blazer/adapters/drill_adapter"
23
- require "blazer/adapters/druid_adapter"
24
- require "blazer/adapters/elasticsearch_adapter"
25
- require "blazer/adapters/hive_adapter"
26
- require "blazer/adapters/ignite_adapter"
27
- require "blazer/adapters/influxdb_adapter"
28
- require "blazer/adapters/mongodb_adapter"
29
- require "blazer/adapters/neo4j_adapter"
30
- require "blazer/adapters/opensearch_adapter"
31
- require "blazer/adapters/presto_adapter"
32
- require "blazer/adapters/salesforce_adapter"
33
- require "blazer/adapters/soda_adapter"
34
- require "blazer/adapters/spark_adapter"
35
- require "blazer/adapters/sql_adapter"
36
- require "blazer/adapters/snowflake_adapter"
20
+ require_relative "blazer/adapters/base_adapter"
21
+ require_relative "blazer/adapters/athena_adapter"
22
+ require_relative "blazer/adapters/bigquery_adapter"
23
+ require_relative "blazer/adapters/cassandra_adapter"
24
+ require_relative "blazer/adapters/drill_adapter"
25
+ require_relative "blazer/adapters/druid_adapter"
26
+ require_relative "blazer/adapters/elasticsearch_adapter"
27
+ require_relative "blazer/adapters/hive_adapter"
28
+ require_relative "blazer/adapters/ignite_adapter"
29
+ require_relative "blazer/adapters/influxdb_adapter"
30
+ require_relative "blazer/adapters/neo4j_adapter"
31
+ require_relative "blazer/adapters/opensearch_adapter"
32
+ require_relative "blazer/adapters/presto_adapter"
33
+ require_relative "blazer/adapters/salesforce_adapter"
34
+ require_relative "blazer/adapters/soda_adapter"
35
+ require_relative "blazer/adapters/spark_adapter"
36
+ require_relative "blazer/adapters/sql_adapter"
37
+ require_relative "blazer/adapters/snowflake_adapter"
37
38
 
38
39
  # engine
39
- require "blazer/engine"
40
+ require_relative "blazer/engine"
40
41
 
41
42
  module Blazer
42
43
  class Error < StandardError; end
@@ -66,8 +67,6 @@ module Blazer
66
67
  attr_accessor :forecasting
67
68
  attr_accessor :async
68
69
  attr_accessor :images
69
- attr_accessor :query_viewable
70
- attr_accessor :query_editable
71
70
  attr_accessor :override_csp
72
71
  attr_accessor :slack_oauth_token
73
72
  attr_accessor :slack_webhook_url
@@ -118,7 +117,7 @@ module Blazer
118
117
  @settings ||= begin
119
118
  path = Rails.root.join("config", "blazer.yml").to_s
120
119
  if File.exist?(path)
121
- YAML.load(ERB.new(File.read(path)).result)
120
+ YAML.safe_load(ERB.new(File.read(path)).result, aliases: true)
122
121
  else
123
122
  {}
124
123
  end
@@ -135,13 +134,6 @@ module Blazer
135
134
  end
136
135
  end
137
136
 
138
- # TODO move to Statement and remove in 3.0.0
139
- def self.extract_vars(statement)
140
- # strip commented out lines
141
- # and regex {1} or {1,2}
142
- statement.to_s.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
143
- end
144
-
145
137
  def self.run_checks(schedule: nil)
146
138
  checks = Blazer::Check.includes(:query)
147
139
  checks = checks.where(schedule: schedule) if schedule
@@ -225,6 +217,11 @@ module Blazer
225
217
  slack_oauth_token.present? || slack_webhook_url.present?
226
218
  end
227
219
 
220
+ # TODO show warning on invalid access token
221
+ def self.maps?
222
+ mapbox_access_token.present? && mapbox_access_token.start_with?("pk.")
223
+ end
224
+
228
225
  def self.uploads?
229
226
  settings.key?("uploads")
230
227
  end
@@ -250,6 +247,22 @@ module Blazer
250
247
  adapters[name] = adapter
251
248
  end
252
249
 
250
+ def self.anomaly_detectors
251
+ @anomaly_detectors ||= {}
252
+ end
253
+
254
+ def self.register_anomaly_detector(name, &anomaly_detector)
255
+ anomaly_detectors[name] = anomaly_detector
256
+ end
257
+
258
+ def self.forecasters
259
+ @forecasters ||= {}
260
+ end
261
+
262
+ def self.register_forecaster(name, &forecaster)
263
+ forecasters[name] = forecaster
264
+ end
265
+
253
266
  def self.archive_queries
254
267
  raise "Audits must be enabled to archive" unless Blazer.audit
255
268
  raise "Missing status column - see https://github.com/ankane/blazer#23" unless Blazer::Query.column_names.include?("status")
@@ -264,21 +277,6 @@ module Blazer
264
277
  end
265
278
  end
266
279
 
267
- Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
268
- Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
269
- Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
270
- Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
271
- Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
272
- Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
273
- Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
274
- Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
275
- Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
276
- Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
277
- Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
278
- Blazer.register_adapter "opensearch", Blazer::Adapters::OpensearchAdapter
279
- Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
280
- Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
281
- Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
282
- Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
283
- Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
284
- Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
280
+ require_relative "blazer/adapters"
281
+ require_relative "blazer/anomaly_detectors"
282
+ require_relative "blazer/forecasters"
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Chart.js Contributors
3
+ Copyright (c) 2014-2022 Chart.js Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Chart.js Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2019 Andrew Kane
1
+ Copyright (c) 2013-2023 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Sasha Koss and Lesha Koss https://kossnocorp.mit-license.org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018-2021 Jukka Kurkela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.