tap 0.7.9
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.
- data/MIT-LICENSE +21 -0
- data/README +71 -0
- data/Rakefile +117 -0
- data/bin/tap +63 -0
- data/lib/tap.rb +15 -0
- data/lib/tap/app.rb +739 -0
- data/lib/tap/file_task.rb +354 -0
- data/lib/tap/generator.rb +29 -0
- data/lib/tap/generator/generators/config/USAGE +0 -0
- data/lib/tap/generator/generators/config/config_generator.rb +23 -0
- data/lib/tap/generator/generators/config/templates/config.erb +2 -0
- data/lib/tap/generator/generators/file_task/USAGE +0 -0
- data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
- data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
- data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
- data/lib/tap/generator/generators/root/USAGE +0 -0
- data/lib/tap/generator/generators/root/root_generator.rb +36 -0
- data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
- data/lib/tap/generator/generators/root/templates/app.yml +19 -0
- data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
- data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
- data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
- data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
- data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
- data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
- data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
- data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
- data/lib/tap/generator/generators/task/USAGE +0 -0
- data/lib/tap/generator/generators/task/task_generator.rb +21 -0
- data/lib/tap/generator/generators/task/templates/task.erb +21 -0
- data/lib/tap/generator/generators/task/templates/test.erb +29 -0
- data/lib/tap/generator/generators/workflow/USAGE +0 -0
- data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
- data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
- data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
- data/lib/tap/generator/options.rb +26 -0
- data/lib/tap/generator/usage.rb +26 -0
- data/lib/tap/root.rb +275 -0
- data/lib/tap/script/console.rb +7 -0
- data/lib/tap/script/destroy.rb +8 -0
- data/lib/tap/script/generate.rb +8 -0
- data/lib/tap/script/run.rb +111 -0
- data/lib/tap/script/server.rb +12 -0
- data/lib/tap/support/audit.rb +415 -0
- data/lib/tap/support/batch_queue.rb +165 -0
- data/lib/tap/support/combinator.rb +114 -0
- data/lib/tap/support/logger.rb +91 -0
- data/lib/tap/support/rap.rb +38 -0
- data/lib/tap/support/run_error.rb +20 -0
- data/lib/tap/support/template.rb +81 -0
- data/lib/tap/support/templater.rb +155 -0
- data/lib/tap/support/versions.rb +63 -0
- data/lib/tap/task.rb +448 -0
- data/lib/tap/test.rb +320 -0
- data/lib/tap/test/env_vars.rb +16 -0
- data/lib/tap/test/inference_methods.rb +298 -0
- data/lib/tap/test/subset_methods.rb +260 -0
- data/lib/tap/version.rb +3 -0
- data/lib/tap/workflow.rb +73 -0
- data/test/app/config/addition_template.yml +6 -0
- data/test/app/config/batch.yml +2 -0
- data/test/app/config/empty.yml +0 -0
- data/test/app/config/erb.yml +1 -0
- data/test/app/config/template.yml +6 -0
- data/test/app/config/version-0.1.yml +1 -0
- data/test/app/config/version.yml +1 -0
- data/test/app/lib/app_test_task.rb +2 -0
- data/test/app_class_test.rb +33 -0
- data/test/app_test.rb +1372 -0
- data/test/file_task/config/batch.yml +2 -0
- data/test/file_task/config/configured.yml +1 -0
- data/test/file_task/old_file_one.txt +0 -0
- data/test/file_task/old_file_two.txt +0 -0
- data/test/file_task_test.rb +1041 -0
- data/test/root/alt_lib/alt_module.rb +4 -0
- data/test/root/lib/absolute_alt_filepath.rb +2 -0
- data/test/root/lib/alternative_filepath.rb +2 -0
- data/test/root/lib/another_module.rb +2 -0
- data/test/root/lib/nested/some_module.rb +4 -0
- data/test/root/lib/no_module_included.rb +0 -0
- data/test/root/lib/some/module.rb +4 -0
- data/test/root/lib/some_class.rb +2 -0
- data/test/root/lib/some_module.rb +3 -0
- data/test/root/load_path/load_path_module.rb +2 -0
- data/test/root/load_path/skip_module.rb +2 -0
- data/test/root/mtime/older.txt +0 -0
- data/test/root/unload/full_path.rb +2 -0
- data/test/root/unload/loaded_by_nested.rb +2 -0
- data/test/root/unload/nested/nested_load.rb +6 -0
- data/test/root/unload/nested/nested_with_ext.rb +4 -0
- data/test/root/unload/nested/relative_path.rb +4 -0
- data/test/root/unload/older.rb +2 -0
- data/test/root/unload/unload_base.rb +9 -0
- data/test/root/versions/another.yml +0 -0
- data/test/root/versions/file-0.1.2.yml +0 -0
- data/test/root/versions/file-0.1.yml +0 -0
- data/test/root/versions/file.yml +0 -0
- data/test/root_test.rb +483 -0
- data/test/support/audit_test.rb +449 -0
- data/test/support/batch_queue_test.rb +320 -0
- data/test/support/combinator_test.rb +249 -0
- data/test/support/logger_test.rb +31 -0
- data/test/support/template_test.rb +122 -0
- data/test/support/templater/erb.txt +2 -0
- data/test/support/templater/erb.yml +2 -0
- data/test/support/templater/somefile.txt +2 -0
- data/test/support/templater_test.rb +192 -0
- data/test/support/versions_test.rb +71 -0
- data/test/tap_test_helper.rb +4 -0
- data/test/tap_test_suite.rb +4 -0
- data/test/task/config/batch.yml +2 -0
- data/test/task/config/batched.yml +2 -0
- data/test/task/config/configured.yml +1 -0
- data/test/task/config/example.yml +1 -0
- data/test/task/config/overriding.yml +2 -0
- data/test/task/config/task_with_config.yml +1 -0
- data/test/task/config/template.yml +4 -0
- data/test/task_class_test.rb +118 -0
- data/test/task_execute_test.rb +233 -0
- data/test/task_test.rb +424 -0
- data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
- data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
- data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
- data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
- data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
- data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
- data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
- data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
- data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
- data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
- data/test/test/inference_methods_test.rb +311 -0
- data/test/test/subset_methods_test.rb +115 -0
- data/test/test_test.rb +233 -0
- data/test/workflow_test.rb +108 -0
- metadata +274 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require 'tap/support/template'
|
|
2
|
+
|
|
3
|
+
module Tap
|
|
4
|
+
module Support
|
|
5
|
+
|
|
6
|
+
# Templater generates permutations of a hash, treating it as a template for other
|
|
7
|
+
# hashes. Templater looks for various flags on the end of keys to signify operations
|
|
8
|
+
# like method execution ('!') and replacement ('=').
|
|
9
|
+
#
|
|
10
|
+
# module TemplateMethods
|
|
11
|
+
# def method_call(input)
|
|
12
|
+
# self["result"] = "#{input} was processed"
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# t = Templater.new(
|
|
17
|
+
# "key"=> "value",
|
|
18
|
+
# "method_call!" => "method input",
|
|
19
|
+
# "replacement=" => "replacement input")
|
|
20
|
+
# t.run_methods(TemplateMethods)
|
|
21
|
+
# t.run_replace do |input|
|
|
22
|
+
# "#{input} was replaced"
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# t.templates
|
|
26
|
+
# # => [{"key" => "value",
|
|
27
|
+
# "result" => "method input was processed",
|
|
28
|
+
# "replacement" => "replacement input was replaced"}]
|
|
29
|
+
#
|
|
30
|
+
# == Creating Template Methods
|
|
31
|
+
#
|
|
32
|
+
# The default Template methods may not be sufficient for your needs, but making new
|
|
33
|
+
# methods (as above) is easy. Some simple rules apply:
|
|
34
|
+
#
|
|
35
|
+
# - Any method can be called from run_methods provided it has a single input.
|
|
36
|
+
# - Modules with the methods will extend the hash templates so 'self' indicates the
|
|
37
|
+
# template itself.
|
|
38
|
+
# - New templates should be added by pushing the template to the templater, which
|
|
39
|
+
# will already be set by the time the method executes
|
|
40
|
+
# (ex: self.templater << new_template).
|
|
41
|
+
#
|
|
42
|
+
# Also, it's important to note that ANY empty templates at the end of run_methods
|
|
43
|
+
# are cleared from the templates array. Thus, if you want to remove a template from
|
|
44
|
+
# within a method, simply clear self.
|
|
45
|
+
#
|
|
46
|
+
# See the Template code for examples.
|
|
47
|
+
class Templater
|
|
48
|
+
|
|
49
|
+
# Convenience method for making a new Templater, running methods
|
|
50
|
+
# with the input modules, and making replacements using the input
|
|
51
|
+
# block. Returns the resulting templates.
|
|
52
|
+
def self.make_templates(hash, *modules, &block)
|
|
53
|
+
t = Templater.new(hash)
|
|
54
|
+
t.run_methods(*modules)
|
|
55
|
+
t.run_replace(&block)
|
|
56
|
+
t.templates
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :templates
|
|
60
|
+
|
|
61
|
+
# Creates a new Templater with the input hash as the initial template.
|
|
62
|
+
def initialize(hash)
|
|
63
|
+
@templates = []
|
|
64
|
+
self << hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Shortcut to add a template to the templater, unless the template is empty.
|
|
68
|
+
def <<(template)
|
|
69
|
+
self.templates << template unless template.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Iterates through the pairs in each template looking for keys matching the
|
|
73
|
+
# regexp (using =~). When a matching key is found, it is removed from the
|
|
74
|
+
# template and then the template, the key, and the corresponding value are
|
|
75
|
+
# passed to the block for further processing. Iteration restarts after each
|
|
76
|
+
# match and empty templates are removed upon completion. All templates are
|
|
77
|
+
# extended with Template before iteration.
|
|
78
|
+
#
|
|
79
|
+
# Notes:
|
|
80
|
+
# - run_templater is the foundation of the other run_* methods and may be used
|
|
81
|
+
# for custom template processing.
|
|
82
|
+
# - since =~ is used for comparison, only String keys will be selected
|
|
83
|
+
# under normal circumstances.
|
|
84
|
+
def run_templater(regexp) # :yields: template, key, value
|
|
85
|
+
templates.each do |template|
|
|
86
|
+
unless template.respond_to?(:templater)
|
|
87
|
+
template.extend Template
|
|
88
|
+
template.templater = self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
template.each_pair do |key, value|
|
|
92
|
+
next unless key =~ regexp
|
|
93
|
+
|
|
94
|
+
# remove the key
|
|
95
|
+
template.delete(key)
|
|
96
|
+
yield(template, key, value)
|
|
97
|
+
|
|
98
|
+
# restart the loop as key/value pairs may have changed
|
|
99
|
+
retry
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
templates.delete_if {|t| t.empty?}
|
|
104
|
+
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Iterates through the pairs in each template looking for and executing method calls
|
|
109
|
+
# as per run_templater. Method calls are indicated by an exclamation attached to the
|
|
110
|
+
# key, like 'do_something!'.
|
|
111
|
+
#
|
|
112
|
+
# Templates are extended with the input modules before the method call. The modules
|
|
113
|
+
# should provide the methods (an error will be raised if the specified method is not
|
|
114
|
+
# defined). Note, templates will by default be extended with the Template module.
|
|
115
|
+
#
|
|
116
|
+
# module MethodModule
|
|
117
|
+
# def method_call(input)
|
|
118
|
+
# self["result"] = "#{input} was processed"
|
|
119
|
+
# end
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# t = Templater.new('method_call!' => 'input')
|
|
123
|
+
# t.run_methods(MethodModule) # calls 'do_something' with the input 'some value',
|
|
124
|
+
# # after removing the key-value pair
|
|
125
|
+
# t.templates # => [{"result" => "input was processed"}]
|
|
126
|
+
#
|
|
127
|
+
# Notes:
|
|
128
|
+
# - The method called is the method minus the exclamation, so 'do_something!' calls
|
|
129
|
+
# 'do_something' within the template and 'do_something!!' calls 'do_something!'
|
|
130
|
+
# - Iteration restarts after each method call; inserted method calls will be executed
|
|
131
|
+
def run_methods(*modules)
|
|
132
|
+
run_templater(/!$/) do |template, key, value|
|
|
133
|
+
modules.each {|mod| template.extend mod}
|
|
134
|
+
template.send(key.chop, value)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Iterates through the pairs in each template looking for replacement keys and
|
|
139
|
+
# executing the block to make the appropriate replacement value, as per
|
|
140
|
+
# run_templater. Replacement keys are indicated by an equals sign attached to
|
|
141
|
+
# the key, like 'key='. The equals sign will be chopped off for these keys, and
|
|
142
|
+
# the value replaced with the return value of the block.
|
|
143
|
+
#
|
|
144
|
+
# t = Templater.new('replacement=' => 'some value')
|
|
145
|
+
# t.run_replace {|value| "#{value} was replaced"}
|
|
146
|
+
# t.templates # => [{"replacement" => "some value was replaced"}]
|
|
147
|
+
#
|
|
148
|
+
def run_replace # :yields: value
|
|
149
|
+
run_templater(/=$/) do |template, key, value|
|
|
150
|
+
template[key.chop] = yield(value)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Tap
|
|
2
|
+
module Support
|
|
3
|
+
|
|
4
|
+
# Version provides methods for adding, removing, and incrementing versions
|
|
5
|
+
# at the end of filepaths. Versions are all formatted like:
|
|
6
|
+
# 'filepath-version.extension'.
|
|
7
|
+
#
|
|
8
|
+
module Versions
|
|
9
|
+
|
|
10
|
+
# Adds a version to the filepath. Versioned filepaths follow the format:
|
|
11
|
+
# 'path-version.extension'. If no version is specified, then the filepath
|
|
12
|
+
# is returned.
|
|
13
|
+
#
|
|
14
|
+
# version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
|
|
15
|
+
#
|
|
16
|
+
def version(path, version)
|
|
17
|
+
version = version.to_s.strip
|
|
18
|
+
if version.empty?
|
|
19
|
+
path
|
|
20
|
+
else
|
|
21
|
+
extname = File.extname(path)
|
|
22
|
+
path.chomp(extname) + '-' + version + extname
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Increments the version of the filepath by the specified increment.
|
|
27
|
+
#
|
|
28
|
+
# increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
|
|
29
|
+
# increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
|
|
30
|
+
#
|
|
31
|
+
def increment(path, increment)
|
|
32
|
+
path, version = deversion(path)
|
|
33
|
+
|
|
34
|
+
# split the version and increment into integer arrays of equal length
|
|
35
|
+
increment, version = [increment, version].collect do |vstr|
|
|
36
|
+
begin
|
|
37
|
+
vstr.to_s.split(/\./).collect {|v| v.to_i}
|
|
38
|
+
rescue
|
|
39
|
+
raise "Bad version or increment: #{vstr}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
version.concat Array.new(increment.length - version.length, 0) if increment.length > version.length
|
|
43
|
+
|
|
44
|
+
# add the increment to version
|
|
45
|
+
0.upto(version.length-1) do |i|
|
|
46
|
+
version[i] += (increment[i] || 0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
self.version(path, version.join("."))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Splits the version from the input path, then returns the path and version.
|
|
53
|
+
# If no version is specified, then the returned version will be nil.
|
|
54
|
+
#
|
|
55
|
+
# deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
|
|
56
|
+
# deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
|
|
57
|
+
#
|
|
58
|
+
def deversion(path)
|
|
59
|
+
path =~ /^(.*)-(\d(\.?\d)*)(.*)?/ ? [$1 + $4, $2] : [path, nil]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/tap/task.rb
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
require 'monitor'
|
|
2
|
+
|
|
3
|
+
module Tap
|
|
4
|
+
|
|
5
|
+
# == Overview
|
|
6
|
+
#
|
|
7
|
+
# Tasks are the basic executable unit of Tap. When an App executes
|
|
8
|
+
# a Task, the Task will be passed an array of inputs which it then
|
|
9
|
+
# processes into outputs. Tasks are joined into workflows using
|
|
10
|
+
# conditions that define when a set of inputs are ready for execution,
|
|
11
|
+
# and what to do with the outputs when a the Task completes. Tasks
|
|
12
|
+
# therefore fundamentally consist of a condition_block, a process
|
|
13
|
+
# method, and an on_complete_block.
|
|
14
|
+
#
|
|
15
|
+
# Tasks are configurable. By default each task will be configured
|
|
16
|
+
# with the class default_config, which can be set when the class is
|
|
17
|
+
# defined.
|
|
18
|
+
#
|
|
19
|
+
# class ConfiguredTask < Tap::Task
|
|
20
|
+
# set_default_config :one => 'one', :two => 'two'
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# t = ConfiguredTask.new
|
|
24
|
+
# t.config # => {:one => 'one', :two => 'two'}
|
|
25
|
+
#
|
|
26
|
+
# Tasks also have a name (based on the class by default) which is
|
|
27
|
+
# used during initialization to lookup and merge any configurations
|
|
28
|
+
# in the corresponding config_file. Additional, overriding
|
|
29
|
+
# configurations may be provided during initialization as well.
|
|
30
|
+
#
|
|
31
|
+
# t.name # => "configured_task"
|
|
32
|
+
# t.app[:config] # => "/path/to/app/config"
|
|
33
|
+
# t.config_file # => "/path/to/app/config/configured_task.yml"
|
|
34
|
+
#
|
|
35
|
+
# # [/path/to/app/config/example.yml]
|
|
36
|
+
# # one: ONE
|
|
37
|
+
#
|
|
38
|
+
# t = ConfiguredTask.new "example", :three => 'three'
|
|
39
|
+
# t.name # => "example"
|
|
40
|
+
# t.config_file # => "/path/to/app/config/example.yml"
|
|
41
|
+
# t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
|
|
42
|
+
#
|
|
43
|
+
# === Batches
|
|
44
|
+
#
|
|
45
|
+
# Tasks are designed to facilitate batch processing of inputs. When
|
|
46
|
+
# a task queues inputs to itself, all inputs are collected into a
|
|
47
|
+
# single batch until execution at which time all inputs are passed
|
|
48
|
+
# to the task at once. If the task is iterative (the default), then
|
|
49
|
+
# each of these inputs is processed individually:
|
|
50
|
+
#
|
|
51
|
+
# runlist = []
|
|
52
|
+
# t1 = Task.new {|task, input| runlist << input}
|
|
53
|
+
# t1.queue 1
|
|
54
|
+
# t1.queue 2,3
|
|
55
|
+
# t1.app.run
|
|
56
|
+
#
|
|
57
|
+
# runlist # => [1,2,3]
|
|
58
|
+
#
|
|
59
|
+
# However, tasks are also designed to facilitate batch processing
|
|
60
|
+
# using a variety (ie batch) of configurations. When a task is
|
|
61
|
+
# queued, each task in the batch attribute will be queued as well.
|
|
62
|
+
#
|
|
63
|
+
# runlist = []
|
|
64
|
+
# t1.batch # => [t1]
|
|
65
|
+
# t2 = t1.create_batch_task
|
|
66
|
+
# t1.batch # => [t1, t2]
|
|
67
|
+
#
|
|
68
|
+
# t1.queue 1
|
|
69
|
+
# t2.queue 2,3
|
|
70
|
+
# t1.app.run
|
|
71
|
+
#
|
|
72
|
+
# runlist # => [1,2,3, 1,2,3]
|
|
73
|
+
#
|
|
74
|
+
# In the example, runlist reflects that t1 and t2 were run in succession
|
|
75
|
+
# with the same [1,2,3] inputs. Configuration files may specify an array
|
|
76
|
+
# of configuration templates; each is translated into a batched task.
|
|
77
|
+
#
|
|
78
|
+
# # [/path/to/app/config/batch.yml]
|
|
79
|
+
# # - one: ONE
|
|
80
|
+
# # - one: ANOTHER ONE
|
|
81
|
+
#
|
|
82
|
+
# t = ConfiguredTask.new "batch"
|
|
83
|
+
# t.batch.size # => 2
|
|
84
|
+
# t1, t2 = t.batch
|
|
85
|
+
#
|
|
86
|
+
# t1.name # => "batch"
|
|
87
|
+
# t1.config_template # => {:one => 'ONE'}
|
|
88
|
+
# t1.config # => {:one => 'ONE', :two => 'two'}
|
|
89
|
+
#
|
|
90
|
+
# t2.name # => "batch"
|
|
91
|
+
# t2.config_template # => {:one => 'ANOTHER ONE'}
|
|
92
|
+
# t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
|
|
93
|
+
#
|
|
94
|
+
# In addition, configuration files are processed to make the definition
|
|
95
|
+
# of configuration templates more concise and powerful
|
|
96
|
+
# (see Tap::Support::Templater for more details). The most basic form
|
|
97
|
+
# merges each variation with the rest of the file to produce the final
|
|
98
|
+
# array of templates.
|
|
99
|
+
#
|
|
100
|
+
# # [/path/to/app/config/template.yml]
|
|
101
|
+
# # one: ONE
|
|
102
|
+
# # variations!:
|
|
103
|
+
# # - two: TWO
|
|
104
|
+
# # - three: THREE
|
|
105
|
+
#
|
|
106
|
+
# t = ConfiguredTask.new "template"
|
|
107
|
+
# t.batch.size # => 2
|
|
108
|
+
# t1, t2 = t.batch
|
|
109
|
+
#
|
|
110
|
+
# t1.config # => {:one => 'ONE', :two => 'TWO'}
|
|
111
|
+
# t2.config # => {:one => 'ONE', :two => 'two', :three => 'THREE'}
|
|
112
|
+
#
|
|
113
|
+
# All together, these features make it easy to process a batch of inputs
|
|
114
|
+
# using a batch of configurations -- all encapsulated in a workflow-ready
|
|
115
|
+
# architecture.
|
|
116
|
+
class Task < Monitor
|
|
117
|
+
write_inheritable_hash(:default_config, {})
|
|
118
|
+
class_inheritable_hash(:default_config)
|
|
119
|
+
|
|
120
|
+
write_inheritable_attribute(:iterative, true)
|
|
121
|
+
class_inheritable_reader(:iterative)
|
|
122
|
+
|
|
123
|
+
class << self
|
|
124
|
+
|
|
125
|
+
# Sets the defualt configurations for the task class. Configurations
|
|
126
|
+
# are inherited, but can be overridden or added to in subclasses. Use
|
|
127
|
+
# options (:make_accessors, :make_readers, :make_writers) to create
|
|
128
|
+
# accessors for configurations.
|
|
129
|
+
#
|
|
130
|
+
# class SomeTask < Tap::Task
|
|
131
|
+
# set_default_config({
|
|
132
|
+
# :key => 'value'},
|
|
133
|
+
# :make_accessors => true)
|
|
134
|
+
# end
|
|
135
|
+
#
|
|
136
|
+
# t = SomeTask.new
|
|
137
|
+
# t.key # => 'value'
|
|
138
|
+
# t.key = 'another'
|
|
139
|
+
# t.config # => {:key => 'another'}
|
|
140
|
+
#
|
|
141
|
+
def set_default_config(hash={}, options={})
|
|
142
|
+
keys = hash.keys
|
|
143
|
+
self.write_inheritable_hash(:default_config, hash)
|
|
144
|
+
|
|
145
|
+
self.config_accessor(*keys) if options[:make_accessors]
|
|
146
|
+
self.config_reader(*keys) if options[:make_readers]
|
|
147
|
+
self.config_writer(*keys) if options[:make_writers]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Creates a configuration writer for the input keys. Works like
|
|
151
|
+
# attr_writer, except the value is written to config, rather than
|
|
152
|
+
# a local variable.
|
|
153
|
+
def config_writer(*keys)
|
|
154
|
+
keys.each do |key|
|
|
155
|
+
define_method("#{key}=") do |value|
|
|
156
|
+
config[key] = value
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Creates a configuration reader for the input keys. Works like
|
|
162
|
+
# attr_reader, except the value is read from config, rather than
|
|
163
|
+
# a local variable.
|
|
164
|
+
def config_reader(*keys)
|
|
165
|
+
keys.each do |key|
|
|
166
|
+
define_method(key) do
|
|
167
|
+
config[key]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Creates configuration accessors for the input keys. Works like
|
|
173
|
+
# attr_accessor, except the value is read from and written to config,
|
|
174
|
+
# rather than a local variable.
|
|
175
|
+
def config_accessor(*keys)
|
|
176
|
+
config_writer(*keys)
|
|
177
|
+
config_reader(*keys)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Sets the default iteration behavior for a task class
|
|
181
|
+
# to iterate inputs during task execution.
|
|
182
|
+
def iterate
|
|
183
|
+
self.write_inheritable_attribute(:iterative, true)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Sets the default iteration behavior for a task class
|
|
187
|
+
# to not iterate inputs during task execution.
|
|
188
|
+
def do_not_iterate
|
|
189
|
+
self.write_inheritable_attribute(:iterative, false)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
attr_reader :name, :config, :config_template, :app, :batch, :condition_block, :task_block, :on_complete_block, :results
|
|
194
|
+
attr_accessor :multithread, :iterate
|
|
195
|
+
|
|
196
|
+
# Creates a new Task with the specified attributes.
|
|
197
|
+
def initialize(name=nil, config=nil, app=App.instance, &task_block)
|
|
198
|
+
super() # required for Monitor
|
|
199
|
+
|
|
200
|
+
self.name = name.nil? ? self.class.to_s.underscore : name
|
|
201
|
+
self.app = app
|
|
202
|
+
|
|
203
|
+
self.condition_block = nil
|
|
204
|
+
self.task_block = task_block
|
|
205
|
+
self.on_complete_block = nil
|
|
206
|
+
self.results = []
|
|
207
|
+
self.multithread = false
|
|
208
|
+
self.iterate = self.class.iterative
|
|
209
|
+
self.batch = []
|
|
210
|
+
|
|
211
|
+
# collect all configuration templates from the app
|
|
212
|
+
templates = []
|
|
213
|
+
app.config_templates(self.config_file).each do |template|
|
|
214
|
+
templates.concat make_config_templates(template)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# be sure there is at least one template and
|
|
218
|
+
# add a task to batch for every template
|
|
219
|
+
templates << {} if templates.empty?
|
|
220
|
+
templates.each do |template|
|
|
221
|
+
self.create_batch_task(template, config)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Creates a batched task based on the config_template and overrides,
|
|
226
|
+
# and adds the task to batch. The batched task will be a duplicate
|
|
227
|
+
# of the current task but with new configurations.
|
|
228
|
+
#
|
|
229
|
+
# This method can be overridden to initialize variables that are
|
|
230
|
+
# specific to a batch task, for instance variables depending on
|
|
231
|
+
# batch_index.
|
|
232
|
+
def create_batch_task(config_template={}, overrides={})
|
|
233
|
+
task = (self.batch.empty? ? self : self.dup)
|
|
234
|
+
task.config_template = config_template.symbolize_keys
|
|
235
|
+
task.batch << task
|
|
236
|
+
task.config = overrides
|
|
237
|
+
task
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Returns true if the batch size is greater than one
|
|
241
|
+
# (the one being the current task itself).
|
|
242
|
+
def batched?
|
|
243
|
+
batch.length > 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Returns the index of the current task in batch.
|
|
247
|
+
def batch_index
|
|
248
|
+
batch.index(self)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Returns true if multithread is true.
|
|
252
|
+
def multithread?
|
|
253
|
+
multithread
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Returns true if iterate is true. If iterate has not been set for the task,
|
|
257
|
+
# then iterate? returns the value of the class iterative variable.
|
|
258
|
+
def iterate?
|
|
259
|
+
iterate
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Returns the path to the configuration file used to make the task configuration
|
|
263
|
+
# templates. By default this is the YAML file for the task name relative to the
|
|
264
|
+
# application config directory:
|
|
265
|
+
#
|
|
266
|
+
# t.app['config'] # => '/path/to/config'
|
|
267
|
+
# t.name # => 'some/task'
|
|
268
|
+
# t.config_file # => '/path/to/config/some/task.yml'
|
|
269
|
+
#
|
|
270
|
+
def config_file
|
|
271
|
+
# since this is called during initialization before they
|
|
272
|
+
# are set, take care that this does not depend on batch,
|
|
273
|
+
# config, or config_template
|
|
274
|
+
app.filepath('config', self.name + ".yml")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Configures the task with the given configuration overrides. A Task has three
|
|
278
|
+
# configuration sources: the class default_config, the config_template loaded from
|
|
279
|
+
# config_file, and the inputs to this method. Configurations from these
|
|
280
|
+
# sources are merged as: default_config.merge(config_template).merge(overrides)
|
|
281
|
+
#
|
|
282
|
+
# Configurations are symbolized before they are merged. If overrides is nil, then
|
|
283
|
+
# they will be treated as an empty hash.
|
|
284
|
+
def config=(overrides)
|
|
285
|
+
overrides = overrides.nil? ? {} : overrides.symbolize_keys
|
|
286
|
+
@config = self.class.default_config.merge(config_template).merge(overrides)
|
|
287
|
+
|
|
288
|
+
self.config
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Sets a condition block for the task. Raises an error if condition_block is
|
|
292
|
+
# already set, unless override = true.
|
|
293
|
+
def condition(override=false, &block) # :yields: self, inputs
|
|
294
|
+
raise "Condition for task already set: #{name}" unless condition_block.nil? || override
|
|
295
|
+
self.condition_block = block
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Sets a block to execute when the task completes execution. Raises an error
|
|
299
|
+
# if on_complete_block is already set, unless override = true.
|
|
300
|
+
def on_complete(override=false, &block) # :yields: results
|
|
301
|
+
raise "On complete for task already set: #{name}" unless on_complete_block.nil? || override
|
|
302
|
+
self.on_complete_block = block
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Returns true if no condition block is specified, or the condition
|
|
306
|
+
# block evaluates to true with the given inputs.
|
|
307
|
+
def executable?(inputs)
|
|
308
|
+
condition_block ? condition_block.call(self, inputs) : true
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Execute the actions associated with this task. Returns an array of Audits;
|
|
312
|
+
# one for each input value. Raises an error if the task is not executable with
|
|
313
|
+
# the inputs, and executes the on_complete block when finished.
|
|
314
|
+
#
|
|
315
|
+
# Execute is synchronized such that a task can only execute on one thread at a
|
|
316
|
+
# time.
|
|
317
|
+
def execute(*inputs)
|
|
318
|
+
synchronize do
|
|
319
|
+
self.results = nil
|
|
320
|
+
raise "Condition not met." unless executable?(inputs)
|
|
321
|
+
|
|
322
|
+
inputs = case
|
|
323
|
+
when inputs.empty? then inputs
|
|
324
|
+
when iterate? then Support::Audit.register(*inputs)
|
|
325
|
+
else [Support::Audit.merge(*inputs)]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
before_execute
|
|
329
|
+
begin
|
|
330
|
+
self.results = inputs.each do |input|
|
|
331
|
+
output = process( input._current )
|
|
332
|
+
input._record(self, output)
|
|
333
|
+
end
|
|
334
|
+
rescue
|
|
335
|
+
on_execute_error($!)
|
|
336
|
+
end
|
|
337
|
+
after_execute
|
|
338
|
+
|
|
339
|
+
on_complete_block.call(results) if on_complete_block
|
|
340
|
+
results
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The method for processing inputs into outputs. Override this method in
|
|
345
|
+
# subclasses to provide class-specific process logic. By default the
|
|
346
|
+
# input is passed to the task_block (provided during initialization).
|
|
347
|
+
# Simply returns the input if no task_block is set.
|
|
348
|
+
def process(input)
|
|
349
|
+
task_block.nil? ? input : task_block.call(self, input)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Enqueues self to the app queue with the inputs.
|
|
353
|
+
def enq(*inputs)
|
|
354
|
+
app.queue.enq(self, *inputs)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Logs the inputs to the application logger (via app.log)
|
|
358
|
+
def log(action, msg="", level=Logger::INFO)
|
|
359
|
+
# TODO - add a task identifier?
|
|
360
|
+
app.log(action, msg, level)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
protected
|
|
364
|
+
|
|
365
|
+
attr_writer :name, :config_template, :app, :batch, :condition_block, :task_block, :on_complete_block, :results
|
|
366
|
+
|
|
367
|
+
# Hook to execute code before inputs are processed.
|
|
368
|
+
def before_execute() end
|
|
369
|
+
|
|
370
|
+
# Hook to execute code after inputs are processed, but before the on_complete block.
|
|
371
|
+
def after_execute() end
|
|
372
|
+
|
|
373
|
+
# Hook to handle unhandled errors from processing inputs on a task level.
|
|
374
|
+
# By default on_execute_error simply re-raises the unhandled error.
|
|
375
|
+
def on_execute_error(err)
|
|
376
|
+
raise err
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Hook to provide class-specific processing of the config_file template
|
|
380
|
+
# (ie app.config_template(config_file)). By default the template is
|
|
381
|
+
# processed using Support::Templater.make_templates.
|
|
382
|
+
def make_config_templates(template)
|
|
383
|
+
Support::Templater.make_templates(template)
|
|
384
|
+
# this would be a security hole... can't do it.
|
|
385
|
+
# if you can set :data then you could read
|
|
386
|
+
# any accessible file.
|
|
387
|
+
#{ |filename| app.read(:data, filename) }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
class Hold # :nodoc:
|
|
394
|
+
attr_accessor :help_doc
|
|
395
|
+
|
|
396
|
+
def register_doc(*section_headers)
|
|
397
|
+
section_headers << "Command Line Usage" if section_headers.empty?
|
|
398
|
+
self.help_doc = [caller.first.sub(/:\d+$/, ''), section_headers.collect {|section| section.strip}]
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def help
|
|
402
|
+
return "No help available for #{self}" unless help_doc
|
|
403
|
+
|
|
404
|
+
lines = []
|
|
405
|
+
help_file, sections = help_doc
|
|
406
|
+
File.open(help_file) do |file|
|
|
407
|
+
in_section = nil
|
|
408
|
+
while line = file.gets
|
|
409
|
+
next unless line =~ /^#(.*)/
|
|
410
|
+
line = $1.strip
|
|
411
|
+
|
|
412
|
+
if line =~ /^=+(.*)/
|
|
413
|
+
line = $1.strip
|
|
414
|
+
in_section = sections.delete(line)
|
|
415
|
+
line += ":"
|
|
416
|
+
else
|
|
417
|
+
line = " " + line
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
lines << line if in_section
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
return lines.join("\n")
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Queues self to app with the input.
|
|
428
|
+
#def <<(input)
|
|
429
|
+
#queue(input)
|
|
430
|
+
#end
|
|
431
|
+
|
|
432
|
+
# Ticks the monitor by printing '.', or the specified message.
|
|
433
|
+
# Only ticks if called from within a monitor block.
|
|
434
|
+
#def tick_monitor(action=nil, msg="", level=Logger::INFO)
|
|
435
|
+
# app.tick_monitor(action, msg, level)
|
|
436
|
+
#end
|
|
437
|
+
|
|
438
|
+
# Prints the hash of statistics if called within a monitor block.
|
|
439
|
+
#def monitor_stats(hash)
|
|
440
|
+
# app.monitor_stats(hash)
|
|
441
|
+
#end
|
|
442
|
+
|
|
443
|
+
# Times the execution of the block and enables the various monitor
|
|
444
|
+
# methods (tick_monitor, monitor_stats).
|
|
445
|
+
# def monitor(action="beginning", msg="", &block)
|
|
446
|
+
# app.monitor(action,msg, &block)
|
|
447
|
+
#end
|
|
448
|
+
end
|