jbgutierrez-delicious_api 1.0.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.
@@ -0,0 +1,43 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ module DeliciousApi
4
+ class Bundle < Base
5
+
6
+ # Bundle name
7
+ attr_accessor :name
8
+
9
+ # Tags <tt>Array</tt>
10
+ attr_accessor :tags
11
+
12
+ ##
13
+ # Bundle initialize method
14
+ # ==== Parameters
15
+ # * <tt>name</tt> - Bundle name
16
+ # * <tt>tags</tt> - An optional <tt>Array</tt> of tags
17
+ # ==== Result
18
+ # An new instance of the current class
19
+ def initialize(name, tags=[])
20
+ @name = name
21
+ @tags = tags
22
+ end
23
+
24
+ # Retrieves a list of tag bundles from Delicious
25
+ # ==== Parameters
26
+ # * <tt>limit</tt> - An integer determining the limit on the number of tag bundles that should be returned.
27
+ def self.all(limit = 10)
28
+ self.wrapper.get_all_bundles(limit)
29
+ end
30
+
31
+ # Updates a tag bundle at Delicious
32
+ def save
33
+ validate_presence_of :name, :tags
34
+ wrapper.set_bundle(@name, @tags.join(' ')) || raise(OperationFailed)
35
+ end
36
+
37
+ # Deletes a tag bundle from Delicious
38
+ def delete
39
+ validate_presence_of :name
40
+ wrapper.delete_bundle(@name) || raise(OperationFailed)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_support'
2
+
3
+ Dir[File.dirname(__FILE__) + "/extensions/*.rb"].sort.each do |path|
4
+ require path
5
+ end
@@ -0,0 +1,6 @@
1
+ class Hash
2
+ def assert_required_keys(*required_keys)
3
+ missing_keys = [required_keys].flatten - keys
4
+ raise(ArgumentError, "Missing required key(s): #{missing_keys.join(", ")}") unless missing_keys.empty?
5
+ end
6
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ module DeliciousApi
4
+ class Tag < Base
5
+
6
+ # Tag name
7
+ attr_accessor :name
8
+
9
+ # An alias for the tag name
10
+ alias :tag :name
11
+
12
+ # Number of times used
13
+ attr_reader :count
14
+
15
+ ##
16
+ # Tag initialize method
17
+ # ==== Parameters
18
+ # * <tt>name</tt> - Tag name
19
+ # * <tt>params</tt> - An optional <tt>Hash</tt> containing any combination of the instance attributes
20
+ # ==== Result
21
+ # An new instance of the current class
22
+ def initialize(name, params = {})
23
+ params.symbolize_keys!.assert_valid_keys(:name, :tag, :count)
24
+ params.merge!(:name => name, :original_name => name)
25
+ assign params
26
+ end
27
+
28
+ # Retrieves a list of tags and number of times used from Delicious
29
+ def self.all
30
+ self.wrapper.get_all_tags
31
+ end
32
+
33
+ # Deletes a tag from Delicious
34
+ def delete
35
+ validate_presence_of :name
36
+ wrapper.delete_tag(@name) || raise(OperationFailed)
37
+ end
38
+
39
+ # Updates a tag name at Delicious (if necessary)
40
+ def save
41
+ validate_presence_of :name
42
+ unless @original_name == @name
43
+ wrapper.rename_tag(@original_name, @name) || raise(OperationFailed)
44
+ @original_name = @name
45
+ end
46
+ end
47
+
48
+ protected
49
+ attr_accessor :original_name
50
+
51
+ end
52
+ end
@@ -0,0 +1,307 @@
1
+ require 'hpricot'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'uri'
5
+
6
+ module DeliciousApi
7
+
8
+ class HTTPError < DeliciousApiError; end
9
+
10
+ class Wrapper
11
+
12
+ # del.icio.us account username
13
+ attr_reader :user
14
+
15
+ # del.icio.us account password
16
+ attr_reader :password
17
+
18
+ # request user agent
19
+ attr_reader :user_agent
20
+
21
+ # requests time gap
22
+ attr_reader :waiting_time_gap
23
+
24
+ # http client
25
+ attr_reader :http_client
26
+
27
+ ##
28
+ # Wrapper initialize method
29
+ # ==== Parameters
30
+ # * <tt>user</tt> - Delicious username
31
+ # * <tt>password</tt> - Delicious password
32
+ # * <tt>options</tt> - A <tt>Hash</tt> containing any of the following:
33
+ # - <tt>user_agent</tt> - User agent to sent to the server.
34
+ # - <tt>waiting_time_gap</tt> - Time gap between requests. By default is set to 1.
35
+ # ==== Result
36
+ # An new instance of the current class
37
+ def initialize(user, password, options = {})
38
+ raise ArgumentError if (user.nil? || password.nil?)
39
+ options.assert_valid_keys(:user_agent, :waiting_time_gap)
40
+ @user = user
41
+ @password = password
42
+ @user_agent = options[:user_agent] || default_user_agent
43
+ @waiting_time_gap = options[:waiting_time_gap] || 1
44
+ end
45
+
46
+ # API URL to add a new bookmark
47
+ API_URL_ADD_BOOKMARK = '/v1/posts/add?'
48
+ # API URL to delete an existing bookmark
49
+ API_URL_DELETE_BOOKMARK = '/v1/posts/delete?'
50
+ # API URL to get a collection of bookmarks filtered by date
51
+ API_URL_GET_BOOKMARKS_BY_DATE = '/v1/posts/get?'
52
+ # API URL to get the most recent bookmarks
53
+ API_URL_RECENT_BOOKMARKS = '/v1/posts/recent?'
54
+ # API URL to get all the bookmarks
55
+ API_URL_ALL_BOOKMARKS = '/v1/posts/all?'
56
+ # API URL to get all the tag
57
+ API_URL_ALL_TAGS = '/v1/tags/get'
58
+ # API URL to rename an existing tag
59
+ API_URL_RENAME_TAG = '/v1/tags/rename?'
60
+ # API URL to delete an existing tag
61
+ API_URL_DELETE_TAG = '/v1/tags/delete?'
62
+ # API URL to get popular, recommended and network tags for a particular url
63
+ API_URL_SUGGEST_TAG = '/v1/posts/suggest?'
64
+ # API URL to get all of a user's bundles.
65
+ API_URL_ALL_BUNDLES = '/v1/tags/bundles/all?'
66
+ # API URL to set a tag bundle
67
+ API_URL_SET_BUNDLE = '/v1/tags/bundles/set?'
68
+ # API URL to delete an existing bundle
69
+ API_URL_DELETE_BUNDLE = '/v1/tags/bundles/delete?'
70
+
71
+ ##
72
+ # Add a bookmark to Delicious
73
+ # ==== Parameters
74
+ # * <tt>url</tt> - the url of the item.
75
+ # * <tt>description</tt> - the description of the item.
76
+ # * <tt>options</tt> - A <tt>Hash</tt> containing any of the following:
77
+ # - <tt>extended</tt> - notes for the item.
78
+ # - <tt>tags</tt> - tags for the item (space delimited).
79
+ # - <tt>dt</tt> - datestamp of the item (format "CCYY-MM-DDThh:mm:ssZ"). Requires a LITERAL "T" and "Z" like in ISO8601 at http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example: "1984-09-01T14:21:31Z"
80
+ # - <tt>replace=no</tt> - don't replace bookmark if given url has already been posted.
81
+ # - <tt>shared=no</tt> - make the item private
82
+ # ==== Result
83
+ # * <tt>true</tt> if the bookmark was successfully added
84
+ # * <tt>false</tt> if the addition failed
85
+ def add_bookmark(url, description, options = {})
86
+ options.assert_valid_keys(:extended, :tags, :dt, :replace, :shared)
87
+ options[:url], options[:description] = url, description
88
+ doc = process_request(API_URL_ADD_BOOKMARK + options.to_query)
89
+ doc.at('result')['code'] == 'done'
90
+ end
91
+
92
+ ##
93
+ # Delete a bookmark from Delicious
94
+ # ==== Parameters
95
+ # * <tt>url</tt> - the url of the item.
96
+ # ==== Result
97
+ # * <tt>true</tt> if the bookmark was successfully deleted
98
+ # * <tt>false</tt> if the deletion failed
99
+ def delete_bookmark(url)
100
+ options = { :url => url }
101
+ doc = process_request(API_URL_DELETE_BOOKMARK + options.to_query)
102
+ doc.at('result')['code'] == 'done' || doc.at('result')['code'] == 'item not found'
103
+ end
104
+
105
+ ##
106
+ # Returns one or more bookmarks on a single day matching the arguments. If no date or url is given, most recent date will be used.
107
+ # ==== Parameters
108
+ # * <tt>dt</tt> - Filter by this date, defaults to the most recent date on which bookmarks were saved.
109
+ # * <tt>options</tt> - A <tt>Hash</tt> containing any of the following:
110
+ # - <tt>tag</tt> - [TAG,TAG,...TAG] Filter by this tag.
111
+ # - <tt>url</tt> - Fetch a bookmark for this URL, regardless of date.
112
+ # - <tt>hashes</tt> - [MD5,MD5,...,MD5] Fetch multiple bookmarks by one or more URL MD5s regardless of date, separated by URL-encoded spaces (ie. '+').
113
+ # - <tt>meta=yes</tt> - Include change detection signatures on each item in a 'meta' attribute. Clients wishing to maintain a synchronized local store of bookmarks should retain the value of this attribute - its value will change when any significant field of the bookmark changes.
114
+ # ==== Result
115
+ # An <tt>Array</tt> of <tt>Bookmarks</tt> matching the criteria
116
+ def get_bookmarks_by_date(dt, options)
117
+ options = { :dt => dt } unless dt.nil?
118
+ options.assert_valid_keys(:tag, :dt, :url, :hashes, :meta)
119
+ doc = process_request(API_URL_GET_BOOKMARKS_BY_DATE + options.to_query)
120
+ (doc/'posts/post').collect{ |post| Bookmark.new(post['href'], post.attributes) }
121
+ end
122
+
123
+ ##
124
+ # Returns a <tt>Bookmark</tt> for the <tt>url</tt>
125
+ # ==== Parameters
126
+ # * <tt>url</tt> - Fetch a bookmark for this URL.
127
+ # ==== Result
128
+ # A <tt>Bookmark</tt> matching the criteria or nil
129
+ def get_bookmark_by_url(url)
130
+ get_bookmarks_by_date(nil, :url=> url).first
131
+ end
132
+
133
+ ##
134
+ # Returns a list of the most recent bookmarks, filtered by argument. Maximum 100.
135
+ # ==== Parameters
136
+ # * <tt>tag</tt> - Filter by this tag.
137
+ # * <tt>count</tt> - Number of items to retrieve (Default:15, Maximum:100).
138
+ # ==== Result
139
+ # An <tt>Array</tt> of <tt>Bookmarks</tt> matching the criteria
140
+ def get_recent_bookmarks(options = {})
141
+ options.assert_valid_keys(:tag, :count)
142
+ doc = process_request(API_URL_RECENT_BOOKMARKS + options.to_query)
143
+ (doc/'posts/post').collect{ |post| Bookmark.new(post['href'], post.attributes) }
144
+ end
145
+
146
+ ##
147
+ # Returns a list with all the bookmarks, filtered by argument.
148
+ # ==== Parameters
149
+ # * <tt>options</tt> - A <tt>Hash</tt> containing any of the following:
150
+ # - <tt>tag</tt> - Filter by this tag.
151
+ # - <tt>start</tt> - Start returning bookmarks this many results into the set.
152
+ # - <tt>results</tt> - Return this many results
153
+ # - <tt>fromdt</tt> - Filter for posts on this date or later (format "CCYY-MM-DDThh:mm:ssZ"). Requires a LITERAL "T" and "Z" like in ISO8601 at http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example: "1984-09-01T14:21:31Z"
154
+ # - <tt>todt</tt> - Return this many results (format "CCYY-MM-DDThh:mm:ssZ"). Requires a LITERAL "T" and "Z" like in ISO8601 at http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example: "1984-09-01T14:21:31Z"
155
+ # - <tt>meta=yes</tt> - Include change detection signatures on each item in a 'meta' attribute. Clients wishing to maintain a synchronized local store of bookmarks should retain the value of this attribute - its value will change when any significant field of the bookmark changes.
156
+ # ==== Result
157
+ # An <tt>Array</tt> of <tt>Bookmarks</tt> matching the criteria
158
+ def get_all_bookmarks(options = {})
159
+ options.assert_valid_keys(:tag, :start, :results, :fromdt, :todt, :meta)
160
+ doc = process_request(API_URL_ALL_BOOKMARKS + options.to_query)
161
+ (doc/'posts/post').collect{ |post| Bookmark.new(post['href'], post.attributes) }
162
+ end
163
+
164
+ ##
165
+ # Returns a list of tags and number of times used by a user.
166
+ # ==== Result
167
+ # An <tt>Array</tt> of <tt>Tags</tt>
168
+ def get_all_tags
169
+ doc = process_request(API_URL_ALL_TAGS)
170
+ (doc/'tags/tag').collect{ |tag| Tag.new(tag['tag'], tag.attributes) }
171
+ end
172
+
173
+ ##
174
+ # Rename an existing tag with a new tag name.
175
+ # ==== Parameters
176
+ # * <tt>old_name</tt> - Original tag name.
177
+ # * <tt>new_name</tt> - New tag name.
178
+ # ==== Result
179
+ # * <tt>true</tt> if the tag was successfully renamed
180
+ # * <tt>false</tt> otherwise
181
+ def rename_tag(old_name, new_name)
182
+ options = { :old => old_name, :new => new_name }
183
+ doc = process_request(API_URL_RENAME_TAG + options.to_query)
184
+ doc.at('result')['code'] == 'done'
185
+ end
186
+
187
+ # Delete a tag from Delicious
188
+ # ==== Parameters
189
+ # * <tt>tag_to_delete</tt> - tag name to delete.
190
+ # ==== Result
191
+ # * <tt>true</tt> if the tag was successfully deleted
192
+ # * <tt>false</tt> if the deletion failed
193
+ def delete_tag(tag_to_delete)
194
+ options = { :tag => tag_to_delete }
195
+ doc = process_request(API_URL_DELETE_TAG + options.to_query)
196
+ doc.at('result')['code'] == 'done'
197
+ end
198
+
199
+ ##
200
+ # Returns a list of popular tags, recommended tags and network tags for the given url.
201
+ # This method is intended to provide suggestions for tagging a particular url.
202
+ # ==== Parameters
203
+ # * <tt>url</tt> - URL for which you'd like suggestions.
204
+ # ==== Result
205
+ # A <tt>Hash</tt> containing three arrays of <tt>Tags</tt>: <tt>:popular</tt>, <tt>:recommended</tt> and <tt>:network</tt>
206
+ def get_suggested_tags_for_url(url)
207
+ options = { :url => url }
208
+ doc = process_request(API_URL_SUGGEST_TAG + options.to_query)
209
+ result = { }
210
+ result[:popular] = (doc/'suggest/popular').collect{ |tag| Tag.new(tag.inner_html) }
211
+ result[:recommended] = (doc/'suggest/recommended').collect{ |tag| Tag.new(tag.inner_html) }
212
+ result[:network] = (doc/'suggest/network').collect{ |tag| Tag.new(tag.inner_html) }
213
+ result
214
+ end
215
+
216
+ ##
217
+ # Retrieve all of a user's bundles.
218
+ # ==== Parameters
219
+ # * <tt>options</tt> - A <tt>Hash</tt> containing any of the following:
220
+ # - <tt>bundle</tt> - Fetch just the named bundle.
221
+ # ==== Result
222
+ # An <tt>Array</tt> of <tt>Bundles</tt> matching the criteria
223
+ def get_all_bundles(options = {})
224
+ options.assert_valid_keys(:bundle)
225
+ doc = process_request(API_URL_ALL_BUNDLES + options.to_query)
226
+ (doc/'bundles/bundle').collect{ |bundle| Bundle.new(bundle['name'], bundle['tags'].split(' ')) }
227
+ end
228
+
229
+ ##
230
+ # Returns the user <tt>Bundle</tt> with the given <tt>name</tt>
231
+ # ==== Parameters
232
+ # * <tt>name</tt> - User's bundle name.
233
+ # ==== Result
234
+ # A <tt>Bundle</tt> matching the criteria or nil
235
+ def get_bundle_by_name(name)
236
+ get_all_bundles(:bundle => name).first
237
+ end
238
+
239
+ ##
240
+ # Assign a set of tags to a single bundle, wipes away previous settings for bundle.
241
+ # ==== Parameters
242
+ # * <tt>name</tt> - bundle's name.
243
+ # * <tt>tags</tt> - tags for the bundle (space delimited).
244
+ # ==== Result
245
+ # * <tt>true</tt> if the bundle was set
246
+ # * <tt>false</tt> if the bundle was not set
247
+ def set_bundle(name, tags)
248
+ options = { :bundle => name, :tags => tags }
249
+ doc = process_request(API_URL_SET_BUNDLE + options.to_query)
250
+ doc.at('result').inner_html == 'ok'
251
+ end
252
+
253
+ ##
254
+ # Delete a bundle from Delicious
255
+ # ==== Parameters
256
+ # * <tt>name</tt> - name of the bundle
257
+ # ==== Result
258
+ # * <tt>true</tt> if the bundle was successfully deleted
259
+ # * <tt>false</tt> if the deletion failed
260
+ def delete_bundle(name)
261
+ options = { :bundle => name }
262
+ doc = process_request(API_URL_DELETE_BUNDLE + options.to_query)
263
+ doc.at('result')['code'] == 'done'
264
+ end
265
+
266
+ private
267
+
268
+ def process_request(url)
269
+ init_http_client if @http_client.nil?
270
+ response = make_web_request(url)
271
+ Hpricot.XML(response.body)
272
+ end
273
+
274
+ def init_http_client
275
+ @http_client = Net::HTTP.new('api.del.icio.us', 443)
276
+ @http_client.use_ssl = true
277
+ @http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
278
+ end
279
+
280
+ def make_web_request(url)
281
+ http_client.start do |http|
282
+ req = Net::HTTP::Get.new(url, {'User-Agent' => @user_agent} )
283
+ req.basic_auth(@user, @password)
284
+ current_time = Time.now
285
+ @@last_request ||= current_time - waiting_time_gap
286
+ current_window = [current_time - @@last_request, waiting_time_gap].max
287
+ sleep(current_window) if current_window <= waiting_time_gap
288
+ response = @http_client.request(req)
289
+ case response
290
+ when Net::HTTPSuccess
291
+ return response
292
+ when Net::HTTPUnauthorized # 401 - HTTPUnauthorized
293
+ raise HTTPError, 'Invalid username or password'
294
+ when Net::HTTPServiceUnavailable # 503 - HTTPServiceUnavailable
295
+ raise HTTPError, 'You have been throttled. Try increasing the time gap between requests.'
296
+ else
297
+ raise HTTPError, "HTTP #{response.code}: #{response.message}"
298
+ end
299
+ end
300
+ end
301
+
302
+ def default_user_agent
303
+ return "#{NAME}/#{VERSION} (Ruby/#{RUBY_VERSION})"
304
+ end
305
+
306
+ end
307
+ end
@@ -0,0 +1,23 @@
1
+ module CustomMacros
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def configure_wrapper
8
+ before(:all) do
9
+ Tag.wrapper = mock("base.wrapper", :null_object => true)
10
+ end
11
+ after(:all) do
12
+ Tag.wrapper = nil
13
+ end
14
+ end
15
+
16
+ def freeze_time
17
+ before(:each) do
18
+ time_now = Time.now.utc
19
+ Time.stub!(:now).and_return(time_now)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module CustomMatchers
2
+ def take_more_than(seconds)
3
+ simple_matcher("to take more than #{seconds} seconds") { |given| given > seconds }
4
+ end
5
+ end