Module: Fbe
- Defined in:
- lib/fbe.rb
Overview
The main and only module of this gem.
- Author
-
Yegor Bugayenko ([email protected])
- Copyright
-
Copyright © 2024-2025 Zerocracy
- License
-
MIT
Defined Under Namespace
Modules: Middleware Classes: Award, Conclude, FakeOctokit, Graph, Iterate
Constant Summary collapse
- VERSION =
Current version of the gem (changed by
.rultor.yml
on every release) '0.26.3'
Class Method Summary collapse
-
.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>
Generates policies/bylaws from Liquid templates.
-
.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, time: Time) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Conclude and evals it with the block provided.
-
.copy(source, target, except: []) ⇒ Integer
Makes a copy of a fact, moving all properties to a new fact.
-
.delete(fact, *props, fb: Fbe.fb, id: '_id') ⇒ Factbase::Fact
Delete properties from a fact by creating a new fact without them.
-
.enter(badge, why, options: $options, loog: $loog) { ... } ⇒ Object
Enter a new valve in the Zerocracy system.
-
.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase
Returns an instance of
Factbase
(cached). -
.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph
Creates an instance of Graph.
-
.if_absent(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ nil, Factbase::Fact
Injects a fact if it’s absent in the factbase, otherwise returns nil.
-
.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String
Converts GitHub repository and issue IDs into a formatted issue reference.
-
.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) { ... } ⇒ Object
Creates an instance of Iterate and evaluates it with the provided block.
-
.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact
Ensures exactly one fact exists with the specified attributes in the factbase.
-
.mask_to_regex(mask) ⇒ Regexp
Converts a repository mask pattern to a regular expression.
-
.octo(options: $options, global: $global, loog: $loog) ⇒ Hash
Makes a call to the GitHub API.
-
.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact
Overwrites a property in the fact by recreating the entire fact.
-
.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object
Takes configuration parameter from the “PMP” fact.
-
.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil
Run the block provided every X days based on PMP configuration.
-
.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil
Run the block provided every X hours based on PMP configuration.
-
.sec(fact, prop = :seconds) ⇒ String
Converts number of seconds into human-readable time format.
-
.unmask_repos(options: $options, global: $global, loog: $loog, quota_aware: true) ⇒ Array<String>
Resolves repository masks to actual GitHub repository names.
-
.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String
Converts a GitHub user ID into a formatted username string.
Class Method Details
.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>
Generates policies/bylaws from Liquid templates.
Using the templates stored in the assets/bylaws
directory, this function creates a hash where keys are bylaw names (derived from filenames) and values are the rendered formulas. Templates can use three parameters to control the strictness and generosity of the bylaws.
38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/fbe/bylaws.rb', line 38 def Fbe.bylaws(anger: 2, love: 2, paranoia: 2) raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5 raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5 raise "The 'paranoia' must be in the [1..4] interval: #{paranoia.inspect}" unless paranoia.positive? && paranoia < 5 home = File.join(__dir__, '../../assets/bylaws') raise "The directory with templates is absent #{home.inspect}" unless File.exist?(home) Dir[File.join(home, '*.liquid')].to_h do |f| formula = Liquid::Template.parse(File.read(f)).render( 'anger' => anger, 'love' => love, 'paranoia' => paranoia ) [File.basename(f).gsub(/\.liquid$/, ''), formula] end end |
.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, time: Time) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Conclude and evals it with the block provided.
20 21 22 23 24 25 26 27 28 |
# File 'lib/fbe/conclude.rb', line 20 def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, time: Time, &) raise 'The fb is nil' if fb.nil? raise 'The $judge is not set' if judge.nil? raise 'The $global is not set' if global.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:, time:) c.instance_eval(&) end |
.copy(source, target, except: []) ⇒ Integer
Existing properties in target are preserved (not overwritten)
Makes a copy of a fact, moving all properties to a new fact.
All properties from the source
will be copied to the target
, except those listed in the except
array. Only copies properties that don’t already exist in the target. Multi-valued properties are copied with all their values.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/fbe/copy.rb', line 26 def Fbe.copy(source, target, except: []) raise 'The source is nil' if source.nil? raise 'The target is nil' if target.nil? raise 'The except is nil' if except.nil? copied = 0 source.all_properties.each do |k| next unless target[k].nil? next if except.include?(k) source[k].each do |v| target.send(:"#{k}=", v) copied += 1 end end copied end |
.delete(fact, *props, fb: Fbe.fb, id: '_id') ⇒ Factbase::Fact
Delete properties from a fact by creating a new fact without them.
This method doesn’t modify the original fact. Instead, it deletes the existing fact from the factbase and creates a new one with all properties except those specified for deletion.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/fbe/delete.rb', line 25 def Fbe.delete(fact, *props, fb: Fbe.fb, id: '_id') raise 'The fact is nil' if fact.nil? i = fact[id] raise "There is no #{id.inspect} in the fact" if i.nil? i = i.first before = {} fact.all_properties.each do |k| next if props.include?(k) fact[k].each do |v| before[k] = v end end fb.query("(eq #{id} #{i})").delete! c = fb.insert before.each do |k, v| c.send(:"#{k}=", v) end c end |
.enter(badge, why, options: $options, loog: $loog) { ... } ⇒ Object
Requires $options and $loog global variables to be set
In testing mode (options.testing != nil), bypasses valve recording
Enter a new valve in the Zerocracy system.
A valve is a checkpoint or gate in the processing pipeline. This method records the entry into a valve with a reason, unless in testing mode.
28 29 30 31 32 33 34 35 36 |
# File 'lib/fbe/enter.rb', line 28 def Fbe.enter(badge, why, options: $options, loog: $loog, &) raise 'The badge is nil' if badge.nil? raise 'The why is nil' if why.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? return yield unless .testing.nil? baza = BazaRb.new('api.zerocracy.com', 443, .zerocracy_token, loog:) baza.enter(.job_name, badge, why, .job_id.to_i, &) end |
.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase
Returns an instance of Factbase
(cached).
Instead of using $fb directly, it is recommended to use this utility method. It will not only return the global factbase, but will also make sure it’s properly decorated and cached.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/fbe/fb.rb', line 29 def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog) raise 'The fb is nil' if fb.nil? raise 'The $global is not set' if global.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? global[:fb] ||= begin rules = Dir.glob(File.join('rules', '*.fe')).map { |f| File.read(f) } fbe = Factbase::Rules.new( fb, "(and \n#{rules.join("\n")}\n)", uid: '_id' ) fbe = Factbase::Pre.new(fbe) do |f, fbt| max = fbt.query('(max _id)').one f._id = (max.nil? ? 0 : max) + 1 f._time = Time.now f._version = "#{Factbase::VERSION}/#{Judges::VERSION}/#{.action_version}" f._job = .job_id unless .job_id.nil? end Factbase::Impatient.new( Factbase::Logged.new( Factbase::SyncFactbase.new( Factbase::IndexedFactbase.new( Factbase::CachedFactbase.new( fbe ) ) ), loog ), timeout: 60 ) end end |
.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph
Creates an instance of Graph.
17 18 19 20 21 22 23 24 25 |
# File 'lib/fbe/github_graph.rb', line 17 def Fbe.github_graph(options: $options, global: $global, loog: $loog) global[:github_graph] ||= if .testing.nil? Fbe::Graph.new(token: .github_token || ENV.fetch('GITHUB_TOKEN', nil)) else loog.debug('The connection to GitHub GraphQL API is mocked') Fbe::Graph::Fake.new end end |
.if_absent(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ nil, Factbase::Fact
String values are properly escaped in queries
Time values are converted to UTC ISO8601 format for comparison
Injects a fact if it’s absent in the factbase, otherwise returns nil.
Checks if a fact with the same property values already exists. If not, creates a new fact. System properties (_id, _time, _version) are excluded from the uniqueness check.
Here is what you do when you want to add a fact to the factbase, but don’t want to make a duplicate of an existing one:
require 'fbe/if_absent'
n = Fbe.if_absent do |f|
f.what = 'something'
f.details = 'important'
end
return if n.nil? # Fact already existed
n.when = Time.now # Add additional properties to the new fact
This code will definitely create one fact with what
equals to something
and details
equals to important
, while the when
will be equal to the time of its first creation.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/fbe/if_absent.rb', line 48 def Fbe.if_absent(fb: Fbe.fb) attrs = {} f = others(map: attrs) do |*args| k = args[0] if k.end_with?('=') k = k[0..-2].to_sym v = args[1] raise "Can't set #{k} to nil" if v.nil? raise "Can't set #{k} to empty string" if v.is_a?(String) && v.empty? @map[k] = v else @map[k.to_sym] end end yield f q = attrs.except('_id', '_time', '_version').map do |k, v| vv = v.to_s if v.is_a?(String) vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'" elsif v.is_a?(Time) vv = v.utc.iso8601 end "(eq #{k} #{vv})" end.join(' ') q = "(and #{q})" before = fb.query(q).each.to_a.first return nil if before n = fb.insert attrs.each { |k, v| n.send(:"#{k}=", v) } n end |
.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String
Requires ‘repository’ and ‘issue’ properties in the fact
Repository names are cached to reduce GitHub API calls
Converts GitHub repository and issue IDs into a formatted issue reference.
Takes the repository
and issue
properties from the provided fact
, queries the GitHub API to get the repository’s full name, and formats it as a standard GitHub issue reference (e.g., “zerocracy/fbe#42”). Results are cached globally to minimize API calls.
30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/fbe/issue.rb', line 30 def Fbe.issue(fact, options: $options, global: $global, loog: $loog) raise 'The fact is nil' if fact.nil? raise 'The $global is not set' if global.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? rid = fact['repository'] raise "There is no 'repository' property" if rid.nil? rid = rid.first.to_i issue = fact['issue'] raise "There is no 'issue' property" if issue.nil? issue = issue.first.to_i "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(rid)}##{issue}" end |
.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) { ... } ⇒ Object
Creates an instance of Iterate and evaluates it with the provided block.
This is a convenience method that creates an iterator instance and evaluates the DSL block within its context. The iterator processes repositories defined in options.repositories, executing queries and managing state for each.
38 39 40 41 42 43 44 45 |
# File 'lib/fbe/iterate.rb', line 38 def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &) raise 'The fb is nil' if fb.nil? raise 'The $global is not set' if global.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? c = Fbe::Iterate.new(fb:, loog:, options:, global:) c.instance_eval(&) end |
.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact
System attributes (_id, _time, _version) are ignored when matching
Ensures exactly one fact exists with the specified attributes in the factbase.
This method creates a new fact if none exists with the given attributes, or returns an existing fact if one already matches. Useful for preventing duplicate facts while ensuring required facts exist.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/fbe/just_one.rb', line 35 def Fbe.just_one(fb: Fbe.fb) attrs = {} f = others(map: attrs) do |*args| k = args[0] if k.end_with?('=') @map[k[0..-2].to_sym] = args[1] else @map[k.to_sym] end end yield f q = attrs.except('_id', '_time', '_version').map do |k, v| vv = v.to_s if v.is_a?(String) vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'" elsif v.is_a?(Time) vv = v.utc.iso8601 end "(eq #{k} #{vv})" end.join(' ') q = "(and #{q})" before = fb.query(q).each.to_a.first return before unless before.nil? n = fb.insert attrs.each { |k, v| n.send(:"#{k}=", v) } n end |
.mask_to_regex(mask) ⇒ Regexp
Converts a repository mask pattern to a regular expression.
23 24 25 26 27 |
# File 'lib/fbe/unmask_repos.rb', line 23 def Fbe.mask_to_regex(mask) org, repo = mask.split('/') raise "Org '#{org}' can't have an asterisk" if org.include?('*') Regexp.compile("#{org}/#{repo.gsub('*', '.*')}", Regexp::IGNORECASE) end |
.octo(options: $options, global: $global, loog: $loog) ⇒ Hash
Makes a call to the GitHub API.
It is supposed to be used instead of Octokit::Client
, because it is pre-configured and enables additional features, such as retrying, logging, and caching.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/fbe/octo.rb', line 41 def Fbe.octo(options: $options, global: $global, loog: $loog) raise 'The $global is not set' if global.nil? raise 'The $options is not set' if .nil? raise 'The $loog is not set' if loog.nil? global[:octo] ||= begin trace = [] if .testing.nil? o = Octokit::Client.new token = .github_token if token.nil? loog.debug("The 'github_token' option is not provided") token = ENV.fetch('GITHUB_TOKEN', nil) if token.nil? loog.debug("The 'GITHUB_TOKEN' environment variable is not set") else loog.debug("The 'GITHUB_TOKEN' environment was provided") end else loog.debug("The 'github_token' option was provided (#{token.length} chars)") end if token.nil? loog.warn('Accessing GitHub API without a token!') elsif token.empty? loog.warn('The GitHub API token is an empty string, won\'t use it') else o = Octokit::Client.new(access_token: token) end o.auto_paginate = true o.per_page = 100 o. = { request: { open_timeout: 15, timeout: 15 } } stack = Faraday::RackBuilder.new do |builder| builder.use( Faraday::Retry::Middleware, exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [ Octokit::TooManyRequests, Octokit::ServiceUnavailable ], max: 4, interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4, methods: [:get], backoff_factor: 2 ) builder.use(Octokit::Response::RaiseError) builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter) builder.use(Fbe::Middleware::RateLimit) builder.use(Fbe::Middleware::Trace, trace, ignores: [:fresh]) if .sqlite_cache maxsize = Filesize.from(.sqlite_cache_maxsize || '100M').to_i maxvsize = Filesize.from(.sqlite_cache_maxvsize || '100K').to_i cache_min_age = .sqlite_cache_min_age&.to_i store = Fbe::Middleware::SqliteStore.new( .sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:, ttl: 24, cache_min_age: ) loog.info( "Using HTTP cache in SQLite file: #{store.path} (" \ "#{File.exist?(store.path) ? Filesize.from(File.size(store.path).to_s).pretty : 'file is absent'}, " \ "max size: #{Filesize.from(maxsize.to_s).pretty}, max vsize: #{Filesize.from(maxvsize.to_s).pretty})" ) builder.use( Faraday::HttpCache, store:, serializer: JSON, shared_cache: false, logger: Loog::NULL ) else loog.info("No HTTP cache in SQLite file, because 'sqlite_cache' option is not provided") builder.use( Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL ) end builder.adapter(Faraday.default_adapter) end o.middleware = stack o = Verbose.new(o, log: loog) unless token.nil? || token.empty? loog.info( "Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \ "#{o.rate_limit.remaining} quota remaining)" ) end else loog.debug('The connection to GitHub API is mocked') o = Fbe::FakeOctokit.new end o = decoor(o, loog:, trace:) do def print_trace!(all: false, max: 5) if @trace.empty? @loog.debug('GitHub API trace is empty') else grouped = @trace.select { |e| e[:duration] > 0.05 || all }.group_by do |entry| uri = URI.parse(entry[:url]) query = uri.query query = "?#{query.ellipsized(40)}" if query "#{uri.scheme}://#{uri.host}#{uri.path}#{query}" end = grouped .sort_by { |_path, entries| -entries.count } .map do |path, entries| [ ' ', path.gsub(%r{^https://api.github.com/}, '/'), ': ', entries.count, " (#{entries.sum { |e| e[:duration] }.seconds})" ].join end .take(max) .join("\n") @loog.info( "GitHub API trace (#{grouped.count} URLs vs #{@trace.count} requests, " \ "#{@origin.rate_limit!.remaining} quota left):\n#{}" ) @trace.clear end end def off_quota?(threshold: 50) left = @origin.rate_limit!.remaining if left < threshold @loog.info("Too much GitHub API quota consumed already (#{left} < #{threshold}), stopping") true else @loog.debug("Still #{left} GitHub API quota left (>#{threshold})") false end end def user_name_by_id(id) raise 'The ID of the user is nil' if id.nil? raise 'The ID of the user must be an Integer' unless id.is_a?(Integer) json = @origin.user(id) name = json[:login].downcase @loog.debug("GitHub user ##{id} has a name: @#{name}") name end def repo_id_by_name(name) raise 'The name of the repo is nil' if name.nil? json = @origin.repository(name) id = json[:id] raise "Repository #{name} not found" if id.nil? @loog.debug("GitHub repository #{name.inspect} has an ID: ##{id}") id end def repo_name_by_id(id) raise 'The ID of the repo is nil' if id.nil? raise 'The ID of the repo must be an Integer' unless id.is_a?(Integer) json = @origin.repository(id) name = json[:full_name].downcase @loog.debug("GitHub repository ##{id} has a name: #{name}") name end end o = intercepted(o) do |e, m, _args, _r| if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota? raise "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()" end end o end end |
.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact
This operation preserves all other properties during recreation
If property already has the same single value, no changes are made
Overwrites a property in the fact by recreating the entire fact.
If the property doesn’t exist in the fact, it will be added. If it does exist, the entire fact will be destroyed, a new fact created with all existing properties, and the specified property set with the new value.
It is important that the fact has the _id
property. If it doesn’t, an exception will be raised.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/fbe/overwrite.rb', line 30 def Fbe.overwrite(fact, property, value, fb: Fbe.fb) raise 'The fact is nil' if fact.nil? raise 'The fb is nil' if fb.nil? raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String) return fact if !fact[property].nil? && fact[property].size == 1 && fact[property].first == value before = {} fact.all_properties.each do |prop| before[prop.to_s] = fact[prop] end id = fact['_id']&.first raise 'There is no _id in the fact, cannot use Fbe.overwrite' if id.nil? raise "No facts by _id = #{id}" if fb.query("(eq _id #{id})").delete!.zero? n = fb.insert before[property.to_s] = [value] before.each do |k, vv| next unless n[k].nil? vv.each do |v| n.send(:"#{k}=", v) end end n end |
.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object
Takes configuration parameter from the “PMP” fact.
The factbase may have a few facts with the what
set to pmp
(stands for the “project management plan”). These facts contain information that configures the project. It is expected that every fact with the what
set to pmp
also contains the area
property, which is set to one of nine values: scope
, time
, cost
, etc. (the nine process areas in the PMBOK).
If a proper pmp fact is not found or the property is absent in the fact, this method throws an exception. The factbase must contain PMP-related facts. Most probably, a special judge must fill it up with such a fact.
The method uses a double nested ‘others` block to create a chainable interface that allows accessing configuration like:
Fbe.pmp.hr.reward_points
Fbe.pmp.cost.hourly_rate
Fbe.pmp.time.deadline
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/fbe/pmp.rb', line 43 def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) others do |*args1| area = args1.first unless %w[cost scope hr time procurement risk integration quality communication].include?(area.to_s) raise "Invalid area #{area.inspect} (not part of PMBOK)" end others do |*args2| param = args2.first f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.to_a.first raise "Unknown area #{area.inspect}" if f.nil? r = f[param] raise "Unknown property #{param.inspect} in the #{area.inspect} area" if r.nil? r.first end end end |
.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil
Skips execution if judge was run within the interval period
The ‘since’ property is added to the fact when p_since_days is provided
Run the block provided every X days based on PMP configuration.
Executes a block periodically based on PMP (Project Management Plan) settings. The block will only run if it hasn’t been executed within the specified interval. Creates a fact recording when the judge was last run.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/fbe/regularly.rb', line 31 def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &) raise 'The area is nil' if area.nil? raise 'The p_every_days is nil' if p_every_days.nil? raise 'The fb is nil' if fb.nil? raise 'The $judge is not set' if judge.nil? raise 'The $loog is not set' if loog.nil? pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_days}))").each.to_a.first interval = pmp.nil? ? 7 : pmp[p_every_days].first unless fb.query( "(and (eq what '#{judge}') (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))" ).each.to_a.empty? loog.debug("#{$judge} statistics have recently been collected, skipping now") return end f = fb.insert f.what = judge f.when = Time.now unless p_since_days.nil? days = pmp.nil? ? 28 : pmp[p_since_days].first since = Time.now - (days * 24 * 60 * 60) f.since = since end yield f nil end |
.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil
Skips execution if judge was run within the interval period
Overwrites the ‘when’ property of existing judge fact
Run the block provided every X hours based on PMP configuration.
Similar to Fbe.regularly but works with hour intervals instead of days. Executes a block periodically, maintaining a single fact that tracks the last execution time. The fact is overwritten on each run rather than creating new facts.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/fbe/repeatedly.rb', line 33 def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &) raise 'The area is nil' if area.nil? raise 'The p_every_hours is nil' if p_every_hours.nil? raise 'The fb is nil' if fb.nil? raise 'The $judge is not set' if judge.nil? raise 'The $loog is not set' if loog.nil? pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_hours}))").each.to_a.first hours = pmp.nil? ? 24 : pmp[p_every_hours].first unless fb.query( "(and (eq what '#{judge}') (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))" ).each.to_a.empty? loog.debug("#{$judge} has recently been executed, skipping now") return end f = fb.query("(and (eq what '#{judge}'))").each.to_a.first if f.nil? f = fb.insert f.what = judge end Fbe.overwrite(f, 'when', Time.now) yield fb.query("(and (eq what '#{judge}'))").each.to_a.first nil end |
.sec(fact, prop = :seconds) ⇒ String
Uses the tago gem’s ago method for formatting
Converts number of seconds into human-readable time format.
The number of seconds is taken from the fact
provided, usually stored there in the seconds
property. The seconds are formatted into a human-readable string like “3 days ago” or “5 hours ago” using the tago gem.
25 26 27 28 29 30 |
# File 'lib/fbe/sec.rb', line 25 def Fbe.sec(fact, prop = :seconds) s = fact[prop.to_s] raise "There is no #{prop.inspect} property" if s.nil? s = s.first.to_i (Time.now + s).ago end |
.unmask_repos(options: $options, global: $global, loog: $loog, quota_aware: true) ⇒ Array<String>
Exclusion patterns must start with ‘-’ (e.g., ‘-org/pattern*’)
Results are shuffled to distribute load when processing
Resolves repository masks to actual GitHub repository names.
Takes a comma-separated list of repository masks from options and expands wildcards by querying GitHub API. Supports inclusion and exclusion patterns. Archived repositories are automatically filtered out.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/fbe/unmask_repos.rb', line 57 def Fbe.unmask_repos(options: $options, global: $global, loog: $loog, quota_aware: true) raise 'Repositories mask is not specified' unless .repositories raise 'Repositories mask is empty' if .repositories.empty? repos = [] octo = Fbe.octo(loog:, global:, options:) masks = (.repositories || '').split(',') masks.reject { |m| m.start_with?('-') }.each do |mask| unless mask.include?('*') repos << mask next end re = Fbe.mask_to_regex(mask) octo.repositories(mask.split('/')[0]).each do |r| repos << r[:full_name] if re.match?(r[:full_name]) end end masks.select { |m| m.start_with?('-') }.each do |mask| re = Fbe.mask_to_regex(mask[1..]) repos.reject! { |r| re.match?(r) } end repos.reject! { |repo| octo.repository(repo)[:archived] } raise "No repos found matching: #{.repositories.inspect}" if repos.empty? repos.shuffle! loog.debug("Scanning #{repos.size} repositories: #{repos.joined}...") repos.each { |repo| octo.repository(repo) } return repos unless block_given? repos.each do |repo| if quota_aware && octo.off_quota? $loog.info("No GitHub quota left, it is time to stop at #{repo}") break end yield repo end end |
.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String
Results are cached to reduce GitHub API calls
Subject to GitHub API rate limits
Converts a GitHub user ID into a formatted username string.
The ID of the user (integer) is expected to be stored in the who
property of the provided fact
. This function makes a live request to GitHub API to retrieve the username. The result is cached globally to minimize API calls. For example, the ID 526301
will be converted to “@yegor256”.
29 30 31 32 33 34 |
# File 'lib/fbe/who.rb', line 29 def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog) id = fact[prop.to_s] raise "There is no #{prop.inspect} property" if id.nil? id = id.first.to_i "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(id)}" end |