bas 1.7.1 → 1.7.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63490ab5165fd3aa1793a7521272869addb0ebd691d65e7e300b757af770b6fe
4
- data.tar.gz: 834e5ff6ebe43795ddfd94fa19f7d3435cd6ae99fc24902abdec2a8c3215ce3d
3
+ metadata.gz: 87acac8ff5c199705abe5b3e41d72834bf104298aa2533740dd40a637f7d7ca2
4
+ data.tar.gz: d8bc727403b7eaaf2211116fb0e697a0c7b709022b43528ac1d4afffb25c7167
5
5
  SHA512:
6
- metadata.gz: 97a51249e553a947453084004b2c4701f801bd43a9b14c439721fe1f3e2262813ac541ce0326262997757368eddfc46e08390938f116eef3eb75ecdb80d5e41d
7
- data.tar.gz: 2d0d10c08ae2c5a1810e88c624e42d11dbf16ecacc45184185f0ff55caf27a0819b37baeb645991d3eac1ace9595dd1fedba1c4c7939460d824acfaa0fcc1d86
6
+ metadata.gz: e2fa969db931a22fa52e973ff7438e68f3cace2a25dd625394230c13b2939f0aa0190f3068ce69a16346bc9d7cc53aa52c7fde54f9cf76c97801f37f9c4b20c3
7
+ data.tar.gz: bd4443f7a5ce9c638936cbb73062148bf77c7fe1721eba3470f602d6cfcbb5b7d0c6dca815e3e79425875a921224cfc0fee5ebf16922fe50426524273d354195
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ # 1.7.2 (04.07.2025)
4
+ - [Improve Elasticsearch mapping creation](https://github.com/kommitters/bas/pull/140)
5
+ - [Fix in Elasticsearch shared storage update_stage method](https://github.com/kommitters/bas/pull/139)
6
+ - [feat: Add Elasticsearch Shared Storage](https://github.com/kommitters/bas/issues/138)
7
+
3
8
  # 1.7.1 (11.02.2025)
4
9
  - [Fix absolute_path in order to execute the scripts](https://github.com/kommitters/bas/pull/133)
5
10
 
data/Gemfile CHANGED
@@ -15,8 +15,8 @@ gem "simplecov-lcov", "~> 0.8.0"
15
15
  gem "vcr"
16
16
  gem "webmock"
17
17
 
18
+ gem "elasticsearch", "~> 8.0"
18
19
  gem "httparty"
19
-
20
20
  gem "pg", "~> 1.5", ">= 1.5.4"
21
21
 
22
22
  group :test do
@@ -29,4 +29,5 @@ group :test do
29
29
  gem "net-smtp", "~> 0.4.0.1"
30
30
  gem "octokit", "~> 8.1.0"
31
31
  gem "openssl", "~> 3.2"
32
+ gem "timecop"
32
33
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent-ruby"
4
+ require "date"
4
5
 
5
6
  module Bas
6
7
  module Orchestrator
@@ -10,6 +11,7 @@ module Bas
10
11
  # This class initializes a thread pool and processes scheduled scripts based on
11
12
  # time intervals, specific days, or exact times.
12
13
  #
14
+ # rubocop:disable Metrics/ClassLength
13
15
  class Manager
14
16
  def initialize(schedules)
15
17
  @last_executions = Hash.new(0.0)
@@ -30,9 +32,7 @@ module Bas
30
32
  loop do
31
33
  @actual_time = Time.new
32
34
 
33
- execute_interval(script) if interval?(script)
34
- execute_day(script) if day?(script) && time?(script)
35
- execute_time(script) if time?(script) && !day?(script)
35
+ procces_schedule(script)
36
36
 
37
37
  sleep 0.1
38
38
  rescue StandardError => e
@@ -40,6 +40,13 @@ module Bas
40
40
  end
41
41
  end
42
42
 
43
+ def procces_schedule(script)
44
+ execute_interval(script) if interval?(script)
45
+ execute_day(script) if day?(script) && time?(script)
46
+ execute_time(script) if time?(script) && !day?(script)
47
+ execute_custom_rule(script, @actual_time) if custom_rule?(script)
48
+ end
49
+
43
50
  def execute_interval(script)
44
51
  return unless time_in_milliseconds - @last_executions[script[:path]] >= script[:interval]
45
52
 
@@ -59,6 +66,57 @@ module Bas
59
66
  @last_executions[script[:path]] = current_time
60
67
  end
61
68
 
69
+ # rubocop:disable Metrics/MethodLength
70
+ def execute_custom_rule(script, current_moment)
71
+ rule = script[:custom_rule]
72
+ case rule[:type]
73
+ when "last_day_of_week_in_month"
74
+ execute_last_day_of_week_in_month(script, rule, current_moment)
75
+ when "last_day_of_month"
76
+ execute_last_day_of_month(script, rule, current_moment)
77
+ when "last_day_of_week"
78
+ execute_last_day_of_week(script, rule, current_moment)
79
+ when "last_day_of_year"
80
+ execute_last_day_of_year(script, rule, current_moment)
81
+ else
82
+ puts "Unknown custom rule type: #{rule[:type]} for script '#{script[:path]}'"
83
+ end
84
+ end
85
+ # rubocop:enable Metrics/MethodLength
86
+
87
+ def execute_last_day_of_week_in_month(script, rule, current_moment)
88
+ return unless rule[:time]&.include?(current_moment.strftime("%H:%M"))
89
+ return unless today_is_last_day_of_week_in_month?(current_moment, rule[:day_of_week])
90
+
91
+ execute_once_per_time(script, current_moment.strftime("%H:%M"))
92
+ end
93
+
94
+ def execute_last_day_of_month(script, rule, current_moment)
95
+ return unless rule[:time]&.include?(current_moment.strftime("%H:%M"))
96
+ return unless current_moment.to_date == end_of_month(current_moment.to_date)
97
+
98
+ execute_once_per_time(script, current_moment.strftime("%H:%M"))
99
+ end
100
+
101
+ def execute_last_day_of_week(script, rule, current_moment)
102
+ return unless rule[:time]&.include?(current_moment.strftime("%H:%M"))
103
+
104
+ target_wday = Date::DAYNAMES.index { |name| name.casecmp(rule[:day_of_week]).zero? }
105
+ return if target_wday.nil?
106
+
107
+ today = current_moment.to_date
108
+ return unless today.wday == target_wday && last_occurrence_in_week?(today, target_wday)
109
+
110
+ execute_once_per_time(script, current_moment.strftime("%H:%M"))
111
+ end
112
+
113
+ def execute_last_day_of_year(script, rule, current_moment)
114
+ return unless rule[:time]&.include?(current_moment.strftime("%H:%M"))
115
+ return unless current_moment.month == 12 && current_moment.day == 31
116
+
117
+ execute_once_per_time(script, current_moment.strftime("%H:%M"))
118
+ end
119
+
62
120
  def interval?(script)
63
121
  script[:interval]
64
122
  end
@@ -71,6 +129,10 @@ module Bas
71
129
  script[:day]
72
130
  end
73
131
 
132
+ def custom_rule?(script)
133
+ script[:custom_rule] && script[:custom_rule][:type]
134
+ end
135
+
74
136
  def time_in_milliseconds
75
137
  @actual_time.to_f * 1000
76
138
  end
@@ -83,6 +145,41 @@ module Bas
83
145
  @actual_time.strftime("%A")
84
146
  end
85
147
 
148
+ def today_is_last_day_of_week_in_month?(time_obj, target_day_name)
149
+ date = time_obj.to_date
150
+ target_wday = Date::DAYNAMES.index { |name| name.casecmp(target_day_name).zero? }
151
+
152
+ return false if target_wday.nil?
153
+ return false unless date.wday == target_wday
154
+
155
+ last_target_day_of_month?(date)
156
+ end
157
+
158
+ def last_occurrence_in_week?(date, target_wday)
159
+ current_date = date
160
+ next_occurrence = current_date + 7
161
+ next_occurrence.wday == target_wday && last_target_day_of_month?(current_date)
162
+ end
163
+
164
+ def last_target_day_of_month?(date)
165
+ current_date = date
166
+ next_occurrence = current_date + 7
167
+
168
+ next_occurrence.month != current_date.month
169
+ end
170
+
171
+ def end_of_month(date)
172
+ next_month = date.month == 12 ? Date.new(date.year + 1, 1, 1) : Date.new(date.year, date.month + 1, 1)
173
+ next_month - 1
174
+ end
175
+
176
+ def execute_once_per_time(script, execution_time)
177
+ return if @last_executions[script[:path]] == execution_time
178
+
179
+ execute(script)
180
+ @last_executions[script[:path]] = execution_time
181
+ end
182
+
86
183
  def execute(script)
87
184
  puts "Executing #{script[:path]} at #{current_time}"
88
185
  absolute_path = File.expand_path(script[:path], __dir__)
@@ -90,5 +187,6 @@ module Bas
90
187
  system("ruby", absolute_path)
91
188
  end
92
189
  end
190
+ # rubocop:enable Metrics/ClassLength
93
191
  end
94
192
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "types/read"
5
+ require_relative "../utils/elasticsearch/request"
6
+ require_relative "../version"
7
+
8
+ module Bas
9
+ module SharedStorage
10
+ ##
11
+ # The SharedStorage::Elasticsearch class serves as a shared storage implementation to read and write on
12
+ # a shared storage defined as a elasticsearch database
13
+ #
14
+ class Elasticsearch < Bas::SharedStorage::Base
15
+ def read
16
+ params = {
17
+ connection: read_options[:connection], index: read_options[:index],
18
+ query: read_body, method: :search
19
+ }
20
+
21
+ result = Utils::Elasticsearch::Request.execute(params)
22
+ @read_response = build_read_response(result)
23
+ end
24
+
25
+ def write(data)
26
+ params = {
27
+ connection: write_options[:connection], index: write_options[:index],
28
+ body: write_body(data), method: :index
29
+ }
30
+
31
+ create_mapping
32
+ @write_response = Utils::Elasticsearch::Request.execute(params).body
33
+ end
34
+
35
+ def set_in_process
36
+ return if read_options[:avoid_process].eql?(true) || read_response.id.nil?
37
+
38
+ update_stage(read_response.id, "in process")
39
+ end
40
+
41
+ def set_processed
42
+ return if read_options[:avoid_process].eql?(true) || read_response.id.nil?
43
+
44
+ update_stage(read_response.id, "processed")
45
+ end
46
+
47
+ private
48
+
49
+ # rubocop:disable Metrics/MethodLength
50
+ def create_mapping
51
+ short_text_properties = {
52
+ type: "text",
53
+ fields: {
54
+ keyword: { type: "keyword", ignore_above: 256 }
55
+ }
56
+ }
57
+
58
+ params = {
59
+ connection: write_options[:connection],
60
+ index: write_options[:index],
61
+ body: {
62
+ mappings: {
63
+ properties: {
64
+ data: { type: "object" },
65
+ tag: short_text_properties,
66
+ archived: { type: "boolean" },
67
+ inserted_at: { type: "date", format: "yyyy-MM-dd HH:mm:ss Z" },
68
+ stage: short_text_properties,
69
+ status: short_text_properties,
70
+ error_message: { type: "object" },
71
+ version: short_text_properties
72
+ }
73
+ }
74
+ },
75
+ method: :create_mapping
76
+ }
77
+
78
+ Utils::Elasticsearch::Request.execute(params)
79
+ end
80
+
81
+ def read_body
82
+ return read_options[:query] if read_options[:query].is_a?(Hash)
83
+
84
+ {
85
+ query: {
86
+ bool: {
87
+ must: [
88
+ { match: { status: "success" } }, { match: { tag: read_options[:tag] } },
89
+ { match: { archived: false } }, { match: { stage: "unprocessed" } }
90
+ ]
91
+ }
92
+ },
93
+ sort: [{ inserted_at: { order: "asc" } }]
94
+ }
95
+ end
96
+
97
+ def write_body(data)
98
+ if data[:success]
99
+ return {
100
+ data: data[:success], tag: write_options[:tag],
101
+ archived: false, inserted_at: Time.now.strftime("%Y-%m-%d %H:%M:%S %z"),
102
+ stage: "unprocessed", status: "success", error_message: nil, version: Bas::VERSION
103
+ }
104
+ end
105
+
106
+ {
107
+ data: nil, tag: write_options[:tag], archived: false,
108
+ inserted_at: Time.now.strftime("%Y-%m-%d %H:%M:%S %z"), stage: "unprocessed",
109
+ status: "failed", error_message: data[:error], version: Bas::VERSION
110
+ }
111
+ end
112
+ # rubocop:enable Metrics/MethodLength
113
+
114
+ def build_read_response(result)
115
+ first_hit = result["hits"]["hits"].empty? ? {} : result["hits"]["hits"].first
116
+ Bas::SharedStorage::Types::Read.new(
117
+ first_hit["_id"], first_hit["_source"]["data"].to_json, first_hit.dig("_source", "inserted_at")
118
+ )
119
+ end
120
+
121
+ def update_stage(id, stage)
122
+ params = {
123
+ connection: read_options[:connection], index: read_options[:index], method: :update,
124
+ body: {
125
+ query: { ids: { values: [id] } },
126
+ script: { source: "ctx._source.stage = params.new_value", params: { new_value: stage } }
127
+ }
128
+ }
129
+
130
+ response = Utils::Elasticsearch::Request.execute(params)
131
+ return unless response["updated"].zero?
132
+
133
+ raise StandardError, "Document #{id} not found, so it was not updated"
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "elasticsearch"
4
+
5
+ module Utils
6
+ module Elasticsearch
7
+ ##
8
+ # This module is a ElasticsearchDB utility to make requests to a Elasticsearch database.
9
+ #
10
+ module Request
11
+ # Implements the request process logic to the ElasticsearchDB index.
12
+ #
13
+ # <br>
14
+ # <b>Params:</b>
15
+ # * <tt>connection</tt> Connection parameters to the database: `host`, `port`, `index`, `user`, `password`.
16
+ # * <b>query</b>:
17
+ # * <tt>String</tt>: String with the Elasticsearch query to be executed.
18
+ # * <tt>Hash</tt>: Hash with the Elasticsearch query to be executed.
19
+ # * <tt>method</tt>: Method to be executed.
20
+ # Allowed methods are: `:search`, `:index`, `:update`, `:create_mapping`.
21
+ #
22
+ # <br>
23
+ # <b>returns</b> <tt>Elasticsearch::Response</tt>
24
+ #
25
+ class << self
26
+ def execute(params)
27
+ client = ::Elasticsearch::Client.new(
28
+ host: params[:connection][:host],
29
+ port: params[:connection][:port],
30
+ user: params[:connection][:user],
31
+ password: params[:connection][:password],
32
+ api_versioning: false,
33
+ transport_options: build_ssl_options(params[:connection])
34
+ )
35
+
36
+ perform_request(params, client)
37
+ end
38
+
39
+ private
40
+
41
+ def build_ssl_options(connection_params)
42
+ ssl_options = {}
43
+ ssl_options[:ca_file] = connection_params[:ca_file] if connection_params[:ca_file]
44
+ ssl_options[:verify] = connection_params[:ssl_verify] if connection_params.key?(:ssl_verify)
45
+ { ssl: ssl_options }
46
+ end
47
+
48
+ def perform_request(params, client)
49
+ case params[:method]
50
+ when :index
51
+ index_document(params, client)
52
+ when :search
53
+ search(params, client)
54
+ when :update
55
+ update_documents(params, client)
56
+ when :create_mapping
57
+ create_mapping(params, client)
58
+ end
59
+ end
60
+
61
+ def search(params, client)
62
+ search_params = { index: params[:index] }
63
+ search_params[:size] = 1 # return only one document
64
+ if params[:query].is_a?(Hash)
65
+ search_params[:body] = params[:query]
66
+ else
67
+ search_params[:q] = params[:query]
68
+ end
69
+
70
+ client.search(**search_params)
71
+ end
72
+
73
+ def index_document(params, client)
74
+ client.index(index: params[:index], body: params[:body])
75
+ end
76
+
77
+ def update_documents(params, client)
78
+ client.update_by_query(index: params[:index], body: params[:body], wait_for_completion: true, refresh: true)
79
+ end
80
+
81
+ def create_mapping(params, client)
82
+ return if client.indices.exists?(index: params[:index])
83
+
84
+ client.indices.create(index: params[:index], body: params[:body])
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/bas/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bas
4
4
  # Gem version
5
- VERSION = "1.7.1"
5
+ VERSION = "1.7.2"
6
6
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bas
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 1.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kommitters Open Source
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: elasticsearch
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: httparty
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -65,11 +79,13 @@ files:
65
79
  - lib/bas/orchestrator/manager.rb
66
80
  - lib/bas/shared_storage/base.rb
67
81
  - lib/bas/shared_storage/default.rb
82
+ - lib/bas/shared_storage/elasticsearch.rb
68
83
  - lib/bas/shared_storage/postgres.rb
69
84
  - lib/bas/shared_storage/types/read.rb
70
85
  - lib/bas/utils/digital_ocean/request.rb
71
86
  - lib/bas/utils/discord/integration.rb
72
87
  - lib/bas/utils/discord/request.rb
88
+ - lib/bas/utils/elasticsearch/request.rb
73
89
  - lib/bas/utils/exceptions/function_not_implemented.rb
74
90
  - lib/bas/utils/exceptions/invalid_process_response.rb
75
91
  - lib/bas/utils/github/octokit_client.rb
@@ -106,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
122
  - !ruby/object:Gem::Version
107
123
  version: '0'
108
124
  requirements: []
109
- rubygems_version: 3.6.2
125
+ rubygems_version: 3.6.7
110
126
  specification_version: 4
111
127
  summary: BAS - Business automation suite
112
128
  test_files: []