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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +42 -1
- data/docs/GUIDE.md +18 -4
- data/examples/README.md +33 -45
- data/examples/progress_demo.rb +154 -0
- data/lib/taski/execution/base_progress_display.rb +45 -0
- data/lib/taski/execution/execution_context.rb +2 -21
- data/lib/taski/execution/executor.rb +0 -8
- data/lib/taski/execution/simple_progress_display.rb +76 -29
- data/lib/taski/execution/task_output_router.rb +2 -1
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +3 -3
- metadata +2 -8
- data/examples/data_pipeline_demo.rb +0 -231
- data/examples/large_tree_demo.rb +0 -519
- data/examples/nested_section_demo.rb +0 -161
- data/examples/parallel_progress_demo.rb +0 -72
- data/examples/simple_progress_demo.rb +0 -80
- data/examples/system_call_demo.rb +0 -56
- data/examples/tree_progress_demo.rb +0 -164
|
@@ -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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
216
|
+
[]
|
|
169
217
|
end
|
|
170
218
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
parts
|
|
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
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
|
-
|
|
166
|
-
if
|
|
167
|
-
|
|
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.
|
|
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/
|
|
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"
|