async_pipeline 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c923079b7f9cb16b354ff63bdde17ec7102978a533a8bf7e21ef7cef618fee39
4
+ data.tar.gz: 54dee3f2a5690fd5a34e3603067b2f5ce3261044a07948a35871014eaa702e3c
5
+ SHA512:
6
+ metadata.gz: e4447679eea0e2298c72a946531ec422cd3caefe7d36acbcda3c61b2cddcc5ee3df9c3502586a556d0e2671f095fb6971908bcfca70277ae6a160c2115dd4f73
7
+ data.tar.gz: 791d1a309697cd7c940566493ff5c335900d746d310da969d8addf25b7d98e1e18442bfaef56b0a6a5239a9c6f603b1fcee1cb1b99f0279fb5c28d959b79d040
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ DisabledByDefault: true
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Layout/LineLength:
14
+ Max: 120
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.3
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # AsyncPipeline
2
+
3
+ An experimental thing to do stuff for you as you want it to do.
4
+
5
+ ## Installation
6
+
7
+ In order to install this gem, one must first invent the universe.
8
+
9
+ ## Usage
10
+
11
+ Probably don't. Cause I'm still figuring it out.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/async_pipeline/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "async_pipeline"
7
+ spec.version = AsyncPipeline::VERSION
8
+ spec.authors = ["Pete Kinnecom"]
9
+ spec.email = ["git@k7u7.com"]
10
+
11
+ spec.summary = "Define a pipeline of async tasks and their starting conditions"
12
+ spec.homepage = "https://github.com/petekinnecom/async_pipeline"
13
+ spec.license = "WTFPL"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = spec.homepage
19
+
20
+ spec.files = Dir.chdir(__dir__) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ (File.expand_path(f) == __FILE__) ||
23
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ class Changeset
5
+ CreateDelta = Struct.new(:type, :attributes, keyword_init: true) do
6
+ def apply(store)
7
+ store.create(type: type, attributes: attributes)
8
+ end
9
+ end
10
+
11
+ UpdateDelta = Struct.new(:model, :delta, keyword_init: true) do
12
+ def apply(store)
13
+ current_model = store.find(id: model.id, type: model.class)
14
+
15
+ # Todo: detect if changed underfoot
16
+
17
+ store.update(
18
+ id: model.id,
19
+ type: model.class,
20
+ attributes: current_model.attributes.merge(delta)
21
+ )
22
+ end
23
+ end
24
+
25
+ attr_reader :deltas
26
+ def initialize
27
+ @deltas = []
28
+ end
29
+
30
+ def deltas?
31
+ !@deltas.empty?
32
+ end
33
+
34
+ def create(type, attributes)
35
+ @deltas << CreateDelta.new(type: type, attributes: attributes)
36
+ end
37
+
38
+ def update(model, delta)
39
+ @deltas << UpdateDelta.new(model: model, delta: delta)
40
+ end
41
+
42
+ def apply(...)
43
+ deltas.each { _1.apply(...) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ module AsyncPipeline
2
+ class Chunk
3
+ def self.build_chunks(store)
4
+ target = self.target
5
+ type = target.values.first
6
+ records = store.all(type: type)
7
+
8
+ if target.key?(:collection)
9
+ new(
10
+ target: records,
11
+ store: store,
12
+ changeset: Changeset.new
13
+ )
14
+ else
15
+ records.map { |record|
16
+ new(
17
+ target: record,
18
+ store: store,
19
+ changeset: Changeset.new
20
+ )
21
+ }
22
+ end
23
+ end
24
+
25
+ attr_accessor :target, :store, :changeset
26
+
27
+ def initialize(target:, store:, changeset:)
28
+ @target = target
29
+ @store = store
30
+ @changeset = changeset
31
+ end
32
+
33
+ def id
34
+ extra = (
35
+ if self.class.target.key?(:item)
36
+ { id: target.id }
37
+ else
38
+ {}
39
+ end
40
+ )
41
+
42
+ self.class.target.merge(extra)
43
+ end
44
+
45
+ def should_perform?
46
+ !done? && ready?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ module Model
5
+ module InstanceMethods
6
+ attr_reader :attributes
7
+
8
+ def initialize(attributes)
9
+ @attributes = attributes
10
+ end
11
+ end
12
+
13
+ def self.extended(base)
14
+ base.include(InstanceMethods)
15
+ end
16
+
17
+ def inherited(base)
18
+ base.instance_variable_set(:@attributes, attributes.dup)
19
+ end
20
+
21
+ def attributes
22
+ @attributes ||= {}
23
+ end
24
+
25
+ def attribute(name, **opts)
26
+ attributes[name] = opts
27
+
28
+ define_method(name, &Ractor.make_shareable(Proc.new { attributes.fetch(name) }))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "processor"
4
+ require_relative "registry"
5
+ require_relative "store"
6
+ require_relative "stores/yaml"
7
+
8
+ module AsyncPipeline
9
+ module Pipeline
10
+ module InstanceMethods
11
+ def initialize(data)
12
+ @data = data
13
+ end
14
+
15
+ def start
16
+ Processor.call(
17
+ store: store,
18
+ chunkables: chunkables,
19
+ registry: registry
20
+ )
21
+ end
22
+
23
+ def data
24
+ store.to_h
25
+ end
26
+
27
+ private
28
+
29
+ def store
30
+ @store ||= self.class.store.build(data: @data, dir: dir, registry: registry)
31
+ end
32
+
33
+ def dir
34
+ @dir ||= Dir.mktmpdir
35
+ end
36
+
37
+ def registry
38
+ self.class.registry
39
+ end
40
+
41
+ def chunkables
42
+ self.class.chunkables
43
+ end
44
+ end
45
+
46
+ def self.extended(base)
47
+ base.include(InstanceMethods)
48
+ end
49
+
50
+ def inherited(base)
51
+ base.instance_variable_set(:@models, models.dup)
52
+ base.instance_variable_set(:@chunkables, chunkables.dup)
53
+ end
54
+
55
+ def store(klass = nil)
56
+ @store = klass if klass
57
+ @store
58
+ end
59
+
60
+ def chunkables
61
+ @chunkables ||= []
62
+ end
63
+
64
+ def registry
65
+ @registry ||= Registry.new
66
+ end
67
+
68
+ def model(klass, as_type:)
69
+ registry.register(as_type, klass)
70
+ end
71
+
72
+ def chunk(klass)
73
+ chunkables << klass
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "changeset"
4
+ require_relative "read_only_store"
5
+
6
+ module AsyncPipeline
7
+ class Processor
8
+ Message = Struct.new(:type, :payload) do
9
+ def to_s(...)
10
+ inspect(...)
11
+ end
12
+ def inspect(...)
13
+ "<Message #{type} (#{payload.class}) >"
14
+ end
15
+ end
16
+
17
+ Ms = Module.new do
18
+ def self.g(...)
19
+ Message.new(...)
20
+ end
21
+
22
+ def msg(...)
23
+ Message.new(...)
24
+ end
25
+ end
26
+
27
+ def self.Msg(type, payload = nil)
28
+ Message.new(type, payload)
29
+ end
30
+
31
+ def self.call(...)
32
+ new(...).call
33
+ end
34
+
35
+ attr_reader :store, :chunkables, :registry
36
+ def initialize(store:, chunkables:, registry:)
37
+ @store = store
38
+ @chunkables = chunkables
39
+ @registry = registry
40
+ end
41
+
42
+ def call
43
+ Log.info("scheduler_channel: #{scheduler_channel.object_id}")
44
+ scheduler_channel.send(Ms.g(:requeue))
45
+
46
+ workers
47
+ loop do
48
+ r, msg = Ractor.select(scheduler_channel, changeset_channel, work_channel, ticker)
49
+
50
+ case r
51
+ when ticker
52
+ case msg.type
53
+ when :tick
54
+ changeset_channel.send(Ms.g(:flush_queue))
55
+ end
56
+ when scheduler_channel
57
+ puts "scheduler_channel: #{msg}"
58
+ case msg.type
59
+ when :enqueue
60
+ work_channel.send(Ms.g(:process, msg.payload))
61
+ when :all_chunks_processed
62
+ scheduler_channel.close_outgoing
63
+ work_channel.close_outgoing
64
+ changeset_channel.close_outgoing
65
+ else
66
+ # scheduler_channel.close_outgoing
67
+ # raise "Unknown message: #{msg}"
68
+ end
69
+ when changeset_channel
70
+ case msg.type
71
+ when :chunks_updated
72
+ scheduler_channel.send(Ms.g(:requeue, msg.payload))
73
+ when :chunks_processed
74
+ scheduler_channel.send(Ms.g(:chunks_processed, msg.payload))
75
+ else
76
+ # raise "Unknown message: #{msg}"
77
+ end
78
+ when work_channel
79
+ case msg.type
80
+ when :changeset
81
+ changeset_channel.send(Ms.g(:changeset, msg.payload))
82
+ else
83
+ # raise "Unknown message: #{msg}"
84
+ end
85
+ end
86
+ end
87
+
88
+ store.to_h
89
+ end
90
+
91
+ def changeset_channel
92
+ @changeset_channel ||= Ractor.new(store) do |store, channel|
93
+ chunks = []
94
+ loop do
95
+ msg = receive
96
+ case msg.type
97
+ when :changeset
98
+ chunks << msg.payload
99
+ when :flush_queue
100
+ next unless chunks.any?
101
+
102
+ was_updated = store.apply(chunks.map(&:changeset))
103
+ if was_updated
104
+ Ractor.yield(Ms.g(:chunks_updated, chunks.map(&:id)))
105
+ else
106
+ Ractor.yield(Ms.g(:chunks_processed, chunks.map(&:id)))
107
+ end
108
+
109
+ chunks = []
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def ticker
116
+ @ticker ||= Ractor.new do
117
+ loop do
118
+ sleep 0.5
119
+ Ractor.yield(Ms.g(:tick))
120
+ end
121
+ end
122
+ end
123
+
124
+ def scheduler_channel
125
+ @scheduler_channel ||= Ractor.new(store, chunkables, changeset_channel) do |store, chunkables, changeset_channel|
126
+ status = {}
127
+ loop do
128
+ msg = receive
129
+
130
+ # we update chunk_ids on both messages.
131
+ chunk_ids = msg.payload || []
132
+ chunk_ids.each do |chunk_id|
133
+ status[chunk_id] = :processed
134
+ end
135
+
136
+ case msg.type
137
+ when :requeue
138
+ reader = store.reader
139
+
140
+ Log.info "model_one: #{store.reader.all(type: :model_one).count}"
141
+ chunkables
142
+ .map { _1.build_chunks(reader) }
143
+ .flatten
144
+ .each do |c|
145
+ if status[c.id] != :queued && c.should_perform?
146
+ puts "enqueuing: #{c.id}"
147
+ status[c.id] = :queued
148
+ Ractor.yield(Ms.g(:enqueue, c))
149
+ end
150
+ end
151
+ end
152
+
153
+ if status.values.all? { _1 == :processed }
154
+ Ractor.yield(Ms.g(:all_chunks_processed))
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ def work_channel
161
+ @work_channel ||= Ractor.new(work_pool) do |work_pool|
162
+ loop do
163
+ msg = receive
164
+ case msg.type
165
+ when :process
166
+ chunk = msg.payload
167
+ work_pool.send(Ms.g(:chunk, chunk))
168
+ when :changeset
169
+ Ractor.yield(Ms.g(:changeset, msg.payload))
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def work_pool
176
+ @work_pool ||= Ractor.new do
177
+ loop do
178
+ Ractor.yield Ractor.receive
179
+ end
180
+ end
181
+ end
182
+
183
+ def workers
184
+ @workers ||= 4.times.map { |i|
185
+ Ractor.new(work_pool, work_channel, name: "worker-#{i}") do |work_pool, work_channel|
186
+ loop do
187
+ r, msg = Ractor.select(work_pool)
188
+
189
+ case msg.type
190
+ when :chunk
191
+ chunk = msg.payload
192
+ puts "starting perform: #{chunk.id}"
193
+ chunk.perform
194
+ puts "finished perform: #{chunk.id}"
195
+ work_channel.send(Ms.g(:changeset, chunk))
196
+ end
197
+ end
198
+ end
199
+ }
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ class ReadyOnlyStore
5
+ attr_reader :store
6
+ def initialize(store)
7
+ @store = store
8
+ end
9
+
10
+ def find(...)
11
+ store.find(...)
12
+ end
13
+
14
+ def all(...)
15
+ store.all(...)
16
+ end
17
+
18
+ def everything(...)
19
+ store.everything(...)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ class Registry
5
+ def build(type, attributes)
6
+ lookup.fetch(type).new(attributes)
7
+ end
8
+
9
+ def register(type, klass)
10
+ lookup[type] = klass
11
+ end
12
+
13
+ def type_for(type)
14
+ return type if lookup.key?(type)
15
+
16
+ reverse_lookup = lookup.invert
17
+
18
+ return reverse_lookup[type] if reverse_lookup.key?(type)
19
+
20
+ raise ArgumentError, "Unknown type: #{type}"
21
+ end
22
+
23
+ private
24
+
25
+ def lookup
26
+ @lookup ||= {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ class Shell
5
+ class << self
6
+ Result = Struct.new(:success, :stdout, :stderr, keyword_init: true) do
7
+ def success?
8
+ success
9
+ end
10
+ end
11
+
12
+ def run!(...)
13
+ run(...).tap { raise "command failed: #{command}" unless _1.success? }
14
+ end
15
+
16
+ def run(command)
17
+ Open3.popen3(command) do |_in, stdout, stderr, wait_thr|
18
+ process_stdout = []
19
+ stdout_thr = Thread.new do
20
+ while line = stdout.gets&.chomp
21
+ yield(:stdout, line) if block_given?
22
+ process_stdout << line
23
+ end
24
+ end
25
+
26
+ process_stderr = []
27
+ stderr_thr = Thread.new do
28
+ while line = stderr.gets&.chomp
29
+ yield(:stderr, line) if block_given?
30
+ process_stderr << line
31
+ end
32
+ end
33
+
34
+ [
35
+ stderr_thr,
36
+ stdout_thr,
37
+ ].each(&:join)
38
+
39
+ Result.new(
40
+ success: wait_thr.value.success?,
41
+ stdout: process_stdout.join("\n"),
42
+ stderr: process_stderr.join("\n"),
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ class Store
5
+ attr_reader :db, :changeset
6
+ def initialize(db:, changeset:)
7
+ @changeset = changeset
8
+ @db = db
9
+ end
10
+
11
+ def find(...)
12
+ db.find(...)
13
+ end
14
+
15
+ def all(...)
16
+ db.find(...)
17
+ end
18
+
19
+ def create(...)
20
+ changeset.create(...)
21
+ end
22
+
23
+ def update(...)
24
+ changeset.update(...)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "yaml"
5
+
6
+ module AsyncPipeline
7
+ module Stores
8
+ class Yaml
9
+ class Reader
10
+ attr_reader :registry, :data
11
+ def initialize(registry:, data:)
12
+ @registry = registry
13
+ @data = data
14
+ end
15
+
16
+ def find(id:, type:)
17
+ type = registry.type_for(type)
18
+ attrs = (data[type] || []).find { |attrs| attrs[:id] == id }
19
+ registry.build(type, attrs)
20
+ end
21
+
22
+ def everything
23
+ data.each_with_object({}) do |(type, subdata), all_of_it|
24
+ all_of_it[type] = subdata.map { |attrs| registry.build(type, attrs) }
25
+ end
26
+ end
27
+
28
+ def all(type:)
29
+ type = registry.type_for(type)
30
+
31
+ data
32
+ .fetch(type, [])
33
+ .map { registry.build(type, _1) }
34
+ end
35
+ end
36
+
37
+ class Writer
38
+ attr_reader :path, :registry, :yaml_before, :data
39
+ def initialize(path:, registry:)
40
+ @path = path
41
+ @registry = registry
42
+ @yaml_before = File.read(path)
43
+ @data = YAML.unsafe_load(@yaml_before)
44
+ end
45
+
46
+ def apply(changesets)
47
+ changesets.each { _1.apply(self) }
48
+ yaml_after = data.to_yaml
49
+
50
+ if yaml_before != yaml_after
51
+ File.write(path, yaml_after)
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ def find(id:, type:)
59
+ type = registry.type_for(type)
60
+ attrs = (data[type] || []).find { |attrs| attrs[:id] == id }
61
+ registry.build(type, attrs)
62
+ end
63
+
64
+ def create(type:, attributes:)
65
+ type = registry.type_for(type)
66
+
67
+ data[type] ||= {}
68
+ data[type] << { id: SecureRandom.uuid }.merge(attributes)
69
+ end
70
+
71
+ def update(id:, type:, attributes:)
72
+ type = registry.type_for(type)
73
+ attrs = (data[type] || []).find { |attrs| attrs[:id] == id }
74
+ raise "Not found" unless attrs
75
+ attrs.merge!(attributes)
76
+ end
77
+ end
78
+
79
+ def self.build(data:, dir:, registry:)
80
+ path = File.join(dir, "data.yml")
81
+
82
+ File.write(path, data.to_yaml)
83
+
84
+ new(
85
+ path: path,
86
+ registry: registry
87
+ )
88
+ end
89
+
90
+ attr_reader :path, :registry
91
+ def initialize(path:, registry:)
92
+ @path = path
93
+ @registry = registry
94
+ end
95
+
96
+ def reader
97
+ Reader.new(data: read, registry: registry)
98
+ end
99
+
100
+ def apply(changesets)
101
+ Writer.new(path: path, registry: registry).apply(changesets)
102
+ end
103
+
104
+ def to_h
105
+ read
106
+ end
107
+
108
+ private
109
+
110
+ def read
111
+ YAML.unsafe_load_file(path)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncPipeline
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "async_pipeline/version"
4
+ require_relative "async_pipeline/model"
5
+ require_relative "async_pipeline/chunk"
6
+ require_relative "async_pipeline/pipeline"
7
+
8
+ require "logger"
9
+
10
+ module AsyncPipeline
11
+ class Error < StandardError; end
12
+ # Your code goes here...
13
+
14
+ class RactorLogger < Ractor
15
+ def self.new
16
+ super do
17
+ # STDOUT cannot be referenced but $stdout can
18
+ logger = ::Logger.new($stdout)
19
+ logger.formatter = proc do |severity, datetime, progname, msg|
20
+ "#{datetime.to_i} [#{severity}] #{msg}\n"
21
+ end
22
+
23
+ # Run the requested operations on our logger instance
24
+ while data = receive
25
+ logger.public_send(data[0], *data[1])
26
+ end
27
+ end
28
+ end
29
+
30
+ def log(level, msg)
31
+ ractor_name = Ractor.current.name || Ractor.current.inspect
32
+
33
+ send([level, "#{ractor_name}: #{msg}"])
34
+ end
35
+
36
+ def info(msg)
37
+ log(:info, msg)
38
+ end
39
+
40
+ def error(msg)
41
+ log(:error, msg)
42
+ end
43
+
44
+ def warn(msg)
45
+ log(:warn, msg)
46
+ end
47
+
48
+ def debug(msg)
49
+ log(:debug, msg)
50
+ end
51
+ end
52
+
53
+ Log = RactorLogger.new
54
+
55
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async_pipeline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pete Kinnecom
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-05-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - git@k7u7.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - ".ruby-version"
22
+ - README.md
23
+ - Rakefile
24
+ - async_pipeline.gemspec
25
+ - lib/async_pipeline.rb
26
+ - lib/async_pipeline/changeset.rb
27
+ - lib/async_pipeline/chunk.rb
28
+ - lib/async_pipeline/model.rb
29
+ - lib/async_pipeline/pipeline.rb
30
+ - lib/async_pipeline/processor.rb
31
+ - lib/async_pipeline/read_only_store.rb
32
+ - lib/async_pipeline/registry.rb
33
+ - lib/async_pipeline/shell.rb
34
+ - lib/async_pipeline/store.rb
35
+ - lib/async_pipeline/stores/yaml.rb
36
+ - lib/async_pipeline/version.rb
37
+ homepage: https://github.com/petekinnecom/async_pipeline
38
+ licenses:
39
+ - WTFPL
40
+ metadata:
41
+ allowed_push_host: https://rubygems.org
42
+ homepage_uri: https://github.com/petekinnecom/async_pipeline
43
+ source_code_uri: https://github.com/petekinnecom/async_pipeline
44
+ changelog_uri: https://github.com/petekinnecom/async_pipeline
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.0.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.4.19
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Define a pipeline of async tasks and their starting conditions
64
+ test_files: []