Automating Library Books
nkirsch says:
I use RTM, the API, and a Python script to crawl my local library's RSS feed. I automatically find books that have been checked out and their due date and create RTM tasks for each item (marked with the library location and a tag for book). When I return the book, it automatically marks the task as complete.
I no longer have to worry about keeping track of when books are due - RTM takes care of it all for me!
I no longer have to worry about keeping track of when books are due - RTM takes care of it all for me!
anna.santos says:
I love this idea. I check out tons of books for my kids so it is always a challenge to keep track of them. I don't understand how you do it though.
emily (Remember The Milk) says:
Hi nkirsch,
Awesome tip -- just wanted to let you know that you're this week's Tips & Tricks Tuesday winner. We've upgraded your RTM account to have a free year of Pro. :)
Awesome tip -- just wanted to let you know that you're this week's Tips & Tricks Tuesday winner. We've upgraded your RTM account to have a free year of Pro. :)
espressodoppio says:
Me too I found the tip useful but don't have an idea how to realize it. Would it be possible to post an Howto?
(closed account) says:
Wow. Simply amazing. +1 on the request to post a how-to.
nkirsch says:
Sure - try not to beat me up too much. ;)
I'm not sure the tabs will come through - if there is a better way to post, let me know.
library_script:
#!/usr/bin/python2.5
import user
import feedparser
import datetime
import re
import rtm
import sys
DUE_NMK='URL'
DUE_EK='URL'
RDY_NMK='URL'
DUE_DATE=re.compile('.*Date Due: (.*) ;', re.DOTALL)
RDY_UP=re.compile('.*Ready for pickup.*', re.DOTALL)
TASK='list:Home AND status:incomplete AND location:"Queen Anne Public Library"'
if __name__ == '__main__':
rtm.rtm_auth()
query = TASK
to_return = rtm.rtm_tasks(query)
entries = []
for url in [DUE_NMK, DUE_EK]:
feed = feedparser.parse(url)
# Find and correct entries which are due.
for entry in feed['entries']:
match = DUE_DATE.match(entry['summary'])
if not match:
continue
name = entry['title']
if name[-2:] == " /":
name = name[:-2]
if name[0:3] == "Due":
name = name[name.find(':')+1:]
name = name.strip()
due = match.group(1)
entries.append((name, due))
if len(sys.argv) > 1:
print '%s due %s' % (name, due)
tasks = [task for task in to_return if task['name'].find(name) > -1]
if not tasks:
rtm.rtm_add('Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')
else:
d1 = datetime.datetime.strptime(tasks[0]['task']['due'], "%Y-%m-%dT%H:%M:%SZ")
d2 = datetime.datetime.strptime(due, "%m/%d/%Y")
if d2 - d1:
rtm.rtm_task_update(tasks[0]['task']['id'], tasks[0]['id'], 'Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')
for task in to_return:
book = [entry for entry in entries if task['name'].find(entry[0]) > 0]
if not book:
rtm.rtm_task_complete(task['task']['id'], task['id'], 'Home')
for url in []:
feed = feedparser.parse(url)
for entry in feed['entries']:
print 'TITLE', entry['title']
print 'SUMMARY', entry['summary']
rtm.py:
#!/usr/bin/python
import urllib2
import urllib
import hashlib
import cjson
import getopt
import sys
API_KEY='KEY'
API_SECRET='SECRET'
API_URL='http://api.rememberthemilk.com/services/rest/'
API_AUTH='http://api.rememberthemilk.com/services/auth/'
API_TOKEN=None
def rtm_helper(method, iparams, url, execute):
params = iparams.copy()
params.update({
'api_key':API_KEY,
'format':'json'})
if method:
params['method'] = method
keys = params.keys()
keys.sort()
pairs=reduce(lambda y,x:y+x+params[x], keys, '')
signature=hashlib.md5(API_SECRET + pairs).hexdigest()
params.update({'api_sig':signature})
parameter_string = urllib.urlencode(params.items())
rest_url = '%s?%s' % (url, parameter_string)
if execute:
json = urllib2.urlopen(rest_url).read()
return cjson.decode(json)['rsp']
return rest_url
def rtm_auth():
global API_TOKEN
if API_TOKEN:
return
try:
API_TOKEN = open('rtm.auth').read().strip()
except:
frob = rtm_method('rtm.auth.getFrob')['frob']
print rtm_helper(None, {'perms':'delete', 'frob':frob}, API_AUTH, False)
raw_input("Copy URL to web browser, authorize, and hit enter to continue.")
resp = rtm_method('rtm.auth.getToken', {'frob':frob})
auth = resp['auth']
open('rtm.auth', 'w').write(auth['token'])
API_TOKEN = auth['token']
def rtm_method(method, iparams = {}):
global API_TOKEN
if API_TOKEN:
iparams.update({'auth_token':API_TOKEN})
return rtm_helper(method, iparams, API_URL, True)
def rtm_location(name):
locations = rtm_method('rtm.locations.getList')['locations']['location']
for location in locations:
if location['name'] == name:
return location['id']
raise 'Location cannot be found.'
def rtm_lists(name):
tasklists = rtm_method('rtm.lists.getList')['lists']['list']
for tasklist in tasklists:
if tasklist['name'] == name:
return tasklist['id']
raise 'List cannot be found.'
def rtm_add(listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)
task = rtm_method('rtm.tasks.add',
{'timeline':timeline, 'name':name, 'list_id': listid})
taskseries = task['list']['taskseries']['id']
taskid = task['list']['taskseries']['task']['id']
task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task
def rtm_task_complete(taskid, taskseries, listname):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
task = rtm_method('rtm.tasks.complete',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid})
assert(task['stat'] == 'ok'), task
def rtm_task_update(taskid, taskseries, listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)
task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task
def rtm_tasks(task_filter):
tasks = []
result = rtm_method('rtm.tasks.getList', {'filter':task_filter})['tasks']
if not result:
return tasks
if not result.has_key('list'):
return tasks
revision = result['rev']
if not isinstance(result['list'], list):
result['list'] = [result['list']]
for tlist in result['list']:
list_id = tlist['id']
if not isinstance(tlist['taskseries'], list):
tlist['taskseries'] = [tlist['taskseries']]
for tseries in tlist['taskseries']:
taskseries_id = tseries['id']
task = tseries.copy()
task['list_id'] = list_id
task['taskseries_id'] = taskseries_id
tasks.append(task)
return tasks
I'm not sure the tabs will come through - if there is a better way to post, let me know.
library_script:
#!/usr/bin/python2.5
import user
import feedparser
import datetime
import re
import rtm
import sys
DUE_NMK='URL'
DUE_EK='URL'
RDY_NMK='URL'
DUE_DATE=re.compile('.*Date Due: (.*) ;', re.DOTALL)
RDY_UP=re.compile('.*Ready for pickup.*', re.DOTALL)
TASK='list:Home AND status:incomplete AND location:"Queen Anne Public Library"'
if __name__ == '__main__':
rtm.rtm_auth()
query = TASK
to_return = rtm.rtm_tasks(query)
entries = []
for url in [DUE_NMK, DUE_EK]:
feed = feedparser.parse(url)
# Find and correct entries which are due.
for entry in feed['entries']:
match = DUE_DATE.match(entry['summary'])
if not match:
continue
name = entry['title']
if name[-2:] == " /":
name = name[:-2]
if name[0:3] == "Due":
name = name[name.find(':')+1:]
name = name.strip()
due = match.group(1)
entries.append((name, due))
if len(sys.argv) > 1:
print '%s due %s' % (name, due)
tasks = [task for task in to_return if task['name'].find(name) > -1]
if not tasks:
rtm.rtm_add('Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')
else:
d1 = datetime.datetime.strptime(tasks[0]['task']['due'], "%Y-%m-%dT%H:%M:%SZ")
d2 = datetime.datetime.strptime(due, "%m/%d/%Y")
if d2 - d1:
rtm.rtm_task_update(tasks[0]['task']['id'], tasks[0]['id'], 'Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')
for task in to_return:
book = [entry for entry in entries if task['name'].find(entry[0]) > 0]
if not book:
rtm.rtm_task_complete(task['task']['id'], task['id'], 'Home')
for url in []:
feed = feedparser.parse(url)
for entry in feed['entries']:
print 'TITLE', entry['title']
print 'SUMMARY', entry['summary']
rtm.py:
#!/usr/bin/python
import urllib2
import urllib
import hashlib
import cjson
import getopt
import sys
API_KEY='KEY'
API_SECRET='SECRET'
API_URL='http://api.rememberthemilk.com/services/rest/'
API_AUTH='http://api.rememberthemilk.com/services/auth/'
API_TOKEN=None
def rtm_helper(method, iparams, url, execute):
params = iparams.copy()
params.update({
'api_key':API_KEY,
'format':'json'})
if method:
params['method'] = method
keys = params.keys()
keys.sort()
pairs=reduce(lambda y,x:y+x+params[x], keys, '')
signature=hashlib.md5(API_SECRET + pairs).hexdigest()
params.update({'api_sig':signature})
parameter_string = urllib.urlencode(params.items())
rest_url = '%s?%s' % (url, parameter_string)
if execute:
json = urllib2.urlopen(rest_url).read()
return cjson.decode(json)['rsp']
return rest_url
def rtm_auth():
global API_TOKEN
if API_TOKEN:
return
try:
API_TOKEN = open('rtm.auth').read().strip()
except:
frob = rtm_method('rtm.auth.getFrob')['frob']
print rtm_helper(None, {'perms':'delete', 'frob':frob}, API_AUTH, False)
raw_input("Copy URL to web browser, authorize, and hit enter to continue.")
resp = rtm_method('rtm.auth.getToken', {'frob':frob})
auth = resp['auth']
open('rtm.auth', 'w').write(auth['token'])
API_TOKEN = auth['token']
def rtm_method(method, iparams = {}):
global API_TOKEN
if API_TOKEN:
iparams.update({'auth_token':API_TOKEN})
return rtm_helper(method, iparams, API_URL, True)
def rtm_location(name):
locations = rtm_method('rtm.locations.getList')['locations']['location']
for location in locations:
if location['name'] == name:
return location['id']
raise 'Location cannot be found.'
def rtm_lists(name):
tasklists = rtm_method('rtm.lists.getList')['lists']['list']
for tasklist in tasklists:
if tasklist['name'] == name:
return tasklist['id']
raise 'List cannot be found.'
def rtm_add(listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)
task = rtm_method('rtm.tasks.add',
{'timeline':timeline, 'name':name, 'list_id': listid})
taskseries = task['list']['taskseries']['id']
taskid = task['list']['taskseries']['task']['id']
task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task
def rtm_task_complete(taskid, taskseries, listname):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
task = rtm_method('rtm.tasks.complete',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid})
assert(task['stat'] == 'ok'), task
def rtm_task_update(taskid, taskseries, listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)
task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task
def rtm_tasks(task_filter):
tasks = []
result = rtm_method('rtm.tasks.getList', {'filter':task_filter})['tasks']
if not result:
return tasks
if not result.has_key('list'):
return tasks
revision = result['rev']
if not isinstance(result['list'], list):
result['list'] = [result['list']]
for tlist in result['list']:
list_id = tlist['id']
if not isinstance(tlist['taskseries'], list):
tlist['taskseries'] = [tlist['taskseries']]
for tseries in tlist['taskseries']:
taskseries_id = tseries['id']
task = tseries.copy()
task['list_id'] = list_id
task['taskseries_id'] = taskseries_id
tasks.append(task)
return tasks
nkirsch says:
I'm happy to help others with this - if you send me a private note (is that possible?) with a link to your library, I will see if they have an RSS feed that could be used.
I could potentially turn this into a free web app if other libraries work in a similar fashion.
I could potentially turn this into a free web app if other libraries work in a similar fashion.
pixl.dave says:
@nhirsch I would recommend to also post the two snippets to a code sharing site like http://dpaste.com
This is a django paste, so you have proper indentation of code and colored syntax for the language... make sure you click the hold check box otherwise these snippets will be deleted automatically after 7 days.
If that is not of convenience to you, than maybe you can archive the two python scripts and share them on a free sharing site.
Either way thanks for this experiment of yours :)
This is a django paste, so you have proper indentation of code and colored syntax for the language... make sure you click the hold check box otherwise these snippets will be deleted automatically after 7 days.
If that is not of convenience to you, than maybe you can archive the two python scripts and share them on a free sharing site.
Either way thanks for this experiment of yours :)
voyagerfan5761 says:
This sounds like an awesome project. How about putting it up on GitHub?
hibbers says:
I would LOVE to know how to implement this - haven't used the API before - if you have time to write a how-to that would be awesome :-)
Log in
to post a reply.