piperb 0.1.1

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +78 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +353 -0
  6. data/Rakefile +10 -0
  7. data/claude.md +346 -0
  8. data/coverage/.last_run.json +6 -0
  9. data/coverage/.resultset.json +1500 -0
  10. data/coverage/.resultset.json.lock +0 -0
  11. data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png +0 -0
  12. data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
  13. data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_both.png +0 -0
  14. data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc.png +0 -0
  15. data/coverage/assets/0.13.2/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
  16. data/coverage/assets/0.13.2/application.css +1 -0
  17. data/coverage/assets/0.13.2/application.js +7 -0
  18. data/coverage/assets/0.13.2/colorbox/border.png +0 -0
  19. data/coverage/assets/0.13.2/colorbox/controls.png +0 -0
  20. data/coverage/assets/0.13.2/colorbox/loading.gif +0 -0
  21. data/coverage/assets/0.13.2/colorbox/loading_background.png +0 -0
  22. data/coverage/assets/0.13.2/favicon_green.png +0 -0
  23. data/coverage/assets/0.13.2/favicon_red.png +0 -0
  24. data/coverage/assets/0.13.2/favicon_yellow.png +0 -0
  25. data/coverage/assets/0.13.2/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  26. data/coverage/assets/0.13.2/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  27. data/coverage/assets/0.13.2/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  28. data/coverage/assets/0.13.2/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  29. data/coverage/assets/0.13.2/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  30. data/coverage/assets/0.13.2/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  31. data/coverage/assets/0.13.2/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  32. data/coverage/assets/0.13.2/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  33. data/coverage/assets/0.13.2/images/ui-icons_222222_256x240.png +0 -0
  34. data/coverage/assets/0.13.2/images/ui-icons_2e83ff_256x240.png +0 -0
  35. data/coverage/assets/0.13.2/images/ui-icons_454545_256x240.png +0 -0
  36. data/coverage/assets/0.13.2/images/ui-icons_888888_256x240.png +0 -0
  37. data/coverage/assets/0.13.2/images/ui-icons_cd0a0a_256x240.png +0 -0
  38. data/coverage/assets/0.13.2/loading.gif +0 -0
  39. data/coverage/assets/0.13.2/magnify.png +0 -0
  40. data/coverage/index.html +16329 -0
  41. data/lib/piperb/cache.rb +121 -0
  42. data/lib/piperb/dag.rb +196 -0
  43. data/lib/piperb/errors.rb +61 -0
  44. data/lib/piperb/executor/base.rb +244 -0
  45. data/lib/piperb/executor/parallel.rb +127 -0
  46. data/lib/piperb/executor/sequential.rb +79 -0
  47. data/lib/piperb/pipeline.rb +90 -0
  48. data/lib/piperb/result.rb +116 -0
  49. data/lib/piperb/step.rb +92 -0
  50. data/lib/piperb/version.rb +5 -0
  51. data/lib/piperb.rb +39 -0
  52. data/piperb.gemspec +33 -0
  53. metadata +99 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29ceb7d0f30eeef6c0386ddfb3530ddcb51cf1f76d8fdc9d6c51d98e9305bbc2
4
+ data.tar.gz: 4bde8fce73584804ae69b1b8dafb84a508050b4934d5ce24a50b975a272f7b8d
5
+ SHA512:
6
+ metadata.gz: 22186fa4ca372d5aa9419e60dd6503cc839e73c381ecb6b098969d3cebb62253afbb541575e9e0e7f475916c183c188644d68f1b61688cc4df7b11d256890e98
7
+ data.tar.gz: 4f89a72e832278b4adfc79e38f2303f9574224cb14c2f28718d46dc190b3866dca2994c1710cbd6f28f953dc12ffd934cbee8ec0b2a704bd15db4cf4b142e7f8
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,78 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.1
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+ Exclude:
9
+ - 'vendor/**/*'
10
+ - 'bin/**/*'
11
+
12
+ Style/Documentation:
13
+ Enabled: false
14
+
15
+ Style/FrozenStringLiteralComment:
16
+ Enabled: true
17
+
18
+ Style/MultilineBlockChain:
19
+ Enabled: false
20
+
21
+ Metrics/BlockLength:
22
+ Exclude:
23
+ - 'spec/**/*'
24
+ - 'flowrb.gemspec'
25
+
26
+ Metrics/MethodLength:
27
+ Max: 30
28
+
29
+ Metrics/AbcSize:
30
+ Max: 30
31
+
32
+ Metrics/ClassLength:
33
+ Max: 150
34
+
35
+ Metrics/ParameterLists:
36
+ Max: 8
37
+
38
+ Layout/LineLength:
39
+ Max: 120
40
+ Exclude:
41
+ - 'spec/**/*'
42
+ - 'flowrb.gemspec'
43
+
44
+ Naming/PredicateMethod:
45
+ Enabled: false
46
+
47
+ Naming/VariableNumber:
48
+ Exclude:
49
+ - 'spec/**/*'
50
+
51
+ Naming/MethodParameterName:
52
+ Exclude:
53
+ - 'spec/**/*'
54
+
55
+ RSpec/SpecFilePathFormat:
56
+ Enabled: false
57
+
58
+ Lint/EmptyBlock:
59
+ Exclude:
60
+ - 'spec/**/*'
61
+
62
+ RSpec/MultipleExpectations:
63
+ Enabled: false
64
+
65
+ RSpec/MultipleDescribes:
66
+ Enabled: false
67
+
68
+ RSpec/ExampleLength:
69
+ Enabled: false
70
+
71
+ RSpec/NestedGroups:
72
+ Max: 4
73
+
74
+ RSpec/DescribeClass:
75
+ Exclude:
76
+ - 'spec/integration/**/*'
77
+ - 'spec/flowrb/step_retry_spec.rb'
78
+ - 'spec/flowrb/step_timeout_spec.rb'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Flowline Contributors
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,353 @@
1
+ # Piperb
2
+
3
+ A Ruby dataflow and pipeline library with declarative step definitions, automatic dependency resolution, parallel/sequential execution, and built-in retry/timeout support.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'piperb'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ gem install piperb
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - **Declarative step definitions** - Define what each step does, not how to orchestrate them
28
+ - **Automatic dependency resolution** - Steps execute in the correct order based on dependencies
29
+ - **Parallel execution** - Independent steps run concurrently using threads
30
+ - **Retries with backoff** - Automatic retry with linear or exponential backoff strategies
31
+ - **Timeouts** - Per-step execution time limits
32
+ - **Conditional execution** - Skip steps based on runtime conditions
33
+ - **Luigi-style caching** - Resume failed pipelines from the last successful step
34
+ - **Zero runtime dependencies** - Pure Ruby using only stdlib
35
+
36
+ ## Quick Start
37
+
38
+ ```ruby
39
+ require 'piperb'
40
+
41
+ pipeline = Piperb.define do
42
+ step :fetch do
43
+ [1, 2, 3]
44
+ end
45
+
46
+ step :transform, depends_on: :fetch do |data|
47
+ data.map { |n| n * 2 }
48
+ end
49
+
50
+ step :load, depends_on: :transform do |data|
51
+ data.sum
52
+ end
53
+ end
54
+
55
+ result = pipeline.run
56
+ result.success? # => true
57
+ result[:load].output # => 12
58
+ result[:load].duration # => 0.001
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Basic Pipeline
64
+
65
+ ```ruby
66
+ pipeline = Piperb.define do
67
+ step :fetch_users do
68
+ User.all.to_a
69
+ end
70
+
71
+ step :enrich, depends_on: :fetch_users do |users|
72
+ users.map { |u| enrich_from_api(u) }
73
+ end
74
+
75
+ step :export_csv, depends_on: :enrich do |users|
76
+ CSV.generate { |csv| users.each { |u| csv << u.to_a } }
77
+ end
78
+
79
+ step :export_json, depends_on: :enrich do |users|
80
+ users.to_json
81
+ end
82
+
83
+ # Multiple dependencies - outputs passed as keyword arguments
84
+ step :notify, depends_on: [:export_csv, :export_json] do |export_csv:, export_json:|
85
+ Notifier.send("Exported #{export_csv.lines.count} rows")
86
+ end
87
+ end
88
+
89
+ result = pipeline.run
90
+ ```
91
+
92
+ ### Parallel Execution
93
+
94
+ Steps at the same "level" (no inter-dependencies) run concurrently:
95
+
96
+ ```ruby
97
+ pipeline = Piperb.define do
98
+ step :fetch_users do
99
+ fetch_from_api("/users")
100
+ end
101
+
102
+ step :fetch_orders do
103
+ fetch_from_api("/orders")
104
+ end
105
+
106
+ # fetch_users and fetch_orders run in parallel
107
+
108
+ step :generate_report, depends_on: [:fetch_users, :fetch_orders] do |fetch_users:, fetch_orders:|
109
+ { users: fetch_users, orders: fetch_orders }
110
+ end
111
+ end
112
+
113
+ # Parallel execution
114
+ result = pipeline.run(executor: :parallel)
115
+
116
+ # Parallel with thread limit
117
+ result = pipeline.run(executor: :parallel, max_threads: 4)
118
+ ```
119
+
120
+ ### Step Retries
121
+
122
+ Steps can be configured to automatically retry on failure:
123
+
124
+ ```ruby
125
+ pipeline = Piperb.define do
126
+ step :fetch_api, retries: 3, retry_delay: 2 do
127
+ HTTP.get("https://api.example.com/data")
128
+ end
129
+
130
+ # Exponential backoff: delays of 1s, 2s, 4s
131
+ step :flaky_service, retries: 3, retry_delay: 1, retry_backoff: :exponential do
132
+ ExternalService.call
133
+ end
134
+
135
+ # Linear backoff: delays of 1s, 2s, 3s
136
+ step :another_service, retries: 3, retry_delay: 1, retry_backoff: :linear do
137
+ AnotherService.call
138
+ end
139
+
140
+ # Conditional retry - only retry on specific errors
141
+ step :selective_retry, retries: 3, retry_if: ->(error) { error.is_a?(IOError) } do
142
+ risky_operation
143
+ end
144
+ end
145
+
146
+ result = pipeline.run
147
+ result[:fetch_api].retries # => number of retries that occurred
148
+ ```
149
+
150
+ **Retry Options:**
151
+ - `retries: n` - Maximum retry attempts (default: 0)
152
+ - `retry_delay: seconds` - Wait time between retries (default: 0)
153
+ - `retry_backoff: :exponential | :linear` - Backoff strategy for delays
154
+ - `retry_if: ->(error) { ... }` - Only retry if condition returns true
155
+
156
+ ### Step Timeouts
157
+
158
+ Steps can be configured with execution timeouts:
159
+
160
+ ```ruby
161
+ pipeline = Piperb.define do
162
+ step :slow_operation, timeout: 30 do
163
+ # Will raise TimeoutError if not complete in 30 seconds
164
+ long_running_computation
165
+ end
166
+
167
+ # Combine timeout with retries
168
+ step :unreliable, timeout: 10, retries: 3, retry_delay: 5 do
169
+ external_api_call
170
+ end
171
+ end
172
+
173
+ result = pipeline.run
174
+ result[:slow_operation].timed_out? # => true if step timed out
175
+ ```
176
+
177
+ ### Conditional Execution
178
+
179
+ Steps can be conditionally executed based on runtime conditions:
180
+
181
+ ```ruby
182
+ pipeline = Piperb.define do
183
+ step :config do
184
+ { feature_enabled: true, skip_export: false }
185
+ end
186
+
187
+ # Only runs when if: condition returns truthy
188
+ step :feature_a, depends_on: :config, if: ->(cfg) { cfg[:feature_enabled] } do |cfg|
189
+ 'feature A result'
190
+ end
191
+
192
+ # Skipped when unless: condition returns truthy
193
+ step :export, depends_on: :config, unless: ->(cfg) { cfg[:skip_export] } do |cfg|
194
+ 'export result'
195
+ end
196
+
197
+ # Handles nil from skipped dependency
198
+ step :finalize, depends_on: :feature_a do |input|
199
+ input.nil? ? 'dependency was skipped' : "got: #{input}"
200
+ end
201
+ end
202
+
203
+ result = pipeline.run
204
+ result[:feature_a].skipped? # => false
205
+ ```
206
+
207
+ **Conditional Behavior:**
208
+ - `if: condition` - Runs step only when condition returns truthy
209
+ - `unless: condition` - Skips step when condition returns truthy
210
+ - Skipped steps return `nil` output with `:skipped` status
211
+ - Dependent steps receive `nil` for skipped dependency outputs
212
+ - Skipped steps don't count as failures (pipeline still succeeds)
213
+
214
+ ### Caching (Luigi-style)
215
+
216
+ Steps can be cached to enable resuming failed pipelines from the last successful step:
217
+
218
+ ```ruby
219
+ # Using a file-based cache (persists across runs)
220
+ pipeline.run(cache: './cache')
221
+
222
+ # Using a memory cache (for testing)
223
+ cache = Piperb::Cache::MemoryStore.new
224
+ pipeline.run(cache: cache)
225
+
226
+ # Force re-execution (ignores cache)
227
+ pipeline.run(cache: './cache', force: true)
228
+ ```
229
+
230
+ #### Step-level Cache Control
231
+
232
+ ```ruby
233
+ pipeline = Piperb.define do
234
+ # This step is cached (default behavior)
235
+ step :fetch_data do
236
+ expensive_api_call
237
+ end
238
+
239
+ # This step is never cached
240
+ step :current_time, cache: false do
241
+ Time.now
242
+ end
243
+
244
+ # Custom cache key based on input
245
+ step :process, depends_on: :fetch_data, cache_key: ->(input) { "process_#{input[:id]}" } do |data|
246
+ transform(data)
247
+ end
248
+ end
249
+ ```
250
+
251
+ #### Resume Failed Pipeline
252
+
253
+ ```ruby
254
+ # First run - step 2 fails, but step 1 is cached
255
+ begin
256
+ pipeline.run(cache: './cache')
257
+ rescue Piperb::StepError
258
+ puts "Pipeline failed, but progress was saved"
259
+ end
260
+
261
+ # Second run - step 1 loads from cache, step 2 retries
262
+ result = pipeline.run(cache: './cache')
263
+ ```
264
+
265
+ **Cache Options:**
266
+ - `cache: path` - File-based cache directory
267
+ - `cache: store` - Custom cache store implementing `Piperb::Cache::Base`
268
+ - `force: true` - Ignore cache and re-execute all steps
269
+ - Step option `cache: false` - Disable caching for specific steps
270
+ - Step option `cache_key: lambda` - Custom cache key based on input
271
+
272
+ ### Input Passing Strategy
273
+
274
+ - **No dependencies**: receives `initial_input` or empty args
275
+ - **Single dependency**: output passed directly as argument
276
+ - **Multiple dependencies**: outputs passed as keyword arguments
277
+
278
+ ```ruby
279
+ # Single dependency - direct argument
280
+ step :process, depends_on: :fetch do |data|
281
+ data.map(&:transform)
282
+ end
283
+
284
+ # Multiple dependencies - keyword arguments
285
+ step :merge, depends_on: [:csv, :json] do |csv:, json:|
286
+ { csv: csv, json: json }
287
+ end
288
+
289
+ # Initial input
290
+ result = pipeline.run(initial_input: { date: Date.today })
291
+ ```
292
+
293
+ ### Mermaid Diagram Generation
294
+
295
+ ```ruby
296
+ pipeline = Piperb.define do
297
+ step :fetch do; end
298
+ step :process, depends_on: :fetch do |_|; end
299
+ step :save, depends_on: :process do |_|; end
300
+ end
301
+
302
+ puts pipeline.to_mermaid
303
+ # graph TD
304
+ # fetch --> process
305
+ # process --> save
306
+ ```
307
+
308
+ ## Error Handling
309
+
310
+ ```ruby
311
+ Piperb::Error # Base error
312
+ Piperb::CycleError # Circular dependency detected
313
+ Piperb::MissingDependencyError # Unknown dependency referenced
314
+ Piperb::DuplicateStepError # Step name already exists
315
+ Piperb::StepError # Step execution failed
316
+ Piperb::TimeoutError # Step exceeded timeout duration
317
+ ```
318
+
319
+ `StepError` wraps the original error and includes partial results:
320
+
321
+ ```ruby
322
+ begin
323
+ pipeline.run
324
+ rescue Piperb::StepError => e
325
+ e.step_name # => :failed_step
326
+ e.original_error # => the underlying exception
327
+ e.partial_results # => results from completed steps
328
+ end
329
+ ```
330
+
331
+ ## Development
332
+
333
+ ```bash
334
+ bundle install
335
+ bundle exec rspec # Run tests (597 examples)
336
+ bundle exec rubocop # Run linter
337
+ bundle exec rake # Run both tests and linter
338
+ ```
339
+
340
+ ## Test Coverage
341
+
342
+ - Line Coverage: ~97%
343
+ - Branch Coverage: ~90%
344
+ - 597 test examples
345
+
346
+ ## Requirements
347
+
348
+ - Ruby >= 3.1.0
349
+ - No runtime dependencies (pure Ruby, stdlib only)
350
+
351
+ ## License
352
+
353
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]