taski 0.8.1 → 0.8.3

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.
@@ -35,6 +35,7 @@ module Taski
35
35
  @renderer_thread = nil
36
36
  @running = false
37
37
  @section_candidates = {} # section_class => [candidate_classes]
38
+ @section_candidate_subtrees = {} # section_class => { candidate_class => subtree_node }
38
39
  end
39
40
 
40
41
  protected
@@ -49,14 +50,10 @@ module Taski
49
50
  @tasks[impl_class] ||= TaskProgress.new
50
51
  @tasks[impl_class].is_impl_candidate = false
51
52
 
52
- # Mark unselected candidates as completed (skipped)
53
- candidates = @section_candidates[section_class] || []
54
- candidates.each do |candidate|
55
- next if candidate == impl_class
56
- progress = @tasks[candidate]
57
- next unless progress
58
- progress.run_state = :completed
59
- end
53
+ # Mark the section itself as completed (it's represented by its impl)
54
+ @tasks[section_class]&.run_state = :completed
55
+
56
+ mark_unselected_candidates_completed(section_class, impl_class)
60
57
  end
61
58
 
62
59
  # Template method: Determine if display should activate
@@ -101,17 +98,50 @@ module Taski
101
98
 
102
99
  task_class = node[:task_class]
103
100
 
104
- # If this is a section, collect its implementation candidates
101
+ # If this is a section, collect its implementation candidates and their subtrees
105
102
  if node[:is_section]
106
- candidates = node[:children]
107
- .select { |c| c[:is_impl_candidate] }
108
- .map { |c| c[:task_class] }
103
+ candidate_nodes = node[:children].select { |c| c[:is_impl_candidate] }
104
+ candidates = candidate_nodes.map { |c| c[:task_class] }
109
105
  @section_candidates[task_class] = candidates unless candidates.empty?
106
+
107
+ # Store subtrees for each candidate to mark descendants as completed when not selected
108
+ subtrees = {}
109
+ candidate_nodes.each { |c| subtrees[c[:task_class]] = c }
110
+ @section_candidate_subtrees[task_class] = subtrees unless subtrees.empty?
110
111
  end
111
112
 
112
113
  node[:children].each { |child| collect_section_candidates(child) }
113
114
  end
114
115
 
116
+ # Mark unselected candidates and their exclusive subtrees as completed (skipped)
117
+ def mark_unselected_candidates_completed(section_class, impl_class)
118
+ selected_deps = collect_all_dependencies(impl_class)
119
+ candidates = @section_candidates[section_class] || []
120
+ subtrees = @section_candidate_subtrees[section_class] || {}
121
+
122
+ candidates.each do |candidate|
123
+ next if candidate == impl_class
124
+ mark_subtree_completed(subtrees[candidate], exclude: selected_deps)
125
+ end
126
+ end
127
+
128
+ # Recursively mark all pending tasks in a subtree as completed (skipped)
129
+ # Only marks :pending tasks to avoid overwriting :running or :completed states
130
+ # @param node [Hash] The subtree node
131
+ # @param exclude [Set<Class>] Set of task classes to exclude (dependencies of selected impl)
132
+ def mark_subtree_completed(node, exclude: Set.new)
133
+ return unless node
134
+
135
+ task_class = node[:task_class]
136
+ mark_task_as_skipped(task_class) unless exclude.include?(task_class)
137
+ node[:children].each { |child| mark_subtree_completed(child, exclude: exclude) }
138
+ end
139
+
140
+ def mark_task_as_skipped(task_class)
141
+ progress = @tasks[task_class]
142
+ progress.run_state = :completed if progress&.run_state == :pending
143
+ end
144
+
115
145
  def render_live
116
146
  @monitor.synchronize do
117
147
  @spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
@@ -148,36 +178,53 @@ module Taski
148
178
  def build_status_line
149
179
  running_tasks = @tasks.select { |_, p| p.run_state == :running }
150
180
  cleaning_tasks = @tasks.select { |_, p| p.clean_state == :cleaning }
151
- completed = @tasks.values.count { |p| p.run_state == :completed }
152
- failed = @tasks.values.count { |p| p.run_state == :failed }
153
- total = @tasks.size
181
+ pending_tasks = @tasks.select { |_, p| p.run_state == :pending }
182
+ failed_count = @tasks.values.count { |p| p.run_state == :failed }
183
+ done_count = @tasks.values.count { |p| p.run_state == :completed || p.run_state == :failed }
184
+
185
+ status_icon = determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
186
+ task_names = format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
154
187
 
155
- spinner = SPINNER_FRAMES[@spinner_index]
156
- status_icon = if failed > 0
188
+ primary_task = running_tasks.keys.first || cleaning_tasks.keys.first
189
+ output_suffix = build_output_suffix(primary_task)
190
+
191
+ build_status_parts(status_icon, done_count, @tasks.size, task_names, output_suffix)
192
+ end
193
+
194
+ def determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
195
+ # Show success only when no failed, running, cleaning, or pending tasks
196
+ # This prevents showing checkmark briefly when mark_subtree_completed marks tasks
197
+ if failed_count > 0
157
198
  "#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]}"
158
- elsif running_tasks.any? || cleaning_tasks.any?
199
+ elsif running_tasks.any? || cleaning_tasks.any? || pending_tasks.any?
200
+ spinner = SPINNER_FRAMES[@spinner_index]
159
201
  "#{COLORS[:yellow]}#{spinner}#{COLORS[:reset]}"
160
202
  else
161
203
  "#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]}"
162
204
  end
205
+ end
163
206
 
164
- # Get current task names
207
+ def format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
208
+ # Prioritize: cleaning > running > pending
165
209
  current_tasks = if cleaning_tasks.any?
166
- cleaning_tasks.keys.map { |t| short_name(t) }
210
+ cleaning_tasks.keys
211
+ elsif running_tasks.any?
212
+ running_tasks.keys
213
+ elsif pending_tasks.any?
214
+ pending_tasks.keys.first(3)
167
215
  else
168
- running_tasks.keys.map { |t| short_name(t) }
216
+ []
169
217
  end
170
218
 
171
- task_names = current_tasks.first(3).join(", ")
172
- task_names += "..." if current_tasks.size > 3
173
-
174
- # Get last output message if available
175
- output_suffix = build_output_suffix(running_tasks.keys.first || cleaning_tasks.keys.first)
219
+ names = current_tasks.first(3).map { |t| short_name(t) }.join(", ")
220
+ names += "..." if current_tasks.size > 3
221
+ names
222
+ end
176
223
 
177
- parts = ["#{status_icon} [#{completed}/#{total}]"]
178
- parts << task_names if task_names && !task_names.empty?
224
+ def build_status_parts(status_icon, done_count, total, task_names, output_suffix)
225
+ parts = ["#{status_icon} [#{done_count}/#{total}]"]
226
+ parts << task_names unless task_names.empty?
179
227
  parts << "|" << output_suffix if output_suffix
180
-
181
228
  parts.join(" ")
182
229
  end
183
230
 
@@ -22,9 +22,10 @@ module Taski
22
22
  READ_BUFFER_SIZE = 4096
23
23
  MAX_RECENT_LINES = 30 # Maximum number of recent lines to keep per task
24
24
 
25
- def initialize(original_stdout)
25
+ def initialize(original_stdout, execution_context = nil)
26
26
  super()
27
27
  @original = original_stdout
28
+ @execution_context = execution_context
28
29
  @pipes = {} # task_class => TaskOutputPipe
29
30
  @thread_map = {} # Thread => task_class
30
31
  @recent_lines = {} # task_class => Array<String>
data/lib/taski/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.8.1"
4
+ VERSION = "0.8.3"
5
5
  end
data/lib/taski.rb CHANGED
@@ -162,9 +162,9 @@ module Taski
162
162
  # @param text [String] The message text to display
163
163
  def self.message(text)
164
164
  @message_monitor.synchronize do
165
- context = Execution::ExecutionContext.current
166
- if context&.output_capture_active?
167
- context.queue_message(text)
165
+ progress = progress_display
166
+ if progress&.respond_to?(:queue_message)
167
+ progress.queue_message(text)
168
168
  else
169
169
  $stdout.puts(text)
170
170
  end
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.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa
@@ -59,18 +59,12 @@ files:
59
59
  - examples/README.md
60
60
  - examples/args_demo.rb
61
61
  - examples/clean_demo.rb
62
- - examples/data_pipeline_demo.rb
63
62
  - examples/group_demo.rb
64
- - examples/large_tree_demo.rb
65
63
  - examples/message_demo.rb
66
- - examples/nested_section_demo.rb
67
- - examples/parallel_progress_demo.rb
64
+ - examples/progress_demo.rb
68
65
  - examples/quick_start.rb
69
66
  - examples/reexecution_demo.rb
70
67
  - examples/section_demo.rb
71
- - examples/simple_progress_demo.rb
72
- - examples/system_call_demo.rb
73
- - examples/tree_progress_demo.rb
74
68
  - lib/taski.rb
75
69
  - lib/taski/args.rb
76
70
  - lib/taski/env.rb
@@ -1,231 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Taski Data Pipeline Example
5
- #
6
- # This example demonstrates a realistic data processing pipeline:
7
- # - Section API for switching data sources (production vs test)
8
- # - Multiple data sources fetched in parallel
9
- # - Data transformation and aggregation
10
- #
11
- # Run: ruby examples/data_pipeline_demo.rb
12
- # Disable progress: TASKI_PROGRESS_DISABLE=1 ruby examples/data_pipeline_demo.rb
13
-
14
- require_relative "../lib/taski"
15
-
16
- # Section: Data source abstraction
17
- # Switch between production API and test fixtures
18
- class DataSourceSection < Taski::Section
19
- interfaces :users, :sales, :activities
20
-
21
- def impl
22
- (ENV["USE_TEST_DATA"] == "true") ? TestData : ProductionData
23
- end
24
-
25
- # Production: Fetch from APIs (simulated with delays)
26
- class ProductionData < Taski::Task
27
- def run
28
- puts " [ProductionData] Fetching from APIs..."
29
- sleep(0.3)
30
-
31
- @users = [
32
- {id: 1, name: "Alice", department: "Engineering"},
33
- {id: 2, name: "Bob", department: "Sales"},
34
- {id: 3, name: "Charlie", department: "Engineering"},
35
- {id: 4, name: "Diana", department: "Marketing"}
36
- ]
37
-
38
- @sales = [
39
- {user_id: 2, amount: 1000, date: "2024-01"},
40
- {user_id: 2, amount: 1500, date: "2024-02"},
41
- {user_id: 4, amount: 800, date: "2024-01"}
42
- ]
43
-
44
- @activities = [
45
- {user_id: 1, action: :commit, count: 45},
46
- {user_id: 3, action: :commit, count: 32},
47
- {user_id: 1, action: :review, count: 12},
48
- {user_id: 3, action: :review, count: 8}
49
- ]
50
-
51
- puts " [ProductionData] Loaded #{@users.size} users, #{@sales.size} sales, #{@activities.size} activities"
52
- end
53
- end
54
-
55
- # Test: Minimal fixture data (no delays)
56
- class TestData < Taski::Task
57
- def run
58
- puts " [TestData] Loading test fixtures..."
59
-
60
- @users = [
61
- {id: 1, name: "Test User", department: "Test Dept"}
62
- ]
63
-
64
- @sales = [
65
- {user_id: 1, amount: 100, date: "2024-01"}
66
- ]
67
-
68
- @activities = [
69
- {user_id: 1, action: :commit, count: 10}
70
- ]
71
-
72
- puts " [TestData] Loaded minimal test data"
73
- end
74
- end
75
- end
76
-
77
- # Section: Report format selection
78
- class ReportFormatSection < Taski::Section
79
- interfaces :format_report
80
-
81
- def impl
82
- (ENV["REPORT_FORMAT"] == "json") ? JsonFormat : TextFormat
83
- end
84
-
85
- class TextFormat < Taski::Task
86
- def run
87
- @format_report = ->(report) {
88
- report.map do |dept, stats|
89
- "#{dept}: #{stats[:user_count]} users, $#{stats[:total_sales]} sales"
90
- end.join("\n")
91
- }
92
- end
93
- end
94
-
95
- class JsonFormat < Taski::Task
96
- def run
97
- require "json"
98
- @format_report = ->(report) { JSON.pretty_generate(report) }
99
- end
100
- end
101
- end
102
-
103
- # Transform: Enrich users with sales data
104
- class EnrichWithSales < Taski::Task
105
- exports :users_with_sales
106
-
107
- def run
108
- users = DataSourceSection.users
109
- sales = DataSourceSection.sales
110
-
111
- sales_by_user = sales.group_by { |s| s[:user_id] }
112
- .transform_values { |records| records.sum { |r| r[:amount] } }
113
-
114
- @users_with_sales = users.map do |user|
115
- user.merge(total_sales: sales_by_user[user[:id]] || 0)
116
- end
117
-
118
- puts " [EnrichWithSales] Enriched #{@users_with_sales.size} users"
119
- end
120
- end
121
-
122
- # Transform: Enrich users with activity data
123
- class EnrichWithActivities < Taski::Task
124
- exports :users_with_activities
125
-
126
- def run
127
- users = DataSourceSection.users
128
- activities = DataSourceSection.activities
129
-
130
- activities_by_user = activities.group_by { |a| a[:user_id] }
131
- .transform_values do |records|
132
- records.to_h { |r| [r[:action], r[:count]] }
133
- end
134
-
135
- @users_with_activities = users.map do |user|
136
- user.merge(activities: activities_by_user[user[:id]] || {})
137
- end
138
-
139
- puts " [EnrichWithActivities] Enriched #{@users_with_activities.size} users"
140
- end
141
- end
142
-
143
- # Aggregate: Combine enrichments into profiles
144
- class BuildProfiles < Taski::Task
145
- exports :profiles
146
-
147
- def run
148
- users_sales = EnrichWithSales.users_with_sales
149
- users_activities = EnrichWithActivities.users_with_activities
150
-
151
- activities_map = users_activities.to_h { |u| [u[:id], u[:activities]] }
152
-
153
- @profiles = users_sales.map do |user|
154
- user.merge(activities: activities_map[user[:id]] || {})
155
- end
156
-
157
- puts " [BuildProfiles] Built #{@profiles.size} profiles"
158
- end
159
- end
160
-
161
- # Output: Generate department report
162
- class GenerateReport < Taski::Task
163
- exports :report, :formatted_output
164
-
165
- def run
166
- profiles = BuildProfiles.profiles
167
- formatter = ReportFormatSection.format_report
168
-
169
- by_department = profiles.group_by { |p| p[:department] }
170
-
171
- @report = by_department.transform_values do |dept_users|
172
- {
173
- user_count: dept_users.size,
174
- total_sales: dept_users.sum { |u| u[:total_sales] },
175
- total_commits: dept_users.sum { |u| u[:activities][:commit] || 0 },
176
- total_reviews: dept_users.sum { |u| u[:activities][:review] || 0 }
177
- }
178
- end
179
-
180
- @formatted_output = formatter.call(@report)
181
- puts " [GenerateReport] Generated report for #{@report.size} departments"
182
- end
183
- end
184
-
185
- # Demo execution
186
- puts "Taski Data Pipeline Demo"
187
- puts "=" * 50
188
-
189
- puts "\n1. Dependency Tree"
190
- puts "-" * 50
191
- puts GenerateReport.tree
192
-
193
- puts "\n2. Production Data (default)"
194
- puts "-" * 50
195
- ENV["USE_TEST_DATA"] = "false"
196
- ENV["REPORT_FORMAT"] = "text"
197
-
198
- start_time = Time.now
199
- GenerateReport.run
200
- elapsed = Time.now - start_time
201
-
202
- puts "\nReport (text format):"
203
- puts GenerateReport.formatted_output
204
- puts "\nCompleted in #{elapsed.round(3)}s"
205
-
206
- puts "\n" + "=" * 50
207
- puts "\n3. Test Data with JSON Format"
208
- puts "-" * 50
209
- ENV["USE_TEST_DATA"] = "true"
210
- ENV["REPORT_FORMAT"] = "json"
211
-
212
- # Reset all tasks for fresh execution
213
- [DataSourceSection, ReportFormatSection, EnrichWithSales,
214
- EnrichWithActivities, BuildProfiles, GenerateReport].each(&:reset!)
215
-
216
- start_time = Time.now
217
- GenerateReport.run
218
- elapsed = Time.now - start_time
219
-
220
- puts "\nReport (JSON format):"
221
- puts GenerateReport.formatted_output
222
- puts "\nCompleted in #{elapsed.round(3)}s"
223
-
224
- puts "\n" + "=" * 50
225
- puts "Pipeline demonstration complete!"
226
- puts "\nKey concepts demonstrated:"
227
- puts " - DataSourceSection: Switch between production/test data"
228
- puts " - ReportFormatSection: Switch output format (text/JSON)"
229
- puts " - Parallel execution of independent transforms"
230
- puts "\nTo disable progress display:"
231
- puts " TASKI_PROGRESS_DISABLE=1 ruby examples/data_pipeline_demo.rb"