google tasks, terminal, and geek tool

Over the course of the past few years, I’ve used many different todo list managers. I’ve tried OmniFocus (too expensive, especially for both the desktop and iPhone versions), EasyTask (which is very nice, and has a good iPhone client too), Things, Remember the Milk, and a few more. The closest I’ve come to an app that I really like is The Hit List, which I got in a mac bundle years ago. That app has been in beta for, like, 3 years with a very unresponsive developer. In fact it’s User’s discussion list is more a place for people to muse on whether or not a v1.0 of the app will ever actually appear. (Current rumor is that it showed up briefly in Apple’s OS X AppStore.)

My relationship to task managers, and my occasional interest in GTD that has fueled that relationship, have always felt way overly complicated. I do need reminders of the things I need to do, but invariably my lists sit in programs I rarely open, and do nothing to actually enhance my productivity. I also messed with most of those programs when I was using iCal and Apple Mail, and several of them worked well enough with those programs. More recently, I’ve felt completely ambivalent over the whole GTD approach, with contexts, and projects, and mixed temporalities. I find that I do the best with simple lists of things to do. For a little while I used the command line Todo.txt client that still allowed for tagging/contexts/projects/etc., and displayed that on my desktop with Geek Tool. Fun, geeky, and as complicated or easy as one might like. Not at all integrated with email.

On the topic of productivity for academics, I’ve consolidated my calendars and email in gmail out of shear convenience. I like the workflow of gmail– especially now with priority messages. I can simply archive messages with a single keystroke when it’s time to clean out the inbox and the like. I’ve used Google Tasks in concert with gmail before, and find it attractive for its simplicity. If I want project lists, I can simply start a new list. I also like that it’s integrated with my calendar, etc. That said, I do like having my list displayed on my desktop with Geek Tool, something that was impossible to do without an API for Google Tasks. To my surprise and delight, after many years of being pestered for it, Google finally released an API for Tasks a few days ago. So, yesterday I hacked together some code to interact with Tasks from the command line and to display my lists on my desktop using Geek Tool. The latter is slightly complicated because of the move to OAuth for authentication, but I got it working nonetheless. As it stands now, the cli script only allows listing tasks, adding tasks (and new lists), and clearing completed tasks. I’ll add updating and deleting individual tasks, but it’s slightly more complicated given the way tasks are ID’d in the API.

As with other applications that require authentication to interact with google services, your Tasks app needs to be authorized to make requests. So, for initial set-up there are two things you must do.

  1. Register your new application with google’s API Console by activating the Google Task API, and providing the requested information for a new app. This process will give you a ClientID, a ClientSecret Key, and an Application Developer Key that are necessary to authenticate the application.
  2. Download and install the google API python developer’s library. I first installed using easy_install, but it didn’t work. So, you should use download the tar package, unzip it, open the terminal, change directory to the unzipped folder, and execute the command sudo python setup.py install.
  3. Authenticating the application the first time you run the script below from a new place (for example, the first time you run it from the command line, there will be a dialogue to approve access to your Tasks data. Paste the link provided into your browser, accept the access to your Tasks, then paste the provided key back into the terminal when asked for it.

The script below for using Tasks executes the authentication, and then interacts with Tasks based on arguments you pass via the command line. It stores secret codes in the Mac OS X keychain via the keyring module. The other dependencies are either from the Standard Python Library, or included in the google API library. Here’s the script:

#!/usr/bin/env python
# encoding: utf-8
"""
myTasks.py

Created by Chad Black on 2011-05-15.

Authentication adapted almost completely from Google's example at
https://code.google.com/apis/tasks/v1/using.html#authentication.

I just added keychain storage.
"""

import gflags
import httplib2
import keyring
import sys


from apiclient.discovery import build
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.tools import run

FLAGS = gflags.FLAGS

# Set up a Flow object to be used if we need to authenticate. This
# sample uses OAuth 2.0, and we set up the OAuth2WebServerFlow with
# the information it needs to authenticate. Note that it is called
# the Web Server Flow, but it can also handle the flow for native
# applications
# The client_id and client_secret are copied from the API Access tab on
# the Google APIs Console
FLOW = OAuth2WebServerFlow(
    client_id='653996198112.apps.googleusercontent.com',
    client_secret=keyring.get_password('tasksClient', 'YOUR USERNAME'),
    scope='https://www.googleapis.com/auth/tasks',
    user_agent='myTasks/v1')

# To disable the local server feature, uncomment the following line:
FLAGS.auth_local_webserver = False

# If the Credentials don't exist or are invalid, run through the native client
# flow. The Storage object will ensure that if successful the good
# Credentials will get written back to a file.
storage = Storage('tasks.dat')
credentials = storage.get()
if credentials is None or credentials.invalid == True:
  credentials = run(FLOW, storage)

# Create an httplib2.Http object to handle our HTTP requests and authorize it
# with our good Credentials.
http = httplib2.Http()
http = credentials.authorize(http)

# Build a service object for interacting with the API. Visit
# the Google APIs Console
# to get a developerKey for your own application.
service = build(serviceName='tasks', version='v1', http=http,
       developerKey=keyring.get_password('googleDevKey', 'YOUR USERNAME'))


def main(*argv):
 
# Display all tasks in all lists.	
# ex.: tasks ls

	if sys.argv[1] == 'ls':
		tasklists = service.tasklists().list().execute()
		for tasklist in tasklists['items']:
			print tasklist['title']
			listID=tasklist['id']
			tasks = service.tasks().list(tasklist=listID).execute()
			for task in tasks['items']:
				dueDate=''
				if 'due' in task: 
					fullDueDate=str(task['due'])
					dueDate=fullDueDate[:10] 
				print '    '+task['title']+' : '+dueDate
			print


# To add a task, the command is 'n'. Then, pass three arguments-- listName, newTask, dueDate.
# ex: tasks n listName "This is my task." 2011-01-01

	if sys.argv[1] == 'n':
		listName = sys.argv[2]
		task = {
		 	'title': sys.argv[3], 
		 	'due': sys.argv[4]+'T12:00:00.000Z',
			}					
		tasklists = service.tasklists().list().execute()
		listID = None
		for tasklist in tasklists['items']:
			if listName == tasklist['title']:
				listID=tasklist['id']
				break
		if listID == None:
			tasklist = {
		  	'title': listName,
		  	}
			result = service.tasklists().insert(body=tasklist).execute()
			listID = result['id']				
		newTask = service.tasks().insert(tasklist=listID, body=task).execute()

# clear completed tasks from all lists
# tasks c
	if sys.argv[1] == 'c':
		tasklists = service.tasklists().list().execute()
		for tasklist in tasklists['items']:
			listID = tasklist['id']
			
			service.tasks().clear(tasklist=listID, body='').execute()


if __name__ == '__main__':
	main()	

I link this script to a bash alias called tasks. To do that, add this line to your .profile or .bash_profile file:

alias tasks='python /Path/to/your/script/myTasks.py'

To check your tasks from the terminal, simply enter:

$ tasks ls

To add a task to a list, enter:

$ tasks n listName "New task title/description" dueDate

If you enter a list name that doesn’t exist, that list will be created. Due dates have to be in this form: YYYY-MM-DD. To clear completed tasks from your lists (which appear here as normal tasks), enter:

$ tasks c

The API returns JSON objects, which are converted to dictionaries in python, for both lists and tasks, each of which have unique IDs. These ID strings are awkward, but required to, for example, place a task in a specific list. The script matches IDs by searching dictionaries for matching list names. This is harder to do with individual tasks, which are also dictionaries. I could do some regex matching of part of the string of a task title, or simply match titles by entering the whole title of the task for deletion or updating. But that seems to me a bit of a pain on the user end. So, I think I might append sequential numbers to the dictionary of task items, and use that for ID matching.

At any rate, displaying a list of tasks on the desktop with Geek Tool is complicated in this situation because of the need for first time authentication. If you simply try to run the bash command python /path/to/script/myTasks.py from Geek Tool, google with ask for authentication. That’s a problem, because Geek Tool’s output is simply stout. It doesn’t allow for interaction. To get around this, I used an automator workflow to run the command and write to a text file I call geekTask.txt. I have Geek Tool run the workflow every couple of hours with the bash command

automator /path/to/workflow/geekTasks.workflow

Then, a few minutes later, in a separate Geek Tool box, I run the command

head -n 300 /path/to/textfile/geekTask.txt

That outputs the first 300 lines from the text file, and I have my todo lists displayed on my desktop. It’s a bit hackish, but works nonetheless.

What I like about this workflow, is I can use Tasks when I’m in gmail, while working in the terminal, on my iPhone, etc., and yet the resulting lists aren’t overly complicated with contexts, projects, and the other GTD cruft that ends up getting in the way of me actually getting things done.

UPDATE:/
I’ve added the ability to mark messages as completed or delete them from a specified list, and posted it here. Also, as is clear in the comments below, keyring is a python package in my script used to store your secret keys in your OS operating system. It’s also optional.

About

Associate Professor of Early Latin America Department of History University of Tennessee-Knoxville

Tagged with: , , ,
Posted in programming
25 comments on “google tasks, terminal, and geek tool
  1. This is awesome! I’ve been hoping for an API for this exact reason. Thanks for putting this together.

    I seem to be having a problem, though. I’ve got the script set up and the alias in place, but when I run the tasks alias, it doesn’t prompt me for the key, it just gives me this:

    Jotunn:~ whitsongordon$ tasks ls
    Traceback (most recent call last):
    File “/Volumes/Data/Users/whitsongordon/Documents/Scripts/myTasks.py”, line 17, in
    import keyring
    ImportError: No module named keyring

    Did I do something wrong? I tried adding my API key and my username to the script manually, but I think I’m missing something, because I still get the same error.

  2. ctb says:

    Hi Whitson– keyring is a python module that you have to install. I use it quite frequently, and have written about it in other posts (here and here), and just forgot to explain it on this one. If you don’t want to use keyring, then comment out or delete the line at the top of the code that says import keyring, and paste your developer key and secret key in to the script manually.

    I like keyring, though, and it’s very easy to use. There are instructions on how to install it here. It is cross platform compatible, and setting a key is a simple single line of code entered on on your python interpreter.

    Let me know if you can’t get it working!

  3. ctb says:

    Also, I’ve improved this script to add completion and deletion of tasks, and I’ll put the new version up in a couple of days after I get a chance to clean up the code and make it more pythonic.

  4. Alright, one step closer–got the keyring module installed, but now I’m getting this error when I paste in my key:

    ERROR:root:Failed to retrieve access token: {“error”:”invalid_client”}
    The authentication has failed.

  5. ctb says:

    Sorry it’s taken me so long to respond– I’m out on the road travelling.

    OK, so you need to set the keys for each of those called by this script. To do that you need to do this from the python interpreter:

    >>keyring.set_password('NAME OF KEY', 'A USERNAME', 'YOUR PASSWORD/KEY')

    So, you substitute the name of the key — for example, above I call the secret key “tasksClient”. You use whatever username you want, and then the key from google API.

    That should save the key in your OSX keychain. You can check this with the OS X program Keychain Access.

    Does that make sense?

  6. Ugh, that is not working either.

    We’re talking about the Python Shell, right? This is what happens when I try to do that:

    >>> keyring.set_password(‘tasksClient’, ‘EMAIL’, ‘Secret’)

    Traceback (most recent call last):
    File “”, line 1, in
    keyring.set_password(‘tasksClient’, ‘oops’, ‘dangit’)
    NameError: name ‘keyring’ is not defined

    Which are the three keys called by the script? I have a client ID, an API key, and the key that keyring gives me when it sends me to that URL (I assume that’s the “secret key”?)

    I’m wondering if there’s something wrong with the way I installed keyring. Especially because if I type “import keyring” into the Python Shell it just tells me it can’t find the module “Keyring”, even though I’ve installed it.

  7. hahahaha I love how I tried to hide my email and key but didn’t realize I pasted them in in the output too lol. Annoying that you can’t edit comments on WordPress.

  8. ctb says:

    Fixed that for ya. :)

    So, in the python shell — ie, when you open the Terminal and you write python at the bash prompt $, this puts you in the python interpreter. Whenever you want to do something in python that uses a module/library (even many that are part of the standard library like os), you still have to import that module. So, in order to set a key in the OSX keychain with keyring, you first have to import the module:


    >>> import keyring
    >>> keyring.set_password('serviceName', 'userName', 'key')

    You’re right– there are only two calls to keyring in that script.

    Again, if you don’t mind having the info in a file on your disk, you can simply erase or comment out the keyring import and replace those parts of the code with your own data.

  9. Okay, so I’m starting to realize that this whole thing is a little more complicated than I realized and I think you’re giving me too much benefit of the doubt that I know stuff. For example, because of dumb luck, I just realized I had to paste in my own client_id in the script. I need things like that explained to me.

    Keyring still isn’t working. When it points me to the Google API page and asks me to allow it access, it still doesn’t accept the key it gives me. If I go through the python module, it still sends me to the Google page to allow access, which doesn’t help at all.

    If I comment out the import keyring line and paste in my keys themselves (again, you might need to be more specific about which keys I’m pasting where–I’m assuming my googleDevKey is the “API Key” under “Simple API Access” on the Google API Console), I just get syntax errors on the FLAGS.auth_local_webserver=False line. If I comment that out, I get syntax errors on the storag=Storage(‘tasks.dat’) line.

    I’m very new at working with APIs, and I now realize this tutorial wasn’t meant for someone that hasn’t worked with APIs. I think I need the steps broken down a bit more–I’m missing a lot of things along the way that I didn’t know I needed to do.

  10. […] one of my favorite recent posts, Chad Black has a nifty, if technical, explanation of how he manages Google Tasks from the command line, which he can then access anywhere in a lightweight […]

  11. ctb says:

    Hi Whitson–

    So, when you registered your new app on the Google API Console (before running this script), you were provided with three keys: a Client ID key and a Client Secret Key, and separately an API Key. I left my Client ID key in the open because it’s a public key, and without the secret you can’t do anything. I’m storing the Client Secret Key and my API Key in my OS X keychain. It seems that keyring is part of what is tripping you up. So, erase the keyring import line. Then, change the line client_secret=keyring.get_password('tasksClient', 'YOUR USERNAME') to client_secret='YOURSecretKEY'. Then, change the line service = build(serviceName='tasks', version='v1', http=http,
    developerKey=keyring.get_password('googleDevKey', 'YOUR USERNAME'))
    to this: service=build(serviceName='tasks', version='v1', http=http, developerKey='YOUR_API_KEY').

    I think that you other problems are related to authorization problems around these keys.

    The way that oAuth works is that an application looking for authorized access will prompt a new web page to open where google will ask you if you want to allow access. This process is started by the combination of the public and private client keys provided to you by the google API console. Once you sort that out, the first time you run the program from the CLI, you’ll be given a url to paste into your browser, which opens the page where google asks for your permission to give access to your tasks. When you say yes, it gives you a final key that you copy, take back to the Terminal, and paste in when asked for it.

    In the end, this does take a little coding know how. But hey, you said you wanted to learn some python this summer. No time like the present! :)

  12. Okay, so right. I’ve already done those things. But when I do that, I get:

    File “/Volumes/Data/Users/whitsongordon/Documents/Scripts/myTasks.py”, line 39
    FLAGS.auth_local_webserver = False
    ^
    SyntaxError: invalid syntax

    When I run “tasks ls”.

    I assume I’m not supposed to be commenting out the FLAGS.auth_local_webserver line, but if I do I then get a syntax error on a *different* line:

    File “/Volumes/Data/Users/whitsongordon/Documents/Scripts/myTasks.py”, line 44
    storage = Storage(‘tasks.dat’)
    ^
    SyntaxError: invalid syntax

  13. ctb says:

    OK. So, that means that when you changed the code above that line, the section associated with the definition of FLOW, you inadvertently left out some parentheses or or quotation tick or something. Tell you what, why don’t you email me you file — you might have to save as a .txt file depending on your email client — and I’ll straighten out what the problem is. I’m imagining that you don’t know yet just what to look for with python syntax but it’s something very easy to fix once you do. Oh, and my email is chad -at- chadblack.net.

  14. Looking at it, it looked like I was missing the close parenthesis on that line, but then I added it in and got another error on the same line. I’ll email it to you.

    Am I missing something or is your email address not listed on the site?

  15. ctb says:

    Added it to the comment above, but probably after you answered. It’s chad -at- chadblack.net.

  16. Oliver says:

    Your python script with keyring works like charm, can you give some hints for the automator workflow, my shell script does not work;( This is my last step to get geektools work! Thanks

  17. ctb says:

    Hi Oliver–

    The workflow runs myTasks.py and writes it to a text file. I trigger it using GeekTool, and have it set to run every 30min or so. Then, the second GeekTool window invokes the shell script to print out that text file. I have this one set to run on a different schedule to ensure the latest task list shows up.

    The workflow should do this:
    1. Run a Shell Script:
    python path/to/script/myTasks.py ls
    2. New Text File.
    Save as geekTasks.txt.
    Save wherever you want, but remember the location!
    Check the box for ‘Replace existing file.’

    -Chad

  18. Peter Florijn says:

    Hi,

    I love your script and got it irking due to all the comments.

    When I give the $ python GTasks.py ls command it produces a list of my tasks but ends in:

    raceback (most recent call last):
    File “GTasks.py”, line 117, in
    main()
    File “GTasks.py”, line 76, in main
    for task in tasks[‘items’]:
    KeyError: ‘items’

    I’m not experienced enough in python to debug this. Any suggestions ?

  19. ctb says:

    Hi Peter– essentially, I think that the error is indicating you have a task that is missing one of the essential elements, either a title or a due date. Sometimes Google Tasks will have a blank task in your list, which would produce an error, or if you have a task without a due date assigned to it, it could produce such an error. Have a look at your tasks through the website and see if that’s the case. Given that you get a list of your tasks, I’d wager that there is a blank task at the end of your list.

  20. Peter says:

    Had some time to debug. The reason for the error

    File “GTasks.py”, line 117, in
    main()
    File “GTasks.py”, line 76, in main
    for task in tasks[‘items’]:
    KeyError: ‘items’

    is that I had some list with no items. So the script is not checking for this.

    I noticed in your post that you were planning to put this script on Github. I support this and would like to see this script developed further.

  21. ctb says:

    Peter– that’s what I meant by a blank task. To the script, this would return a task with no items and through the error.

    I rewrote the script last night using optparse, but haven’t added any error handling yet. The new one is on my github page.

  22. Hey
    Using this on Linux.
    Just commented out the keyring module import and pasted directly the keys from google on to the script. it’s a bit messy but it’s working for me.

    Great stuff!

  23. ctb says:

    Hi Artur–

    Are you using the script above, or the much improved version on github? It has much better argument handling, improved unicode handling, more commands, and a –help menu.

  24. […] pythonic historians, and on making a static-site digital history archive. I did a series of posts (here, here, and here) on integrating Google Tasks on the command line. And, in that never-ending quest […]

  25. Gabriel Girard says:

    Hi,

    I am trying to use your scripts, but I can’t find my secret key in the google console. Has google changed the way of authentification?

    Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

parecer:
parecer:

Hacer juicio ú dictamen acerca de alguna cosa... significando que el objeto excita el juicio ú dictamen en la persona que le hace.

Deducir ante el Juez la accion ú derecho que se tiene, ó las excepciones que excluyen la accion contrária.

RAE 1737 Academia autoridades
Buy my book!



Chad Black

About:
I, your humble contributor, am Chad Black. You can also find me on the web here.
%d bloggers like this: