qron 0.9.0 → 1.0.0
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 +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +56 -3
- data/lib/qron.rb +180 -39
- data/qron.gemspec +2 -1
- metadata +30 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: f0b1115e1f100badef504e6d0837e35049add1811e63f4bcd5c588001f32aec5
         | 
| 4 | 
            +
              data.tar.gz: 8df3d58f2c5cf42669372d0ac427c94a47aa3c5a48e364cde5cca55b7d8dbb2e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: c6dd7efe16ccda47ebe916fb75ad99cf63100399ca55b1c586502b987f04697aad3fff5a871ccae4a1c731223e07f9f08687e4feb0ef8cf0d1746c1f4fed186e
         | 
| 7 | 
            +
              data.tar.gz: 75dec291fa552c5f4ab0f9d9141db0853035f67f85c2b6f5afd744f2b34fde90c7a5d8dccb0201a34d03aa8eb5345a70f95de082f177c9b277d638c050a1ab0b
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,7 +2,17 @@ | |
| 2 2 | 
             
            # CHANGELOG.md
         | 
| 3 3 |  | 
| 4 4 |  | 
| 5 | 
            -
            ##  | 
| 5 | 
            +
            ## qron 1.0.0 released 2025-04-01
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            * Implement "settings"
         | 
| 8 | 
            +
            * Implement reload: true
         | 
| 9 | 
            +
            * Rework scheduling thread sleep timing
         | 
| 10 | 
            +
            * Implement @reboot schedule
         | 
| 11 | 
            +
            * Implement #on_error and #on_tab_error
         | 
| 12 | 
            +
            * Implement #on_tick
         | 
| 13 | 
            +
             | 
| 14 | 
            +
             | 
| 15 | 
            +
            ## qron 0.9.0 released 2025-03-23
         | 
| 6 16 |  | 
| 7 17 | 
             
            * Initial release
         | 
| 8 18 |  | 
    
        data/README.md
    CHANGED
    
    | @@ -10,9 +10,10 @@ A stupid Ruby cron thread that wakes up from time to time to perform according | |
| 10 10 | 
             
            to what's written in a crontab.
         | 
| 11 11 |  | 
| 12 12 | 
             
            Given `etc/qrontab_dev`:
         | 
| 13 | 
            -
            ```
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            * * * * * | 
| 13 | 
            +
            ```ruby
         | 
| 14 | 
            +
              @reboot       p [ :hello, "just started" ]
         | 
| 15 | 
            +
              * * * * *     p [ :hello, :min, Time.now ]
         | 
| 16 | 
            +
              * * * * * *   p [ :hello, :sec, Time.now ]
         | 
| 16 17 | 
             
            ```
         | 
| 17 18 |  | 
| 18 19 | 
             
            and
         | 
| @@ -40,6 +41,58 @@ Uses [fugit](https://github.com/floraison/fugit) for cron parsing and | |
| 40 41 |  | 
| 41 42 | 
             
            A little brother to [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
         | 
| 42 43 |  | 
| 44 | 
            +
             | 
| 45 | 
            +
            ### `reload: true`
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            Specifying `reload: true` when initializing tells the `Qron` instance to reload its crontab file at every tick.
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            (Qron ticks usually every minute, unless it has one or more second precision crons specified, in which case it ticks every second).
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            ```ruby
         | 
| 52 | 
            +
            require 'qron'
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            q = Qron.new(tab: 'etc/qrontab_dev', reload: true)
         | 
| 55 | 
            +
            ```
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            ### Timezones
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            It's OK to use timezones in the qrontab file:
         | 
| 60 | 
            +
            ```ruby
         | 
| 61 | 
            +
              30 * * * *     Asia/Tokyo        p [ :tokyo, :min, Time.now ]
         | 
| 62 | 
            +
              30 4 1,15 * 5  Europe/Budapest   p [ :budapest, :min, Time.now ]
         | 
| 63 | 
            +
            ```
         | 
| 64 | 
            +
             | 
| 65 | 
            +
             | 
| 66 | 
            +
            ### "Settings"
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            A qrontab file accepts, cron and commands but also "settings" that set
         | 
| 69 | 
            +
            variables in the context passed to commands:
         | 
| 70 | 
            +
            ```ruby
         | 
| 71 | 
            +
              #
         | 
| 72 | 
            +
              # settings
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              a = 1 + 2
         | 
| 75 | 
            +
              b = Time.now
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              #
         | 
| 78 | 
            +
              # actual crons
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              * * * * * *  pp [ :ctx, ctx ]
         | 
| 81 | 
            +
            ```
         | 
| 82 | 
            +
            where the puts might output something like:
         | 
| 83 | 
            +
            ```ruby
         | 
| 84 | 
            +
            [ :ctx,
         | 
| 85 | 
            +
              { time: 'Time instance...',
         | 
| 86 | 
            +
                cron: 'Fugit::Cron instance...',
         | 
| 87 | 
            +
                command: 'pp [ :ctx, ctx ]',
         | 
| 88 | 
            +
                qron: 'The Qron instance...',
         | 
| 89 | 
            +
                a: 3,
         | 
| 90 | 
            +
                b: 'Time instance...' } ]
         | 
| 91 | 
            +
            ```
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            A context is instantied and prepare for each command when it triggers.
         | 
| 94 | 
            +
             | 
| 95 | 
            +
             | 
| 43 96 | 
             
            ## LICENSE
         | 
| 44 97 |  | 
| 45 98 | 
             
            MIT, see [LICENSE.txt](LICENSE.txt)
         | 
    
        data/lib/qron.rb
    CHANGED
    
    | @@ -6,14 +6,24 @@ require 'stagnum' | |
| 6 6 |  | 
| 7 7 | 
             
            class Qron
         | 
| 8 8 |  | 
| 9 | 
            -
              VERSION = '0. | 
| 9 | 
            +
              VERSION = '1.0.0'.freeze
         | 
| 10 10 |  | 
| 11 11 | 
             
              attr_reader :options
         | 
| 12 | 
            -
              attr_reader :tab, :thread, :started, : | 
| 12 | 
            +
              attr_reader :tab, :thread, :started, :work_pool
         | 
| 13 | 
            +
              attr_reader :tab_res, :tab_mtime
         | 
| 14 | 
            +
              attr_reader :listeners
         | 
| 13 15 |  | 
| 14 16 | 
             
              def initialize(opts={})
         | 
| 15 17 |  | 
| 16 18 | 
             
                @options = opts
         | 
| 19 | 
            +
                @options[:reload] = false unless opts.has_key?(:reload)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                @tab = nil
         | 
| 22 | 
            +
                @tab_res = nil
         | 
| 23 | 
            +
                @tab_mtime = Time.now
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                @booted = false
         | 
| 26 | 
            +
                @listeners = []
         | 
| 17 27 |  | 
| 18 28 | 
             
                start unless opts[:start] == false
         | 
| 19 29 | 
             
              end
         | 
| @@ -21,28 +31,29 @@ class Qron | |
| 21 31 | 
             
              def start
         | 
| 22 32 |  | 
| 23 33 | 
             
                @started = Time.now
         | 
| 24 | 
            -
                @last_sec = @started.to_i
         | 
| 25 34 |  | 
| 26 35 | 
             
                @work_pool ||=
         | 
| 27 | 
            -
                  Stagnum::Pool.new(" | 
| 36 | 
            +
                  Stagnum::Pool.new("qron-#{Qron::VERSION}-pool", @options[:workers] || 3)
         | 
| 28 37 |  | 
| 29 38 | 
             
                @thread =
         | 
| 30 39 | 
             
                  Thread.new do
         | 
| 40 | 
            +
                    Thread.current[:name] =
         | 
| 41 | 
            +
                      @options[:thread_name] || "qron-#{Qron::VERSION}-thread"
         | 
| 31 42 | 
             
                    loop do
         | 
| 32 43 | 
             
                      break if @started == nil
         | 
| 33 44 | 
             
                      now = Time.now
         | 
| 34 | 
            -
                       | 
| 35 | 
            -
                       | 
| 36 | 
            -
                      sleep 0.7 + (0.5 * rand)
         | 
| 45 | 
            +
                      tick(now)
         | 
| 46 | 
            +
                      sleep(determine_sleep_time(now))
         | 
| 37 47 | 
             
                    end
         | 
| 38 48 | 
             
                  end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                # TODO rescue perform...
         | 
| 41 49 | 
             
              end
         | 
| 42 50 |  | 
| 43 51 | 
             
              def stop
         | 
| 44 52 |  | 
| 45 53 | 
             
                @started = nil
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                @thread.kill
         | 
| 56 | 
            +
                @thread = nil
         | 
| 46 57 | 
             
              end
         | 
| 47 58 |  | 
| 48 59 | 
             
              def join
         | 
| @@ -50,52 +61,182 @@ class Qron | |
| 50 61 | 
             
                @thread && @thread.join
         | 
| 51 62 | 
             
              end
         | 
| 52 63 |  | 
| 53 | 
            -
               | 
| 64 | 
            +
              # In some deployments, another thread ticks the qron instance. So #tick(now)
         | 
| 65 | 
            +
              # is a public method.
         | 
| 66 | 
            +
              #
         | 
| 67 | 
            +
              def tick(now)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                fetch_tab
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                @tab.each do |cron, command|
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  perform(now, cron, command) if cron_match?(cron, now)
         | 
| 74 | 
            +
                end
         | 
| 54 75 |  | 
| 55 | 
            -
             | 
| 76 | 
            +
                @booted = true
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                trigger_event(:on_tick, time: now)
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
              def fetch_tab
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                return @tab if @tab && @options[:reload] == false
         | 
| 56 84 |  | 
| 57 85 | 
             
                t = @options[:crontab] || @options[:tab] || 'qrontab'
         | 
| 86 | 
            +
                m = mtime(t)
         | 
| 58 87 |  | 
| 59 | 
            -
                 | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                 | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
                    next a if l == ''
         | 
| 67 | 
            -
                    next a if l.start_with?('#')
         | 
| 68 | 
            -
                    ll5 = l.split(/\s+/, 6)
         | 
| 69 | 
            -
                    ll6 = ll5.pop.split(/\s+/, 2)
         | 
| 70 | 
            -
                    ll5 = ll5.join(' ')
         | 
| 71 | 
            -
                    ll6, r = *ll6
         | 
| 72 | 
            -
                    c = Fugit::Cron.parse("#{ll5} #{ll6}")
         | 
| 73 | 
            -
                    unless c
         | 
| 74 | 
            -
                      c = Fugit::Cron.parse(ll5)
         | 
| 75 | 
            -
                      r = "#{ll6} #{r}"
         | 
| 76 | 
            -
                    end
         | 
| 77 | 
            -
                    a << [ c, r ]
         | 
| 78 | 
            -
                    a }
         | 
| 88 | 
            +
                if m > @tab_mtime
         | 
| 89 | 
            +
                  @tab = nil
         | 
| 90 | 
            +
                  @tab_tempo = nil
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
                @tab_mtime = m
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                @tab ||= parse(t)
         | 
| 79 95 | 
             
              end
         | 
| 80 96 |  | 
| 81 | 
            -
              def  | 
| 97 | 
            +
              def on_tab_error(&block); @listeners << [ :on_tab_error, block ]; end
         | 
| 98 | 
            +
              #def on_tick_error(&block); @listeners << [ :on_tick_error, block ]; end
         | 
| 99 | 
            +
              def on_perform_error(&block); @listeners << [ :on_perform_error, block ]; end
         | 
| 82 100 |  | 
| 83 | 
            -
             | 
| 101 | 
            +
              def on_error(&block)
         | 
| 102 | 
            +
                @listeners << [ :on_tab_error, block ]
         | 
| 103 | 
            +
                @listeners << [ :on_perform_error, block ]
         | 
| 104 | 
            +
              end
         | 
| 84 105 |  | 
| 85 | 
            -
             | 
| 106 | 
            +
              def on_tick(&block); @listeners << [ :on_tick, block ]; end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              def trigger_event(event_name, ctx)
         | 
| 86 109 |  | 
| 87 | 
            -
             | 
| 110 | 
            +
                @listeners.each { |name, block| block.call(ctx) if name == event_name }
         | 
| 111 | 
            +
              end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
              protected
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              def mtime(t)
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                if t.is_a?(String) && t.count("\n") < 1 && File.exist?(t)
         | 
| 118 | 
            +
                  File.mtime(t)
         | 
| 119 | 
            +
                else
         | 
| 120 | 
            +
                  Time.now
         | 
| 88 121 | 
             
                end
         | 
| 122 | 
            +
              end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
              def parse(t)
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                case t
         | 
| 127 | 
            +
                when Array then parse_lines(t)
         | 
| 128 | 
            +
                when /\n/ then parse_lines(t.lines)
         | 
| 129 | 
            +
                when String then parse_file(t)
         | 
| 130 | 
            +
                else fail(ArgumentError.new("cannot parse instance of #{t.class}"))
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
              rescue => err
         | 
| 89 134 |  | 
| 90 | 
            -
                 | 
| 135 | 
            +
                trigger_event(:on_tab_error, time: Time.now, error: err)
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                []
         | 
| 91 138 | 
             
              end
         | 
| 92 139 |  | 
| 93 | 
            -
              def  | 
| 140 | 
            +
              def parse_file(path)
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                parse_lines(File.readlines(path))
         | 
| 143 | 
            +
              end
         | 
| 94 144 |  | 
| 95 | 
            -
             | 
| 145 | 
            +
              def parse_lines(ls)
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                ls.map { |l| parse_line(l) }.compact
         | 
| 148 | 
            +
              end
         | 
| 96 149 |  | 
| 97 | 
            -
             | 
| 150 | 
            +
              def parse_line(l)
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                l = l.strip
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                return nil if l == ''
         | 
| 155 | 
            +
                return nil if l.start_with?('#')
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                parse_setting(l) ||
         | 
| 158 | 
            +
                parse_special(l) ||
         | 
| 159 | 
            +
                parse_cron(l, 7) || parse_cron(l, 6) || parse_cron(l, 5) ||
         | 
| 160 | 
            +
                fail(ArgumentError.new("could not parse }#{l}{"))
         | 
| 161 | 
            +
              end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
              def parse_setting(line)
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                m = line.match(/^([a-z][_0-9a-zA-Z]*)\s+=\s+(.+)$/)
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                m ? [ 'setting', "ctx[:#{m[1]}] = #{m[2]}" ] : nil
         | 
| 168 | 
            +
              end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
              def parse_special(line)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                line.start_with?(/@reboot\s/) ?
         | 
| 173 | 
            +
                  [ '@reboot', line.split(/\s+/, 2).last ] :
         | 
| 174 | 
            +
                  nil
         | 
| 175 | 
            +
              end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
              def parse_cron(line, word_count)
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                ll = line.split(/\s+/, word_count + 1)
         | 
| 180 | 
            +
                c, r = Fugit::Cron.parse(ll.take(word_count).join(' ')), ll.last
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                c ? [ c, r] : nil
         | 
| 183 | 
            +
              end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
              def cron_match?(cron, time)
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                if cron == '@reboot'
         | 
| 188 | 
            +
                  @booted == false
         | 
| 189 | 
            +
                elsif cron.is_a?(Fugit::Cron)
         | 
| 190 | 
            +
                  cron.match?(time)
         | 
| 191 | 
            +
                else
         | 
| 192 | 
            +
                  false # well...
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
              end
         | 
| 195 | 
            +
             | 
| 196 | 
            +
              def perform(now, cron, command)
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                @work_pool.enqueue(make_context(now, cron, command)) do |ctx|
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                  Kernel.eval("Proc.new { |ctx| #{command} }").call(ctx)
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                rescue => err
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                  trigger_event(:on_perform_error, time: Time.now, error: err)
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
              end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
              def make_context(now, cron, command)
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                ctx = { time: now, cron: cron, command: command, qron: self }
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                @tab.each do |c, command|
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                  Kernel.eval("Proc.new { |ctx| #{command} }").call(ctx) if c == 'setting'
         | 
| 98 215 | 
             
                end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                ctx
         | 
| 218 | 
            +
              end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
              def determine_sleep_time(now)
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                @tab_res ||=
         | 
| 223 | 
            +
                  @tab.find { |c, _| c.is_a?(Fugit::Cron) && c.resolution == :second } ?
         | 
| 224 | 
            +
                    :second : :minute
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                res = @tab_res == :second ? 1.0 : 60.0
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                res - (now.to_f % res) + 0.021
         | 
| 229 | 
            +
              end
         | 
| 230 | 
            +
            end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
             | 
| 233 | 
            +
            # Should it be part of fugit?
         | 
| 234 | 
            +
            #
         | 
| 235 | 
            +
            class Fugit::Cron
         | 
| 236 | 
            +
             | 
| 237 | 
            +
              def resolution
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                seconds == [ 0 ] ? :minute : :second
         | 
| 99 240 | 
             
              end
         | 
| 100 241 | 
             
            end
         | 
| 101 242 |  | 
    
        data/qron.gemspec
    CHANGED
    
    | @@ -37,7 +37,8 @@ A Ruby thread that wakes up in time to perform what's ordered in its crontab | |
| 37 37 | 
             
                "#{s.name}.gemspec",
         | 
| 38 38 | 
             
              ]
         | 
| 39 39 |  | 
| 40 | 
            -
               | 
| 40 | 
            +
              s.add_runtime_dependency 'fugit', '~> 1.11'
         | 
| 41 | 
            +
              s.add_runtime_dependency 'stagnum', '~> 1.0'
         | 
| 41 42 |  | 
| 42 43 | 
             
              s.add_development_dependency 'probatio', '~> 1.0'
         | 
| 43 44 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,42 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: qron
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 1.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - John Mettraux
         | 
| 8 8 | 
             
            bindir: bin
         | 
| 9 9 | 
             
            cert_chain: []
         | 
| 10 | 
            -
            date: 2025- | 
| 10 | 
            +
            date: 2025-04-01 00:00:00.000000000 Z
         | 
| 11 11 | 
             
            dependencies:
         | 
| 12 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 13 | 
            +
              name: fugit
         | 
| 14 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 15 | 
            +
                requirements:
         | 
| 16 | 
            +
                - - "~>"
         | 
| 17 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 18 | 
            +
                    version: '1.11'
         | 
| 19 | 
            +
              type: :runtime
         | 
| 20 | 
            +
              prerelease: false
         | 
| 21 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 22 | 
            +
                requirements:
         | 
| 23 | 
            +
                - - "~>"
         | 
| 24 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 25 | 
            +
                    version: '1.11'
         | 
| 26 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 27 | 
            +
              name: stagnum
         | 
| 28 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 29 | 
            +
                requirements:
         | 
| 30 | 
            +
                - - "~>"
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: '1.0'
         | 
| 33 | 
            +
              type: :runtime
         | 
| 34 | 
            +
              prerelease: false
         | 
| 35 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 36 | 
            +
                requirements:
         | 
| 37 | 
            +
                - - "~>"
         | 
| 38 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 39 | 
            +
                    version: '1.0'
         | 
| 12 40 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 13 41 | 
             
              name: probatio
         | 
| 14 42 | 
             
              requirement: !ruby/object:Gem::Requirement
         |