kaal-sinatra 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '056818dc738336966c9f49347c9d5c848b8a326491e54d7eeb92f98846f745a2'
4
+ data.tar.gz: c6f2b304ec788996da55d66c9e0239aeac58e45853f9a7a9fe205fe3a2217b07
5
+ SHA512:
6
+ metadata.gz: 81b6f5416d682df2d9e3b4e16fa88c7a35deb41c2bbd2c11a558c3a7d9190043cf59ba5a877eafca5a3897203ad10b73ba0b352ddd8730d48d3fab0ab2214946
7
+ data.tar.gz: 14c4d191e3a7b813c5753f4ff70e391a1fd8fa2786eb2a65cda56f74ee327a692de9baac67f0e4f7ef4ea4223e3ed2f177c2a98b84d613cb7ca9e112be65bbcd
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Codevedas Inc. and the Kaal Authors
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.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Kaal::Sinatra
2
+
3
+ Sinatra integration gem for Kaal.
4
+
5
+ `kaal-sinatra` depends on:
6
+
7
+ - `kaal`
8
+ - `kaal-sequel`
9
+ - `sinatra`
10
+
11
+ It owns the Sinatra integration surface:
12
+
13
+ - explicit boot wiring for Sinatra apps
14
+ - backend wiring for memory, redis, SQLite, PostgreSQL, and MySQL
15
+ - scheduler file boot loading relative to the Sinatra app root
16
+ - opt-in scheduler startup and shutdown helpers
17
+ - Sinatra-specific test coverage and dummy apps
18
+
19
+ ## Install
20
+
21
+ ```ruby
22
+ gem 'kaal-sinatra'
23
+ gem 'redis' # for redis
24
+ gem 'sqlite3' # or pg / mysql2 for SQL
25
+ ```
26
+
27
+ If you use SQL persistence, create the Kaal tables using Sequel migrations. `kaal-sequel` exposes templates for:
28
+
29
+ - SQLite: `kaal_dispatches`, `kaal_locks`, `kaal_definitions`
30
+ - PostgreSQL: `kaal_dispatches`, `kaal_definitions`
31
+ - MySQL: `kaal_dispatches`, `kaal_definitions`
32
+
33
+ Your app should also provide `config/scheduler.yml`.
34
+
35
+ ## What It Provides
36
+
37
+ - Sinatra-native wiring on top of the Kaal engine
38
+ - explicit backend injection for memory and custom backends
39
+ - redis convenience wiring when the app passes a redis client
40
+ - automatic SQL backend selection from the Sequel adapter unless the app passes `adapter:`
41
+ - explicit lifecycle helpers so web processes do not implicitly start background scheduler threads
42
+
43
+ ## Classic Sinatra
44
+
45
+ ```ruby
46
+ require 'sinatra'
47
+ require 'kaal/sinatra'
48
+
49
+ class ExampleHeartbeatJob
50
+ def self.perform(*)
51
+ puts 'heartbeat'
52
+ end
53
+ end
54
+
55
+ register Kaal::Sinatra::Extension
56
+
57
+ Kaal::Sinatra.register!(
58
+ settings,
59
+ backend: Kaal::Backend::MemoryAdapter.new,
60
+ scheduler_config_path: 'config/scheduler.yml',
61
+ namespace: 'my-app',
62
+ start_scheduler: false
63
+ )
64
+ ```
65
+
66
+ ## Modular Sinatra
67
+
68
+ ```ruby
69
+ require 'sinatra/base'
70
+ require 'redis'
71
+ require 'kaal/sinatra'
72
+
73
+ class ExampleHeartbeatJob
74
+ def self.perform(*)
75
+ puts 'heartbeat'
76
+ end
77
+ end
78
+
79
+ class App < Sinatra::Base
80
+ REDIS = Redis.new(url: ENV.fetch('REDIS_URL'))
81
+
82
+ register Kaal::Sinatra::Extension
83
+
84
+ kaal redis: REDIS,
85
+ scheduler_config_path: 'config/scheduler.yml',
86
+ namespace: 'my-app',
87
+ start_scheduler: false
88
+ end
89
+ ```
90
+
91
+ ## SQL Backends
92
+
93
+ For SQL-backed Sinatra apps, pass a Sequel connection:
94
+
95
+ ```ruby
96
+ require 'sequel'
97
+
98
+ database = Sequel.connect(ENV.fetch('DATABASE_URL'))
99
+
100
+ Kaal::Sinatra.register!(
101
+ settings,
102
+ database: database,
103
+ adapter: 'postgres', # optional when Sequel can infer it
104
+ scheduler_config_path: 'config/scheduler.yml'
105
+ )
106
+ ```
107
+
108
+ ## Lifecycle
109
+
110
+ `kaal-sinatra` does not auto-start the scheduler by default.
111
+
112
+ If you want the web process to run it:
113
+
114
+ ```ruby
115
+ Kaal::Sinatra.start!
116
+ ```
117
+
118
+ To stop it explicitly:
119
+
120
+ ```ruby
121
+ Kaal::Sinatra.stop!
122
+ ```
123
+
124
+ If you pass `start_scheduler: true` to the extension or `Kaal::Sinatra.register!`, the addon starts the scheduler and installs an `at_exit` shutdown hook for that managed scheduler instance.
125
+
126
+ Preferred deployment model:
127
+
128
+ - run the scheduler in a dedicated process when possible
129
+ - use web-process startup only when you intentionally want co-located scheduling
130
+
131
+ ## Public API
132
+
133
+ - `Kaal::Sinatra.register!(app, backend: nil, database: nil, redis: nil, scheduler_config_path: 'config/scheduler.yml', namespace: nil, start_scheduler: false, adapter: nil)`
134
+ - `Kaal::Sinatra.configure_backend!(backend: nil, database: nil, redis: nil, adapter: nil, configuration: Kaal.configuration)`
135
+ - `Kaal::Sinatra.load_scheduler_file!(root:, environment: nil)`
136
+ - `Kaal::Sinatra.start!`
137
+ - `Kaal::Sinatra.stop!`
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ bin/rspec-unit
143
+ bin/rspec-e2e memory
144
+ REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
145
+ bin/rspec-e2e sqlite
146
+ DATABASE_URL=postgres://postgres:postgres@localhost:5432/kaal_test_auto bin/rspec-e2e pg
147
+ DATABASE_URL=mysql2://root:rootROOT!1@127.0.0.1:3306/kaal_test_auto bin/rspec-e2e mysql
148
+ bin/rubocop
149
+ bin/reek
150
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ require 'bundler/setup'
8
+ require 'bundler/gem_tasks'
data/bin/reek ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright Codevedas Inc. 2025-present
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ require 'pty'
9
+
10
+ root_dir = File.expand_path('..', __dir__)
11
+ Dir.chdir(root_dir)
12
+
13
+ command = ['bundle', 'exec', 'reek', *ARGV]
14
+ output = +''
15
+ status = nil
16
+
17
+ PTY.spawn(*command) do |reader, _writer, pid|
18
+ reader.each { |line| output << line }
19
+ rescue Errno::EIO
20
+ nil
21
+ ensure
22
+ _, status = Process.wait2(pid)
23
+ end
24
+
25
+ print(output.gsub("\r\n", "\n"))
26
+ exit(status.exitstatus || 1)
data/bin/rspec ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright Codevedas Inc. 2025-present
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+ # This file was generated by Bundler.
10
+ #
11
+ # The application 'rspec' is installed as part of a gem, and
12
+ # this file is here to facilitate running it.
13
+ #
14
+
15
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
16
+
17
+ bundle_binstub = File.expand_path('bundle', __dir__)
18
+
19
+ if File.file?(bundle_binstub)
20
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
21
+ load(bundle_binstub)
22
+ else
23
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
24
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
25
+ end
26
+ end
27
+
28
+ require 'rubygems'
29
+ require 'bundler/setup'
30
+
31
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rspec-e2e ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ADAPTER="${1:-memory}"
5
+ shift || true
6
+
7
+ E2E="1" NO_COVERAGE=1 bin/rspec spec/e2e --tag "integration:${ADAPTER}" "$@"
data/bin/rspec-unit ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ bin/rspec spec/kaal --tag ~feature --tag ~integration "$@"
data/bin/rubocop ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright Codevedas Inc. 2025-present
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+ # This file was generated by Bundler.
10
+ #
11
+ # The application 'rubocop' is installed as part of a gem, and
12
+ # this file is here to facilitate running it.
13
+ #
14
+
15
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
16
+
17
+ bundle_binstub = File.expand_path('bundle', __dir__)
18
+
19
+ if File.file?(bundle_binstub)
20
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
21
+ load(bundle_binstub)
22
+ else
23
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
24
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
25
+ end
26
+ end
27
+
28
+ require 'rubygems'
29
+ require 'bundler/setup'
30
+
31
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/update-bundle ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ bundle update
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ module Kaal
8
+ module Sinatra
9
+ VERSION = '0.3.0'
10
+ end
11
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ require 'pathname'
8
+ require 'kaal/sequel'
9
+ require 'sinatra/base'
10
+ require 'kaal/sinatra/version'
11
+
12
+ module Kaal
13
+ # Sinatra integration surface for Kaal.
14
+ module Sinatra
15
+ class << self
16
+ def register!(
17
+ app,
18
+ backend: nil,
19
+ database: nil,
20
+ redis: nil,
21
+ scheduler_config_path: 'config/scheduler.yml',
22
+ namespace: nil,
23
+ start_scheduler: false,
24
+ adapter: nil
25
+ )
26
+ configuration = Kaal.configuration
27
+ normalized_scheduler_config_path = scheduler_config_path.to_s.strip
28
+ normalized_namespace = namespace.to_s.strip
29
+ configuration.scheduler_config_path = normalized_scheduler_config_path unless normalized_scheduler_config_path.empty?
30
+ configuration.namespace = normalized_namespace unless normalized_namespace.empty?
31
+
32
+ configure_backend!(backend:, database:, redis:, adapter:, configuration:)
33
+ load_scheduler_file!(root: root_path_for(app), environment: environment_name_for(app))
34
+
35
+ start_managed_scheduler! if start_scheduler
36
+ app
37
+ end
38
+
39
+ def configure_backend!(backend: nil, database: nil, redis: nil, adapter: nil, configuration: Kaal.configuration)
40
+ current_backend = configuration.backend
41
+ return current_backend if current_backend
42
+
43
+ return configuration.backend = backend if backend
44
+ return configuration.backend = build_redis_backend(redis, configuration) if redis
45
+
46
+ explicit_adapter = adapter.to_s.strip
47
+ unless explicit_adapter.empty?
48
+ raise ArgumentError, 'database is required when adapter is provided' unless database
49
+
50
+ backend_name = normalize_backend_name(explicit_adapter)
51
+ raise ArgumentError, "Unsupported Sinatra datastore backend: #{adapter.inspect}" unless backend_name
52
+
53
+ return configuration.backend = build_backend(backend_name, database)
54
+ end
55
+
56
+ return configuration.backend = Kaal::Backend::MemoryAdapter.new unless database
57
+
58
+ backend_name = detect_backend_name(database, adapter:)
59
+ raise ArgumentError, 'Unsupported Sinatra datastore backend; use memory, redis, sqlite, postgres, or mysql' unless backend_name
60
+
61
+ return configuration.backend = build_backend(backend_name, database)
62
+ end
63
+
64
+ def detect_backend_name(database, adapter: nil)
65
+ explicit_adapter = normalize_backend_name(adapter)
66
+ return explicit_adapter if explicit_adapter
67
+
68
+ inferred_adapter = database_adapter_name(database)
69
+ normalize_backend_name(inferred_adapter)
70
+ end
71
+
72
+ def load_scheduler_file!(root:, environment: nil)
73
+ runtime_context = Kaal::Runtime::RuntimeContext.new(
74
+ root_path: root,
75
+ environment_name: environment || Kaal::Runtime::RuntimeContext.environment_name_from(ENV)
76
+ )
77
+
78
+ Kaal::Runtime::SchedulerBootLoader.new(
79
+ configuration_provider: -> { Kaal.configuration },
80
+ logger: Kaal.configuration.logger,
81
+ runtime_context: runtime_context,
82
+ load_scheduler_file: -> { Kaal.load_scheduler_file!(runtime_context:) }
83
+ ).load_on_boot!
84
+ end
85
+
86
+ def start!
87
+ Kaal.start!
88
+ end
89
+
90
+ def stop!(timeout: 30)
91
+ Kaal.stop!(timeout:)
92
+ end
93
+
94
+ private
95
+
96
+ def build_redis_backend(redis, configuration)
97
+ Kaal::Backend::RedisAdapter.new(redis, namespace: configuration.namespace)
98
+ end
99
+
100
+ def build_backend(backend_name, database)
101
+ case backend_name
102
+ when 'sqlite'
103
+ Kaal::Backend::DatabaseAdapter.new(database)
104
+ when 'postgres'
105
+ Kaal::Backend::PostgresAdapter.new(database)
106
+ when 'mysql'
107
+ Kaal::Backend::MySQLAdapter.new(database)
108
+ end
109
+ end
110
+
111
+ def database_adapter_name(database)
112
+ return if database.nil?
113
+
114
+ database_type = database.database_type if database.respond_to?(:database_type)
115
+ return database_type.to_s unless database_type.to_s.strip.empty?
116
+
117
+ adapter_scheme = database.adapter_scheme if database.respond_to?(:adapter_scheme)
118
+ adapter_scheme.to_s
119
+ end
120
+
121
+ def normalize_backend_name(adapter_name)
122
+ adapter = adapter_name.to_s.strip.downcase
123
+ return if adapter.empty?
124
+ return 'sqlite' if adapter.include?('sqlite')
125
+ return 'postgres' if adapter.include?('postgres')
126
+ return 'mysql' if adapter.include?('mysql') || adapter.include?('trilogy')
127
+
128
+ nil
129
+ end
130
+
131
+ def root_path_for(app)
132
+ settings = settings_for(app)
133
+ root = settings.root if settings.respond_to?(:root)
134
+ Pathname.new(root || Dir.pwd)
135
+ end
136
+
137
+ def environment_name_for(app)
138
+ settings = settings_for(app)
139
+ environment = settings.environment if settings.respond_to?(:environment)
140
+ environment.to_s.empty? ? Kaal::Runtime::RuntimeContext.environment_name_from(ENV) : environment.to_s
141
+ end
142
+
143
+ def settings_for(app)
144
+ app.respond_to?(:settings) ? app.settings : app
145
+ end
146
+
147
+ def start_managed_scheduler!
148
+ return if Kaal.running?
149
+
150
+ start!
151
+ install_shutdown_hook
152
+ end
153
+
154
+ def install_shutdown_hook
155
+ return if @shutdown_hook_installed
156
+
157
+ @shutdown_hook_installed = true
158
+ Kernel.at_exit do
159
+ if Kaal.running?
160
+ begin
161
+ stop!
162
+ rescue StandardError => e
163
+ Kaal.configuration.logger&.error("Failed to stop Kaal during Sinatra shutdown: #{e.message}")
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ # Sinatra extension that wires Kaal into classic and modular apps.
171
+ module Extension
172
+ def self.registered(app)
173
+ app.extend(ClassMethods)
174
+ end
175
+
176
+ # DSL helpers installed onto Sinatra apps.
177
+ module ClassMethods
178
+ def kaal(**)
179
+ Kaal::Sinatra.register!(self, **)
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kaal-sinatra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Nitesh Purohit
8
+ - Codevedas Inc.
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: kaal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: kaal-sequel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.3.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '5.0'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.0'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ description: " Kaal-Sinatra provides a thin integration layer that wires Kaal and
76
+ Kaal-Sequel into Sinatra applications with explicit lifecycle control.\n"
77
+ email:
78
+ - nitesh.purohit.it@gmail.com
79
+ - team@codevedas.com
80
+ executables: []
81
+ extensions: []
82
+ extra_rdoc_files: []
83
+ files:
84
+ - LICENSE
85
+ - README.md
86
+ - Rakefile
87
+ - bin/reek
88
+ - bin/rspec
89
+ - bin/rspec-e2e
90
+ - bin/rspec-unit
91
+ - bin/rubocop
92
+ - bin/update-bundle
93
+ - lib/kaal/sinatra.rb
94
+ - lib/kaal/sinatra/version.rb
95
+ homepage: https://github.com/Code-Vedas/kaal
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ bug_tracker_uri: https://github.com/Code-Vedas/kaal/issues
100
+ changelog_uri: https://github.com/Code-Vedas/kaal/blob/main/CHANGELOG.md
101
+ documentation_uri: https://kaal.codevedas.com
102
+ homepage_uri: https://github.com/Code-Vedas/kaal
103
+ source_code_uri: https://github.com/Code-Vedas/kaal.git
104
+ funding_uri: https://github.com/sponsors/Code-Vedas
105
+ support_uri: https://kaal.codevedas.com/support
106
+ rubygems_uri: https://rubygems.org/gems/kaal-sinatra
107
+ rubygems_mfa_required: 'true'
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '3.2'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.6.7
123
+ specification_version: 4
124
+ summary: Sinatra integration for Kaal, a distributed cron scheduler for Ruby.
125
+ test_files: []