taski 0.8.2 → 0.9.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +65 -50
  4. data/docs/GUIDE.md +41 -56
  5. data/examples/README.md +10 -29
  6. data/examples/clean_demo.rb +25 -65
  7. data/examples/large_tree_demo.rb +356 -0
  8. data/examples/message_demo.rb +0 -1
  9. data/examples/progress_demo.rb +13 -24
  10. data/examples/reexecution_demo.rb +8 -44
  11. data/lib/taski/execution/execution_facade.rb +150 -0
  12. data/lib/taski/execution/executor.rb +156 -357
  13. data/lib/taski/execution/registry.rb +15 -19
  14. data/lib/taski/execution/scheduler.rb +161 -140
  15. data/lib/taski/execution/task_observer.rb +41 -0
  16. data/lib/taski/execution/task_output_router.rb +41 -58
  17. data/lib/taski/execution/task_wrapper.rb +123 -219
  18. data/lib/taski/execution/worker_pool.rb +238 -64
  19. data/lib/taski/logging.rb +105 -0
  20. data/lib/taski/progress/layout/base.rb +600 -0
  21. data/lib/taski/progress/layout/filters.rb +126 -0
  22. data/lib/taski/progress/layout/log.rb +27 -0
  23. data/lib/taski/progress/layout/simple.rb +166 -0
  24. data/lib/taski/progress/layout/tags.rb +76 -0
  25. data/lib/taski/progress/layout/theme_drop.rb +84 -0
  26. data/lib/taski/progress/layout/tree.rb +300 -0
  27. data/lib/taski/progress/theme/base.rb +224 -0
  28. data/lib/taski/progress/theme/compact.rb +58 -0
  29. data/lib/taski/progress/theme/default.rb +25 -0
  30. data/lib/taski/progress/theme/detail.rb +48 -0
  31. data/lib/taski/progress/theme/plain.rb +40 -0
  32. data/lib/taski/static_analysis/analyzer.rb +5 -17
  33. data/lib/taski/static_analysis/dependency_graph.rb +19 -1
  34. data/lib/taski/static_analysis/visitor.rb +1 -39
  35. data/lib/taski/task.rb +44 -58
  36. data/lib/taski/test_helper/errors.rb +1 -1
  37. data/lib/taski/test_helper.rb +21 -35
  38. data/lib/taski/version.rb +1 -1
  39. data/lib/taski.rb +60 -61
  40. data/sig/taski.rbs +194 -203
  41. metadata +31 -8
  42. data/examples/section_demo.rb +0 -195
  43. data/lib/taski/execution/base_progress_display.rb +0 -364
  44. data/lib/taski/execution/execution_context.rb +0 -390
  45. data/lib/taski/execution/plain_progress_display.rb +0 -76
  46. data/lib/taski/execution/simple_progress_display.rb +0 -206
  47. data/lib/taski/execution/tree_progress_display.rb +0 -643
  48. data/lib/taski/section.rb +0 -74
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taski
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: liquid
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: prism
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -60,27 +74,36 @@ files:
60
74
  - examples/args_demo.rb
61
75
  - examples/clean_demo.rb
62
76
  - examples/group_demo.rb
77
+ - examples/large_tree_demo.rb
63
78
  - examples/message_demo.rb
64
79
  - examples/progress_demo.rb
65
80
  - examples/quick_start.rb
66
81
  - examples/reexecution_demo.rb
67
- - examples/section_demo.rb
68
82
  - lib/taski.rb
69
83
  - lib/taski/args.rb
70
84
  - lib/taski/env.rb
71
- - lib/taski/execution/base_progress_display.rb
72
- - lib/taski/execution/execution_context.rb
85
+ - lib/taski/execution/execution_facade.rb
73
86
  - lib/taski/execution/executor.rb
74
- - lib/taski/execution/plain_progress_display.rb
75
87
  - lib/taski/execution/registry.rb
76
88
  - lib/taski/execution/scheduler.rb
77
- - lib/taski/execution/simple_progress_display.rb
89
+ - lib/taski/execution/task_observer.rb
78
90
  - lib/taski/execution/task_output_pipe.rb
79
91
  - lib/taski/execution/task_output_router.rb
80
92
  - lib/taski/execution/task_wrapper.rb
81
- - lib/taski/execution/tree_progress_display.rb
82
93
  - lib/taski/execution/worker_pool.rb
83
- - lib/taski/section.rb
94
+ - lib/taski/logging.rb
95
+ - lib/taski/progress/layout/base.rb
96
+ - lib/taski/progress/layout/filters.rb
97
+ - lib/taski/progress/layout/log.rb
98
+ - lib/taski/progress/layout/simple.rb
99
+ - lib/taski/progress/layout/tags.rb
100
+ - lib/taski/progress/layout/theme_drop.rb
101
+ - lib/taski/progress/layout/tree.rb
102
+ - lib/taski/progress/theme/base.rb
103
+ - lib/taski/progress/theme/compact.rb
104
+ - lib/taski/progress/theme/default.rb
105
+ - lib/taski/progress/theme/detail.rb
106
+ - lib/taski/progress/theme/plain.rb
84
107
  - lib/taski/static_analysis/analyzer.rb
85
108
  - lib/taski/static_analysis/dependency_graph.rb
86
109
  - lib/taski/static_analysis/visitor.rb
@@ -1,195 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Section API Basics Example
4
- # This example demonstrates runtime implementation selection with the Section API
5
-
6
- # Section API is perfect for:
7
- # - Environment-specific implementations (dev/staging/prod)
8
- # - Different service adapters (AWS/GCP/local)
9
- # - Clean abstraction with guaranteed interfaces
10
-
11
- require_relative "../lib/taski"
12
-
13
- # Example 1: Database Configuration Section
14
- # This section provides database configuration with different implementations
15
- # for development and production environments
16
- class DatabaseSection < Taski::Section
17
- # Define the interface that implementations must provide
18
- interfaces :host, :port, :username, :password, :database_name, :pool_size
19
-
20
- # Select implementation based on environment
21
- # Note: Must return a Task class - .run is automatically called
22
- # No 'self' needed - just define as instance method!
23
- def impl
24
- if ENV["RAILS_ENV"] == "production"
25
- Production
26
- else
27
- Development
28
- end
29
- end
30
-
31
- # Production implementation with secure settings
32
- # No exports needed - automatically inherited from interfaces
33
- class Production < Taski::Task
34
- def run
35
- @host = "prod-db.example.com"
36
- @port = 5432
37
- @username = "app_user"
38
- @password = ENV["DB_PASSWORD"] || "secure_password"
39
- @database_name = "myapp_production"
40
- @pool_size = 25
41
- end
42
- end
43
-
44
- # Development implementation with local settings
45
- class Development < Taski::Task
46
- def run
47
- @host = "localhost"
48
- @port = 5432
49
- @username = "dev_user"
50
- @password = "dev_password"
51
- @database_name = "myapp_development"
52
- @pool_size = 5
53
- end
54
- end
55
- end
56
-
57
- # Example 2: API Configuration Section
58
- # This section provides API endpoints and credentials
59
- class ApiSection < Taski::Section
60
- interfaces :base_url, :api_key, :timeout, :retry_count
61
-
62
- # No 'self' needed - just define as instance method!
63
- def impl
64
- # Select based on feature flag
65
- # Note: Must return a Task class - .run is automatically called
66
- if ENV["USE_STAGING_API"] == "true"
67
- Staging
68
- else
69
- Production
70
- end
71
- end
72
-
73
- class Production < Taski::Task
74
- def run
75
- @base_url = "https://api.example.com/v1"
76
- @api_key = ENV["PROD_API_KEY"] || "prod-key-123"
77
- @timeout = 30
78
- @retry_count = 3
79
- end
80
- end
81
-
82
- class Staging < Taski::Task
83
- def run
84
- @base_url = "https://staging-api.example.com/v1"
85
- @api_key = ENV["STAGING_API_KEY"] || "staging-key-456"
86
- @timeout = 60
87
- @retry_count = 1
88
- end
89
- end
90
- end
91
-
92
- # Example 3: Task that depends on multiple sections
93
- class ApplicationSetup < Taski::Task
94
- exports :config_summary
95
-
96
- def run
97
- puts "Setting up application with configuration:"
98
- puts "Database: #{DatabaseSection.host}:#{DatabaseSection.port}/#{DatabaseSection.database_name}"
99
- puts "API: #{ApiSection.base_url}"
100
- puts "Pool size: #{DatabaseSection.pool_size}"
101
- puts "API timeout: #{ApiSection.timeout}s"
102
-
103
- @config_summary = {
104
- database: {
105
- host: DatabaseSection.host,
106
- port: DatabaseSection.port,
107
- database: DatabaseSection.database_name,
108
- pool_size: DatabaseSection.pool_size
109
- },
110
- api: {
111
- base_url: ApiSection.base_url,
112
- timeout: ApiSection.timeout,
113
- retry_count: ApiSection.retry_count
114
- }
115
- }
116
- end
117
- end
118
-
119
- # Example 4: Complex dependency chain with sections
120
- class DatabaseConnection < Taski::Task
121
- exports :connection
122
-
123
- def run
124
- puts "Connecting to database..."
125
- # Use section configuration to create connection
126
- connection_string = "postgresql://#{DatabaseSection.username}:#{DatabaseSection.password}@#{DatabaseSection.host}:#{DatabaseSection.port}/#{DatabaseSection.database_name}"
127
- @connection = "Connected to: #{connection_string} (pool: #{DatabaseSection.pool_size})"
128
- puts @connection
129
- end
130
- end
131
-
132
- class ApiClient < Taski::Task
133
- exports :client
134
-
135
- def run
136
- puts "Initializing API client..."
137
- @client = "API Client: #{ApiSection.base_url} (timeout: #{ApiSection.timeout}s, retries: #{ApiSection.retry_count})"
138
- puts @client
139
- end
140
- end
141
-
142
- class Application < Taski::Task
143
- def run
144
- puts "\n=== Starting Application ==="
145
-
146
- # Dependencies are automatically resolved
147
- # DatabaseConnection and ApiClient will be executed first
148
- # which triggers execution of their respective sections
149
-
150
- puts "\nDatabase ready: #{DatabaseConnection.connection}"
151
- puts "API ready: #{ApiClient.client}"
152
-
153
- puts "\nApplication configuration summary:"
154
- puts ApplicationSetup.config_summary.inspect
155
-
156
- puts "\n=== Application Started Successfully ==="
157
- end
158
- end
159
-
160
- puts "Taski Section Configuration Example"
161
- puts "=" * 50
162
-
163
- puts "\n1. Development Environment (default)"
164
- ENV["RAILS_ENV"] = "development"
165
- ENV["USE_STAGING_API"] = "false"
166
-
167
- # Reset all tasks to ensure fresh build
168
- [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
169
-
170
- Application.run
171
-
172
- puts "\n" + "=" * 50
173
- puts "\n2. Production Environment with Staging API"
174
- ENV["RAILS_ENV"] = "production"
175
- ENV["USE_STAGING_API"] = "true"
176
-
177
- # Reset all tasks to see different configuration
178
- [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
179
-
180
- Application.run
181
-
182
- puts "\n" + "=" * 50
183
- puts "\n3. Dependency Tree Visualization"
184
- puts "\nApplication dependency tree:"
185
- puts Application.tree
186
-
187
- puts "\nDatabaseConnection dependency tree:"
188
- puts DatabaseConnection.tree
189
-
190
- puts "\nApiClient dependency tree:"
191
- puts ApiClient.tree
192
-
193
- puts "\n" + "=" * 50
194
- puts "\nSection dependency resolution successfully demonstrated!"
195
- puts "Notice how sections appear in the dependency trees and logs."
@@ -1,364 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "monitor"
4
-
5
- module Taski
6
- module Execution
7
- # Base class for progress display implementations.
8
- # Provides common task tracking and lifecycle management.
9
- # Subclasses override template methods for custom rendering.
10
- class BaseProgressDisplay
11
- # Shared task progress tracking
12
- class TaskProgress
13
- # Run lifecycle tracking
14
- attr_accessor :run_state, :run_start_time, :run_end_time, :run_error, :run_duration
15
- # Clean lifecycle tracking
16
- attr_accessor :clean_state, :clean_start_time, :clean_end_time, :clean_error, :clean_duration
17
- # Display properties
18
- attr_accessor :is_impl_candidate
19
- # Group tracking
20
- attr_accessor :groups, :current_group_index
21
-
22
- def initialize
23
- # Run lifecycle
24
- @run_state = :pending
25
- @run_start_time = nil
26
- @run_end_time = nil
27
- @run_error = nil
28
- @run_duration = nil
29
- # Clean lifecycle
30
- @clean_state = nil # nil means clean hasn't started
31
- @clean_start_time = nil
32
- @clean_end_time = nil
33
- @clean_error = nil
34
- @clean_duration = nil
35
- # Display
36
- @is_impl_candidate = false
37
- # Groups
38
- @groups = []
39
- @current_group_index = nil
40
- end
41
-
42
- # Returns the most relevant state for display
43
- def state
44
- @clean_state || @run_state
45
- end
46
-
47
- # Legacy accessors for backward compatibility
48
- def start_time
49
- @clean_start_time || @run_start_time
50
- end
51
-
52
- def end_time
53
- @clean_end_time || @run_end_time
54
- end
55
-
56
- def error
57
- @clean_error || @run_error
58
- end
59
-
60
- def duration
61
- @clean_duration || @run_duration
62
- end
63
- end
64
-
65
- # Tracks the progress of a group within a task
66
- class GroupProgress
67
- attr_accessor :name, :state, :start_time, :end_time, :duration, :error, :last_message
68
-
69
- def initialize(name)
70
- @name = name
71
- @state = :pending
72
- @start_time = nil
73
- @end_time = nil
74
- @duration = nil
75
- @error = nil
76
- @last_message = nil
77
- end
78
- end
79
-
80
- def initialize(output: $stdout)
81
- @output = output
82
- @tasks = {}
83
- @monitor = Monitor.new
84
- @nest_level = 0
85
- @root_task_class = nil
86
- @output_capture = nil
87
- @start_time = nil
88
- @message_queue = []
89
- end
90
-
91
- # Set the output capture for getting task output
92
- # @param capture [ThreadOutputCapture] The output capture instance
93
- def set_output_capture(capture)
94
- @monitor.synchronize do
95
- @output_capture = capture
96
- end
97
- end
98
-
99
- # Set the root task to build tree structure
100
- # Only sets root task if not already set (prevents nested executor overwrite)
101
- # @param root_task_class [Class] The root task class
102
- def set_root_task(root_task_class)
103
- @monitor.synchronize do
104
- return if @root_task_class # Don't overwrite existing root task
105
- @root_task_class = root_task_class
106
- on_root_task_set
107
- end
108
- end
109
-
110
- # Register which impl was selected for a section
111
- # @param section_class [Class] The section class
112
- # @param impl_class [Class] The selected implementation class
113
- def register_section_impl(section_class, impl_class)
114
- @monitor.synchronize do
115
- on_section_impl_registered(section_class, impl_class)
116
- end
117
- end
118
-
119
- # @param task_class [Class] The task class to register
120
- def register_task(task_class)
121
- @monitor.synchronize do
122
- return if @tasks.key?(task_class)
123
- @tasks[task_class] = TaskProgress.new
124
- on_task_registered(task_class)
125
- end
126
- end
127
-
128
- # @param task_class [Class] The task class to check
129
- # @return [Boolean] true if the task is registered
130
- def task_registered?(task_class)
131
- @monitor.synchronize do
132
- @tasks.key?(task_class)
133
- end
134
- end
135
-
136
- # @param task_class [Class] The task class to update
137
- # @param state [Symbol] The new state
138
- # @param duration [Float] Duration in milliseconds (for completed tasks)
139
- # @param error [Exception] Error object (for failed tasks)
140
- def update_task(task_class, state:, duration: nil, error: nil)
141
- @monitor.synchronize do
142
- progress = @tasks[task_class]
143
- # Register task if not already registered (for late-registered tasks)
144
- progress ||= @tasks[task_class] = TaskProgress.new
145
-
146
- apply_state_transition(progress, state, duration, error)
147
- on_task_updated(task_class, state, duration, error)
148
- end
149
- end
150
-
151
- # @param task_class [Class] The task class
152
- # @return [Symbol] The task state
153
- def task_state(task_class)
154
- @monitor.synchronize do
155
- @tasks[task_class]&.state
156
- end
157
- end
158
-
159
- # Update group state for a task.
160
- # @param task_class [Class] The task class containing the group
161
- # @param group_name [String] The name of the group
162
- # @param state [Symbol] The new state (:running, :completed, :failed)
163
- # @param duration [Float, nil] Duration in milliseconds (for completed groups)
164
- # @param error [Exception, nil] Error object (for failed groups)
165
- def update_group(task_class, group_name, state:, duration: nil, error: nil)
166
- @monitor.synchronize do
167
- progress = @tasks[task_class]
168
- return unless progress
169
-
170
- apply_group_state_transition(progress, group_name, state, duration, error)
171
- on_group_updated(task_class, group_name, state, duration, error)
172
- end
173
- end
174
-
175
- def start
176
- should_start = false
177
- @monitor.synchronize do
178
- @nest_level += 1
179
- return if @nest_level > 1 # Already running from outer executor
180
- return unless should_activate?
181
-
182
- @start_time = Time.now
183
- should_start = true
184
- end
185
-
186
- return unless should_start
187
-
188
- on_start
189
- end
190
-
191
- def stop
192
- should_stop = false
193
- @monitor.synchronize do
194
- @nest_level -= 1 if @nest_level > 0
195
- return unless @nest_level == 0
196
-
197
- should_stop = true
198
- end
199
-
200
- return unless should_stop
201
-
202
- on_stop
203
- flush_queued_messages
204
- end
205
-
206
- # Queue a message to be displayed after progress display stops.
207
- # Thread-safe for concurrent task execution.
208
- # @param text [String] The message text to queue
209
- def queue_message(text)
210
- @monitor.synchronize { @message_queue << text }
211
- end
212
-
213
- protected
214
-
215
- # Template methods - override in subclasses
216
-
217
- # Called when root task is set. Override to build tree structure.
218
- def on_root_task_set
219
- # Default: no-op
220
- end
221
-
222
- # Called when a section impl is registered.
223
- def on_section_impl_registered(section_class, impl_class)
224
- # Default: no-op
225
- end
226
-
227
- # Called when a task is registered.
228
- def on_task_registered(task_class)
229
- # Default: no-op
230
- end
231
-
232
- # Called when a task state is updated.
233
- def on_task_updated(task_class, state, duration, error)
234
- # Default: no-op
235
- end
236
-
237
- # Called when a group state is updated.
238
- def on_group_updated(task_class, group_name, state, duration, error)
239
- # Default: no-op
240
- end
241
-
242
- # Called to determine if display should activate.
243
- # @return [Boolean] true if display should start
244
- def should_activate?
245
- true
246
- end
247
-
248
- # Called when display starts.
249
- def on_start
250
- # Default: no-op
251
- end
252
-
253
- # Called when display stops.
254
- def on_stop
255
- # Default: no-op
256
- end
257
-
258
- # Shared tree traversal for subclasses
259
-
260
- # Register all tasks from a tree structure recursively
261
- def register_tasks_from_tree(node)
262
- return unless node
263
-
264
- task_class = node[:task_class]
265
- @tasks[task_class] ||= TaskProgress.new
266
- @tasks[task_class].is_impl_candidate = true if node[:is_impl_candidate]
267
-
268
- node[:children].each { |child| register_tasks_from_tree(child) }
269
- end
270
-
271
- # Utility methods for subclasses
272
-
273
- # Get short name of a task class
274
- def short_name(task_class)
275
- return "Unknown" unless task_class
276
- task_class.name&.split("::")&.last || task_class.to_s
277
- end
278
-
279
- # Format duration for display
280
- def format_duration(ms)
281
- if ms >= 1000
282
- "#{(ms / 1000.0).round(1)}s"
283
- else
284
- "#{ms.round(1)}ms"
285
- end
286
- end
287
-
288
- # Check if output is a TTY
289
- def tty?
290
- @output.tty?
291
- end
292
-
293
- private
294
-
295
- # Flush all queued messages to output.
296
- # Called when progress display stops.
297
- def flush_queued_messages
298
- messages = @monitor.synchronize { @message_queue.dup.tap { @message_queue.clear } }
299
- messages.each { |msg| @output.puts(msg) }
300
- end
301
-
302
- # Apply state transition to TaskProgress
303
- def apply_state_transition(progress, state, duration, error)
304
- case state
305
- # Run lifecycle states
306
- when :pending
307
- progress.run_state = :pending
308
- when :running
309
- progress.run_state = :running
310
- progress.run_start_time = Time.now
311
- when :completed
312
- progress.run_state = :completed
313
- progress.run_end_time = Time.now
314
- progress.run_duration = duration if duration
315
- when :failed
316
- progress.run_state = :failed
317
- progress.run_end_time = Time.now
318
- progress.run_error = error if error
319
- # Clean lifecycle states
320
- when :cleaning
321
- progress.clean_state = :cleaning
322
- progress.clean_start_time = Time.now
323
- when :clean_completed
324
- progress.clean_state = :clean_completed
325
- progress.clean_end_time = Time.now
326
- progress.clean_duration = duration if duration
327
- when :clean_failed
328
- progress.clean_state = :clean_failed
329
- progress.clean_end_time = Time.now
330
- progress.clean_error = error if error
331
- end
332
- end
333
-
334
- # Apply state transition to GroupProgress
335
- def apply_group_state_transition(progress, group_name, state, duration, error)
336
- case state
337
- when :running
338
- group = GroupProgress.new(group_name)
339
- group.state = :running
340
- group.start_time = Time.now
341
- progress.groups << group
342
- progress.current_group_index = progress.groups.size - 1
343
- when :completed
344
- group = progress.groups.find { |g| g.name == group_name && g.state == :running }
345
- if group
346
- group.state = :completed
347
- group.end_time = Time.now
348
- group.duration = duration
349
- end
350
- progress.current_group_index = nil
351
- when :failed
352
- group = progress.groups.find { |g| g.name == group_name && g.state == :running }
353
- if group
354
- group.state = :failed
355
- group.end_time = Time.now
356
- group.duration = duration
357
- group.error = error
358
- end
359
- progress.current_group_index = nil
360
- end
361
- end
362
- end
363
- end
364
- end