Thursday, January 5, 2012

google gdata oauth

there's a great big honkin awesome library for python called gdata - it lets you interface with a ton of their APIs, including spreadsheets and docs. We use it internally at SimpleRelevance to write all sorts of logging directly to shared spreadsheets - a big process will finish and have reams of data to share, but rather than parse out a flatfile every time, I wrote a wrapper that takes the data and spits it into a beautifully formatted google spreadsheet. From there, charts and such are easy.

We also now use it externally - clients can log in and authenticate through oauth with google, and then we can write their predictions to a spreadsheet in their own account.

You probably know about oauth - it's nice because the client never has to supply us with any login credentials - the whole thing is very secure. Unfortunately, it was a little painful to set up. Like, 1 hour of productive work and 2 hours of fighting with stupid. Why?

I'll tell you why.
  1. There are a lot of outdated tutorials.
  2. The gdata plugin, while awesome, has tons of legacy code and 2 completely different and mostly working ways of doing everything.
  3. There are tutorials for the old path, tutorials for the new, and tutorials that mix the two.
  4. This page has the most beautiful, well-written, cogent, perfect, comprehensive example of how to set up oauth with gdata. Unfortunately it gets confusing in a crucial bit at the end.
Although it's great, check it out. It has examples every step of the way in 4 different languages, for both gdata paths. That's 8 examples every step of the way (and you know that oauth2 is a 3 step process - that's around 24 pieces of code).

It's actually spot on all the way through to the end. The confusing part comes when you have to exchange your oauth token for the longterm access token - the thing that actually authenticates and lets you access stuff.
The tutorial has this line:
access_token = client.UpgradeToOAuthAccessToken()  # calls SetOAuthToken() for you

But I had trouble. Frankly, I messed up. But I couldn't get it to work until I did this:
client.UpgradeToOAuthAccessToken()
access_token = client.token_store.find_token(oauth_token.scopes[0])


It turns out that the first line actually does returns the access_token, so the google tutorial is correct. This was fixed some time in the last something or other. Used to be you had to use that second line to get it. Older tutorials don't reflect this change. I was getting tired by then. I missed the boat. Cue frustration.

Then, I my second point of confusion: the request token key and secret (leg 1) are the same as the oauth token key and secret (leg 2) but the access token key and secret are totally different and new. In retrospect, this makes perfect sense (from a security POV), but at the time I was baffled. Don't try to upgrade your oauth token and then save its key and secret to authenticate with. It won't work.

The google tutorial tells you to save it but leaves it to you to figure out how to use the access token object. Really, it's easy:
save access_token.key
save access_token.secret.

You can use the session, you can use a database, you can use a session stored in your database, whatever you want! I think the access token lasts for a while.

Anyway, oauth is hard, but I'm really getting the hang of it. Feel free to email me if you need help with this one.
facepalms: 4.5.

Monday, December 26, 2011

django multidb support for admin site by subdomain

usecase: the client logs in to the admin site by going to clientname.sitename.com/client_admin.

Why? because it's nice to use the django admin site. Because we want the client to only access the client's database.

How?
first off, setup your DNS to forward *.sitename to your server. Than in your server config (I use nginx), send *.sitename to uwsgi.
(note: if you're not using uwsgi with django here, you'll have to use your imagination).
In your wsgi script (you know, the one that launches django for each incoming request?), put something like this:

class RequestWSGIHandler(django.core.handlers.wsgi.WSGIHandler):
def get_response(self,request):
domain = request.__dict__['META']['HTTP_HOST']
domain = domain.split('.')
if len(domain) > 3:
db_name = domain[0]
print "caught domain %s"%db_name
os.environ['SITE_NAME'] = db_name
return super(RequestWSGIHandler,self).get_response(request)

# Lastly, load handler.
# application = django.core.handlers.wsgi.WSGIHandler()
application = RequestWSGIHandler()

Yes, it's kind of quick and dirty. I've subclassed the django wsgi handler to intercept every request, grab the subdomain if there is one, and set it in the OS env. Then using the (happily outdated) python 2.6* super() syntax, I call the standard wsgi handler.

Then when you get to settings.py, you can grab that env variable if it's there and save it to your settings object. It's now available when you need it.

For instance!!!, you can subclass admin.ModelAdmin to support multiple databases (see here). In the linked example (you have to scroll halfway down, they basically overwrite the key class methods and add in "using=new database" to all db queries) the new database is static, but since you now have a dynamic database name in settings.whatever you called it, you can pick your extra database access name on the fly. hurrah.


A caveat:
(this is a bit scary...) in the django documentation, the good django devs say to always import settings from django.conf. I wonder why? Maybe to avoid the same multithread concurrency issues I very briefly glossed over in my last post. Who knows. Problem is, if you do that with the above example, some of your calls (though not all) won't pick up your dynamic subdomain-influenced database. Why the hell not? Beats me. I have "from django.conf import settings" everywhere in that project except that file, for which I use "import settings".

and as a further caveat, keep in mind that if you import the settings from django.conf in a file that's loaded before admin.py, or wherever you want the real dynamic settings, it won't work!

I didn't really go into it, but this is a 4.5 facepalm issue right here.

Dynamic Databases in Django

My current (paid) project has me managing a separate database for every client in Django. This has been a great challenge. Since 1.2, Django has had multidb support, so that's not hard - the hard part is all of the edge cases.

For instance, we want to be able to add clients. On the fly. We plan to have many - like more than 20. So we certainly don't want to have our database definition in settings, all written out like the Django tutorials. At the very least, a loop over db names.

def add_db_to_databases(DATABASES,name):
if name in DATABASES:
return DATABASES
DATABASES[name] = {
'HOST': 'localhost',
'PORT': '',
'NAME': name,
'USER': '',
'PASSWORD': '',
'ENGINE': '',
'OPTIONS': {
'autocommit': True,
}
}
return DATABASES

for name in pro_dbs.names:
DATABASES = add_db_to_databases(DATABASES,name)

What I did there is take the names from another python file, which contains a simple python list of names.

I needed to be able to add clients on the fly. This is the hard part; as of yet I have two stumbling blocks with only partial workarounds.
  1. I'd love to have the database names in a database themselves. Soooo much better than reading the python file with the names into a list, appending the new name to the list, than writing back to the file. But how to load from a database in the django settings file itself? It's been engineered not to allowed that.
  2. I'd love to be able to update settings without restarting the server. You can do certain things in that vein by messing with django.conf.settings, but it's unclear how well that'll hold up under multithreading.

All in all, not a facepalm worthy subject, but very interesting.

Wednesday, October 5, 2011

web scraping and microformats

This proved very helpful to me. It's a short, concise explanation of how to extract data from the structured markup of a website in python. It's a no-brainer, but sometimes you need someone to show you the no-brainer before it becomes one for you.

The bottom line is that html is a string. It's also a tree. You can parse it either way. Microformats and the semantic web aren't only useful to big spidering companies like the goog; they're useful to all of us.

Saturday, July 30, 2011

Twitter is useless?

So I had a great idea for a JQuery plugin. What's better, I was going to write it too.

A single line initializes and adds the plugin to your page, which then allows people to tweet from said page, including a shortened link to said page, hash tags of your choice - but that's just the beginning. Then, the plugin retrieves all tweets with the same shortened link, organizes them using create_date and @whoever, and displays them on your page as hierarchical comments.

Basically, a totally painless and free commenting system hosted and authenticated by twitter, displayed on your page with one line. I like. Except for one problem - twitter search doesn't work for included URLs.

If anybody knows how to possibly retrieve all tweets about a given url (or the last 200), please let me know. It's a terrible wasted potential not to allow this.

For instance, here's a twitter account of mine from another project: . The last couple tweets have bitly links. You'd think a twitter search for the link would show up the tweet, but NO! nothing going. Even when you switch from their ridiculous "popular tweets" to "all tweets". What kind of tweet search misses on unique text from a tweet?

facepalms = only 2, but general annoyance is through the roof.

UPDATE:

I take it back - some urls hit, some don't. Even better. Possibly, broken urls don't hit? But I'm not sure.

Tuesday, July 12, 2011

holy sweet victory

Just figured out something that's been bugging me nonstop for weeks, which has no documentation online: the jquery plugin fancybox doesn't like to have its stuff overwritten, which is to say, you shouldn't define something like this:
var fancy_box_options = fancy_box_defaults;
fancy_box_options.type = 'iframe';
fancy_box_options.width = 696;
fancy_box_options.height = 499;
fancy_box_options.scrolling = 'no';
fancy_box_options["onStart"] = function(link){
var new_link = $.param.querystring($(link).attr("href"), "gmail=" + $("#gmail").val());
$(link).attr("href", new_link);
};

$("a.iframe").fancybox(fancy_box_options);
$("a.iframe_defaults").fancybox();


In one function, and then soon after define:
<% if current_user && @popup_help %>
var fancy_box_options = fancy_box_defaults;
fancy_box_options.type = 'iframe';
fancy_box_options.width = 696;
fancy_box_options.height = 499;
fancy_box_options.scrolling = 'no';

fancy_box_options.href = '<%= popup_help_path(:gmail => current_user.email) %>';
$.fancybox(fancy_box_options);


in another. You'll get a wholly unhelpful error about href being undefined.

All better now -
#facepalms only 4, because I already had a lot of trouble with this previously, I'm over it now, and the sweet release of figuring it out is worth so, so much.

Monday, July 11, 2011

ubuntu problems

so for the last while my wireless card has been acting up _sometimes_, on certain wireless networks but not others. I finally got a clue as to what might be going on: wireless N. It's an N card, but the driver/kernel stuff going on in ubuntu has a known problem with certain wireless cards.
Edited my modprobe.d/options.conf with the line:

options iwlagn 11n_disable=1 11n_disable50=1

and then (hopefully won't have to restart) ran:

sudo rmmod iwlagn
sudo modprobe iwlagn

And that seems to have improved things. So far.

#facepalms: like a lot. Until now. Hopefully.