rumember 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
4
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :gemcutter
2
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Tim Pope
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,59 @@
1
+ Rumember
2
+ ========
3
+
4
+ Ruby API and command line client for [Remember The
5
+ Milk](http://www.rememberthemilk.com/).
6
+
7
+ ## Command line usage
8
+
9
+ The sole motivation for this project was a quick way to capture to-dos
10
+ from the command line. As such, I've chosen a chosen a very short
11
+ command name of `ru` (something I'd normally never allow myself to do).
12
+ All arguments are joined with spaces and used to invoke Remember The
13
+ Milk's [Smart Add](http://www.rememberthemilk.com/services/smartadd/)
14
+ feature.
15
+
16
+ ru buy milk #errand
17
+
18
+ Browser based authentication is triggered the first time `ru` is run,
19
+ and the resulting token is cached in `~/.rtm.yml`.
20
+
21
+ I was originally planning to add support for the full range of
22
+ operations possible in Remember The Milk, but after pondering the
23
+ interface, this seems unlikely. I just can't imagine myself forgoing
24
+ the web interface in favor of something like:
25
+
26
+ ru --complete 142857 # Ain't gonna happen
27
+
28
+ ## API Usage
29
+
30
+ The API is a bit more fleshed out than the command line interface, but
31
+ still incomplete, under-documented, and under-tested (I have additional
32
+ integration tests I won't publish because they are specific to my RTM
33
+ account). You'll need to familiarize yourself with [Remember The Milk's
34
+ API](http://www.rememberthemilk.com/services/api/). In particular, you
35
+ need to understand what a timeline is.
36
+
37
+ interface = Rumember.new(api_key, shared_secret)
38
+ interface = Rumember.new # Uses built in credentials
39
+ interface.dispatch('test.echo')
40
+
41
+ account = interface.account(auth_token)
42
+ account = interface.account # browser based and cached
43
+ account = Rumember.account # Rumember.new.account shortcut
44
+
45
+ timeline = account.timeline # cached
46
+ timeline = account.new_timeline # fresh each time
47
+
48
+ timeline.smart_add('buy milk #errand')
49
+
50
+ list = timeline.lists.first
51
+ task = list.tasks.first
52
+ transaction = task.complete
53
+ transaction.undo
54
+
55
+ ## Contributing
56
+
57
+ Please follow [Git commit message best
58
+ practices](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
59
+ when submitting a pull request.
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new(:spec) do |spec|
5
+ spec.pattern = "spec/**/*_spec.rb"
6
+ end
7
+
8
+ task :default => :spec
data/bin/ru ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)),'lib'))
4
+ require 'rumember'
5
+
6
+ Rumember.run(ARGV)
@@ -0,0 +1,156 @@
1
+ class Rumember
2
+
3
+ class Error < RuntimeError
4
+ end
5
+
6
+ class ResponseError < Error
7
+ attr_accessor :code
8
+ end
9
+
10
+ API_KEY = '36f62f69fba7135e8049adbe307ff9ba'
11
+ SHARED_SECRET = '0c33513097c09be4'
12
+
13
+ module Dispatcher
14
+
15
+ def dispatch(method, params = {})
16
+ parent.dispatch(method, self.params.merge(params))
17
+ end
18
+
19
+ def transaction_dispatch(*args)
20
+ response = dispatch(*args)
21
+ yield response if block_given?
22
+ Transaction.new(self, response)
23
+ end
24
+
25
+ def lists
26
+ dispatch('lists.getList')['lists']['list'].map do |list|
27
+ List.new(self, list)
28
+ end
29
+ end
30
+
31
+ def locations
32
+ dispatch('locations.getList')['locations']['location'].map do |list|
33
+ Location.new(self, list)
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ def self.run(argv)
40
+ if argv.empty?
41
+ puts "Logged in as #{account.username}"
42
+ else
43
+ account.smart_add(argv.join(' '))
44
+ end
45
+ rescue Error
46
+ $stderr.puts "#$!"
47
+ exit 1
48
+ rescue Interrupt
49
+ $stderr.puts "Interrupted!"
50
+ exit 130
51
+ end
52
+
53
+ attr_reader :api_key, :shared_secret
54
+
55
+ def initialize(api_key = API_KEY, shared_secret = SHARED_SECRET)
56
+ @api_key = api_key
57
+ @shared_secret = shared_secret
58
+ end
59
+
60
+ def api_sig(params)
61
+ require 'digest/md5'
62
+ Digest::MD5.hexdigest(
63
+ shared_secret + params.sort_by {|k,v| k.to_s}.join
64
+ )
65
+ end
66
+
67
+ def sign(params)
68
+ params = params.merge('api_key' => api_key)
69
+ params.update('api_sig' => api_sig(params))
70
+ end
71
+
72
+ def params(params)
73
+ require 'cgi'
74
+ sign(params).map do |k,v|
75
+ "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
76
+ end.join('&')
77
+ end
78
+
79
+ def auth_url(perms = :delete, extra = {})
80
+ "http://rememberthemilk.com/services/auth?" +
81
+ params({'perms' => perms}.merge(extra))
82
+ end
83
+
84
+ def authenticate
85
+ require 'launchy'
86
+ frob = dispatch('auth.getFrob')['frob']
87
+ Launchy.open(auth_url(:delete, 'frob' => frob))
88
+ first = true
89
+ puts 'Press enter when authentication is complete'
90
+ $stdin.gets
91
+ dispatch('auth.getToken', 'frob' => frob)['auth']
92
+ end
93
+
94
+ def reconfigure
95
+ token = authenticate['token']
96
+ File.open(self.class.config_file,'w') do |f|
97
+ f.puts "auth_token: #{token}"
98
+ end
99
+ end
100
+
101
+ def self.config_file
102
+ File.expand_path('~/.rtm.yml')
103
+ end
104
+
105
+ def account(auth_token = nil)
106
+ if auth_token
107
+ Account.new(self, auth_token)
108
+ else
109
+ require 'yaml'
110
+ @account ||=
111
+ begin
112
+ reconfigure unless File.exist?(self.class.config_file)
113
+ t = YAML.load(File.read(self.class.config_file))['auth_token']
114
+ account(t)
115
+ end
116
+ end
117
+ end
118
+
119
+ alias autoconfigure account
120
+
121
+ def self.account
122
+ @account ||= new.account
123
+ end
124
+
125
+ def url(params)
126
+ "http://api.rememberthemilk.com/services/rest?#{params(params)}"
127
+ end
128
+
129
+ def dispatch(method, params = {})
130
+ require 'json'
131
+ require 'open-uri'
132
+ raw = open(url(params.merge('method' => "rtm.#{method}", 'format' => 'json'))).read
133
+ rsp = JSON.parse(raw)['rsp']
134
+ case rsp['stat']
135
+ when 'fail'
136
+ error = ResponseError.new(rsp['err']['msg'])
137
+ error.code = rsp['err']['code']
138
+ error.set_backtrace caller
139
+ raise error
140
+ when 'ok'
141
+ rsp.delete('stat')
142
+ rsp
143
+ else
144
+ raise ResponseError.new(rsp.inspect)
145
+ end
146
+ end
147
+
148
+ autoload :Abstract, 'rumember/abstract'
149
+ autoload :Account, 'rumember/account'
150
+ autoload :Timeline, 'rumember/timeline'
151
+ autoload :Transaction, 'rumember/transaction'
152
+ autoload :List, 'rumember/list'
153
+ autoload :Location, 'rumember/location'
154
+ autoload :Task, 'rumember/task'
155
+
156
+ end
@@ -0,0 +1,51 @@
1
+ class Rumember
2
+ class Abstract
3
+
4
+ include Dispatcher
5
+ attr_reader :parent
6
+
7
+ def initialize(parent, attributes)
8
+ @parent = parent
9
+ @attributes = attributes
10
+ end
11
+
12
+ def params
13
+ {}
14
+ end
15
+
16
+ def self.reader(*methods, &block)
17
+ methods.each do |method|
18
+ define_method(method) do
19
+ value = @attributes[method.to_s]
20
+ if block && !value.nil?
21
+ yield value
22
+ else
23
+ value
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.integer_reader(*methods)
30
+ reader(*methods) do |value|
31
+ Integer(value)
32
+ end
33
+ end
34
+
35
+ def self.boolean_reader(*methods)
36
+ reader(*methods) do |value|
37
+ value == '1' ? true : false
38
+ end
39
+ methods.each do |method|
40
+ alias_method "#{method}?", method
41
+ end
42
+ end
43
+
44
+ def self.time_reader(*methods)
45
+ reader(*methods) do |value|
46
+ Time.parse(value) unless value.empty?
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ class Rumember
2
+ class Account
3
+
4
+ include Dispatcher
5
+ attr_reader :auth_token
6
+
7
+ def initialize(interface, auth_token)
8
+ @interface = interface
9
+ @auth_token = auth_token
10
+ end
11
+
12
+ def parent
13
+ @interface
14
+ end
15
+
16
+ def params
17
+ {'auth_token' => auth_token}
18
+ end
19
+
20
+ def new_timeline
21
+ Timeline.new(self)
22
+ end
23
+
24
+ def timeline
25
+ @timeline ||= new_timeline
26
+ end
27
+
28
+ def smart_add(name)
29
+ timeline.smart_add(name)
30
+ end
31
+
32
+ def username
33
+ @username ||= dispatch('auth.checkToken')['auth']['user']['username']
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.inspect}: #{username}>"
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ class Rumember
2
+ class List < Abstract
3
+
4
+ reader :name, :filter
5
+ boolean_reader :archived, :deleted, :locked, :smart
6
+ integer_reader :id, :position
7
+ alias to_s name
8
+
9
+ def params
10
+ {'list_id' => id}
11
+ end
12
+
13
+ def tasks
14
+ dispatch('tasks.getList')['tasks']['list']['taskseries'].map do |ts|
15
+ Task.new(self, ts)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ class Rumember
2
+ class Location < Abstract
3
+ integer_reader :id, :zoom
4
+ reader :name, :address
5
+ boolean_reader :viewable
6
+ reader :latitude, :longitude do |l|
7
+ Float(l)
8
+ end
9
+ alias to_s name
10
+ end
11
+ end
@@ -0,0 +1,96 @@
1
+ class Rumember
2
+ class Task < Abstract
3
+ integer_reader :id, :location_id
4
+ time_reader :created, :modified
5
+ reader :name, :source, :url
6
+ alias taskseries_id id
7
+
8
+ def task_id
9
+ Integer(@attributes['task']['id'])
10
+ end
11
+
12
+ def tags
13
+ if @attributes['tags'].empty?
14
+ []
15
+ else
16
+ Array(@attributes['tags']['tag'])
17
+ end
18
+ end
19
+
20
+ def params
21
+ {'taskseries_id' => taskseries_id, 'task_id' => task_id}
22
+ end
23
+
24
+ def transaction_dispatch(*args)
25
+ super(*args) do |response|
26
+ @attributes = response['list']['taskseries']
27
+ end
28
+ end
29
+
30
+ def delete
31
+ transaction_dispatch('tasks.delete')
32
+ end
33
+
34
+ def complete
35
+ transaction_dispatch('tasks.complete')
36
+ end
37
+
38
+ def uncomplete
39
+ transaction_dispatch('tasks.uncomplete')
40
+ end
41
+
42
+ def postpone
43
+ transaction_dispatch('tasks.postpone')
44
+ end
45
+
46
+ def add_tags(tags)
47
+ transaction_dispatch('tasks.addTags', 'tags' => tags)
48
+ end
49
+
50
+ def remove_tags(tags)
51
+ transaction_dispatch('tasks.removeTags', 'tags' => tags)
52
+ end
53
+
54
+ def raise_priority
55
+ transaction_dispatch('tasks.movePriority', 'direction' => 'up')
56
+ end
57
+
58
+ def lower_priority
59
+ transaction_dispatch('tasks.movePriority', 'direction' => 'down')
60
+ end
61
+
62
+ def move_to(list)
63
+ transaction_dispatch(
64
+ 'tasks.moveTo',
65
+ 'from_list_id' => parent.id,
66
+ 'to_list_id' => list.id
67
+ ).tap do
68
+ @parent = list
69
+ end
70
+ end
71
+
72
+ %w(Estimate Name Priority Recurrence URL).each do |attr|
73
+ define_method("set_#{attr.downcase}") do |value|
74
+ transaction_dispatch("tasks.set#{attr}", attr.downcase => value)
75
+ end
76
+ alias_method "#{attr.downcase}=", "set_#{attr.downcase}"
77
+ end
78
+
79
+ def set_tags(tags)
80
+ transaction_dispatch('tasks.setTags', 'tags' => Array(tags).join(' '))
81
+ end
82
+ alias tags= set_tags
83
+
84
+ def set_due_date(url)
85
+ transaction_dispatch('tasks.setDueDate', 'url' => url)
86
+ end
87
+ alias due_date= set_due_date
88
+
89
+ def set_location(id)
90
+ id = id.location_id if id.respond_to?(:location_id)
91
+ transaction_dispatch('tasks.setLocation', id)
92
+ end
93
+ alias location= set_location
94
+
95
+ end
96
+ end
@@ -0,0 +1,20 @@
1
+ class Rumember
2
+ class Timeline
3
+
4
+ include Dispatcher
5
+ attr_reader :id, :parent
6
+
7
+ def initialize(parent, id = nil)
8
+ @parent = parent
9
+ @id = (id || parent.dispatch('timelines.create').fetch('timeline')).to_i
10
+ end
11
+
12
+ def params
13
+ {'timeline' => id}
14
+ end
15
+
16
+ def smart_add(name)
17
+ dispatch('tasks.add', 'parse' => 1, 'name' => name)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ class Rumember
2
+ class Transaction < Abstract
3
+
4
+ def id
5
+ Integer(@attributes['transaction']['id'])
6
+ end
7
+
8
+ def undoable?
9
+ @attributes['transaction']['undoable'] == '1'
10
+ end
11
+
12
+ def undone?
13
+ !!@undone
14
+ end
15
+
16
+ def response
17
+ @attributes
18
+ end
19
+
20
+ def params
21
+ {'transaction_id' => id}
22
+ end
23
+
24
+ def undo
25
+ if undone? || !undoable?
26
+ false
27
+ else
28
+ dispatch('transactions.undo')
29
+ @undone = true
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rumember"
3
+ s.version = "1.0.0"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Tim Pope"]
6
+ s.email = ["code@tpope.n"+'et']
7
+ s.homepage = "http://github.com/tpope/rumember"
8
+ s.summary = "Remember The Milk Ruby API and command line client"
9
+
10
+ s.files = `git ls-files`.split("\n")
11
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ s.require_paths = ["lib"]
14
+
15
+ s.add_runtime_dependency("json", ["~> 1.4.0"])
16
+ s.add_runtime_dependency("launchy", ["~> 0.3.0"])
17
+ s.add_development_dependency("rspec", ["~> 2.5"])
18
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec/spec_helper')
2
+
3
+ describe Rumember::Timeline do
4
+
5
+ let :interface do
6
+ stub.as_null_object
7
+ end
8
+
9
+ let :account do
10
+ Rumember::Account.new(interface, 'key')
11
+ end
12
+
13
+ subject do
14
+ Rumember::Timeline.new(account, 3)
15
+ end
16
+
17
+ its(:parent) { should == account }
18
+ its(:params) { should == { 'timeline' => 3 } }
19
+
20
+ describe '#smart_add' do
21
+ it 'should dispatch rtm.tasks.add' do
22
+ interface.should_receive(:dispatch).with(
23
+ 'tasks.add',
24
+ 'auth_token' => 'key',
25
+ 'name' => 'buy milk',
26
+ 'parse' => 1,
27
+ 'timeline' => 3
28
+ )
29
+ subject.smart_add('buy milk')
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,51 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec/spec_helper')
2
+
3
+ describe Rumember::Transaction do
4
+
5
+ let :response do
6
+ { 'transaction' => { 'id' => '1', 'undoable' => '1' } }
7
+ end
8
+
9
+ let :parent do
10
+ stub.as_null_object
11
+ end
12
+
13
+ subject do
14
+ Rumember::Transaction.new(parent, response)
15
+ end
16
+
17
+ its(:parent) { should == parent }
18
+ its(:response) { should == response }
19
+ its(:id) { should == 1 }
20
+ its(:undoable?) { should be_true }
21
+ its(:params) { should == {'transaction_id' => 1} }
22
+
23
+ describe '#undo' do
24
+ context 'when the transaction is not undoable' do
25
+ let :response do
26
+ { 'transaction' => { 'id' => '1', 'undoable' => '0' } }
27
+ end
28
+
29
+ it 'should not dispatch rtm.transactions.undo' do
30
+ subject.should_not_receive(:dispatch).with('transactions.undo')
31
+ subject.undo
32
+ end
33
+
34
+ it 'should return false' do
35
+ subject.undo.should be_false
36
+ end
37
+ end
38
+
39
+ context 'when the transaction is undoable' do
40
+ it 'should dispatch rtm.transactions.undo' do
41
+ subject.should_receive(:dispatch).with('transactions.undo')
42
+ subject.undo
43
+ end
44
+
45
+ it 'should return true' do
46
+ subject.undo.should be_true
47
+ end
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec/spec_helper')
2
+
3
+ describe Rumember do
4
+ subject do
5
+ Rumember.new('abc123', 'BANANAS')
6
+ end
7
+
8
+ describe '#api_sig' do
9
+ it 'should MD5 the concatenated sorted parameters' do
10
+ subject.api_sig(:yxz => 'foo', :feg => 'bar', :abc => 'baz').should ==
11
+ '82044aae4dd676094f23f1ec152159ba'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(File.dirname(__FILE__)),'lib'))
2
+ require 'rumember'
3
+ begin; require 'rubygems'; rescue LoadError; end
4
+ require 'rspec'
5
+
6
+ RSpec.configure do |config|
7
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rumember
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Pope
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-14 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &75735360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.4.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *75735360
25
+ - !ruby/object:Gem::Dependency
26
+ name: launchy
27
+ requirement: &75735060 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *75735060
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &75734750 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '2.5'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *75734750
47
+ description:
48
+ email:
49
+ - code@tpope.net
50
+ executables:
51
+ - ru
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - MIT-LICENSE
58
+ - README.markdown
59
+ - Rakefile
60
+ - bin/ru
61
+ - lib/rumember.rb
62
+ - lib/rumember/abstract.rb
63
+ - lib/rumember/account.rb
64
+ - lib/rumember/list.rb
65
+ - lib/rumember/location.rb
66
+ - lib/rumember/task.rb
67
+ - lib/rumember/timeline.rb
68
+ - lib/rumember/transaction.rb
69
+ - rumember.gemspec
70
+ - spec/rumember/timeline_spec.rb
71
+ - spec/rumember/transaction_spec.rb
72
+ - spec/rumember_spec.rb
73
+ - spec/spec_helper.rb
74
+ homepage: http://github.com/tpope/rumember
75
+ licenses: []
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 1.8.6
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Remember The Milk Ruby API and command line client
98
+ test_files: []