zzamboni-things2thl 0.7.0 → 0.8.0

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/ChangeLog CHANGED
@@ -1,6 +1,43 @@
1
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
2
+
3
+ * VERSION: Version bump to 0.8.0
4
+
5
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
6
+
7
+ * bin/things2thl, lib/Things2THL.rb: Added new mode of operation
8
+ --inbox (-I) to transfer only tasks from the Inbox.
9
+
10
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
11
+
12
+ * bin/things2thl, lib/Things2THL.rb: Added printing of some
13
+ statistics at the end - number of items created, total time elapsed.
14
+ Can be disabled by specifying -q twice (-qq).
15
+
16
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
17
+
18
+ * bin/things2thl, lib/Things2THL.rb: Added option --sync, which
19
+ causes items (areas, projects and tasks) not to be created if they
20
+ already exist. This is useful if you add more items to Things after
21
+ you have transferred to THL, and want to re-transfer just the new
22
+ ones. This option is disabled by default because the duplicate checking is
23
+ done using only the item's name, so if you have multiple entries
24
+ with the same name in Things, they will only be transferred once
25
+ with --sync enabled.
26
+
27
+ 2009-05-24 Diego Zamboni <diego@zzamboni.org>
28
+
29
+ * bin/things2thl, lib/Things2THL.rb: Added options --areas-as-tags
30
+ and --areas-as-contexts, which specify that areas in Things should
31
+ be transferred to THL as tags/contexts respectively, instead of
32
+ created as separate entities. Thanks to @biomac101 for the suggestion.
33
+
34
+ 2009-05-21 Diego Zamboni <diego@zzamboni.org>
35
+
36
+ * things2thl.gemspec: Regenerated gemspec for version 0.7.0
37
+
1
38
  2009-05-21 Diego Zamboni <diego@zzamboni.org>
2
39
 
3
- * VERSION, lib/Things2THL.rb: Version bump to 0.7.0
40
+ * ChangeLog, VERSION, lib/Things2THL.rb: Version bump to 0.7.0
4
41
 
5
42
  2009-05-21 Diego Zamboni <diego@zzamboni.org>
6
43
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.0
data/bin/things2thl CHANGED
@@ -4,6 +4,7 @@
4
4
  require File.join(File.dirname(__FILE__), *%w".. lib Things2THL")
5
5
  require "optparse"
6
6
  require "ostruct"
7
+ require "time"
7
8
 
8
9
  options=Things2THL.default_options
9
10
  opts = OptionParser.new do |opts|
@@ -14,6 +15,10 @@ opts = OptionParser.new do |opts|
14
15
  puts self
15
16
  exit
16
17
  end
18
+
19
+ def yesno(v)
20
+ "(default: #{v ? 'yes' : 'no'})"
21
+ end
17
22
 
18
23
  opts.separator ''
19
24
  opts.separator("Modes of operation (required):")
@@ -25,14 +30,20 @@ opts = OptionParser.new do |opts|
25
30
  "Convert both projects and areas in Things",
26
31
  " to lists in THL. This implies that",
27
32
  " projects are not nested inside areas.") { options.structure = :projects_areas_as_lists }
33
+ opts.on("-I", "--inbox", "Transfer only Inbox tasks." ) do
34
+ options.inboxonly = true
35
+ options.structure = :projects_as_lists
36
+ end
28
37
 
29
38
  opts.separator ''
30
39
  opts.separator("Options:")
31
- opts.on("--[no-]areas", "Transfer areas from Things (default: #{options.areas ? 'yes' : 'no'})") { |v| options.areas = v }
40
+ opts.on("--[no-]areas", "Transfer areas from Things #{yesno(options.areas)}") { |v| options.areas = v }
41
+ opts.on("--areas-as-tags", "Transfer areas as tags in THL.") { options.areas_as=:tags }
42
+ opts.on("--areas-as-contexts", "Transfer areas as contexts in THL.") { options.areas_as=:contexts }
32
43
  opts.on('--[no-]time-tags',
33
44
  'Consider tags of the form Xmin/Xsec/Xhr as',
34
45
  ' time estimates, set them in THL',
35
- " accordingly (default: #{options.timetags ? 'yes' : 'no'}).") do |value|
46
+ " accordingly #{yesno(options.timetags)}.") do |value|
36
47
  options.timetags = value
37
48
  end
38
49
  opts.on('--[no-]context-tags [REGEX]',
@@ -42,6 +53,7 @@ opts = OptionParser.new do |opts|
42
53
  " Use with no- to disable this feature.") do |regex|
43
54
  options.contexttagsregex = case regex; when false:nil; when "":options.contexttagsregex; else regex; end
44
55
  end
56
+ opts.on("--sync", "Only transfer new items #{yesno(options.sync)}") { options.sync = true }
45
57
  opts.on("--top-level-folder FOLDER", "Do the import inside the named folders,"," instead of the top level",
46
58
  " (Inbox, etc. will also be created there"," instead of their corresponding places)") do |toplevel|
47
59
  options.toplevel = toplevel
@@ -65,11 +77,13 @@ opts = OptionParser.new do |opts|
65
77
 
66
78
  opts.on("-c", "--completed",
67
79
  "Transfer also completed/canceled tasks",
68
- " and projects (default: #{options.completed ? 'yes' : 'no'})") { options.completed = true }
80
+ " and projects #{yesno(options.completed)}") { options.completed = true }
69
81
  opts.on("--[no-]archive-completed",
70
82
  "If transferring completed/canceled tasks,",
71
- " also mark them as archived (default: #{options.archivecompleted ? 'yes' : 'no'})") {|v| options.archivecompleted = v }
72
- opts.on("-q", "--quiet", "Do not print items as they are processed") { options.quiet = true }
83
+ " also mark them as archived #{yesno(options.archivecompleted)}") {|v| options.archivecompleted = v }
84
+ opts.on("-q", "--quiet",
85
+ "Do not print items as they are processed.",
86
+ "Twice: do not print stats at the end.") { options.quiet+=1 }
73
87
  # opts.on("-n", "--dry-run", "Do not create anything in THL, just print the items that would be created") { options.dryrun = true }
74
88
  # opts.on("-n", "--notes", "Shows only tasks with notes") { options[:tasks] = { :onlynotes => true } }
75
89
  # opts.on("-a", "--all", 'Shows all tasks in the focus') { |f| options[:tasks] = { :children => false } }
@@ -100,7 +114,7 @@ end
100
114
  ######################################################################
101
115
 
102
116
  opts.parse!
103
- unless options.structure
117
+ unless options.structure || options.inboxonly
104
118
  puts "Error: you didn't specify the mode of operation to use."
105
119
  puts ''
106
120
  opts.show_usage
@@ -110,27 +124,30 @@ converter = Things2THL.new(options, options.thingsapp, options.thlapp)
110
124
  things = converter.things
111
125
  thl = converter.thl
112
126
 
113
- # Create top-level containers if needed
127
+ # Start timing
128
+ start=Time.new
114
129
 
115
- # First, traverse all areas
116
- if options.areas
117
- puts "Processing Areas of Responsibility" unless options.quiet
118
- things.areas.get.each do |area|
119
- converter.process(Things2THL::ThingsNode.new(area))
130
+ unless options.inboxonly
131
+ # First, traverse all areas
132
+ if options.areas && !options.areas_as
133
+ puts "Processing Areas of Responsibility" unless options.quiet.nonzero?
134
+ things.areas.get.each do |area|
135
+ converter.process(Things2THL::ThingsNode.new(area))
136
+ end
120
137
  end
121
- end
122
138
 
123
- # Next, traverse all projects, putting each one inside its corresponding area
124
- puts "Processing Projects" unless options.quiet
125
- if options.completed
126
- puts " (fetching all projects - this may take a while)" unless options.quiet
127
- projlist=things.projects.get
128
- else
129
- puts " (fetching only open projects - this may take a while)" unless options.quiet
130
- projlist=things.projects[its.status.eq(:open)].get
131
- end
132
- projlist.each do |project|
133
- converter.process(Things2THL::ThingsNode.new(project))
139
+ # Next, traverse all projects, putting each one inside its corresponding area
140
+ puts "Processing Projects" unless options.quiet.nonzero?
141
+ if options.completed
142
+ puts " (fetching all projects - this may take a while)" unless options.quiet.nonzero?
143
+ projlist=things.projects.get
144
+ else
145
+ puts " (fetching only open projects - this may take a while)" unless options.quiet.nonzero?
146
+ projlist=things.projects[its.status.eq(:open)].get
147
+ end
148
+ projlist.each do |project|
149
+ converter.process(Things2THL::ThingsNode.new(project))
150
+ end
134
151
  end
135
152
 
136
153
  # Now do the tasks
@@ -138,13 +155,18 @@ end
138
155
  # - to_dos returns not only tasks, also projects (not areas)
139
156
  # - to-dos returns tasks from all the views: Inbox, Today, Scheduled, Someday, and Next, so we have
140
157
  # to separate them and create the appropriate containers as needed
141
- puts "Processing tasks" unless options.quiet
158
+ puts "Processing tasks" unless options.quiet.nonzero?
159
+ if options.inboxonly
160
+ queryobj=things.lists['Inbox']
161
+ else
162
+ queryobj=things
163
+ end
142
164
  if options.completed
143
- puts " (fetching all tasks - this may take a while)" unless options.quiet
144
- tasklist=things.to_dos.get
165
+ puts " (fetching all tasks - this may take a while)" unless options.quiet.nonzero?
166
+ tasklist=queryobj.to_dos.get
145
167
  else
146
- puts " (fetching only open tasks - this may take a while)" unless options.quiet
147
- tasklist=things.to_dos[its.status.eq(:open)].get
168
+ puts " (fetching only open tasks - this may take a while)" unless options.quiet.nonzero?
169
+ tasklist=queryobj.to_dos[its.status.eq(:open)].get
148
170
  end
149
171
  tasklist.each do |t|
150
172
  task=Things2THL::ThingsNode.new(t)
@@ -152,3 +174,11 @@ tasklist.each do |t|
152
174
  converter.process(task)
153
175
  end
154
176
 
177
+ # End timing
178
+ diff=Time.new-start
179
+
180
+ # Print stats at the end
181
+ unless options.quiet >= 2
182
+ puts "Conversion done in #{diff} seconds."
183
+ converter.created.each_pair { |key,val| puts " #{val} #{key}#{val!=1 ? 's' : ''} created" }
184
+ end
data/lib/Things2THL.rb CHANGED
@@ -263,7 +263,7 @@ module Things2THL
263
263
  ####################################################################
264
264
 
265
265
  class Converter
266
- attr_accessor :options, :things, :thl
266
+ attr_accessor :options, :things, :thl, :created
267
267
 
268
268
  def initialize(opt_struct = nil, things_location = nil, thl_location = nil)
269
269
  @options=opt_struct || Things2THL.default_options
@@ -295,6 +295,14 @@ module Things2THL
295
295
  # id_ of each node that belongs to that focus. Existence of the key
296
296
  # indicates existence in the focus.
297
297
  @cache_focus = {}
298
+
299
+ # Statistics
300
+ @created = {
301
+ :task => 0,
302
+ :list => 0,
303
+ :folder => 0
304
+ }
305
+
298
306
  end
299
307
 
300
308
  # Get the type of the THL node that corresponds to the given Things node,
@@ -360,24 +368,48 @@ module Things2THL
360
368
  end
361
369
  end
362
370
 
371
+ # Simplified version of find_or_create which simply takes :new and :name
372
+ def simple_find_or_create(what, name, parent = @thl.folders_group.get)
373
+ find_or_create({:new => what, :with_properties => { :name => name } }, parent)
374
+ end
375
+
363
376
  # Find or create a list or a folder inside the given parent (or the top-level folders group if not given)
364
- def find_or_create(what, name, parent = @thl.folders_group.get)
365
- unless what == :list || what == :folder
366
- raise "find_or_create: 'what' parameter has to be :list or :folder"
377
+ def find_or_create(props, parent = @thl.folders_group.get)
378
+ puts "find_or_create: props = #{props.inspect}" if $DEBUG
379
+ what=props[:new]
380
+ name=(what==:task) ? props[:with_properties][:title] : props[:with_properties][:name]
381
+ parentclass=parent.class_.get
382
+ unless what == :list || what == :folder || what == :task
383
+ raise "find_or_create: 'props[:new]' parameter has to be :list, :folder or :task"
367
384
  end
368
385
  puts "parent of #{name} = #{parent}" if $DEBUG
369
- if parent.class_.get != :folder
370
- raise "find_or_create: parent is not a folder, it's a #{parent.class_.get}"
386
+ if (what == :folder || what == :list) && parentclass != :folder
387
+ raise "find_or_create: parent is not a folder, it's a #{parentclass}"
388
+ elsif what == :task && parentclass != :list && parentclass != :task
389
+ raise "find_or_create: parent is not a list, it's a #{parentclass}"
371
390
  else
372
- if parent.groups[name].exists
373
- parent.groups[name].get
391
+ if what == :task
392
+ query = parent.tasks[its.title.eq(name)]
393
+ if ! query.get.empty?
394
+ query.get[0]
395
+ else
396
+ @created[what]+=1
397
+ parent.end.make(props)
398
+ end
374
399
  else
375
- parent.end.make(:new => what, :with_properties => {:name => name})
400
+ query = parent.groups[name]
401
+ if query.exists
402
+ query.get
403
+ else
404
+ @created[what]+=1
405
+ parent.end.make(props)
406
+ end
376
407
  end
377
408
  end
378
409
  end
379
410
 
380
411
  def new_folder(name, parent = @thl.folders_group.get)
412
+ @created[:folder]+=1
381
413
  parent.end.make(:new => :folder,
382
414
  :with_properties => { :name => name })
383
415
  end
@@ -388,7 +420,11 @@ module Things2THL
388
420
 
389
421
  unless @top_level_node
390
422
  # Create the top-level node if we don't have it cached yet
391
- @top_level_node=new_folder(options.toplevel)
423
+ if options.sync
424
+ @top_level_node=simple_find_or_create(:folder, options.toplevel)
425
+ else
426
+ @top_level_node=new_folder(options.toplevel)
427
+ end
392
428
  end
393
429
  @top_level_node
394
430
  end
@@ -416,17 +452,17 @@ module Things2THL
416
452
  when 'Trash', 'Today'
417
453
  nil
418
454
  when 'Inbox', 'Next'
419
- find_or_create(:list, focusname, top_level_node)
455
+ simple_find_or_create(:list, focusname, top_level_node)
420
456
  when 'Scheduled', 'Logbook'
421
- find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
457
+ simple_find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
422
458
  when 'Someday'
423
- find_or_create(:folder, focusname, top_level_node)
459
+ simple_find_or_create(:folder, focusname, top_level_node)
424
460
  when 'Projects'
425
461
  if thl_node_type(:project) == :task
426
- find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
462
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
427
463
  else
428
464
  if options.projectsfolder
429
- find_or_create(:folder, options.projectsfolder, top_level_node)
465
+ simple_find_or_create(:folder, options.projectsfolder, top_level_node)
430
466
  else
431
467
  top_level_node
432
468
  end
@@ -443,15 +479,15 @@ module Things2THL
443
479
  when 'Next'
444
480
  top_level_node
445
481
  when 'Scheduled', 'Logbook'
446
- find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
482
+ simple_find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
447
483
  when 'Someday'
448
- find_or_create(:folder, focusname, top_level_node)
484
+ simple_find_or_create(:folder, focusname, top_level_node)
449
485
  when 'Projects'
450
486
  if thl_node_type(:project) == :task
451
- find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
487
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
452
488
  else
453
489
  if options.projectsfolder
454
- find_or_create(:folder, options.projectsfolder, top_level_node)
490
+ simple_find_or_create(:folder, options.projectsfolder, top_level_node)
455
491
  else
456
492
  top_level_node
457
493
  end
@@ -478,7 +514,7 @@ module Things2THL
478
514
  return top_level_for_focus('Someday')
479
515
  else
480
516
  if options.areasfolder
481
- return find_or_create(:folder, options.areasfolder || 'Areas', top_level_node)
517
+ return simple_find_or_create(:folder, options.areasfolder || 'Areas', top_level_node)
482
518
  else
483
519
  return top_level_node
484
520
  end
@@ -491,10 +527,10 @@ module Things2THL
491
527
  if foci.empty?
492
528
  if node.project?
493
529
  tl=top_level_for_node(node.project)
494
- if !tl && node.area?
530
+ if !tl && node.area? && !options.areas_as
495
531
  tl=top_level_for_node(node.area)
496
532
  end
497
- elsif node.area?
533
+ elsif node.area? && !options.areas_as
498
534
  tl=top_level_for_node(node.area)
499
535
  end
500
536
  return tl
@@ -530,7 +566,7 @@ module Things2THL
530
566
  when :area
531
567
  tlcontainer
532
568
  when :project
533
- if options.areas && (options.structure != :projects_areas_as_lists) && node.area?
569
+ if options.areas && !options.areas_as && (options.structure != :projects_areas_as_lists) && node.area?
534
570
  get_cached_or_process(node.area)
535
571
  else
536
572
  tlcontainer
@@ -538,7 +574,7 @@ module Things2THL
538
574
  when :selected_to_do
539
575
  if node.project?
540
576
  get_cached_or_process(node.project)
541
- elsif node.area? && options.areas
577
+ elsif node.area? && options.areas && !options.areas_as
542
578
  get_cached_or_process(node.area)
543
579
  else
544
580
  # It's a loose task
@@ -552,9 +588,9 @@ module Things2THL
552
588
  # so if the container is a folder, we have to create a list to hold the task
553
589
  if container && (container.class_.get == :folder) && (thl_node_type(node) == :task)
554
590
  if node.type == :project
555
- find_or_create(:list, options.projectsfolder || 'Projects', container)
591
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', container)
556
592
  else
557
- find_or_create(:list, loose_tasks_name(container), container)
593
+ simple_find_or_create(:list, loose_tasks_name(container), container)
558
594
  end
559
595
  else
560
596
  container
@@ -566,8 +602,15 @@ module Things2THL
566
602
  new_node_type = thl_node_type(node)
567
603
  new_node_props = props_from_node(node)
568
604
  additional_nodes = new_node_props.delete(:__newnodes__)
569
- result=parent.end.make(:new => new_node_type,
570
- :with_properties => new_node_props )
605
+ new_node_spec = {
606
+ :new => new_node_type,
607
+ :with_properties => new_node_props }
608
+ if options.sync
609
+ result=find_or_create(new_node_spec, parent)
610
+ else
611
+ @created[new_node_type]+=1
612
+ result=parent.end.make(new_node_spec)
613
+ end
571
614
  if node.type == :area || node.type == :project
572
615
  @cache_nodes[node.id_]={}
573
616
  @cache_nodes[node.id_][:things_node] = node
@@ -576,7 +619,12 @@ module Things2THL
576
619
  # Add new nodes
577
620
  if additional_nodes
578
621
  additional_nodes.each do |n|
579
- result.end.make(n)
622
+ if options.sync
623
+ find_or_create(n, result)
624
+ else
625
+ @created[n[:new]]+=1
626
+ result.end.make(n)
627
+ end
580
628
  end
581
629
  end
582
630
  return result
@@ -598,11 +646,11 @@ module Things2THL
598
646
  container=container_for(node)
599
647
  puts "Container for #{node.name}: #{container}" if $DEBUG
600
648
  unless container
601
- puts "Skipping trashed task '#{node.name}'" unless options.quiet
649
+ puts "Skipping trashed task '#{node.name}'" unless options.quiet.nonzero?
602
650
  return
603
651
  end
604
652
 
605
- unless (options.quiet)
653
+ unless (options.quiet.nonzero?)
606
654
  bullet = (node.type == :area) ? "*" : ((node.status == :completed) ? "✓" : (node.status == :canceled) ? "×" : "-")
607
655
  puts bullet + " " + node.name
608
656
  end
@@ -689,17 +737,34 @@ module Things2THL
689
737
  # Process tags
690
738
  def process_tags(node, prop, inherit_project_tags, inherit_area_tags)
691
739
  tasktags = node.tags.map {|t| t.name }
740
+ taskcontexts = []
692
741
  if inherit_project_tags
693
742
  # Merge project and area tags
694
743
  if node.project?
695
744
  tasktags |= node.project.tags.map {|t| t.name }
696
745
  if options.areas && node.project.area?
697
746
  tasktags |= node.project.area.tags.map {|t| t.name }
747
+ if options.areas_as
748
+ case options.areas_as
749
+ when :tags
750
+ tasktags.push(node.project.area.name)
751
+ when :contexts
752
+ taskcontexts.push(node.project.area.name)
753
+ end
754
+ end
698
755
  end
699
756
  end
700
757
  end
701
758
  if options.areas && node.area? && inherit_area_tags
702
759
  tasktags |= node.area.tags.map {|t| t.name }
760
+ if options.areas_as
761
+ case options.areas_as
762
+ when :tags
763
+ tasktags.push(node.area.name)
764
+ when :contexts
765
+ taskcontexts.push(node.area.name)
766
+ end
767
+ end
703
768
  end
704
769
  unless tasktags.empty?
705
770
  # First process time-estimate tags if needed
@@ -742,6 +807,12 @@ module Things2THL
742
807
  end
743
808
  end].join(' ')
744
809
  end
810
+ unless taskcontexts.empty?
811
+ prop[:title] = [prop[:title], taskcontexts.map do |c|
812
+ # Contexts cannot have spaces, we also remove any initial @'s before adding our own.
813
+ "@" + c.gsub(/^@+/, "").gsub(/ /, '_')
814
+ end].join(' ')
815
+ end
745
816
  end
746
817
 
747
818
  # Check if node is in the Today list
@@ -835,13 +906,16 @@ module Things2THL
835
906
  options.database = nil
836
907
  options.structure = nil
837
908
  options.areas = true
838
- options.quiet = false
909
+ options.quiet = 0
839
910
  options.archivecompleted = true
840
911
  options.projectsfolder = nil
841
912
  options.areasfolder = nil
842
913
  options.contexttagsregex = '^@'
843
914
  options.timetagsregex = '^(\d+)(min|sec|hr)$'
844
915
  options.timetags = false
916
+ options.areas_as = nil
917
+ options.sync = false
918
+ options.inboxonly = false
845
919
  return options
846
920
  end
847
921
 
data/things2thl.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{things2thl}
5
- s.version = "0.7.0"
5
+ s.version = "0.8.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Diego Zamboni"]
9
- s.date = %q{2009-05-21}
9
+ s.date = %q{2009-05-25}
10
10
  s.default_executable = %q{things2thl}
11
11
  s.description = %q{Library and command-line tool for migrating Things data to The Hit List}
12
12
  s.email = %q{diego@zzamboni.org}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zzamboni-things2thl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Diego Zamboni
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-21 00:00:00 -07:00
12
+ date: 2009-05-25 00:00:00 -07:00
13
13
  default_executable: things2thl
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency