#!/usr/bin/env python
"""
Interface to the Backpack API as specified at the following location:
http://developer.37signals.com/backpack/
Example:
# Get a specific Page instance.
bp=backpack.Backpack("http://yourusername.backpackit.com/",
"yourApiKeyAsSeenOnYourAccountPage")
thePage=bp.page.get(23852)
# Schedule a reminder for two hours from now
bp.reminder.create("Do this",
backpack.formatTime(backpack.getRelativeTime("later")))
"""
# Copyright (c) 2005 Dustin Sallings <dustin@spy.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# <http://www.opensource.org/licenses/mit-license.php>
import os
import sys
import time
import urllib2
import datetime
import exceptions
import xml.dom.minidom
try:
False
except NameError:
True=1
False=0
TIMEFMT="%Y-%m-%d %H:%M:%S"
def parseTime(timeString):
"""Parse a timestamp from a backpack response."""
return(time.mktime(time.strptime(timeString, TIMEFMT)))
def formatTime(t):
"""Format a timestamp for an API call"""
return(time.strftime(TIMEFMT, time.localtime(t)))
def getRelativeTime(rel, t=None):
"""Get the time relative to the specified time (default to now).
Allowed relative terms:
* fifteen - fifteen minutes from now
* nexthour - five minutes after the beginning of the next hour
* later - two hours from now
* morning - 10:00
* afternoon - 14:00
* evening - 19:00
* coupledays - two days from now
* nextweek - seven days from now
"""
if t is None:
t=time.time()
now=datetime.datetime.fromtimestamp(t)
rv=t
if rel == 'fifteen':
# Fifteen minutes later
rv += (15 * 60)
elif rel == 'later':
# Two hours later
rv += 7200
elif rel == 'nexthour':
# Top of next hour
# Increment by an hour
rv += 3600
# Increment an hour
then=datetime.datetime.fromtimestamp(rv)
# Then set the hour and minute
then=datetime.datetime(then.year, then.month, then.day, then.hour,
5, 0)
rv=time.mktime(then.timetuple())
elif rel == 'morning':
then=datetime.datetime(now.year, now.month, now.day, 10, 0, 0)
rv=time.mktime(then.timetuple())
elif rel == 'afternoon':
then=datetime.datetime(now.year, now.month, now.day, 14, 0, 0)
rv=time.mktime(then.timetuple())
elif rel == 'evening':
then=datetime.datetime(now.year, now.month, now.day, 19, 0, 0)
rv=time.mktime(then.timetuple())
elif rel == 'coupledays':
rv=t + (86400 * 2)
elif rel == 'nextweek':
rv=t + (86400 * 7)
else:
raise ValueError("Unknown rel type: " + rel)
# Make sure the time is in the relative future
while rv < t:
rv += 86400
return rv
class BackpackError(exceptions.Exception):
"""Root exception thrown when a backpack error occurs."""
def __init__(self, code, msg):
exceptions.Exception.__init__(self, msg)
self.code=code;
self.msg=msg
def __repr__(self):
return "<code=%d, msg=%s>" % (self.code, self.msg)
class PageLimitExceeded(BackpackError):
"""Exception thrown when an attempt to create a page fails with a 403."""
def __init__(self, msg):
BackpackError.__init__(self, 403, msg)
class BackpackAPI(object):
"""Interface to the backpack API"""
debug=False
url=None
key=None
def __init__(self, u, k, debug=False):
"""Get a Backpack object to the given URL and key"""
if u[-1] == '/':
u=u[:-1]
self.url=u
self.key=k
self.debug=debug
def _parseListItems(self, document):
"""Parses list items from from a minidom document
Returns a list of (id, completed boolean, item text)
"""
rv=[]
for item in document.getElementsByTagName("item"):
rv.append((int(item.getAttribute("id")),
item.getAttribute("completed") == "true",
unicode(item.firstChild.data)))
return rv
def _parseLists(self, document):
rv=[]
for list in document.getElementsByTagName("list"):
rv.append( (int(list.getAttribute("id")),
unicode(list.getAttribute("name"))) )
return rv
def _parseNotes(self, document):
rv=[]
for note in document.getElementsByTagName("note"):
rv.append( (int(note.getAttribute("id")),
unicode(note.getAttribute("title")),
parseTime(note.getAttribute("created_at")),
unicode(note.firstChild.data).strip()))
return rv
# Parse a backpack document, throwing a BackpackError if the document
# indicates an exception
def _parseDocument(self, docString):
document=xml.dom.minidom.parseString(docString)
# Check for error
responseEl=document.getElementsByTagName("response")[0]
if responseEl.getAttribute("success") != "true":
er=responseEl.getElementsByTagName("error")[0]
raise BackpackError(int(er.getAttribute("code")),
unicode(er.firstChild.data))
return document
# Perform the actual call
def _call(self, path, data=""):
p={'token':self.key, 'extra':data}
reqData="""<request><token>%(token)s</token>%(extra)s</request>""" % p
theUrl=self.url + path
if self.debug:
print ">>(%s)\n%s" % (theUrl, reqData)
req=urllib2.Request(theUrl, reqData, {'X-POST_DATA_FORMAT': 'xml'})
opener=urllib2.build_opener()
o=opener.open(req)
result=o.read()
o.close()
if self.debug:
print "<< %s" % (result,)
return self._parseDocument(result)
class ReminderAPI(BackpackAPI):
"""Backpack reminder API."""
def __init__(self, u, k, debug=False):
"""Get a Reminder object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
# parse the reminders xml
def _parseReminders(self, document):
rv=[]
reminders=document.getElementsByTagName("reminder")
for r in reminders:
timestamp=parseTime(r.getAttribute("remind_at"))
id=int(r.getAttribute("id"))
message=unicode(r.firstChild.data)
rv.append((timestamp, id, message))
return rv
def list(self):
"""Get a list of upcoming reminders.
Returns a list of (timestamp, id, message)"""
x=self._call("/ws/reminders")
return self._parseReminders(x)
def create(self, content, at=None):
"""Create a reminder with the given content.
If a time is not given, the content is expected to start with the
+minute or +hour:minute format as specified by backpack."""
val=""
if at is None:
if content[0] != '+':
raise ValueError("No at, and content not beginning with +")
val="""<content>%s</content>""" % (content,)
else:
val="<content>%s</content><remind_at>%s</remind_at>" \
% (content, at)
x=self._call("/ws/reminders/create",
"<reminder>%s</reminder>" % (val,))
return self._parseReminders(x)
def update(self, id, content, at=None):
"""Update the given reminder.
If a time is not given, only the content will be updated."""
val=""
if at is None:
val="""<content>%s</content>""" % (content,)
else:
val="<content>%s</content><remind_at>%s</remind_at>" \
% (content, at)
x=self._call("/ws/reminders/update/%d" % (id, ),
"<reminder>%s</reminder>" % (val, ))
return self._parseReminders(x)
def destroy(self, id):
"""Delete a reminder"""
x=self._call("/ws/reminders/destroy/%d" % (id,))
class Page(object):
"""An individual page.
* Notes are in the form of (id, title, createdDate, msg).
* Links are in the form of (id, title)
* Tags are in the form of (id, name)
"""
title=None
id=None
emailAddress=None
notes=[]
lists=[]
tags=[]
class SearchResult(object):
"""An individual search result. The object supports the ability to
retrieve its full representation based on the type of result. Retrieving
a writeboard only returns the id at this point, because no Writeboard
API is currently supported"""
bp=None # Backpack instance to enable get
pageId=None
pageTitle=None
type=None
containerId=None
def get(self):
"""Returns the appropriate representation of itself based type
list: Returns the result of Backpack.list.get
note: Returns the result of Backpack.notes.list
writeboard: Returns (page id, page title, writeboard id)
email: Returns the result of Backpack.email.get
"""
if self.type == 'list':
return self.bp.list.get(self.pageId, self.containerId)
elif self.type == 'note':
return self.bp.notes.list(self.pageId)
elif self.type == 'writeboard_link':
return (self.pageId, self.pageTitle, self.containerId)
elif self.type == 'email':
return self.bp.email.get(self.pageId, self.containerId)
class PageAPI(BackpackAPI):
"""Backpack page API."""
def __init__(self, u, k, debug=False):
"""Get a Page object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
# parse the page list xml
def _parsePageList(self, document):
rv=[]
reminders=document.getElementsByTagName("page")
for r in reminders:
id=int(r.getAttribute("id"))
scope=unicode(r.getAttribute("scope"))
title=unicode(r.getAttribute("title"))
rv.append((id, scope, title))
return rv
def _parseSearchResult(self, document):
rv = []
pages = document.getElementsByTagName("page")
for p in pages:
for send in p.getElementsByTagName("send"):
sr = SearchResult()
sr.bp = Backpack(self.url, self.key, self.debug)
sr.pageId = int(p.getAttribute("id"))
sr.pageTitle = unicode(p.getAttribute("title"))
sr.type = send.firstChild.data
sr.containerId = int(send.getAttribute("id"))
rv.append(sr)
return rv
# get an iterator on the named node of the zeroth node of the given list
def __linkIter(self, node, container, elementname):
rv=[]
nlist=node.getElementsByTagName(container)
if len(nlist) > 0:
rv=nlist[0].getElementsByTagName(elementname)
return rv
# Parse the individual page xml
def _parsePage(self, document):
rv=Page()
page=document.getElementsByTagName("page")[0]
rv.title=page.getAttribute("title")
rv.id=int(page.getAttribute("id"))
rv.emailAddress=page.getAttribute("email_address")
rv.notes=self._parseNotes(page)
rv.lists=self._parseLists(page)
for tag in self.__linkIter(page, "tags", "tag"):
rv.tags.append( (int(tag.getAttribute("id")),
unicode(tag.getAttribute("name"))))
return rv
def list(self):
"""List all pages
Returns a list of (id, scope, title) tuples.
"""
x=self._call("/ws/pages/all")
return self._parsePageList(x)
def get(self, id):
"""Get a given page by id.
Returns a Page instance.
"""
x=self._call("/ws/page/%d" % (id,))
return self._parsePage(x)
def create(self, title):
"""Create a new page.
Returns (id, title)"""
data="<page><title>%s</title></page>" \
% (title,)
try:
x=self._call("/ws/pages/new", data)
except urllib2.HTTPError, e:
# A 403 occurs when a page already exists.
if e.code == 403:
raise PageLimitExceeded(title)
else:
raise e
p=x.getElementsByTagName("page")[0]
return (int(p.getAttribute("id")), unicode(p.getAttribute("title")))
def destroy(self, id):
"""Delete a page"""
x=self._call("/ws/page/%d/destroy" % (id,))
def search(self, term):
"""Search for pages containing the term
Returns a list of SearchResult objects.
"""
data="<term>%s</term>" % term
x = self._call("/ws/pages/search", data)
return self._parseSearchResult(x)
def updateTitle(self, id, title):
"""Update a title"""
data="<page><title>%s</title></page>" % (title,)
x=self._call("/ws/page/%d/update_title" % (id,), data)
def duplicate(self, id):
"""Duplicate a page, get the new (id, title)"""
x=self._call("/ws/page/%d/duplicate" % (id,))
p=x.getElementsByTagName("page")[0]
return (int(p.getAttribute("id")), unicode(p.getAttribute("title")))
def share(self, id, emailAddresses=[], isPublic=False):
"""Share this page with others."""
data=""
if len(emailAddresses) > 0:
data+="<email_addresses>%s</email_addresses>" \
% (' '.join(emailAddresses),)
data+="<page><public>%d</public></page>" % (isPublic,)
x=self._call("/ws/page/%d/share" % (id,), data)
def unshare(self, id):
"""Unshare a page."""
x=self._call("/ws/page/%d/unshare_friend_page" % (id,))
def email(self, id):
"""Email yourself a page."""
x=self._call("/ws/page/%d/email" % (id,))
class ExportAPI(PageAPI, ReminderAPI):
"""Export page API."""
def __init__(self, u, k, debug=False):
"""Get an Export object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def _parseDocument(self, docString):
document=xml.dom.minidom.parseString(docString)
responseEl=document.getElementsByTagName("backpack")[0]
return document
def _parseBackup(self, x):
return self._parsePageList(x), self._parseReminders(x)
def export(self):
"""Get export of all data from BackPack
returns (pages, reminders)
"""
x=self._call("/ws/account/export")
return(self._parseBackup(x))
class ListAPI(BackpackAPI):
"""Backpack list API."""
def __init__(self, u, k, debug=False):
"""Get a ListAPI object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def create(self, pageId, name):
"""Creates a new list on the given page
Returns (id, name)
"""
data = "<name>%s</name>" % name
x = self._call("/ws/page/%d/lists/add" % pageId, data)
l = x.getElementsByTagName("list")[0]
return ( int(l.getAttribute("id")), unicode(l.getAttribute("name")) )
def update(self, pageId, listId, name):
"""Changes a list's name"""
data = "<list><name>%s</name></list>" % name
self._call("/ws/page/%d/lists/update/%d" % (pageId, listId), data)
def destroy(self, pageId, listId):
self._call("/ws/page/%d/lists/destroy/%d" % (pageId, listId))
def list(self, pageId):
"""Get a list of lists on the given page
list of (id, name)
"""
x = self._call("/ws/page/%d/lists/list" % pageId)
return self._parseLists(x)
class ListItemAPI(BackpackAPI):
"""Backpack list API."""
MOVE_LOWER='move_lower'
MOVE_HIGHER='move_higher'
MOVE_TO_TOP='move_to_top'
MOVE_TO_BOTTOM='move_to_bottom'
def __init__(self, u, k, debug=False):
"""Get a ListAPI object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def list(self, pageId, listId):
"""Get a list of the items on the given list.
list of (id, completedBoolean, text)
"""
x=self._call("/ws/page/%d/lists/%d/items/list" % (pageId, listId))
return self._parseListItems(x)
def create(self, pageId, listId, text):
"""Create a new entry.
Return (id, completedBoolean, text)"""
data="<item><content>%s</content></item>" % (text,)
x=self._call("/ws/page/%d/lists/%d/items/add" % (pageId, listId), data)
return self._parseListItems(x)[0]
def update(self, pageId, listId, id, text):
"""Update an entry."""
data="<item><content>%s</content></item>" % (text,)
x=self._call("/ws/page/%d/lists/%d/items/update/%d" %
(pageId, listId, id), data)
def toggle(self, pageId, listId, id):
"""Toggle an entry."""
x=self._call("/ws/page/%d/lists/%d/items/toggle/%d" %
(pageId, listId, id))
def destroy(self, pageId, listId, id):
"""Destroy an entry."""
x=self._call("/ws/page/%d/lists/%d/items/destroy/%d" %
(pageId, listId, id))
def move(self, pageId, listId, id, direction):
"""Move an entry.
direction can be 'move_lower', 'move_higher',
'move_to_top', and 'move_to_bottom'
"""
data="<direction>%s</direction>" % (direction,)
x=self._call("/ws/page/%d/lists/%d/items/move/%d" %
(pageId, listId, id), data)
class NoteAPI(BackpackAPI):
"""API to Backpack Notes for a page."""
def __init__(self, u, k, debug=False):
"""Get a NoteAPI object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def list(self, pageId):
"""Get a list of the items on the given page.
list of (id, title, timestamp, text)
"""
x=self._call("/ws/page/%d/notes/list" % pageId)
return self._parseNotes(x)
def create(self, pageId, title, body):
"""Create a new entry.
Return (id, title, timestamp, text)"""
data="<note><title>%s</title><body>%s</body></note>" % (title, body)
x=self._call("/ws/page/%d/notes/create" % (pageId,), data)
return self._parseNotes(x)[0]
def update(self, pageId, noteId, title, body):
"""Update a note."""
data="<note><title>%s</title><body>%s</body></note>" % (title, body)
x=self._call("/ws/page/%d/notes/update/%d" % (pageId, noteId), data)
def destroy(self, pageId, noteId):
"""Delete a note."""
x=self._call("/ws/page/%d/notes/destroy/%d" % (pageId, noteId))
class EmailAPI(BackpackAPI):
"""The backpack Email API"""
def __init__(self, u, k, debug=False):
"""Get a ListAPI object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def _parseEmails(self, x):
rv=[]
for item in x.getElementsByTagName("email"):
rv.append((int(item.getAttribute("id")),
unicode(item.getAttribute("subject")),
parseTime(item.getAttribute("created_at")),
item.firstChild.data))
return rv
def list(self, pageId):
"""Get a list of the email on the given page.
list of (id, subject, timestamp, text)
"""
x=self._call("/ws/page/%d/emails/list" % pageId)
return self._parseEmails(x)
def get(self, pageId, mailId):
"""Get an individual email from the given page.
(id, subject, timestamp, text)
"""
x=self._call("/ws/page/%d/emails/show/%d" % (pageId, mailId))
return self._parseEmails(x)[0]
def destroy(self, pageId, mailId):
"""Delete an email."""
x=self._call("/ws/page/%d/emails/destroy/%d" % (pageId, mailId))
class TagAPI(BackpackAPI):
"""The Backpack Tags API."""
def __init__(self, u, k, debug=False):
"""Get a TagAPI object to the given URL and key"""
BackpackAPI.__init__(self, u, k, debug)
def _parseTaggedPageList(self, x):
rv=[]
for item in x.getElementsByTagName("page"):
rv.append((int(item.getAttribute("id")),
unicode(item.getAttribute("title"))))
return rv
def pagesForTag(self, tagId):
"""Get a list of the pages with a given tag ID.
return a list of (id, title)
"""
x=self._call("/ws/tags/select/%d" % tagId)
return self._parseTaggedPageList(x)
def _cleanTags(self, tags):
"""Clean the given tags for API invocation."""
cleanedTags=[]
for t in tags:
if t.find('"') != -1:
raise exceptions.ValueError("Tags can't have quotes.")
if t.find(' ') != -1:
cleanedTags.append('"%s"' % t)
else:
cleanedTags.append(t)
return cleanedTags
def tagPage(self, pageId, tags):
"""Tag a page with a list of words."""
data="<tags>%s</tags>" % ' '.join(self._cleanTags(tags))
x=self._call("/ws/page/%d/tags/tag" % pageId, data)
class Backpack(object):
"""Interface to all of the backpack APIs.
* page - PageAPI object
* reminder - ReminderAPI object
* list - ListAPI object
* notes - NoteAPI object
* tags - TagAPI object
* email - EmailAPI object
* export - ExportAPI object
"""
reminder=None
page=None
list=None
notes=None
email=None
tags=None
export=None
def __init__(self, url, key, debug=False):
"""Initialize the backpack APIs."""
self.reminder=ReminderAPI(url, key, debug)
self.page=PageAPI(url, key, debug)
self.list=ListAPI(url, key, debug)
self.listItem=ListItemAPI(url, key, debug)
self.notes=NoteAPI(url, key, debug)
self.email=EmailAPI(url, key, debug)
self.tags=TagAPI(url, key, debug)
self.export=ExportAPI(url, key, debug)