gd_bam 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ #BAsh Machinery = BAM
2
+
3
+ This thing is fresh from the oven. It is 0.0.1 so there are lots of rough edges. On the other hand there is enough to make you dangerous. Play with it, break it, let me know.
4
+
5
+ ###What are goals of BAM
6
+ * Be able to spin up a predefined project in hours not days
7
+ * Everything implemented using CloudConnect so we can still leverage the infrastructure and deploy to secure
8
+ * Make some specific modifications easy (new fields from sources)
9
+
10
+ ###What are not goals of BAM
11
+ * Supersede Clover/CC
12
+ * Provide templates for development. The generated grapsh are meant not to be tampered with.
13
+ * define a whole processing language. This might be the next extension but right now the goal is to be able to spin up predefined projects as fast and easily as possible. I am not defining primitives for joins, reformats etc.
14
+
15
+ ##Overview
16
+
17
+ BAM is consisting of two parts. The underlying layer that allows you to build ETLs from prebuild constructs. Second part should make possible to express different configurations in user comprehensible way and configure first layer for specific projects so you do not need to deal with low level stuff when you decide that you want to use Amount instead of Total Price in GoodSales project.
18
+
19
+ ####1st layer
20
+
21
+ There are 3 basic pieces that you will be playing around. Let's have a look at those
22
+
23
+ 1) Tap
24
+ This is a fancy name for source of data. It can be downloader from SF or CSV file. Tap configurations are source specific. Currently there is SF implemented.
25
+
26
+ 2) Sink
27
+ This is the target of your data. The only sink we have currently is GD.
28
+
29
+ 3) Graph
30
+ This is a clover graph. So it plays well with Ultra it needs to be created in a specific way. You can use graphs from the library or those that you provide locally (N/A yet).
31
+
32
+
33
+ 4) Flow
34
+ This is something that describes how the data are flowing. The previous three pieces are the things that you can use in the flow.
35
+
36
+
37
+ ###2nd layer
38
+ TBD
39
+
40
+ ##Installation
41
+
42
+ create a directory `mkdir bam`
43
+ cd into it `cd bam`
44
+
45
+ clone salesforce
46
+ clone bam
47
+ create a Gemfile `touch Gemfile`
48
+
49
+ and put this inside
50
+
51
+ source "https://rubygems.org"
52
+ gem "bam", :path => "./bam"
53
+ gem "salesforce", :path => "./salesforce"
54
+
55
+ make sure you are running ruby 1.9.x
56
+ install bundler `gem install bundler`
57
+
58
+ cd bam
59
+ bundle install
60
+
61
+ cd ..
62
+ bundle install
63
+
64
+ create a project `bundle exec bam scaffold project test`
65
+
66
+ this will create a project
67
+
68
+ ##Sample project
69
+
70
+ now you can go inside `cd test` and generate it
71
+ `bundle exec bam generate`
72
+
73
+ This tap will download users from sf (you have to provide credentials in params.json). It then runs graph called "process user" (this is part of the distribution). This graph concatenates first name and last name together.
74
+
75
+ GoodData::CloverGenerator::DSL::flow("user") do |f|
76
+ tap(:id => "user")
77
+
78
+ graph("process_owner")
79
+ metadata("user") do |m|
80
+ m.remove("FirstName")
81
+ m.remove("LastName")
82
+ m.add(:name => "Name")
83
+ end
84
+
85
+ sink(:id => "user")
86
+ end
87
+
88
+ Now you have to provide it the definition of tap which you can do like this.
89
+
90
+ {
91
+ "source" : "salesforce"
92
+ ,"object" : "User"
93
+ ,"id" : "user"
94
+ ,"fields" : [
95
+ {
96
+ "name" : "Id"
97
+ },
98
+ {
99
+ "name" : "FirstName"
100
+ },
101
+ {
102
+ "name" : "LastName"
103
+ },
104
+ {
105
+ "name" : "Region"
106
+ },
107
+ {
108
+ "name" : "Department"
109
+ }
110
+ ]
111
+ }
112
+
113
+ Also you need to provide a definition for sink which can look somwhow like this.
114
+
115
+ {
116
+ "type" : "dataset"
117
+ ,"id" : "user"
118
+ ,"gd_name" : "user"
119
+ ,"fields" : [
120
+ {
121
+ "name" : "Id"
122
+ },
123
+ {
124
+ "name" : "Name"
125
+ }
126
+ ]
127
+ }
128
+
129
+ For this example to work you need to provide SF and gd credentials. Provide them in params.json. You would need to provide also a project with appropriate project but this is out of scope of this "example" (I am working on tools that would make it easier).
130
+
131
+ Now run `bundle exec bam generate` and there will be a folder with the clover project generated. Open it in CC find main.grf and run it. After crunching for a while you should see data in the project.
132
+
133
+ ### Runtime commands
134
+ Part of the distribution is the bam executable which lets you do several neat things.
135
+
136
+ Run `bam` to get the list of commands
137
+ Run `bam help command` to get help about the command
138
+
139
+ ### deploy directory
140
+ deploys the directory to the server. You can provide the param of the process as a parameter
141
+
142
+ ### generate
143
+ Generates the ETL. The default target directory is clover_project (currently cannot be changed). You can provide --only parameter to specify the name of the flow to be processed if you do not need to generate all flows. Currently you can specify only on in only param
144
+
145
+ ### generate_downloaders
146
+ If you have incremental downloaders in your project it good to deploy them as a separate process. This generates only the downloaders and is meant for exacltly this purpose. If you are interested about why it is a good idea. Take a look here (TBD). The target directory is downloaders_project (currently cannot be changed).
147
+
148
+ ### generate_xmls
149
+ Investigates what is changed and performs the changes in the target project. Uses CL tool behind the scenes. Needs more work
150
+
151
+ ### model_sync
152
+ Syncs the model with the definition in sinks. {Todo} Add interactive addition. Sometimes the new field can actually be a typo or something like that. Possible to uncover with validate_datasets
153
+
154
+ ### run
155
+ TBD
156
+
157
+ ### scaffold
158
+ Takes an argument and creates a scaffold for you. It can scaffold project, flow, sink and tap.
159
+
160
+ ### taps_generate_docs
161
+ In your project there should be a README.md.erb file. By running this command it will be transformed into README.md and put into the project so it can be committed to git. The interpolated params are
162
+ taps
163
+ sinks
164
+
165
+ ### sinks_validate
166
+ Currently works only for SF. Validates that the target SF instance has all the fields in the objects that are specified in the taps definitions.
167
+
168
+ ### validate_datasets
169
+ Vallidates the sinks (currently only GD) with the definitions in the proeject. It looks for fields that are defined inside sinks and are not in the projects missing references etc. More description needed.
data/bin/bam ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ # begin # XXX: Remove this begin/rescue before distributing your app
4
+ require 'bam'
5
+ # rescue LoadError
6
+ # STDERR.puts "In development, you need to use `bundle exec bin/bam` to run your app"
7
+ # STDERR.puts "At install-time, RubyGems will make sure lib, etc. are in the load path"
8
+ # STDERR.puts "Feel free to remove this message from bin/bam now"
9
+ # exit 64
10
+ # end
11
+
12
+ include GLI::App
13
+
14
+ program_desc 'Describe your application here'
15
+
16
+ version Bam::VERSION
17
+
18
+ # desc 'Describe some switch here'
19
+ # switch [:s,:switch]
20
+ #
21
+ desc 'Verbose'
22
+ default_value false
23
+ arg_name 'verbose'
24
+ switch [:v,:verbose]
25
+
26
+
27
+ desc 'Generates clover project based on information in current directory. The default ouptut is the directory ./clover_project'
28
+ # arg_name 'Describe arguments to new here'
29
+ command :generate do |c|
30
+
31
+ c.desc 'generate only specified flow'
32
+ c.arg_name 'only'
33
+ c.flag :only
34
+
35
+ c.action do |global_options,options,args|
36
+ GoodData::CloverGenerator.clobber_clover_project
37
+ GoodData::CloverGenerator.run(options)
38
+ end
39
+ end
40
+
41
+ desc 'Generates clover project for downloaders.'
42
+ # arg_name 'Describe arguments to new here'
43
+ command :generate_downloaders do |c|
44
+
45
+ c.desc 's3 backup'
46
+ c.arg_name 'backup'
47
+ c.flag :backup
48
+
49
+ c.action do |global_options,options,args|
50
+ GoodData::CloverGenerator.clobber_downloader_project
51
+ GoodData::CloverGenerator.generate_downloaders(options)
52
+ end
53
+ end
54
+
55
+ desc 'Validates that the tap has the fields it is claimed it should have. This is supposed to make the mitigate errors during deploy.'
56
+ # arg_name 'Describe arguments to new here'
57
+ command :taps_validate do |c|
58
+ c.action do |global_options,options,args|
59
+ GoodData::CloverGenerator.validate_taps
60
+ end
61
+ end
62
+
63
+ desc 'Validates that the tap has the fields it is claimed it should have. This is supposed to make the mitigate errors during deploy.'
64
+ # arg_name 'Describe arguments to new here'
65
+ command :taps_generate_docs do |c|
66
+ c.action do |global_options,options,args|
67
+ GoodData::CloverGenerator.taps_generate_docs
68
+ end
69
+ end
70
+
71
+ desc 'Lists processes for the project.'
72
+ # arg_name 'Describe arguments to new here'
73
+ command :procs do |c|
74
+
75
+ c.desc 'procs for all projects'
76
+ c.arg_name 'all'
77
+ c.switch :all
78
+
79
+ c.action do |global_options,options,args|
80
+ out = GoodData::CloverGenerator.procs_list(options)
81
+ out.each do |proc|
82
+ puts proc.join(',')
83
+ end
84
+ end
85
+ end
86
+
87
+
88
+ desc 'Validates that the tap has the fields it is claimed it should have. This is supposed to make the mitigate errors during deploy.'
89
+ # arg_name 'Describe arguments to new here'
90
+ command :sinks_validate do |c|
91
+ c.action do |global_options,options,args|
92
+ x = GoodData::CloverGenerator.validate_datasets
93
+ end
94
+ end
95
+
96
+
97
+ desc 'Generates structures'
98
+ arg_name 'what you want to generate project, tap, flow, dataset'
99
+ command :scaffold do |c|
100
+ c.action do |global_options,options,args|
101
+ command = args.first
102
+ fail "You did not provide what I should scaffold. I can generate project, tap, flow, sink nothing else" unless ["project", "tap", "flow", "sink"].include?(command)
103
+ case command
104
+ when "project"
105
+ puts "project"
106
+ directory = args[1]
107
+ fail "Directory has to be provided as an argument. See help" if directory.nil?
108
+ GoodData::CloverGenerator.setup_bash_structure(directory)
109
+ when "flow"
110
+ name = args[1]
111
+ fail "Name of the flow has to be provided as an argument. See help" if name.nil?
112
+ GoodData::CloverGenerator.setup_flow(name)
113
+ when "tap"
114
+ name = args[1]
115
+ fail "Name of the tap has to be provided as an argument. See help" if name.nil?
116
+ GoodData::CloverGenerator.setup_tap(name)
117
+ when "sink"
118
+ name = args[1]
119
+ fail "Name of the sink has to be provided as an argument. See help" if name.nil?
120
+ GoodData::CloverGenerator.setup_sink(name)
121
+ end
122
+ end
123
+ end
124
+
125
+ desc 'Runs the project on server'
126
+ command :run do |c|
127
+ c.action do |global_options,options,args|
128
+ puts "This would run the project. But it is not yet implemented"
129
+ end
130
+ end
131
+
132
+ desc 'Runs the project on server'
133
+ command :model_sync do |c|
134
+
135
+ c.desc 'do not execute'
136
+ c.arg_name 'dry'
137
+ c.switch :dry
138
+
139
+ c.action do |global_options,options,args|
140
+ GoodData::CloverGenerator.model_sync(options)
141
+ end
142
+ end
143
+
144
+
145
+ desc 'Deploys the project on server and schedules it'
146
+ command :deploy do |c|
147
+
148
+ c.desc 'existing process id under which it is going to be redeployed'
149
+ c.arg_name 'process'
150
+ c.flag :process
151
+
152
+ c.desc 'name of the process'
153
+ c.arg_name 'name'
154
+ c.flag :name
155
+
156
+ c.action do |global_options,options,args|
157
+ dir = args.first
158
+ fail "You have to specify directory to deploy as an argument" if dir.nil?
159
+ fail "Specified directory does not exist" unless File.exist?(dir)
160
+ GoodData::CloverGenerator.connect_to_gd
161
+ response = GoodData::CloverGenerator.deploy(dir, options)
162
+ end
163
+ end
164
+
165
+ desc 'Runs the project on server'
166
+ command :run do |c|
167
+
168
+ # c.desc 'existing process id under which it is going to be redeployed'
169
+ # c.arg_name 'process'
170
+ # c.flag :process
171
+
172
+ c.action do |global_options,options,args|
173
+
174
+ dir = args.first
175
+ fail "You have to specify directory to deploy as an argument" if dir.nil?
176
+ fail "Specified directory does not exist" unless File.exist?(dir)
177
+
178
+ verbose = global_options[:v]
179
+
180
+ GoodData::CloverGenerator.connect_to_gd
181
+ GoodData::CloverGenerator.create_email_channel
182
+
183
+ GoodData::CloverGenerator.deploy(args.first, global_options.merge({:name => "temporary"})) do |deploy_response|
184
+
185
+ puts HighLine::color("Executing", HighLine::BOLD) if verbose
186
+ GoodData::CloverGenerator.create_email_channel do
187
+ GoodData::CloverGenerator.execute_process(deploy_response["cloverTransformation"]["links"]["executions"], dir)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+
194
+ pre do |global,command,options,args|
195
+ # Pre logic here
196
+ # Return true to proceed; false to abort and not call the
197
+ # chosen command
198
+ # Use skips_pre before a command to skip this block
199
+ # on that command only
200
+ true
201
+ end
202
+
203
+ post do |global_options,command,options,args|
204
+ # Post logic here
205
+ # Use skips_post before a command to skip this
206
+ # block on that command only
207
+ verbose = global_options[:v]
208
+ puts HighLine::color("DONE", :green) if verbose
209
+ end
210
+
211
+ on_error do |exception|
212
+ pp exception.backtrace
213
+ # Error logic here
214
+ # return false to skip default error handling
215
+ true
216
+ end
217
+
218
+ exit run(ARGV)
@@ -0,0 +1,3 @@
1
+ module Bam
2
+ VERSION = '0.0.1'
3
+ end
data/lib/bam.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'bam/version.rb'
2
+ require 'pry'
3
+ require 'zip/zip'
4
+ require 'fileutils'
5
+ $:.unshift(File.dirname(__FILE__))
6
+ require 'runtime'
7
+ # Add requires for other files you add to your project here, so
8
+ # you just need to require this one file in your bin file
@@ -0,0 +1,259 @@
1
+ require 'terminal-table'
2
+
3
+
4
+
5
+ module GoodData
6
+ module CloverGenerator
7
+ module DSL
8
+
9
+ class RemoveMetadataFieldError < RuntimeError
10
+
11
+ attr_reader :options, :metadata, :field
12
+
13
+ def initialize(message, options={})
14
+ super(message)
15
+ @options = options
16
+ @metadata = options[:metadata]
17
+ @field = options[:field]
18
+ end
19
+ end
20
+
21
+ class Metadata
22
+
23
+ attr_accessor :metadata
24
+
25
+ def name
26
+ metadata[:name]
27
+ end
28
+
29
+ def to_hash
30
+ metadata
31
+ end
32
+
33
+ def initialize(metadata)
34
+ @metadata = metadata
35
+ end
36
+
37
+ def add(options={})
38
+ fail "You have to specify name at the metadata change. You specified #{what}" unless options.has_key?(:name)
39
+ position = options[:position] || 0
40
+ what = {
41
+ :name => options[:name],
42
+ :type => options[:type] || "string"
43
+ }
44
+
45
+ @metadata[:fields].insert(position - 1, what)
46
+ @metadata
47
+ end
48
+
49
+ def remove(what)
50
+ fields = metadata[:fields]
51
+ fail RemoveMetadataFieldError.new("Specified column #{what} was not found", :field => what, :metadata => self) unless fields.detect {|f| f[:name] == what}
52
+ @metadata[:fields] = fields.find_all {|f| f[:name] != what}
53
+ @metadata
54
+ end
55
+
56
+ def change
57
+ yield(self)
58
+ self
59
+ end
60
+ end
61
+
62
+ class Flow
63
+
64
+ attr_accessor :steps, :name
65
+
66
+ def self.define(name="", &script)
67
+ puts "Reading flow #{name}"
68
+ x = self.new
69
+ x.flow_name(name)
70
+ x.instance_eval(&script)
71
+ x
72
+ end
73
+
74
+ def initialize
75
+ @steps = []
76
+ end
77
+
78
+ def flow_name(name)
79
+ @name = name
80
+ end
81
+
82
+ def tap(options={}, &bl)
83
+ step({:type => :tap, :source_name => options[:id]})
84
+ end
85
+
86
+ def sink(options={}, &bl)
87
+ step(:type => :upload, :id => options[:id], &bl)
88
+ end
89
+
90
+ def graph(graph, &bl)
91
+ step(:graph => graph, :type => :user_provided, &bl)
92
+ end
93
+
94
+ def parallel(&bl)
95
+
96
+ end
97
+
98
+ def step(options={}, &bl)
99
+ graph = options[:graph]
100
+ type = options[:type]
101
+
102
+ steps.push(options)
103
+ puts "Running step #{graph}"
104
+ end
105
+
106
+ def metadata(name=nil,options={}, &bl)
107
+ steps.last[:metadata_block] = [] if steps.last[:metadata_block].nil?
108
+ steps.last[:metadata_block] << {:name => name, :block => bl, :out_as => options[:out_as]}
109
+ end
110
+
111
+
112
+ end
113
+
114
+ def self.flow(name="", &bl)
115
+ Flow.define(name, &bl)
116
+ end
117
+
118
+
119
+ class Project
120
+
121
+ attr_accessor :usecases, :name, :dims
122
+
123
+ def self.define(&script)
124
+ print self
125
+ x = self.new
126
+ x.instance_eval(&script)
127
+ x
128
+ end
129
+
130
+ def initialize
131
+ @usecases = []
132
+ end
133
+
134
+ def project_name(name)
135
+ @name = name
136
+ end
137
+
138
+ def use_dims(dims)
139
+ @dims = dims
140
+ end
141
+
142
+ def use_usecase(usecase)
143
+ @usecases << usecase
144
+ end
145
+
146
+
147
+
148
+ def get_sources
149
+ configs = []
150
+ FileUtils.cd('./taps') do
151
+ Dir.glob('*.json').each do|f|
152
+ configs << JSON.parse(File.read(f), :symbolize_names => true)
153
+ end
154
+ end
155
+ configs
156
+ end
157
+
158
+ def print_sources(taps)
159
+ puts
160
+ puts "Printing sources"
161
+ puts "================"
162
+ puts
163
+ taps.each do |tap|
164
+ fail "Provided tap #{tap[:object]} does not seem to be tap" if tap[:type] != "tap"
165
+ if tap[:source] == "salesforce"
166
+ table = Terminal::Table.new(:title => "#{tap[:source]} => #{tap[:object]}", :style => {:width => 30}) do |t|
167
+ tap[:fields].each do |f|
168
+ t << [f[:name], f[:name]]
169
+ end
170
+ end
171
+ puts table
172
+ puts
173
+ end
174
+ end
175
+
176
+ end
177
+
178
+
179
+ def get_datasets
180
+ configs = []
181
+ FileUtils.cd('./sinks') do
182
+ Dir.glob('*.json').each do|f|
183
+ configs << JSON.parse(File.read(f), :symbolize_names => true)
184
+ end
185
+ end
186
+ configs
187
+ end
188
+
189
+ def compare_fields(sources, datasets)
190
+ a = sources.reduce([]) do |memo, source|
191
+ x = source[:object]
192
+ memo.concat(source[:fields].map {|f| [x, f[:name]]})
193
+ memo
194
+ end
195
+
196
+ b = datasets.reduce([]) do |memo, source|
197
+ x = source[:name]
198
+ memo.concat(source[:fields].map {|f| [x, f[:name]]})
199
+ memo
200
+ end
201
+ result = (a | b) - (a & b)
202
+ if result.count > 0
203
+ puts "------------------"
204
+ puts "All fields not in"
205
+ puts "------------------"
206
+ result.each {|x| pp x}
207
+ fail "Some fields form source are not used"
208
+ end
209
+ end
210
+
211
+ def run(repo)
212
+ puts "Running"
213
+
214
+ puts "looking for dimension definitions"
215
+ dims.each do |dim|
216
+ puts "found #{dim}"
217
+ end
218
+
219
+ sources = get_sources
220
+ fail "You have no sources defined" if sources.empty?
221
+ puts "Found #{sources.count} sources"
222
+
223
+
224
+ datasets = get_datasets
225
+ fail "You have no datasets defined" if datasets.empty?
226
+ puts "Found #{datasets.count} sources"
227
+
228
+ puts "Composing the tree"
229
+ you = GoodData::CloverGenerator::Dependency::N.new({
230
+ :name => name,
231
+ :type => "project",
232
+ :provides => [],
233
+ :requires => @usecases
234
+ })
235
+
236
+ provided_dims = @dims.map do |dim_to_provide|
237
+ GoodData::CloverGenerator::Dependency::N.new({
238
+ :package => dim_to_provide.split("/").first,
239
+ :name => dim_to_provide.split("/").last,
240
+ :provides => [dim_to_provide.split("/").last],
241
+ :type => "dim",
242
+ :requires => []
243
+ })
244
+ end
245
+
246
+ provided_dims.each {|x| repo << x}
247
+ # graph = resolve(repo, you)
248
+ # to_dot(graph)
249
+ v = GoodData::CloverGenerator::Dependency::Visitor.new
250
+ end
251
+
252
+ end
253
+
254
+ def self.project(&bl)
255
+ Project.define(&bl)
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,47 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Graph author="fluke" created="Tue Feb 05 15:38:24 PST 2013" guiVersion="3.3.1" id="1360179808937" licenseType="Commercial" modified="Fri Feb 22 12:18:42 PST 2013" modifiedBy="fluke" name="process_name" revision="1.13" showComponentDetails="true">
3
+ <Global>
4
+ <Metadata fileURL="${PROJECT}/metadata/${FLOW}/${NAME}/1_in.xml" id="Metadata0"/>
5
+ <Metadata fileURL="${PROJECT}/metadata/${FLOW}/${NAME}/1_out.xml" id="Metadata1"/>
6
+ <MetadataGroup id="ComponentGroup0" name="metadata"/>
7
+ <Property fileURL="params.txt" id="GraphParameter0"/>
8
+ <Property fileURL="workspace.prm" id="GraphParameter0"/>
9
+ <Dictionary/>
10
+ </Global>
11
+ <Phase number="0">
12
+ <Node enabled="enabled" fileURL="data/1_in.csv" guiHeight="77" guiName="CSV Reader" guiWidth="128" guiX="124" guiY="169" id="DATA_READER0" quoteCharacter="&quot;" quotedStrings="true" skipRows="1" type="DATA_READER"/>
13
+ <Node enabled="enabled" fileURL="data/out.csv" guiHeight="89" guiName="CSV Writer" guiWidth="128" guiX="609" guiY="169" id="DATA_WRITER0" outputFieldNames="true" quoteCharacter="&quot;" quotedStrings="true" type="DATA_WRITER"/>
14
+ <Node enabled="enabled" guiHeight="65" guiName="Reformat" guiWidth="128" guiX="365" guiY="175" id="REFORMAT0" type="REFORMAT">
15
+ <attr name="transform"><![CDATA[//#CTL2
16
+
17
+ // Transforms input record into output record.
18
+ function integer transform() {
19
+ $out.0.* = $in.0.*;
20
+ $out.0.Name = "Docent " + $in.0.Name;
21
+
22
+ return OK;
23
+ }
24
+
25
+ // Called during component initialization.
26
+ // function boolean init() {}
27
+
28
+ // Called during each graph run before the transform is executed. May be used to allocate and initialize resources
29
+ // required by the transform. All resources allocated within this method should be released
30
+ // by the postExecute() method.
31
+ // function void preExecute() {}
32
+
33
+ // Called only if transform() throws an exception.
34
+ // function integer transformOnError(string errorMessage, string stackTrace) {}
35
+
36
+ // Called during each graph run after the entire transform was executed. Should be used to free any resources
37
+ // allocated within the preExecute() method.
38
+ // function void postExecute() {}
39
+
40
+ // Called to return a user-defined error message when an error occurs.
41
+ // function string getMessage() {}
42
+ ]]></attr>
43
+ </Node>
44
+ <Edge fromNode="DATA_READER0:0" guiBendpoints="" guiRouter="Manhattan" id="Edge0" inPort="Port 0 (in)" metadata="Metadata0" outPort="Port 0 (output)" toNode="REFORMAT0:0"/>
45
+ <Edge fromNode="REFORMAT0:0" guiBendpoints="" guiRouter="Manhattan" id="Edge1" inPort="Port 0 (in)" metadata="Metadata1" outPort="Port 0 (out)" toNode="DATA_WRITER0:0"/>
46
+ </Phase>
47
+ </Graph>