pipely 0.0.1

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/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: