remember-the-ruby 0.4.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 ADDED
@@ -0,0 +1 @@
1
+ This will be a command line interface to Remember the Milk
data/bin/rtr ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'remember-the-ruby'))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'rtr'))
5
+
6
+ require 'pp'
7
+
8
+ $storage = RememberTheRuby::Storage.new
9
+ $storage[:cache_timeout] ||= 15.minutes
10
+
11
+ usage_and_exit if ARGV.length < 1
12
+
13
+ command = ARGV.shift
14
+ options = ARGV.dup
15
+
16
+ proc = $methods[command.to_sym]
17
+
18
+ usage_and_exit unless proc
19
+
20
+ proc.call(options)
@@ -0,0 +1,9 @@
1
+ require 'date'
2
+
3
+ class DateTime
4
+
5
+ def blank?
6
+ false
7
+ end
8
+
9
+ end
@@ -0,0 +1,7 @@
1
+ module Enumerable
2
+
3
+ def sorted_by(attribute)
4
+ self.sort_by { |e| e[attribute] }
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class Fixnum
2
+
3
+ def minutes
4
+ self * 60
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class NilClass
2
+
3
+ def blank?
4
+ true
5
+ end
6
+
7
+ end
@@ -0,0 +1,22 @@
1
+ require 'date'
2
+ require 'rubygems'
3
+ require 'term/ansicolor'
4
+
5
+ class String
6
+
7
+ include Term::ANSIColor
8
+
9
+ def to_date
10
+ begin
11
+ date = DateTime.parse(self)
12
+ rescue
13
+ date = nil
14
+ end
15
+ date
16
+ end
17
+
18
+ def blank?
19
+ self == ""
20
+ end
21
+
22
+ end
@@ -0,0 +1,74 @@
1
+ module RememberTheRuby
2
+ class API
3
+
4
+ def initialize(options={})
5
+ @transport = RPC::Transport.new(
6
+ :key => 'cb877b5c450d216ce9e563394dff6c07',
7
+ :secret => '97cbbc48a9b1b54c',
8
+ :frob => options[:frob],
9
+ :token => options[:token]
10
+ )
11
+ end
12
+
13
+ def authorization_url
14
+ self.frob = @transport.auth.get_frob
15
+ @transport.auth.get_authorization_url
16
+ end
17
+
18
+ def authenticate
19
+ self.token = @transport.auth.get_token
20
+ end
21
+
22
+ # first-order objects #####################################################
23
+
24
+ def lists
25
+ @transport.lists.get_list
26
+ end
27
+
28
+ def settings
29
+ @transport.settings.get_list
30
+ end
31
+
32
+ def tasks
33
+ @transport.tasks.get_list
34
+ end
35
+
36
+ # derivative objects ######################################################
37
+
38
+ def default_list
39
+ lists.find(settings["defaultlist"])
40
+ end
41
+
42
+ def tags
43
+ found_tags = EntityList.new(@transport, Tag)
44
+ tasks.each do |task|
45
+ task.tags.each do |tag|
46
+ found = false
47
+ found_tags.each do |found_tag|
48
+ if tag.to_s == found_tag.to_s
49
+ found = true
50
+ break
51
+ end
52
+ end
53
+ found_tags << tag unless found
54
+ end
55
+ end
56
+ found_tags
57
+ end
58
+
59
+ ## accessors ##############################################################
60
+
61
+ def frob
62
+ @transport.frob
63
+ end
64
+
65
+ def frob=(frob)
66
+ @transport.frob = frob
67
+ end
68
+
69
+ def token=(token)
70
+ @transport.token = token
71
+ end
72
+
73
+ end
74
+ end
File without changes
@@ -0,0 +1,57 @@
1
+ module RememberTheRuby
2
+ class Entity < Hash
3
+
4
+ def initialize(transport, data={})
5
+ @transport = transport
6
+ self.merge!(data)
7
+ end
8
+
9
+ def self.from_element(transport, element)
10
+ data = element.attributes.keys.inject({}) do |memo, key|
11
+ memo[key] = element.attributes[key]
12
+ memo
13
+ end
14
+ self.new(transport, data)
15
+ end
16
+
17
+ def self.list_from_elements(transport, rsp, element)
18
+ EntityList.from_element(transport, self, rsp, element)
19
+ end
20
+
21
+ def method_missing(method_name)
22
+ self[method_name]
23
+ end
24
+
25
+ def hydrate_from(&blk)
26
+ entity = yield blk
27
+ entity.each do |key, value|
28
+ self[key] = value
29
+ end
30
+ end
31
+
32
+ alias_method :regular_reader, :[]
33
+ alias_method :regular_writer, :[]=
34
+
35
+ undef id
36
+
37
+ def [](key)
38
+ regular_reader(key.to_s)
39
+ end
40
+
41
+ def []=(key, value)
42
+ regular_writer(key.to_s, value)
43
+ end
44
+
45
+ alias_method :original_lookup, :[]
46
+
47
+ def [](key)
48
+ value = self
49
+ parts = key.to_s.split('/')
50
+ parts.each do |part|
51
+ value = value.original_lookup(part)
52
+ end
53
+ value
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ module RememberTheRuby
2
+ class EntityList < Array
3
+
4
+ attr_accessor :defaults
5
+
6
+ def initialize(transport, type)
7
+ @transport = transport
8
+ @type = type
9
+ @defaults = {}
10
+ end
11
+
12
+ def new
13
+ entity = @type.new(@transport)
14
+ @defaults.each do |key, value|
15
+ entity[key] = value
16
+ end
17
+ entity
18
+ end
19
+
20
+ def find(options)
21
+
22
+ case options
23
+ when String, Integer then
24
+ params = { 'id' => options.to_s }
25
+ amount = :one
26
+ when Hash then
27
+ params = options
28
+ amount = :many
29
+ else
30
+ params = {}
31
+ amount = :many
32
+ end
33
+
34
+ matches = EntityList.new(@transport, @type)
35
+ self.each do |entity|
36
+ matched = true
37
+ params.each do |key, value|
38
+ unless entity[key] == value
39
+ matched = false
40
+ break
41
+ end
42
+ end
43
+ matches << entity if matched
44
+ end
45
+
46
+ amount == :many ? matches : matches.first
47
+ end
48
+
49
+ def self.from_element(transport, type, rsp, xpath)
50
+ entity_list = EntityList.new(transport, type)
51
+ rsp.get_elements(xpath).map do |element|
52
+ entity_list << type.from_element(transport, element)
53
+ end
54
+ entity_list
55
+ end
56
+
57
+ end
58
+ end
File without changes
@@ -0,0 +1,11 @@
1
+ module RememberTheRuby
2
+ class List < Entity
3
+
4
+ def tasks
5
+ tasks = @transport.tasks.get_list(:list_id => self['id'])
6
+ tasks.defaults['list_id'] = self['id']
7
+ tasks
8
+ end
9
+
10
+ end
11
+ end
File without changes
File without changes
@@ -0,0 +1,187 @@
1
+ module RememberTheRuby
2
+ module RPC
3
+
4
+ require 'digest/md5'
5
+ require 'net/http'
6
+ require 'rexml/document'
7
+
8
+ class Transport
9
+
10
+ TRANSPORT_URI = 'http://www.rememberthemilk.com'
11
+
12
+ attr_accessor :key, :secret, :frob, :token
13
+
14
+ def initialize(params)
15
+ @key = params[:key]
16
+ @secret = params[:secret]
17
+ @frob = params[:frob]
18
+ @token = params[:token]
19
+
20
+ @transport_http = build_transport_http
21
+ end
22
+
23
+ public ####################################################################
24
+
25
+ def request(method, params={})
26
+ params[:method] = method
27
+
28
+ uri = URI.encode(rest_path(params))
29
+ data = @transport_http.get(uri)
30
+ doc = REXML::Document.new(data.body)
31
+
32
+ doc.elements['rsp']
33
+ end
34
+
35
+ # rpc types ###############################################################
36
+
37
+ def auth
38
+ @auth ||= Auth.new(self)
39
+ end
40
+
41
+ def lists
42
+ @lists ||= Lists.new(self)
43
+ end
44
+
45
+ def settings
46
+ @settings ||= Settings.new(self)
47
+ end
48
+
49
+ def tasks
50
+ @tasks ||= Tasks.new(self)
51
+ end
52
+
53
+ def timeline
54
+ Timelines.new(self).create
55
+ end
56
+
57
+ # private #################################################################
58
+
59
+ def build_transport_http
60
+ uri = URI::parse(TRANSPORT_URI)
61
+ @transport_http = Net::HTTP.start(uri.host, uri.port)
62
+ end
63
+
64
+ def clear_empty_parameters(params)
65
+ params.dup.each do |k, v|
66
+ params.delete(k) if v.nil? || v == ''
67
+ end
68
+ end
69
+
70
+ def auth_path(params)
71
+ self.service_path('auth', params)
72
+ end
73
+
74
+ def rest_path(params)
75
+ self.service_path('rest', params)
76
+ end
77
+
78
+ def service_path(service, params)
79
+ params[:frob] ||= @frob
80
+ params[:api_key] ||= @key
81
+ params[:auth_token] ||= @token
82
+
83
+ clear_empty_parameters(params)
84
+
85
+ params[:api_sig] = sign_parameters(params)
86
+ "/services/#{service}/?#{flatten_parameters(params)}"
87
+ end
88
+
89
+ def sign_parameters(params)
90
+ signature = @secret
91
+ signature += params.map { |k, v| [k, v].join('') }.sort.join('')
92
+ Digest::MD5.hexdigest(signature)
93
+ end
94
+
95
+ def flatten_parameters(params)
96
+ params.map { |k, v| [k, v].join('=') }.join('&')
97
+ end
98
+
99
+ def hash_from_element(element)
100
+ element.attributes
101
+ end
102
+
103
+ end
104
+
105
+ class Transported
106
+
107
+ def initialize(transport)
108
+ @transport = transport
109
+ end
110
+
111
+ end
112
+
113
+ class Auth < Transported
114
+
115
+ def get_authorization_url(params={})
116
+ params[:perms] ||= 'delete'
117
+ data = Transport::TRANSPORT_URI + @transport.auth_path(params)
118
+ end
119
+
120
+ def get_frob
121
+ rsp = @transport.request('rtm.auth.getFrob')
122
+ rsp.elements['frob'].text
123
+ end
124
+
125
+ def get_token
126
+ rsp = @transport.request('rtm.auth.getToken')
127
+ rsp.elements['auth/token'].text
128
+ end
129
+
130
+ end
131
+
132
+ class Lists < Transported
133
+
134
+ def get_list
135
+ rsp = @transport.request('rtm.lists.getList')
136
+ List.list_from_elements(@transport, rsp, 'lists/list')
137
+ end
138
+
139
+ end
140
+
141
+ class Settings < Transported
142
+
143
+ def get_list
144
+ rsp = @transport.request('rtm.settings.getList')
145
+ rsp.get_elements('settings').first.inject({}) do |memo, element|
146
+ memo[element.name] = element.text
147
+ memo
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+ class Tasks < Transported
154
+
155
+ def add(params={})
156
+ params[:list_id] ||= nil
157
+ params[:name] ||= nil
158
+ params[:parse] ||= nil
159
+
160
+ params[:timeline] = @transport.timeline
161
+ params[:parse] = params[:parse] ? 1 : 0
162
+
163
+ rsp = @transport.request('rtm.tasks.add', params)
164
+ Task.from_element(@transport, rsp.get_elements('list/taskseries').first)
165
+ end
166
+
167
+ def get_list(params={})
168
+ params[:list_id] ||= nil
169
+ params[:filter] ||= nil
170
+ params[:last_sync] ||= nil
171
+ rsp = @transport.request('rtm.tasks.getList', params)
172
+ Task.list_from_elements(@transport, rsp, 'tasks/list/taskseries')
173
+ end
174
+
175
+ end
176
+
177
+ class Timelines < Transported
178
+
179
+ def create(params={})
180
+ rsp = @transport.request('rtm.timelines.create', params)
181
+ rsp.get_elements('timeline').first.text
182
+ end
183
+
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'preferences'
3
+
4
+ module RememberTheRuby
5
+ class Storage < Preferences::Manager
6
+
7
+ def initialize
8
+ super(:remember_the_ruby, :autosave => true)
9
+ end
10
+
11
+ def set_cache_timeout(key, timeout)
12
+ self[:cache] ||= {}
13
+ self[:cache][key] ||= Time.now + timeout
14
+ self.save
15
+ end
16
+
17
+ def cache_timeout_for(key)
18
+ original_reader(:cache) ? original_reader(:cache)[key] : nil
19
+ end
20
+
21
+ def purge_cache!
22
+ return unless self[:cache].is_a?(Enumerable)
23
+ self[:cache].each do |key, timeout|
24
+ self.delete(key)
25
+ end
26
+ self.delete(:cache)
27
+ self.save
28
+ end
29
+
30
+ alias_method :original_reader, :[]
31
+
32
+ def [](key)
33
+ now = Time.now
34
+ timeout = cache_timeout_for(key) || now
35
+ if timeout < now
36
+ self.delete(key)
37
+ self[:cache].delete(key)
38
+ end
39
+ super
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ module RememberTheRuby
2
+ class Tag < Entity
3
+
4
+ def self.from_element(transport, element)
5
+ tag = super
6
+ tag['name'] = element.text
7
+ tag
8
+ end
9
+
10
+ def to_s
11
+ self['name']
12
+ end
13
+
14
+ def tasks
15
+ tasks = @transport.tasks.get_list(:filter => "tag:#{self["name"]}")
16
+ # TODO: add default tag to the tasks list
17
+ tasks
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module RememberTheRuby
2
+ class Task < Entity
3
+
4
+ def self.from_element(transport, element)
5
+ task = super
6
+ task["occurrences"] = TaskOccurrence.list_from_elements(transport, element, 'task')
7
+ task["next"] = task["occurrences"].sorted_by(:due).first
8
+ task["tags"] = Tag.list_from_elements(transport, element, 'tags/tag')
9
+ task
10
+ end
11
+
12
+ def save!(params={})
13
+ hydrate_from do
14
+ @transport.tasks.add(:name => self[:name], :parse => params[:parse])
15
+ end
16
+ # TODO: apply tags
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module RememberTheRuby
2
+ class TaskOccurrence < Entity
3
+
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ # Equivalent to a header guard in C/C++
2
+ # Used to prevent the class/module from being loaded more than once
3
+ unless defined? RememberTheRuby
4
+
5
+ module RememberTheRuby
6
+
7
+ # :stopdoc:
8
+ VERSION = '0.4.1'
9
+ LIBPATH = File.expand_path(File.dirname(__FILE__)) + File::SEPARATOR
10
+ PATH = File.dirname(LIBPATH) + File::SEPARATOR
11
+ # :startdoc:
12
+
13
+ # Returns the version string for the library.
14
+ #
15
+ def self.version
16
+ VERSION
17
+ end
18
+
19
+ # Returns the library path for the module. If any arguments are given,
20
+ # they will be joined to the end of the libray path using
21
+ # <tt>File.join</tt>.
22
+ #
23
+ def self.libpath( *args )
24
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, *args)
25
+ end
26
+
27
+ # Returns the lpath for the module. If any arguments are given,
28
+ # they will be joined to the end of the path using
29
+ # <tt>File.join</tt>.
30
+ #
31
+ def self.path( *args )
32
+ args.empty? ? PATH : ::File.join(PATH, *args)
33
+ end
34
+
35
+ # Utility method used to rquire all files ending in .rb that lie in the
36
+ # directory below this file that has the same name as the filename passed
37
+ # in. Optionally, a specific _directory_ name can be passed in such that
38
+ # the _filename_ does not have to be equivalent to the directory.
39
+ #
40
+ def self.require_all_libs_relative_to(fname)
41
+ search_me = File.expand_path(File.join(File.dirname(fname), '**', '*.rb'))
42
+ Dir.glob(search_me).sort.each { |rb| require rb }
43
+ end
44
+
45
+ end
46
+
47
+ RememberTheRuby.require_all_libs_relative_to __FILE__
48
+
49
+ end # unless defined?
data/lib/rtr/api.rb ADDED
@@ -0,0 +1,22 @@
1
+ def api
2
+
3
+ @api ||= begin
4
+
5
+ api = RememberTheRuby::API.new(:frob => $storage[:frob],
6
+ :token => $storage[:token])
7
+
8
+ unless $storage[:frob]
9
+ system %{open "#{api.authorization_url}"}
10
+ $storage[:frob] = api.frob
11
+ exit
12
+ end
13
+
14
+ unless $storage[:token]
15
+ $storage[:token] = api.authenticate
16
+ end
17
+
18
+ api
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,11 @@
1
+ $methods = {}
2
+
3
+ def register_method(method, &blk)
4
+ $methods[method] = blk
5
+ end
6
+
7
+ def usage_and_exit
8
+ puts "usage: rtr <command> [options]"
9
+ exit 1
10
+ end
11
+
@@ -0,0 +1,67 @@
1
+ require 'term/ansicolor'
2
+ require 'pp'
3
+
4
+ module RTR
5
+ class Commands
6
+
7
+ register_method :add do |options|
8
+ tags, task_name = options.partition { |o| o[0..0] == '@' }
9
+ task_name = task_name.join(' ')
10
+
11
+ task = api.tasks.new
12
+
13
+ task["name"] = task_name
14
+ task["tags"] = tags.map { |tag| tag[1..-1] }
15
+
16
+ task.save!(:parse => true)
17
+
18
+ $storage.purge_cache!
19
+ end
20
+
21
+ register_method :tasks do |options|
22
+
23
+ puts "Oustanding Tasks".white.bold
24
+ puts
25
+
26
+ tasks = $storage[:tasks] || api.default_list.tasks
27
+
28
+ tasks.sorted_by('next/due').reverse.each do |task|
29
+
30
+ now = DateTime.now
31
+
32
+ due = task['next/due']
33
+ due = due.to_date unless due.blank?
34
+
35
+ diff = due.blank? ? nil : due - now
36
+
37
+ color = case
38
+ when diff.nil? then :white
39
+ when diff.abs < 1 && due.day == now.day then :yellow
40
+ when diff < 0 then :red
41
+ else :white
42
+ end
43
+
44
+ title = task.name
45
+ title = title.send(color).bold if color
46
+
47
+ prefix = " *"
48
+ prefix = prefix.send(color).bold if color
49
+
50
+ suffix = task.tags.map { |t| "@#{t.name}" }.join(' ').cyan
51
+
52
+ puts "#{prefix} #{title} #{suffix}"
53
+
54
+ end
55
+
56
+ $storage[:tasks] = tasks
57
+ $storage.set_cache_timeout(:tasks, 30.minutes)
58
+
59
+ end
60
+
61
+ register_method :purge do |options|
62
+ $storage.purge_cache!
63
+ end
64
+
65
+
66
+ end
67
+ end
data/lib/rtr.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Equivalent to a header guard in C/C++
2
+ # Used to prevent the class/module from being loaded more than once
3
+ unless defined? RTR
4
+
5
+ module RTR
6
+
7
+ # Utility method used to rquire all files ending in .rb that lie in the
8
+ # directory below this file that has the same name as the filename passed
9
+ # in. Optionally, a specific _directory_ name can be passed in such that
10
+ # the _filename_ does not have to be equivalent to the directory.
11
+ #
12
+ def self.require_all_libs_relative_to(fname)
13
+ search_me = File.expand_path(File.join(File.dirname(fname), '**', '*.rb'))
14
+ Dir.glob(search_me).sort.each { |rb| require rb }
15
+ end
16
+
17
+ end
18
+
19
+ RTR.require_all_libs_relative_to __FILE__
20
+
21
+ end # unless defined?
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remember-the-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - David Dollar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-30 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: ddollar-preferences
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.1.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: term-ansicolor
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.3
34
+ version:
35
+ description:
36
+ email: ddollar@gmail.com
37
+ executables:
38
+ - rtr
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README
43
+ files:
44
+ - bin/rtr
45
+ - lib/core_ext/datetime.rb
46
+ - lib/core_ext/enumerable.rb
47
+ - lib/core_ext/fixnum.rb
48
+ - lib/core_ext/nil.rb
49
+ - lib/core_ext/string.rb
50
+ - lib/remember-the-ruby/api.rb
51
+ - lib/remember-the-ruby/contact.rb
52
+ - lib/remember-the-ruby/entity.rb
53
+ - lib/remember-the-ruby/entity_list.rb
54
+ - lib/remember-the-ruby/group.rb
55
+ - lib/remember-the-ruby/list.rb
56
+ - lib/remember-the-ruby/location.rb
57
+ - lib/remember-the-ruby/note.rb
58
+ - lib/remember-the-ruby/rpc.rb
59
+ - lib/remember-the-ruby/storage.rb
60
+ - lib/remember-the-ruby/tag.rb
61
+ - lib/remember-the-ruby/task.rb
62
+ - lib/remember-the-ruby/task_occurrence.rb
63
+ - lib/remember-the-ruby.rb
64
+ - lib/rtr/api.rb
65
+ - lib/rtr/command_helper.rb
66
+ - lib/rtr/commands.rb
67
+ - lib/rtr.rb
68
+ - README
69
+ has_rdoc: true
70
+ homepage: http://peervoice.com/software/remember-the-ruby
71
+ licenses: []
72
+
73
+ post_install_message:
74
+ rdoc_options: []
75
+
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ requirements: []
91
+
92
+ rubyforge_project: remember-the-ruby
93
+ rubygems_version: 1.3.5
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: A Ruby interface to Remember the Milk
97
+ test_files: []
98
+