Module: Fbe

Defined in:
lib/fbe.rb,
lib/fbe/pmp.rb,
lib/fbe/unmask_repos.rb
more...

Overview

Unmask.

Author

Yegor Bugayenko (yegor256@gmail.com)

Copyright

Copyright © 2024 Zerocracy

License

MIT

Defined Under Namespace

Modules: Middleware Classes: Award, Conclude, FakeOctokit, Iterate

Constant Summary collapse

VERSION =

Current version of the gem (changed by .rultor.yml on every release)

'0.0.43'

Class Method Summary collapse

Class Method Details

.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global) ⇒ Object

Create a conclude code block.

[View source]

31
32
33
34
# File 'lib/fbe/conclude.rb', line 31

def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, &)
  c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:)
  c.instance_eval(&)
end

.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Object

[View source]

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/fbe/fb.rb', line 33

def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog)
  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|
          max = fb.query('(eq _id (max _id))').each.to_a.first
          f._id = (max.nil? ? 0 : max._id) + 1
          f._time = Time.now
          f._version = "#{Factbase::VERSION}/#{Judges::VERSION}/#{options.judges_action_version}"
        end
      Factbase::Looged.new(fbe, loog)
    end
end

.if_absent(fb: Fbe.fb) {|f| ... } ⇒ Object

Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns NIL.

Yields:

  • (f)
[View source]

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
# File 'lib/fbe/if_absent.rb', line 32

def Fbe.if_absent(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})"
  return nil unless fb.query(q).each.to_a.empty?
  n = fb.insert
  attrs.each { |k, v| n.send("#{k}=", v) }
  n
end

.issue(fact, options: $options, global: $global, loog: $loog) ⇒ Object

[View source]

27
28
29
# File 'lib/fbe/issue.rb', line 27

def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
  "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(fact.repository)}##{fact.issue}"
end

.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) ⇒ Object

Create a conclude code block.

[View source]

31
32
33
34
# File 'lib/fbe/iterate.rb', line 31

def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
  c = Fbe::Iterate.new(fb:, loog:, options:, global:)
  c.instance_eval(&)
end

.just_one(fb: Fbe.fb) {|f| ... } ⇒ Object

Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns the existing one.

Yields:

  • (f)
[View source]

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
# File 'lib/fbe/just_one.rb', line 32

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) ⇒ Object

[View source]

32
33
34
35
36
# File 'lib/fbe/unmask_repos.rb', line 32

def self.mask_to_regex(mask)
  org, repo = mask.split('/')
  raise "Org '#{org}' can't have an asterisk" if org.include?('*')
  Regexp.compile("#{org}/#{repo.gsub('*', '.*')}")
end

.octo(options: $options, global: $global, loog: $loog) ⇒ Object

Interface to GitHub API.

It is supposed to be used instead of Octokit client, because it is pre-configured and enables additional fearues, such as retrying, logging, and caching.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility

[View source]

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
# File 'lib/fbe/octo.rb', line 45

def Fbe.octo(options: $options, global: $global, loog: $loog)
  raise 'The $global is not set' if global.nil?
  global[:octo] ||=
    begin
      if options.testing.nil?
        o = Octokit::Client.new
        token = options.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")
        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)
          loog.info("Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..]})")
        end
        o.auto_paginate = true
        o.per_page = 100
        o.connection_options = {
          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(Fbe::Middleware::Quota, loog:, pause: options.github_api_pause || 60)
            builder.use(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL)
            builder.use(Octokit::Response::RaiseError)
            builder.use(Faraday::Response::Logger, Loog::NULL)
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      decoor(o, loog:) do
        def off_quota
          left = @origin.rate_limit.remaining
          if left < 5
            @loog.info("To much GitHub API quota consumed already (remaining=#{left}), stopping")
            true
          else
            false
          end
        end

        def user_name_by_id(id)
          json = @origin.user(id)
          name = json[:login]
          @loog.debug("GitHub user ##{id} has a name: @#{name}")
          name
        end

        def repo_id_by_name(name)
          json = @origin.repository(name)
          id = json[:id]
          @loog.debug("GitHub repository #{name} has an ID: ##{id}")
          id
        end

        def repo_name_by_id(id)
          json = @origin.repository(id)
          name = json[:full_name]
          @loog.debug("GitHub repository ##{id} has a name: #{name}")
          name
        end
      end
    end
end

.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Object

Overwrite a property in the fact.

If the property doesn’t exist in the fact, it will be added. If it does exist, it will be removed (the entire fact will be destroyed, new fact created, and property set).

It is important that the fact has _id property. If it doesn’t, an exception will be raised.

Parameters:

  • fact (Factbase::Fact)

    The fact to modify

  • property (String)

    The name of the property to set

  • vqlue (Any)

    The value to set

[View source]

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fbe/overwrite.rb', line 40

def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
  raise 'The fact is nil' if fact.nil?
  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
end

.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object

[View source]

31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/fbe/pmp.rb', line 31

def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog)
  others do |*args1|
    area = args1.first
    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}'" if f.nil?
      r = f[param]
      raise "Unknown property '#{param}' in the '#{area}' 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) {|f| ... } ⇒ Object

Yields:

  • (f)
[View source]

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/fbe/regularly.rb', line 28

def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  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
end

.sec(fact, prop = :seconds) ⇒ Object

[View source]

27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/fbe/sec.rb', line 27

def Fbe.sec(fact, prop = :seconds)
  s = fact.send(prop.to_s)
  if s < 60
    format('%d seconds', s)
  elsif s < 60 * 60
    format('%d minutes', s / 60)
  elsif s < 60 * 60 * 24
    format('%d hours', s / (60 * 60))
  else
    format('%d days', s / (60 * 60 * 24))
  end
end

.unmask_repos(options: $options, global: $global, loog: $loog) ⇒ Object

[View source]

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/fbe/unmask_repos.rb', line 38

def self.unmask_repos(options: $options, global: $global, loog: $loog)
  repos = []
  octo = Fbe.octo(loog:, global:, options:)
  masks = (options.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: #{options.repositories}" if repos.empty?
  loog.debug("Scanning #{repos.size} repositories: #{repos.join(', ')}...")
  repos
end

.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ Object

[View source]

27
28
29
# File 'lib/fbe/who.rb', line 27

def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
  "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(fact.send(prop.to_s))}"
end