tap 0.7.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|