twdeps 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .cache_rake_t
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+
6
+ before_script:
7
+ - mkdir ~/.task
8
+ - echo data.location=~/.task > ~/.taskrc
9
+ - task count
10
+
11
+ before_install:
12
+ - echo | sudo add-apt-repository ppa:ultrafredde/ppa
13
+ - sudo apt-get update
14
+ - sudo apt-get install task
15
+ - sudo apt-get install graphviz
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in twdeps.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :test, :test_paths => ['test/unit', 'test/integration'] do
7
+ watch('lib/twdeps.rb'){"test"}
8
+ watch(%r{^lib/twdeps/(.+)\.rb$}){|m| "test/unit/test_#{m[1]}.rb"}
9
+ watch(%r{^test/unit/test_(.+)\.rb$})
10
+ watch('test/test_helper.rb'){"test"}
11
+ watch('test/helpers/**/*'){"test"}
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Nicholas E. Rabenau
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # TaskWarrior Dependency Visualization
2
+
3
+ Visualizes dependencies between TaskWarrior tasks.
4
+
5
+ [![Build Status](https://secure.travis-ci.org/nerab/twdeps.png?branch=master)](http://travis-ci.org/nerab/twdeps)
6
+
7
+ ## Example
8
+
9
+ Given a set of interdependent tasks described in the TaskWarrior [tutorial](http://taskwarrior.org/projects/taskwarrior/wiki/Tutorial2#DEPENDENCIES), the tasks are
10
+
11
+ 1. Exported from TaskWarrior as JSON, then
12
+ 1. Piped into `twdeps`, and finally
13
+ 1. The output is directed to a PNG file.
14
+
15
+ Result:
16
+
17
+ ![party](/nerab/twdeps/raw/master/examples/party.png)
18
+
19
+ ## Installation
20
+
21
+ $ gem install twdeps
22
+
23
+ ## Usage
24
+
25
+ # Create a dependency graph as PNG and pipe it to a file
26
+ # See [Limitations](Limitations) below for why we need the extra task parms
27
+ task export rc.json.array=on rc.verbose=nothing | twdeps > deps.png
28
+
29
+ # Same but spefify output format
30
+ task export | twdeps --format svg > deps.svg
31
+
32
+ # Create a graph from a previously exported file
33
+ task export > tasks.json
34
+ cat tasks.json | twdeps > deps.png
35
+
36
+ # Display graph in browser without creating an intermediate file
37
+ # Requires bcat to be installed
38
+ task export | twdeps --format svg | bcat
39
+
40
+ ## Dependencies
41
+
42
+ The graph is generated with [ruby-graphviz](https://github.com/glejeune/Ruby-Graphviz), which in turn requires a local [Graphviz](http://graphviz.org/) installation (e.g. `brew install graphviz`).
43
+
44
+ [bcat](http://rtomayko.github.com/bcat/) is required for piping into a browser.
45
+
46
+ ## Limitations
47
+
48
+ Due to [two](http://taskwarrior.org/issues/1017) [bugs](http://taskwarrior.org/issues/1013) in JSON export, TaskWarrior 2.0 needs the command line options `rc.json.array=on` and `rc.verbose=nothing`.
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test' << 'test/helpers'
7
+ test.test_files = FileList['test/**/test_*.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/twdeps ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+ Bundler.require
5
+
6
+ require 'trollop'
7
+
8
+ def log(msg)
9
+ STDERR.puts "#{File.basename($0)}: #{msg}"
10
+ end
11
+
12
+ def die(msg = nil)
13
+ log(msg) unless msg.nil?
14
+ exit 1
15
+ end
16
+
17
+ opts = Trollop::options do
18
+ version "#{File.basename($0)} v#{TaskWarrior::Dependencies::VERSION} (c) 2012 Nicolas E. Rabenau"
19
+ banner <<-EOS
20
+ Visualizes dependencies between TaskWarrior tasks.
21
+
22
+ Usage:
23
+ #{File.basename($0)} [options]
24
+
25
+ where [options] are:
26
+
27
+ EOS
28
+ opt :format, "Specify output format", :default => 'svg'
29
+ opt :trace, "Enable trace output", :default => false
30
+ end
31
+
32
+ include TaskWarrior
33
+ include TaskWarrior::Dependencies
34
+
35
+ Trollop::die :format, "must be one of #{Graph.formats.join(', ')}" unless Graph.formats.include?(opts[:format])
36
+
37
+ begin
38
+ repo = Repository.new(ARGF.read)
39
+ master = Graph.new(File.basename($0)) # TODO Move this to a CommandlinePresenter
40
+
41
+ # Add all projects (will add their tasks and dependencies recursively)
42
+ repo.projects.each do |project|
43
+ master << project
44
+ end
45
+
46
+ # Add all project-less tasks as toplevel nodes
47
+ repo.tasks.reject{|t| t.project}.each do |task|
48
+ master << task
49
+ end
50
+
51
+ puts master.render(opts[:format])
52
+ rescue
53
+ if opts[:trace]
54
+ log($!)
55
+ $@.each{|line| log(line)}
56
+ else
57
+ die($!)
58
+ end
59
+ end
Binary file
@@ -0,0 +1,111 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ class NullPresenter
4
+ def attributes
5
+ {:label => 'Unknown', :fontcolor => 'red'}
6
+ end
7
+
8
+ def id
9
+ 'null'
10
+ end
11
+ end
12
+
13
+ # Builds a dependency graph
14
+ #
15
+ # +thing+ is added as node with all of its dependencies. A presenter is used to present the task as node label.
16
+ # +thing.id.to_s+ is called for the identifier. It must be unique within the graph and all of its dependencies.
17
+ #
18
+ # +thing.dependencies(thing)+ is called if +thing+ responds to it. It is expected to return a list
19
+ # of things the thing depends on. Each thing may have its own dependencies which will be resolved recursively.
20
+ #
21
+ # Design influenced by https://github.com/glejeune/Ruby-Graphviz/blob/852ee119e4e9850f682f0a0089285c36ee16280f/bin/gem2gv
22
+ #
23
+ class Graph
24
+ class << self
25
+ def formats
26
+ Constants::FORMATS
27
+ end
28
+ end
29
+
30
+ #
31
+ # Build a new Graph for +thing+
32
+ # # TODO Accept a presenter that would default to GlobalPresenter with {:rankdir => 'BT'}
33
+ def initialize(name = :G, attributes = [])
34
+ @graph = GraphViz::new(name, attributes)
35
+ @dependencies = []
36
+ @edges = []
37
+ end
38
+
39
+ def <<(task_or_project)
40
+ if task_or_project.respond_to?(:dependencies)
41
+ task = task_or_project
42
+ nodeA = find_or_create_node(task)
43
+ create_edges(nodeA, task.dependencies)
44
+
45
+ # resolve all dependencies we don't know yet
46
+ task.dependencies.each do |dependency|
47
+ unless @dependencies.include?(dependency)
48
+ @dependencies << dependency
49
+ self << dependency
50
+ end
51
+ end
52
+ else
53
+ # it's a project
54
+ project = task_or_project
55
+ cluster = Graph.new(presenter(project).id, presenter(project).attributes)
56
+
57
+ project.tasks.each do |task|
58
+ cluster << task
59
+ end
60
+
61
+ # add all nodes and edges from cluster as a subgraph to @graph
62
+ @graph.add_graph(cluster.graph)
63
+ end
64
+ end
65
+
66
+ def render(format)
67
+ @graph.output(format => String)
68
+ end
69
+
70
+ protected
71
+ attr_reader :graph
72
+
73
+ private
74
+ def create_edges(nodeA, nodes)
75
+ nodes.each do |node|
76
+ nodeB = find_or_create_node(node)
77
+ create_edge(nodeA, nodeB)
78
+ end
79
+ end
80
+
81
+ def find_or_create_node(thing)
82
+ @graph.get_node(presenter(thing).id) || create_node(thing)
83
+ end
84
+
85
+ def create_node(thing)
86
+ @graph.add_nodes(presenter(thing).id, presenter(thing).attributes)
87
+ end
88
+
89
+ def create_edge(nodeA, nodeB)
90
+ edge = [nodeA, nodeB]
91
+ unless @edges.include?(edge) # GraphViz lacks get_edge, so we need to track existing edges ourselfes
92
+ @edges << edge
93
+ @graph.add_edges(nodeA, nodeB)
94
+ end
95
+ end
96
+
97
+ def presenter(thing)
98
+ # TODO Will counter-caching the presenters improve performance?
99
+ if thing.nil?
100
+ NullPresenter.new
101
+ else
102
+ if thing.respond_to?(:dependencies)
103
+ TaskPresenter.new(thing)
104
+ else
105
+ ProjectPresenter.new(thing)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,9 @@
1
+ module TaskWarrior
2
+ class PriorityMapper
3
+ class << self
4
+ def map(json)
5
+ {'H' => :high, 'M' => :medium, 'L' => :low}[json]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Project
5
+ attr_reader :name, :tasks
6
+
7
+ include ActiveModel::Validations
8
+ validates :name, :presence => true
9
+ validate :name_may_not_contain_spaces
10
+
11
+ def initialize(name, tasks = [])
12
+ @name = name
13
+ @tasks = tasks
14
+ @tasks.each{|t| t.project = self}
15
+ end
16
+
17
+ def <<(task)
18
+ @tasks << task
19
+ task.project = self
20
+ end
21
+
22
+ def to_s
23
+ "Project #{name} (#{@tasks.size} tasks)"
24
+ end
25
+
26
+ private
27
+ def name_may_not_contain_spaces
28
+ if !name.blank? and name[/\s/]
29
+ errors.add(:name, "may not contain spaces")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ #
4
+ # Presents a project's attributes suitable for a GraphViz cluster
5
+ #
6
+ class ProjectPresenter
7
+ def initialize(project)
8
+ @project = project
9
+ end
10
+
11
+ def attributes
12
+ {:label => @project.name}
13
+ end
14
+
15
+ def id
16
+ "cluster_#{@project.name}"
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/twdeps/tag.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Tag
5
+ attr_reader :name
6
+
7
+ include ActiveModel::Validations
8
+ validates :name, :presence => true
9
+ validate :name_may_not_contain_spaces
10
+
11
+ def initialize(tag_or_name, tasks = [])
12
+ if tag_or_name.respond_to?(:name)
13
+ @name = tag_or_name.name
14
+ @tasks = tag_or_name.tasks
15
+ else
16
+ @name = tag_or_name
17
+ @tasks = []
18
+ end
19
+
20
+ tasks.each{|task|
21
+ self << task
22
+ }
23
+ end
24
+
25
+ def <<(task)
26
+ @tasks << task unless @tasks.include?(task)
27
+ end
28
+
29
+ def tasks
30
+ @tasks #.dup
31
+ end
32
+
33
+ def to_s
34
+ "Tag: #{name} (#{@tasks.size} tasks)"
35
+ end
36
+
37
+ def ==(other)
38
+ name == other.name
39
+ end
40
+
41
+ private
42
+ def name_may_not_contain_spaces
43
+ if !name.blank? and name[/\s/]
44
+ errors.add(:name, "may not contain spaces")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Task
5
+ attr_accessor :description, :id, :entry, :status, :uuid, :project, :dependencies, :parent, :children, :priority
6
+
7
+ include ActiveModel::Validations
8
+ validates :description, :id, :entry, :status, :uuid, :presence => true
9
+
10
+ validates :id, :numericality => { :only_integer => true, :greater_than => 0}
11
+
12
+ validates :uuid, :format => {:with => /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/,
13
+ :message => "'%{value}' does not match the expected format of a UUID"}
14
+
15
+ validates :status, :inclusion => {:in => [:pending, :waiting, :complete], :message => "%{value} is not a valid status"}
16
+
17
+ validates :priority, :inclusion => {
18
+ :in => [:high, :medium, :low],
19
+ :allow_nil => true,
20
+ :allow_blank => true,
21
+ :message => "%{value} is not a valid priority"
22
+ }
23
+
24
+ validate :entry_cannot_be_in_the_future
25
+
26
+ def initialize(description)
27
+ @description = description
28
+ @dependencies = []
29
+ @children = []
30
+ @tags = []
31
+ end
32
+
33
+ def tags
34
+ @tags
35
+ end
36
+
37
+ def to_s
38
+ "Task '#{description}'".tap{|result| result << " <#{uuid}>" if uuid}
39
+ end
40
+
41
+ private
42
+ def entry_cannot_be_in_the_future
43
+ begin
44
+ if !entry.blank? and entry > DateTime.now
45
+ errors.add(:entry, "can't be in the future")
46
+ end
47
+ rescue
48
+ errors.add(:entry, "must be comparable to DateTime")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ module TaskWarrior
2
+ #
3
+ # A DataMapper that makes new Tasks from a JSON representation
4
+ #
5
+ class TaskMapper
6
+ class << self
7
+ def map(json)
8
+ Task.new(json['description']).tap{|t|
9
+ t.id = json['id'].to_i
10
+ t.uuid = json['uuid']
11
+ t.entry = DateTime.parse(json['entry'])
12
+ t.status = json['status'].to_sym
13
+ t.project = json['project']
14
+
15
+ if json['depends']
16
+ if json['depends'].respond_to?(:split)
17
+ t.dependencies = json['depends'].split(',')
18
+ else
19
+ t.dependencies = json['depends']
20
+ end
21
+ end
22
+
23
+ t.parent = json['parent'] # Children will be cross-indexed in the repository
24
+ t.priority = PriorityMapper.map(json['priority'])
25
+ json['tags'].each{|tag| t.tags << tag} if json['tags']
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ #
4
+ # Presents a task's attributes suitable for a GraphViz node
5
+ #
6
+ class TaskPresenter
7
+ def initialize(task)
8
+ @task = task
9
+ end
10
+
11
+ def attributes
12
+ attrs = {:label => @task.description}
13
+ attrs.merge!({:tooltip => "Status: #{@task.status}"})
14
+
15
+ # TODO Once we see the urgency in the JSON export, we can color-code the nodes
16
+ # http://taskwarrior.org/issues/973
17
+ # attrs.merge!({:fillcolor => 'red', :style => 'filled'})
18
+
19
+ attrs.merge!({:fontcolor => 'gray', :color => 'gray'}) if :completed == @task.status
20
+ attrs
21
+ end
22
+
23
+ def id
24
+ @task.uuid
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ require 'json'
2
+
3
+ module TaskWarrior
4
+ class SimpleTag
5
+ attr_reader :name, :tasks
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @tasks = []
10
+ end
11
+
12
+ def <<(task)
13
+ @tasks << task unless @tasks.include?(task)
14
+ end
15
+ end
16
+
17
+ class Repository
18
+ def initialize(input)
19
+ @tasks = {}
20
+ @projects = Hash.new{|hash, key| hash[key] = Project.new(key)}
21
+ @tags = Hash.new{|hash, key| hash[key] = Tag.new(key)}
22
+
23
+ JSON.parse(input).each{|json|
24
+ task = TaskWarrior::TaskMapper.map(json)
25
+ @tasks[task.uuid] = task
26
+ @projects[task.project].tasks << task if task.project
27
+
28
+ # Create a new Tag object in @tags that is the value for each tag name
29
+ task.tags.each{|tag_name| @tags[tag_name] << task}
30
+ }
31
+
32
+ # Replace the uuid of each dependency with the real task
33
+ @tasks.each_value{|task| task.dependencies.map!{|uuid| @tasks[uuid]}}
34
+
35
+ # Replace the project property of each task with a proper Project object carrying a name and all of the project's tasks
36
+ @tasks.each_value{|task| task.project = @projects[task.project] if task.project}
37
+
38
+ # Add child tasks to their parent, but keep them in the global index
39
+ @tasks.each_value do |task|
40
+ if task.parent
41
+ parent = @tasks[task.parent]
42
+
43
+ if parent # we know the parent
44
+ parent.children << task
45
+ task.parent = parent
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def tasks
52
+ # Do not expose child tasks directly
53
+ @tasks.values.reject{|t| t.parent}
54
+ end
55
+
56
+ # direct lookup by uuid
57
+ def [](uuid)
58
+ @tasks[uuid]
59
+ end
60
+
61
+ def projects
62
+ @projects.values
63
+ end
64
+
65
+ def project(name)
66
+ @projects[name] if @projects.has_key?(name)
67
+ end
68
+
69
+ def tags
70
+ @tags.values
71
+ end
72
+
73
+ def tag(name)
74
+ @tags[name] if @tags.has_key?(name)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
data/lib/twdeps.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "twdeps/version"
2
+
3
+ require "twdeps/task"
4
+ require "twdeps/project"
5
+ require "twdeps/tag"
6
+
7
+ require "twdeps/task_mapper"
8
+ require "twdeps/priority_mapper"
9
+ require "twdeps/task_repository"
10
+
11
+ require "twdeps/graph"
12
+ require "twdeps/task_presenter"
13
+ require "twdeps/project_presenter"
14
+
15
+ require "graphviz"
16
+ require "json"
17
+
18
+ module TaskWarrior
19
+ module Dependencies
20
+ # Your code goes here...
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ [{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
2
+ {"id":2,"description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
3
+ {"id":3,"description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
4
+ {"id":4,"description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
5
+ {"id":5,"description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
6
+ {"id":6,"description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
7
+ ]
@@ -0,0 +1,7 @@
1
+ [{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
2
+ {"id":2,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
3
+ {"id":3,"depends":"9f6f3738-1c08-4f45-8eb4-1e90864c7588","description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
4
+ {"id":4,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a,c992448a-f1ea-4982-8461-47f0705ff509","description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
5
+ {"id":5,"depends":"c992448a-f1ea-4982-8461-47f0705ff509","description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
6
+ {"id":6,"depends":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a","description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
7
+ ]