Sunday, September 28, 2008

GData Python API woes continue...

Unfortunately, using the GData API is starting to feel like being in a boat so hastily constructed, that every time you patch one leak, you need to run right to the next one. Today's issue either happened on Google's end or as a consequence of the recent 1.2.1 GData update.

My issue is similar enough to one on the mailing list, that I posted it there. The basic issue is that the gd_client.InsertRow() call works like a charm if your app happens to have a logged in Google-sign-in user. If not it fails in an absolutely unhelpful 404-Not Found (full text below). Moreover--Google's initial response seems to say that this change (requiring a logged in user) was intentional... but clearly breaks any app that relies on stored authsub tokens (up to this point a completely valid and encouraged method).

My hope is that this spate of issues is a short-term anomaly, as we can't afford this level of attention moving forward. I know Google has some good people like Jeff Scudder working on the project. I hope they give him enough team support and/or time to communicate to us early adopters. Specifically, if you are breaking the API expectations, tell us--rather than slipping the change in a 1.2.0=>1.2.1 update which was critical to fix other issues. Hrmph!

For the time being we're going to have to remove the GData code from our app, as we can't afford the downtime even on our relatively vanity app. Looking forward to Google's response.

Environment:Request Method: POSTRequest URL: http://localhost:8081/l/agpyaWdodHJlcGx5chELEgtMYW5kaW5nUGFnZRgCDA/widget.htmlDjango Version: 1.0-beta_2-SVN-unknownPython Version: 2.5.1Installed Applications:('myapp', 'appengine_django', 'django.contrib.auth', 'django.contrib.sessions')Installed Middleware:('django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'utils.auth.Auth')Traceback:File "/Users/.../google-app-engine/rightreply/django/core/handlers/base.py" in get_response  86.                 response = callback(request, *callback_args, **callback_kwargs)File "/Users/.../google-app-engine/rightreply/myapp/landing_pages/views.py" in widget_html  62.       lead.save()File "/Users/.../google-app-engine/rightreply/myapp/models.py" in save  217.     self.submit_to_google_docs()File "/Users/.../google-app-engine/rightreply/myapp/models.py" in submit_to_google_docs  253.     entry = gd_client.InsertRow(dict, self.landing_page.spreadsheet_id, self.landing_page.worksheet_id)File "/Users/.../google-app-engine/rightreply/gdata/spreadsheet/service.py" in InsertRow  325.         converter=gdata.spreadsheet.SpreadsheetsListFromString)File "/Users/.../google-app-engine/rightreply/gdata/service.py" in Post  831.         media_source=media_source, converter=converter)File "/Users/.../google-app-engine/rightreply/gdata/service.py" in PostOrPut  951.           'reason': server_response.reason, 'body': result_body}Exception Type: RequestError at /l/agpyaWdodHJlcGx5chELEgtMYW5kaW5nUGFnZRgCDA/widget.htmlException Value: {'status': 404, 'body': '<HTML>\n<HEAD>\n<TITLE>Not Found</TITLE>\n</HEAD>\n<BODY BGCOLOR="#FFFFFF" TEXT="#000000">\n<H1>Not Found</H1>\n<H2>Error 404</H2>\n</BODY>\n</HTML>\n', 'reason': ''}

2 comments:

Jeff Scudder said...

I've responded on the thread that you linked to, but I'll copy part of my response here.

...The technique I'm about to explain can cause auth tokens to be shared by everyone who uses your App Engine app.

Unless you are using one master account that you want all of your users to share, please do not use this technique.

First, you'll need to tell your Google Data service client that it is running on App Engine:

client = gdata.service.GDataService() # or a subclass
gdata.alt.appengine.run_on_appengine(client)

By default, the above modifies the token_store so that all auth tokens
must be associated with a user. When your app has a current_user, the
token is stored in the datastore, if there is no current user, the
token is discarded. I required that tokens be associated with a user
since some parts of your app can stay in memory across requests to
your app and it is possible that you might write your app in a way
that could cause tokens to be used for the wrong user. I've tried to
make it more difficult for developers to make such a mistake. If you are
using one shared account, then it is ok for all of your app users to
have access to the same auth token. You can replace the token store
with one that keeps all tokens in memory and does not associate them
with any user:

client.token_store = atom.token_store.TokenStore()

Then use ClientLogin(), or ProgrammaticLogin(), etc.

Let me repeat one more time, after you make this change, and depending
on how your client is initialized, it is possible for auth tokens from
one user to stay in memory and be reused in other requests, so make
sure that you really want all users to see data for this account. I
can't emphasize enough that this modification should only be used in
very limited circumstances.

Jonathan Siegel said...

Jeff--thanks for the update, but I'm not sure if setting the line:
client.token_store = atom.token_store.TokenStore()

instead of:
gdata.alt.appengine.run_on_appengine(client)

is all that's needed or if there is more. Is there a complete example showing this functionality that I can reference?

In terms of why this is important--our service submits data from a web form into a google spreadsheet. The AppEngine code needs to access one spreadsheet (which we have the token for) no matter who is submitting the information.

Is this not a supported use case? Given the warnings in your comment it sounds like this is not recommended.