pinboard_api 0.1.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +5 -1
- data/README.md +81 -16
- data/Rakefile +10 -0
- data/lib/core_ext/array.rb +11 -0
- data/lib/core_ext/hash.rb +8 -0
- data/lib/pinboard_api.rb +24 -18
- data/lib/pinboard_api/post.rb +103 -21
- data/lib/pinboard_api/tag.rb +11 -9
- data/lib/pinboard_api/user.rb +1 -1
- data/lib/pinboard_api/version.rb +1 -1
- data/spec/core_ext/array_spec.rb +11 -0
- data/spec/core_ext/hash_spec.rb +18 -0
- data/spec/fixtures/vcr_cassettes/posts/all/custom_count.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/all/custom_tag.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/all/custom_times.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/all/default_values.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/all/not_found.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/dates/custom_tag.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/dates/default_values.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/delete/unsuccessful_class.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/destroy/successful_class.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/destroy/successful_instance.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/destroy/unsuccessful_instance.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/find/found.json +1 -1
- data/spec/fixtures/vcr_cassettes/posts/find/not_found.json +1 -1
- data/spec/fixtures/vcr_cassettes/posts/recent/custom_count.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/recent/custom_tag.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/recent/default_values.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/suggest.json +1 -0
- data/spec/fixtures/vcr_cassettes/posts/update.json +1 -1
- data/spec/fixtures/vcr_cassettes/tags/all.json +1 -1
- data/spec/fixtures/vcr_cassettes/tags/destroy/successful_class.json +1 -0
- data/spec/fixtures/vcr_cassettes/tags/destroy/successful_instance.json +1 -0
- data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_class.json +1 -0
- data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_instance.json +1 -0
- data/spec/fixtures/vcr_cassettes/tags/find/found.json +1 -1
- data/spec/fixtures/vcr_cassettes/tags/find/not_found.json +1 -1
- data/spec/fixtures/vcr_cassettes/tags/rename/successful.json +1 -1
- data/spec/fixtures/vcr_cassettes/tags/rename/unsuccessful.json +1 -1
- data/spec/fixtures/vcr_cassettes/user/secret.json +1 -1
- data/spec/pinboard_api_spec.rb +0 -4
- data/spec/post_spec.rb +358 -29
- data/spec/spec_helper.rb +8 -4
- data/spec/tag_spec.rb +27 -11
- metadata +48 -7
- data/spec/fixtures/vcr_cassettes/tags/delete/successful.json +0 -1
- data/spec/fixtures/vcr_cassettes/tags/delete/unsuccessful.json +0 -1
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# PinboardApi
|
1
|
+
# PinboardApi [![Build Status](https://secure.travis-ci.org/phlipper/pinboard_api.png?branch=master)](http://travis-ci.org/phlipper/pinboard_api) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/phlipper/pinboard_api)
|
2
2
|
|
3
3
|
## Description
|
4
4
|
|
@@ -9,8 +9,13 @@ This client aims to cover all of the Pinboard [API v1](https://pinboard.in/api/)
|
|
9
9
|
|
10
10
|
## Requirements
|
11
11
|
|
12
|
-
* This library requires Ruby 1.9.2 or newer.
|
13
12
|
* You must have a paid Pinboard account to use the API. It is a great service and you can [signup here](https://pinboard.in/signup/) if you don't already have an account.
|
13
|
+
* Currently tested on the following Ruby versions:
|
14
|
+
* 1.9.2
|
15
|
+
* 1.9.3
|
16
|
+
* JRuby (1.9 mode)
|
17
|
+
|
18
|
+
_Note:_ Specs are currently passing on Rubinius with `RBXOPT=-X19` on my local machine but there is a failing spec on [Travis CI](http://travis-ci.org/#!/phlipper/pinboard_api). I will update the `README` with official support for Rubinus once everything runs smoothly on Travis.
|
14
19
|
|
15
20
|
|
16
21
|
## Installation
|
@@ -34,6 +39,22 @@ $ gem install pinboard_api
|
|
34
39
|
```
|
35
40
|
|
36
41
|
|
42
|
+
## Getting Started
|
43
|
+
|
44
|
+
You will need to set your username and password for the Pinboard service.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
PinboardApi.username = "phlipper"
|
48
|
+
PinboardApi.password = "[REDACTED]"
|
49
|
+
```
|
50
|
+
|
51
|
+
You may also set the SSL options which will be passed through to [Faraday](https://github.com/technoweenie/faraday#readme):
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
PinboardApi.ssl_options = { ca_file: "/opt/local/share/curl/curl-ca-bundle.crt" }
|
55
|
+
```
|
56
|
+
|
57
|
+
|
37
58
|
## Usage
|
38
59
|
|
39
60
|
The `PinboardApi` namespace implements the 3 primary object types: `Post`, `Tag`, and `User`.
|
@@ -50,24 +71,63 @@ PinboardApi::Post.update
|
|
50
71
|
```
|
51
72
|
|
52
73
|
* ~~[posts/add](https://pinboard.in/api#posts_add) - add a new bookmark~~
|
53
|
-
*
|
74
|
+
* [posts/delete](https://pinboard.in/api#posts_delete) - delete an existing bookmark
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
post = PinboardApi::Post.find(url: "https://pinboard.in/u:phlipper").first
|
78
|
+
post.destroy
|
79
|
+
# => #<PinboardApi::Post:0x007ffcb5166cf0 @description="Pinboard - antisocial bookmarking", @extended="", @hash="bc857ba651d134be0c9a5267e943c3ce", @url="https://pinboard.in/u:phlipper", @meta=nil, @tags="test", @time="2012-07-11T09:16:14Z">
|
80
|
+
|
81
|
+
PinboardApi::Post.destroy("https://pinboard.in/u:phlipper")
|
82
|
+
# => #<PinboardApi::Post:0x007f98d6946d78 @description="Pinboard - antisocial bookmarking", @extended="", @hash="bc857ba651d134be0c9a5267e943c3ce", @url="https://pinboard.in/u:phlipper", @meta=nil, @tags="test", @time="2012-07-11T09:17:36Z">
|
83
|
+
```
|
84
|
+
|
54
85
|
* [posts/get](https://pinboard.in/api#posts_get) - get bookmark for a single date, or fetch specific items by URL
|
55
86
|
|
56
87
|
```ruby
|
57
88
|
PinboardApi::Post.find(tag: "test")
|
58
|
-
# => [#<PinboardApi::Post:0x007fdce4547388 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @
|
89
|
+
# => [#<PinboardApi::Post:0x007fdce4547388 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @url="http://test.com/", @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
|
59
90
|
|
60
91
|
PinboardApi::Post.find(hash: "dbb720d788ffaeb0afb7572104072f4a", meta: "yes")
|
61
|
-
# => [#<PinboardApi::Post:0x007fac2b9d6690 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @href="http://test.com/", @meta="73b192512e3e4829806f5eee0a6b456d", @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
|
62
|
-
|
63
92
|
PinboardApi::Post.find(dt: Date.parse("2012-07-07"))
|
64
|
-
# => [#<PinboardApi::Post:0x007fac2ba0fdf0 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @href="http://test.com/", @meta=nil, @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
|
65
93
|
```
|
66
94
|
|
67
|
-
*
|
68
|
-
|
69
|
-
|
70
|
-
|
95
|
+
* [posts/dates](https://pinboard.in/api#posts_dates) - list dates on which bookmarks were posted
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
PinboardApi::Post.dates
|
99
|
+
# => [{"count"=>1, "date"=>#<Date: 2012-07-10 ((2456119j,0s,0n),+0s,2299161j)>}, {"count"=>3, "date"=>#<Date: 2012-07-08 ((2456117j,0s,0n),+0s,2299161j)>}, ...]
|
100
|
+
|
101
|
+
PinboardApi::Post.dates(tag: "ruby")
|
102
|
+
```
|
103
|
+
|
104
|
+
* [posts/recent](https://pinboard.in/api#posts_recent) - fetch recent bookmarks
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
PinboardApi::Post.recent
|
108
|
+
# => [#<PinboardApi::Post:0x007ffe150e1fd0 @description="Techniques to Secure Your Website with Ruby on Rails..."> ...]
|
109
|
+
|
110
|
+
PinboardApi::Post.recent(count: 3)
|
111
|
+
PinboardApi::Post.recent(tag: "ruby")
|
112
|
+
PinboardApi::Post.recent(count: 25, tag: ["ruby", "programming"])
|
113
|
+
```
|
114
|
+
|
115
|
+
* [posts/all](https://pinboard.in/api#posts_all) - fetch all bookmarks by date, tag, or range
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
PinboardApi::Post.all
|
119
|
+
# => [#<PinboardApi::Post:0x007ffe150e1fd0 @description="Techniques to Secure Your Website with Ruby on Rails..."> ...]
|
120
|
+
|
121
|
+
PinboardApi::Post.all(tag: %w[ruby programming], meta: true, results: 30)
|
122
|
+
PinboardApi::Post.all(start: 50, fromdt: 2.weeks.ago, todt: 1.week.ago)
|
123
|
+
```
|
124
|
+
|
125
|
+
* [posts/suggest](https://pinboard.in/api#posts_suggest) - fetch popular and recommended tags for a url
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
PinboardApi::Post.suggest("http://blog.com")
|
129
|
+
# => {"popular"=>["hosting", "blogs", "blog", "free"], "recommended"=>["blog", "blogging", "blogs", "free"]}
|
130
|
+
```
|
71
131
|
|
72
132
|
|
73
133
|
### Tag
|
@@ -86,13 +146,11 @@ PinboardApi::Tag.find("leadership")
|
|
86
146
|
|
87
147
|
```ruby
|
88
148
|
tag = PinboardApi::Tag.find("foo")
|
149
|
+
tag.destroy
|
89
150
|
# => #<PinboardApi::Tag:0x007fdce45f56e0 @name="foo", @count=1>
|
90
151
|
|
91
|
-
|
92
|
-
# => #<PinboardApi::Tag:
|
93
|
-
|
94
|
-
tag = PinboardApi::Tag.find("foo")
|
95
|
-
# => nil
|
152
|
+
PinboardApi::Tag.destroy("foo")
|
153
|
+
# => #<PinboardApi::Tag:0x007fdce45f20f8 @name="foo", @count=1>
|
96
154
|
```
|
97
155
|
|
98
156
|
* [tags/rename](https://pinboard.in/api#tags_rename) - rename a tag
|
@@ -116,6 +174,13 @@ PinboardApi::User.secret
|
|
116
174
|
```
|
117
175
|
|
118
176
|
|
177
|
+
## TODO
|
178
|
+
|
179
|
+
* Implement Post.add/create
|
180
|
+
* Implement support for the new `[auth_token](http://pinboard.in/api/#authentication)`
|
181
|
+
* Cleanup/refactor internal exception handling
|
182
|
+
|
183
|
+
|
119
184
|
## Contributing
|
120
185
|
|
121
186
|
1. Fork it
|
data/Rakefile
CHANGED
data/lib/pinboard_api.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require "faraday"
|
2
2
|
require "faraday_middleware"
|
3
3
|
|
4
|
+
require "core_ext/array"
|
5
|
+
require "core_ext/hash"
|
6
|
+
|
4
7
|
require "pinboard_api/post"
|
5
8
|
require "pinboard_api/tag"
|
6
9
|
require "pinboard_api/user"
|
@@ -10,30 +13,33 @@ module PinboardApi
|
|
10
13
|
|
11
14
|
class << self
|
12
15
|
attr_accessor :username, :password, :adapter, :ssl_options
|
16
|
+
end
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
18
|
+
def self.adapter
|
19
|
+
@adapter ||= :net_http
|
20
|
+
end
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
22
|
+
def self.ssl_options
|
23
|
+
@ssl_options ||= {}
|
24
|
+
end
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
def self.api_version
|
27
|
+
"v1"
|
28
|
+
end
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
def self.api_url
|
31
|
+
"https://#{username}:#{password}@api.pinboard.in"
|
32
|
+
end
|
29
33
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
34
|
+
def self.connection
|
35
|
+
Faraday.new(url: api_url, ssl: ssl_options) do |builder|
|
36
|
+
builder.response :logger if ENV["PINBOARD_LOGGER"]
|
37
|
+
builder.response :xml, content_type: /\bxml$/
|
38
|
+
builder.adapter adapter
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
42
|
+
def self.request(path, options = {}, &blk)
|
43
|
+
PinboardApi.connection.get(path, options, &blk)
|
44
|
+
end
|
39
45
|
end
|
data/lib/pinboard_api/post.rb
CHANGED
@@ -1,55 +1,137 @@
|
|
1
1
|
module PinboardApi
|
2
2
|
class Post
|
3
3
|
|
4
|
-
attr_reader :description, :extended, :hash, :
|
4
|
+
attr_reader :description, :extended, :hash, :meta, :url
|
5
5
|
|
6
6
|
def initialize(attributes = {})
|
7
|
+
attributes.stringify_keys!
|
8
|
+
|
7
9
|
@description = attributes["description"]
|
8
10
|
@extended = attributes["extended"]
|
9
11
|
@hash = attributes["hash"]
|
10
|
-
@href = attributes["href"]
|
11
12
|
@meta = attributes["meta"]
|
13
|
+
@url = attributes["url"] || attributes["href"]
|
12
14
|
@tags = attributes["tags"] || attributes["tag"]
|
13
15
|
@time = attributes["time"] || Time.now
|
14
16
|
end
|
15
17
|
|
16
18
|
def time
|
17
|
-
|
18
|
-
@time
|
19
|
-
elsif @time.is_a?(Date)
|
20
|
-
@time.to_time
|
21
|
-
else
|
22
|
-
Time.parse(@time)
|
23
|
-
end
|
19
|
+
@time.is_a?(String) ? Time.parse(@time) : @time.to_time
|
24
20
|
end
|
25
21
|
|
26
22
|
def tags
|
27
23
|
@tags.is_a?(String) ? @tags.split(/\s+/) : @tags
|
28
24
|
end
|
29
25
|
|
26
|
+
def destroy
|
27
|
+
path = "/#{PinboardApi.api_version}/posts/delete"
|
28
|
+
body = PinboardApi.request(path, url: @url).body["result"]
|
29
|
+
|
30
|
+
if body && body.fetch("code", "") == "done"
|
31
|
+
self
|
32
|
+
else
|
33
|
+
raise RuntimeError, "unknown response"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.destroy(url)
|
38
|
+
if post = find(url: url).first
|
39
|
+
post.destroy
|
40
|
+
else
|
41
|
+
raise RuntimeError, "unknown response"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.all(options = {})
|
46
|
+
path = "/#{PinboardApi.api_version}/posts/all"
|
47
|
+
|
48
|
+
tag = tag_param_string(options[:tag])
|
49
|
+
fromdt = dt_param_string(options[:fromdt])
|
50
|
+
todt = dt_param_string(options[:todt])
|
51
|
+
|
52
|
+
response = PinboardApi.request(path) do |req|
|
53
|
+
req.params["tag"] = tag if tag
|
54
|
+
req.params["start"] = options[:start] if options[:start]
|
55
|
+
req.params["results"] = options[:results] if options[:results]
|
56
|
+
req.params["fromdt"] = fromdt if fromdt
|
57
|
+
req.params["todt"] = todt if todt
|
58
|
+
req.params["meta"] = 1 if options[:meta]
|
59
|
+
end
|
60
|
+
|
61
|
+
extract_posts(response.body["posts"])
|
62
|
+
end
|
30
63
|
|
31
64
|
def self.find(options = {})
|
32
65
|
path = "/#{PinboardApi.api_version}/posts/get"
|
33
|
-
response = PinboardApi.
|
66
|
+
response = PinboardApi.request(path) do |req|
|
34
67
|
options.each_pair { |k,v| req.params[k.to_s] = v }
|
35
68
|
end
|
69
|
+
extract_posts(response.body["posts"])
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.last_update
|
73
|
+
path = "/#{PinboardApi.api_version}/posts/update"
|
74
|
+
body = PinboardApi.request(path).body
|
75
|
+
Time.parse(body["update"]["time"])
|
76
|
+
end
|
36
77
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
78
|
+
def self.suggest(url)
|
79
|
+
path = "/#{PinboardApi.api_version}/posts/suggest"
|
80
|
+
response = PinboardApi.request(path, url: url)
|
81
|
+
response.body["suggested"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.recent(options = {})
|
85
|
+
path = "/#{PinboardApi.api_version}/posts/recent"
|
86
|
+
tag = tag_param_string(options[:tag])
|
87
|
+
count = options[:count]
|
88
|
+
|
89
|
+
response = PinboardApi.request(path) do |req|
|
90
|
+
req.params["tag"] = tag if tag
|
91
|
+
req.params["count"] = count if count
|
92
|
+
end
|
93
|
+
|
94
|
+
extract_posts(response.body["posts"])
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.dates(options = {})
|
98
|
+
path = "/#{PinboardApi.api_version}/posts/dates"
|
99
|
+
tag = tag_param_string(options[:tag])
|
100
|
+
|
101
|
+
response = PinboardApi.request(path) do |req|
|
102
|
+
req.params["tag"] = tag if tag
|
103
|
+
end
|
104
|
+
|
105
|
+
dates = response.body["dates"]["date"]
|
106
|
+
dates.map do |date|
|
107
|
+
{ "count" => date["count"].to_i, "date" => Date.parse(date["date"]) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def self.extract_posts(payload)
|
113
|
+
unless payload.respond_to?(:keys) && payload.keys.include?("post")
|
114
|
+
return Array.new
|
115
|
+
end
|
116
|
+
|
117
|
+
# response.body["posts"] - "429 Too Many Requests. Wait 60 seconds before fetching posts/all again."
|
118
|
+
|
119
|
+
payload.inject([]) do |collection, (key, attrs)|
|
120
|
+
if key == "post"
|
121
|
+
Array.wrap(attrs).each do |post|
|
122
|
+
Array.wrap(collection) << new(post)
|
123
|
+
end
|
42
124
|
end
|
43
|
-
|
44
|
-
Array.new
|
125
|
+
collection
|
45
126
|
end
|
46
127
|
end
|
47
128
|
|
48
|
-
def self.
|
49
|
-
|
50
|
-
body = PinboardApi.connection.get(path).body
|
51
|
-
Time.parse(body["update"]["time"])
|
129
|
+
def self.dt_param_string(time)
|
130
|
+
time.nil? ? nil : time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
52
131
|
end
|
53
132
|
|
133
|
+
def self.tag_param_string(tags)
|
134
|
+
tags.nil? ? nil : Array.wrap(tags).join(",")
|
135
|
+
end
|
54
136
|
end
|
55
137
|
end
|
data/lib/pinboard_api/tag.rb
CHANGED
@@ -9,25 +9,22 @@ module PinboardApi
|
|
9
9
|
|
10
10
|
def rename(new_name)
|
11
11
|
path = "/#{PinboardApi.api_version}/tags/rename"
|
12
|
-
response = PinboardApi.
|
13
|
-
req.params["old"] =
|
12
|
+
response = PinboardApi.request(path) do |req|
|
13
|
+
req.params["old"] = @name
|
14
14
|
req.params["new"] = new_name.to_s
|
15
15
|
end
|
16
16
|
body = response.body
|
17
17
|
|
18
18
|
if body["result"] == "done"
|
19
|
-
Tag.new("name" => new_name, "count" =>
|
19
|
+
Tag.new("name" => new_name, "count" => @count)
|
20
20
|
else
|
21
21
|
raise body["result"].to_s
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
25
|
+
def destroy
|
26
26
|
path = "/#{PinboardApi.api_version}/tags/delete"
|
27
|
-
|
28
|
-
req.params["tag"] = self.name
|
29
|
-
end
|
30
|
-
body = response.body
|
27
|
+
body = PinboardApi.request(path, tag: @name).body
|
31
28
|
|
32
29
|
if body["result"] == "done"
|
33
30
|
self
|
@@ -38,13 +35,18 @@ module PinboardApi
|
|
38
35
|
|
39
36
|
def self.all
|
40
37
|
path = "/#{PinboardApi.api_version}/tags/get"
|
41
|
-
body = PinboardApi.
|
38
|
+
body = PinboardApi.request(path).body
|
42
39
|
body["tags"]["tag"].map { |tag| new(tag) }
|
40
|
+
rescue
|
41
|
+
raise RuntimeError, "unknown response"
|
43
42
|
end
|
44
43
|
|
45
44
|
def self.find(name)
|
46
45
|
all.detect { |t| t.name == name }
|
47
46
|
end
|
48
47
|
|
48
|
+
def self.destroy(tag)
|
49
|
+
find(tag).destroy
|
50
|
+
end
|
49
51
|
end
|
50
52
|
end
|