pipely 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,4 @@
1
+ pipely
2
+ ======
3
+
4
+ Visualize pipeline definitions for AWS Data Pipeline
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ require 'rspec/core/rake_task'
11
+ require 'cane/rake_task'
12
+
13
+ RSpec::Core::RakeTask.new do |t|
14
+ t.pattern = 'spec/**/*_spec.rb'
15
+ end
16
+
17
+ Cane::RakeTask.new(:quality) do |cane|
18
+ cane.canefile = '.cane'
19
+ end
20
+
21
+ task :default => [:spec, :quality]
data/bin/pipely ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pipely'
4
+ require 'optparse'
5
+
6
+ output_path = nil
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: pipely [options] definition.json"
10
+
11
+ opts.on("-o", "--output PATH",
12
+ "Path to write PNG file(s) of graph visualization(s)") do |output|
13
+ output_path = output
14
+ end
15
+ end.parse!
16
+
17
+ definition_file = ARGV.last
18
+ definition_json = File.open(definition_file).read
19
+
20
+ output_base = File.basename(definition_file,".*") + '.png'
21
+ output_file = output_path ? File.join(output_path, output_base) : output_base
22
+
23
+ puts "Generating #{output_file}"
24
+ Pipely.draw(definition_json, output_file)
data/lib/pipely.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'pipely/definition'
2
+ require 'pipely/graph_builder'
3
+
4
+ # The top-level module for this gem. It provides the recommended public
5
+ # interface for using Pipely to visualize and manipulate your Data Pipeline
6
+ # definitions.
7
+ #
8
+ module Pipely
9
+
10
+ def self.draw(definition_json, filename, node_attributes=nil)
11
+ definition = Definition.parse(definition_json)
12
+ definition.apply_node_attributes(node_attributes) if node_attributes
13
+
14
+ graph_builder = GraphBuilder.new
15
+
16
+ graph = graph_builder.build(definition.components_for_graph)
17
+ graph.output( :png => filename )
18
+ end
19
+
20
+ end
@@ -0,0 +1,111 @@
1
+ require 'virtus'
2
+ require 'pipely/reference_list'
3
+
4
+ module Pipely
5
+
6
+ # Represents a Component within a Data Pipeline Definition
7
+ # http://amzn.to/16lbBKx
8
+ #
9
+ class Component
10
+
11
+ REFERENCE_KEYS = [
12
+ 'dependsOn',
13
+ 'input',
14
+ 'output',
15
+ 'runsOn',
16
+ 'schedule',
17
+ 'onFail',
18
+ 'onSuccess',
19
+ 'dataFormat',
20
+ ]
21
+
22
+ STATE_COLORS = {
23
+ 'FINISHED' => 'deepskyblue1',
24
+ 'RUNNING' => 'chartreuse',
25
+ 'WAITING_ON_DEPENDENCIES' => 'gray',
26
+ 'WAITING_FOR_RUNNER' => 'bisque4',
27
+ 'FAILED' => 'orangered',
28
+ }
29
+
30
+ include Virtus
31
+
32
+ attribute :id, String
33
+ attribute :type, String
34
+ attribute :color, String
35
+ attribute :execution_state, String
36
+
37
+ attribute :dependsOn, ReferenceList
38
+ attribute :input, ReferenceList
39
+ attribute :output, ReferenceList
40
+ attribute :runsOn, ReferenceList
41
+ attribute :schedule, ReferenceList
42
+ attribute :onFail, ReferenceList
43
+ attribute :onSuccess, ReferenceList
44
+ attribute :dataFormat, ReferenceList
45
+
46
+ def initialize(args)
47
+ @original_args = args.clone
48
+ super
49
+ coerce_references
50
+ end
51
+
52
+ def coerce_references
53
+ REFERENCE_KEYS.each do |key|
54
+ value = send(key)
55
+ unless value.is_a?(ReferenceList)
56
+ send("#{key}=", ReferenceList.new(value))
57
+ end
58
+ end
59
+ end
60
+
61
+ def graphviz_options
62
+ {
63
+ :shape => 'record',
64
+ :label => "{#{label}}",
65
+ :color => color || 'black',
66
+ :fillcolor => STATE_COLORS[execution_state] || 'white',
67
+ :style => 'filled',
68
+ }
69
+ end
70
+
71
+ def dependencies(scope=nil)
72
+ deps = dependsOn.build_dependencies('dependsOn') +
73
+ input.build_dependencies('input') +
74
+ output.build_dependencies('output')
75
+
76
+ if :all == scope
77
+ deps += runsOn.build_dependencies(:runsOn)
78
+ deps += schedule.build_dependencies(:schedule)
79
+ deps += onFail.build_dependencies(:onFail)
80
+ deps += onSuccess.build_dependencies(:onSuccess)
81
+ deps += dataFormat.build_dependencies(:dataFormat)
82
+ end
83
+
84
+ deps
85
+ end
86
+
87
+ def to_json(options={})
88
+ h = @original_args
89
+
90
+ REFERENCE_KEYS.each do |key|
91
+ value = send(key)
92
+
93
+ if value.present?
94
+ h[key] = value
95
+ else
96
+ h.delete(key)
97
+ end
98
+ end
99
+
100
+ h.to_json(options)
101
+ end
102
+
103
+ private
104
+
105
+ def label
106
+ [id, type, execution_state].compact.join('|')
107
+ end
108
+
109
+ end
110
+
111
+ end
@@ -0,0 +1,72 @@
1
+ require 'json'
2
+ require 'pipely/component'
3
+
4
+ module Pipely
5
+
6
+ # Pipely's representation of a Pipeline Definition for AWS Data Pipeline
7
+ # http://amzn.to/1bpW8Ru
8
+ #
9
+ class Definition
10
+
11
+ # Showing all component types leads to an unwieldy graph.
12
+ # TODO: make this list configurable.
13
+ NON_GRAPH_COMPONENT_TYPES = [
14
+ 'Schedule',
15
+ 'SnsAlarm',
16
+ 'Ec2Resource',
17
+ 'EmrCluster',
18
+ 'CSV',
19
+ nil,
20
+ ]
21
+
22
+ def self.parse(content)
23
+ objects = JSON.parse(content)['objects']
24
+ components = objects.map{|obj| Component.new(obj)}
25
+
26
+ new(components)
27
+ end
28
+
29
+ def initialize(components)
30
+ @components = components
31
+ end
32
+
33
+ attr_reader :components
34
+
35
+ def components_for_graph
36
+ components.reject { |component|
37
+ NON_GRAPH_COMPONENT_TYPES.include?(component['type'])
38
+ }
39
+ end
40
+
41
+ def to_json
42
+ { :objects => components }.to_json
43
+ end
44
+
45
+ def apply_component_attributes(component_attributes)
46
+ self.components.each do |component|
47
+ if attributes = component_attributes[component.id]
48
+ component.attributes = attributes
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def get_components(target_component_ids)
56
+ components.select { |component|
57
+ target_component_ids.include?(component.id)
58
+ }
59
+ end
60
+
61
+ def dependencies_of(selected_components)
62
+ all_dependencies = selected_components.map { |component|
63
+ component.dependencies(:all)
64
+ }.flatten.uniq
65
+
66
+ Set.new(get_components(all_dependencies.map(&:target_id)))
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
@@ -0,0 +1,14 @@
1
+ module Pipely
2
+
3
+ # Represents a dependency from one Component on another
4
+ # http://amzn.to/16lbBKx
5
+ #
6
+ class Dependency < Struct.new(:label, :target_id, :color)
7
+
8
+ def color
9
+ super || 'black'
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,51 @@
1
+ require 'json'
2
+ require 'graphviz'
3
+
4
+ module Pipely
5
+
6
+ # Builds a GraphViz graph from a set of Components and their Dependencies
7
+ class GraphBuilder
8
+
9
+ def initialize(graph=nil)
10
+ @graph = graph || GraphViz.new(:G, :type => :digraph)
11
+ end
12
+
13
+ def build(components)
14
+ add_nodes(components)
15
+ add_edges(components)
16
+ @graph
17
+ end
18
+
19
+ private
20
+
21
+ # Represent Components as nodes on the graph
22
+ def add_nodes(components)
23
+ components.each do |component|
24
+ @graph.add_nodes(component.id, component.graphviz_options)
25
+ end
26
+ end
27
+
28
+ # Represent Dependencies as edges on the graph
29
+ def add_edges(components)
30
+ components.each do |component|
31
+ component.dependencies.each do |dependency|
32
+ options = {
33
+ :label => dependency.label,
34
+ :color => dependency.color,
35
+ }
36
+
37
+ options[:dir] = 'back' if ('input' == dependency.label)
38
+
39
+ @graph.add_edges(
40
+ component.id,
41
+ dependency.target_id,
42
+ options
43
+ )
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
@@ -0,0 +1,32 @@
1
+ require 'pipely/dependency'
2
+
3
+ module Pipely
4
+
5
+ # A list of references to Components for managing dependencies
6
+ #
7
+ class ReferenceList
8
+
9
+ def initialize(input)
10
+ @raw_references = [input].flatten.compact
11
+ end
12
+
13
+ def build_dependencies(label)
14
+ @raw_references.map{|h| Dependency.new(label, h['ref'])}
15
+ end
16
+
17
+ def to_json(options={})
18
+ if 1 == @raw_references.count
19
+ @raw_references.first.to_json(options)
20
+ else
21
+ @raw_references.to_json(options)
22
+ end
23
+ end
24
+
25
+ def present?
26
+ !@raw_references.empty?
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
@@ -0,0 +1,3 @@
1
+ module Pipely
2
+ VERSION = "0.0.1" unless defined?(::DataPipelineGraphviz::VERSION)
3
+ end
@@ -0,0 +1,44 @@
1
+ require 'pipely/component'
2
+ require 'pipely/dependency'
3
+
4
+ describe Pipely::Component do
5
+
6
+ subject {
7
+ described_class.new(
8
+ id: 'my-component',
9
+ type: 'OreoSalad',
10
+ dependsOn: {'ref' => 'asdf'},
11
+ input: {'ref' => 'infile'},
12
+ output: {'ref' => 'outfile'},
13
+ color: 'yellow',
14
+ execution_state: 'WAITING_FOR_RUNNER',
15
+ )
16
+ }
17
+
18
+ it 'coerces dependsOn into a ReferenceList' do
19
+ expect(subject.dependsOn).to be_a(Pipely::ReferenceList)
20
+ end
21
+
22
+ describe '#graphviz_options' do
23
+ it 'builds properties for graphviz node representing this component' do
24
+ expect(subject.graphviz_options).to eq({
25
+ :shape => 'record',
26
+ :label => '{my-component|OreoSalad|WAITING_FOR_RUNNER}',
27
+ :color => 'yellow',
28
+ :fillcolor => 'bisque4',
29
+ :style => 'filled',
30
+ })
31
+ end
32
+ end
33
+
34
+ describe '#dependencies' do
35
+ it 'includes dependsOn edges' do
36
+ expect(subject.dependencies).to eq([
37
+ Pipely::Dependency.new('dependsOn', 'asdf'),
38
+ Pipely::Dependency.new('input', 'infile'),
39
+ Pipely::Dependency.new('output', 'outfile'),
40
+ ])
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,56 @@
1
+ require 'pipely/definition'
2
+
3
+ describe Pipely::Definition do
4
+
5
+ subject { described_class.parse(definition_json) }
6
+
7
+ let(:definition_json) {
8
+ <<EOF
9
+ {
10
+ "objects": [
11
+ {
12
+ "id": "DoStuff",
13
+ "type": "ShellCommandActivity",
14
+ "onFail": { "ref": "FailureNotify" }
15
+ },
16
+ {
17
+ "id": "FailureNotify",
18
+ "type": "SnsAlarm"
19
+ }
20
+ ]
21
+ }
22
+ EOF
23
+ }
24
+
25
+ describe '#components' do
26
+ it 'builds a Component for each object in the definition JSON' do
27
+ expect(subject.components.count).to eq(2)
28
+ end
29
+ end
30
+
31
+ describe '#components_for_graph' do
32
+ it 'filters out node types we do not want on the graph' do
33
+ expect(subject.components_for_graph.count).to eq(1)
34
+ end
35
+ end
36
+
37
+ describe '#to_json' do
38
+ it 'renders the components as JSON' do
39
+ original = JSON.parse(definition_json)
40
+ expect(JSON.parse(subject.to_json)).to eq(original)
41
+ end
42
+ end
43
+
44
+ describe '#apply_component_attributes' do
45
+ it 'applies attributes to nodes with matching ids' do
46
+ subject.apply_component_attributes({
47
+ 'DoStuff' => { color: 'pink' },
48
+ })
49
+
50
+ pink_node = subject.components.detect{|n| n.id == 'DoStuff'}
51
+
52
+ expect(pink_node.color).to eq('pink')
53
+ end
54
+ end
55
+
56
+ end
@@ -0,0 +1,11 @@
1
+ require 'pipely/dependency'
2
+
3
+ describe Pipely::Dependency do
4
+
5
+ describe '#color' do
6
+ it 'defaults to "black"' do
7
+ expect(subject.color).to eq('black')
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,42 @@
1
+ require 'pipely/graph_builder'
2
+
3
+ describe Pipely::GraphBuilder do
4
+
5
+ let(:graph) { stub(:graph) }
6
+
7
+ let(:node1) {
8
+ Pipely::Component.new(
9
+ :id => '1',
10
+ :dependsOn => { 'ref' => '2' },
11
+ )
12
+ }
13
+
14
+ let(:node2) {
15
+ Pipely::Component.new(
16
+ :id => '2',
17
+ )
18
+ }
19
+
20
+ subject { described_class.new(graph) }
21
+
22
+ describe '#build' do
23
+ it 'builds a graph from a list of Components' do
24
+ graph.should_receive(:add_nodes).
25
+ with(node1.id, node1.graphviz_options).ordered
26
+
27
+ graph.should_receive(:add_nodes).
28
+ with(node2.id, node2.graphviz_options).ordered
29
+
30
+ graph.should_receive(:add_edges).
31
+ with(
32
+ node1.id,
33
+ node2.id,
34
+ :label => 'dependsOn',
35
+ :color => 'black',
36
+ ).ordered
37
+
38
+ subject.build([node1, node2])
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,45 @@
1
+ require 'pipely/reference_list'
2
+
3
+ describe Pipely::ReferenceList do
4
+
5
+ context 'given nil input' do
6
+ subject { described_class.new(nil) }
7
+
8
+ describe '#build_dependencies' do
9
+ it 'returns an empty array' do
10
+ expect(subject.build_dependencies('dependsOn')).to eq([])
11
+ end
12
+ end
13
+ end
14
+
15
+ context 'given a single input' do
16
+ subject { described_class.new({ 'ref' => 'foo' }) }
17
+
18
+ describe '#build_dependencies' do
19
+ it 'returns an array of the single reference' do
20
+ expect(subject.build_dependencies('dependsOn')).to eq([
21
+ Pipely::Dependency.new('dependsOn', 'foo'),
22
+ ])
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'given an array of references as input' do
28
+ subject {
29
+ described_class.new([
30
+ { 'ref' => 'foo' },
31
+ { 'ref' => 'bar' },
32
+ ])
33
+ }
34
+
35
+ describe '#build_dependencies' do
36
+ it 'returns an array of the single reference' do
37
+ expect(subject.build_dependencies('dependsOn')).to eq([
38
+ Pipely::Dependency.new('dependsOn', 'foo'),
39
+ Pipely::Dependency.new('dependsOn', 'bar'),
40
+ ])
41
+ end
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,38 @@
1
+ require 'pipely'
2
+
3
+ describe Pipely do
4
+ let(:definition_json) { stub }
5
+ let(:filename) { stub }
6
+ let(:definition) { stub }
7
+
8
+ before do
9
+ Pipely::Definition.stub(:parse).with(definition_json) { definition }
10
+ end
11
+
12
+ describe '.draw' do
13
+ let(:components) { stub }
14
+ let(:definition) { stub(:definition, :components_for_graph => components) }
15
+ let(:graph) { stub(:graph, :output => nil) }
16
+
17
+ before do
18
+ Pipely::GraphBuilder.any_instance.stub(:build).with(components) { graph }
19
+ end
20
+
21
+ it 'parses a JSON definition and builds a graph' do
22
+ graph.should_receive(:output).with(:png => filename)
23
+
24
+ described_class.draw(definition_json, filename)
25
+ end
26
+
27
+ context 'with node_attributes' do
28
+ let(:node_attributes) { stub }
29
+
30
+ it 'applies the node_attributes to the definition' do
31
+ definition.should_receive(:apply_node_attributes).with(node_attributes)
32
+
33
+ described_class.draw(definition_json, filename, node_attributes)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pipely
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Gillooly
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-graphviz
16
+ requirement: &70104078435520 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70104078435520
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70104078435100 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70104078435100
36
+ - !ruby/object:Gem::Dependency
37
+ name: virtus
38
+ requirement: &70104078434680 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70104078434680
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &70104078434260 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70104078434260
58
+ - !ruby/object:Gem::Dependency
59
+ name: cane
60
+ requirement: &70104078433840 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70104078433840
69
+ description:
70
+ email:
71
+ - matt@swipely.com
72
+ executables:
73
+ - pipely
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - lib/pipely/component.rb
78
+ - lib/pipely/definition.rb
79
+ - lib/pipely/dependency.rb
80
+ - lib/pipely/graph_builder.rb
81
+ - lib/pipely/reference_list.rb
82
+ - lib/pipely/version.rb
83
+ - lib/pipely.rb
84
+ - Rakefile
85
+ - README.md
86
+ - spec/lib/pipely/component_spec.rb
87
+ - spec/lib/pipely/definition_spec.rb
88
+ - spec/lib/pipely/dependency_spec.rb
89
+ - spec/lib/pipely/graph_builder_spec.rb
90
+ - spec/lib/pipely/reference_list_spec.rb
91
+ - spec/lib/pipely_spec.rb
92
+ - !binary |-
93
+ YmluL3BpcGVseQ==
94
+ homepage: http://github.com/swipely/pipely
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 1.8.11
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Generate dependency graphs from pipeline definitions.
118
+ test_files:
119
+ - spec/lib/pipely/component_spec.rb
120
+ - spec/lib/pipely/definition_spec.rb
121
+ - spec/lib/pipely/dependency_spec.rb
122
+ - spec/lib/pipely/graph_builder_spec.rb
123
+ - spec/lib/pipely/reference_list_spec.rb
124
+ - spec/lib/pipely_spec.rb
125
+ has_rdoc: