Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #6 from dreikanter/master

Token authorization + test coverage
  • Loading branch information...
commit 2538fccb9b970f3ebc4255b22653f3d27c5b7d0f 2 parents 0f1226b + 27d10ad
Morgan Craft authored
11 .editorconfig
... ... @@ -0,0 +1,11 @@
  1 +; top-most EditorConfig file
  2 +root = true
  3 +
  4 +; Unix-style newlines
  5 +[*]
  6 +end_of_line = CRLF
  7 +
  8 +; 4 space indentation
  9 +[*.py]
  10 +indent_style = space
  11 +indent_size = 4
13 .gitignore
... ... @@ -0,0 +1,13 @@
  1 +*.py[co]
  2 +
  3 +# Packages
  4 +*.egg
  5 +*.egg-info
  6 +dist
  7 +build
  8 +bin
  9 +sdist
  10 +MANIFEST
  11 +
  12 +# Do not commit user credentials!
  13 +tests/conf.py
82 pinboard.py
@@ -6,7 +6,7 @@
6 6
7 7 This library was built on top of Paul Mucur's original work on the python-delicious
8 8 which was supported for python 2.3. Morgan became a contributor and ported this library
9   -to pinboard.in when it was announced in December 2010 that delicious servers may be
  9 +to pinboard.in when it was announced in December 2010 that delicious servers may be
10 10 shutting down.
11 11
12 12 The port to pinboard resulted in the inclusion of gzip support
@@ -74,13 +74,33 @@
74 74 AUTH_HANDLER_REALM = 'API'
75 75 AUTH_HANDLER_URI = "https://api.pinboard.in/"
76 76
77   -def open(username, password):
78   - """Open a connection to a pinboard.in account"""
79   - return PinboardAccount(username, password)
80 77
81   -def connect(username, password):
82   - """Open a connection to a pinboard.in account"""
83   - return open(username, password)
  78 +def open(username=None, password=None, token=None):
  79 + """Open a connection to a pinboard.in account
  80 +
  81 + Arguments:
  82 + username -- pinboard.in user name; for canonical authentication
  83 + both user and password should be specified
  84 +
  85 + password -- pinboard.in password
  86 +
  87 + token -- API token; username and password will be ignored
  88 + if the token is defined
  89 +
  90 + Usage:
  91 + >>> open('johnd', 'secret$777')
  92 + >>> open(username='johnd', password='secret$777')
  93 + >>> open(token='johnd:258329B14EB83FD1E449')
  94 +
  95 + Returns:
  96 + New pinboard.PinboardAccount instance."""
  97 + return PinboardAccount(username, password, token)
  98 +
  99 +
  100 +def connect(username=None, password=None, token=None):
  101 + """Open a connection to a pinboard.in account
  102 + (alias for pinboard.open())."""
  103 + return open(username, password, token)
84 104
85 105
86 106 # Custom exceptions
@@ -121,6 +141,7 @@ class DateParamsError(PinboardError):
121 141 '''Date params error'''
122 142 pass
123 143
  144 +
124 145 class PinboardAccount(UserDict):
125 146 """A pinboard.in account"""
126 147
@@ -131,24 +152,31 @@ class PinboardAccount(UserDict):
131 152 # Time of last request so that the one second limit can be enforced.
132 153 __lastrequest = None
133 154
  155 + # Pinboard API token
  156 + # (see http://blog.pinboard.in/2012/07/api_authentication_tokens/)
  157 + __token = None
  158 +
134 159 # Special methods
135 160
136   - def __init__(self, username, password):
  161 + def __init__(self, username=None, password=None, token=None):
137 162 UserDict.__init__(self)
138 163 # Authenticate the URL opener so that it can access Pinboard
139 164 if _debug:
140 165 sys.stderr.write("Initialising Pinboard Account object.\n")
141   - auth_handler = urllib2.HTTPBasicAuthHandler()
142   - auth_handler.add_password("API", "https://api.pinboard.in/", \
143   - username, password)
144   - opener = urllib2.build_opener(auth_handler)
  166 +
  167 + if token:
  168 + self.__token = urllib.quote_plus(token)
  169 + opener = urllib2.build_opener()
  170 + else:
  171 + auth_handler = urllib2.HTTPBasicAuthHandler()
  172 + auth_handler.add_password("API", "https://api.pinboard.in/", \
  173 + username, password)
  174 + opener = urllib2.build_opener(auth_handler)
  175 +
145 176 opener.addheaders = [("User-agent", USER_AGENT), ('Accept-encoding', 'gzip')]
146 177 urllib2.install_opener(opener)
147 178 if _debug:
148 179 sys.stderr.write("URL opener with HTTP authenticiation installed globally.\n")
149   -
150   -
151   - if _debug:
152 180 sys.stderr.write("Time of last update loaded into class dictionary.\n")
153 181
154 182 def __getitem__(self, key):
@@ -171,9 +199,7 @@ def __setitem__(self, key, value):
171 199 self.__postschanged = 1
172 200 return UserDict.__setitem__(self, key, value)
173 201
174   -
175 202 def __request(self, url):
176   -
177 203 # Make sure that it has been at least 1 second since the last
178 204 # request was made. If not, halt execution for approximately one
179 205 # seconds.
@@ -186,7 +212,11 @@ def __request(self, url):
186 212 self.__lastrequest = time.time()
187 213 if _debug:
188 214 sys.stderr.write("Opening %s.\n" % url)
189   -
  215 +
  216 + if self.__token:
  217 + sep = '&' if '?' in url else '?'
  218 + url = "%s%sauth_token=%s" % (url, sep, self.__token)
  219 +
190 220 try:
191 221 ## for pinboard a gzip request is made
192 222 raw_xml = urllib2.urlopen(url)
@@ -195,10 +225,10 @@ def __request(self, url):
195 225 compressedstream = StringIO.StringIO(compresseddata)
196 226 gzipper = gzip.GzipFile(fileobj=compressedstream)
197 227 xml = gzipper.read()
198   -
  228 +
199 229 except urllib2.URLError, e:
200   - raise e
201   -
  230 + raise e
  231 +
202 232 self["headers"] = {}
203 233 for header in raw_xml.headers.headers:
204 234 (name, value) = header.split(": ")
@@ -209,10 +239,7 @@ def __request(self, url):
209 239 if _debug:
210 240 sys.stderr.write("%s opened successfully.\n" % url)
211 241 return minidom.parseString(xml)
212   -
213   -
214   -
215   -
  242 +
216 243 def posts(self, tag="", date="", todt="", fromdt="", count=0):
217 244 """Return pinboard.in bookmarks as a list of dictionaries.
218 245
@@ -319,7 +346,7 @@ def suggest(self, url):
319 346
320 347 popular = [t.firstChild.data for t in tags.getElementsByTagName('popular')]
321 348 recommended = [t.firstChild.data for t in tags.getElementsByTagName('recommended')]
322   -
  349 +
323 350 return {'popular': popular, 'recommended': recommended}
324 351
325 352 def tags(self):
@@ -397,14 +424,13 @@ def dates(self, tag=""):
397 424 self["dates"] = dates
398 425 return dates
399 426
400   -
401 427 # Methods to modify pinboard.in content
402 428
403 429 def add(self, url, description, extended="", tags=(), date="", toread="no", replace="no", shared="yes"):
404 430 """Add a new post to pinboard.in"""
405 431 query = {}
406 432 query["url"] = url
407   - query ["description"] = description
  433 + query["description"] = description
408 434 query["toread"] = toread
409 435 query["replace"] = replace
410 436 query["shared"] = shared
6 tests/sample_conf.py
... ... @@ -0,0 +1,6 @@
  1 +# Copy this file to conf.py and replace values
  2 +# with the real data to enable unit tests.
  3 +
  4 +username = 'johnd'
  5 +password = 'secret$71717'
  6 +token = 'johnd:01234567890123456789'
78 tests/test.py
... ... @@ -0,0 +1,78 @@
  1 +#!/usr/bin/env python
  2 +
  3 +"""Python-Pinboard unit tests.
  4 +
  5 +Before running the tests:
  6 +
  7 +1. Update user credentials at conf.py (see sample_conf.py for example).
  8 +2. Consider to backup your data before running this test on real account
  9 +or use dedicated sandbox account (the second approach is recommended)."""
  10 +
  11 +import conf
  12 +import unittest
  13 +import sys
  14 +import time
  15 +
  16 +sys.path.insert(0, '..')
  17 +
  18 +import pinboard
  19 +
  20 +
  21 +def get_tag_names(tags):
  22 + for tag in tags:
  23 + yield tag['name']
  24 +
  25 +
  26 +class TestPinboardAccount(unittest.TestCase):
  27 +
  28 + def test_token(self):
  29 + p = pinboard.open(token=conf.token)
  30 + self.common_case(p)
  31 +
  32 + def test_canonical(self):
  33 + p = pinboard.open(conf.username, conf.password)
  34 + self.common_case(p)
  35 +
  36 + def common_case(self, p):
  37 + """Add some test bookmark records and than delete them"""
  38 + test_url = 'http://github.com'
  39 + test_tag = '__testing__'
  40 +
  41 + # Adding a test bookmark
  42 + p.add(url=test_url,
  43 + description='GitHub',
  44 + extended='It\'s a GitHub!',
  45 + tags=(test_tag))
  46 +
  47 + posts = p.posts(tag=test_tag)
  48 +
  49 + # Bookmark was added
  50 + self.assertIs(type(posts), list)
  51 + self.assertTrue(posts)
  52 +
  53 + # Looks like there is no immediate consistency between API inputs
  54 + # and outputs, that's why additional delays added here and below.
  55 + time.sleep(3)
  56 +
  57 + # Tags contains new tag
  58 + tags = p.tags()
  59 + self.assertTrue(type(tags), dict)
  60 + self.assertIn(test_tag, get_tag_names(tags))
  61 +
  62 + # Deleting test bookmark(s)
  63 + for post in posts:
  64 + p.delete(post['href'])
  65 +
  66 + time.sleep(3)
  67 +
  68 + # There are no posts with test tag
  69 + posts = p.posts(tag=test_tag)
  70 + self.assertFalse(posts)
  71 +
  72 + # And no test tag any more
  73 + tags = p.tags()
  74 + self.assertNotIn(test_tag, get_tag_names(tags))
  75 +
  76 +
  77 +if __name__ == '__main__':
  78 + unittest.main()

0 comments on commit 2538fcc

Please sign in to comment.
Something went wrong with that request. Please try again.