Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Monday, September 7, 2009

Running SQLAlchemy scripts off of pylon's paste configurations

I've come across a situation where I want to be able to use cron to launch scripts that automate some tasks (fetch info from 3rd party, scramble their data into my database format, write to my database). I want to be able to use the models I've already created in SQLAlchemy, along with the configuration of SQLAlchemy in my paste configuration files (development.ini, production.ini) in these scripts.

I don't need all of the bells and whistles of pylons, just the SQLAlchemy stuff. Luckily, there are some convenience functions that seem to have been made to deal with a situation just like this one. Here's the code:


import sys
from paste.deploy import appconfig
from sqlalchemy import engine_from_config
from myapp.model import init_model

def setup_environment():
if len(sys.argv) != 2:
print 'Usage: Need to specify config file.'
sys.exit(1)

config_filename = sys.argv[1]
config = appconfig('config:%s' % config_filename, relative_to='.')
engine = engine_from_config(config)
init_model(engine)


Of course, replace "myapp" with the name of your application.

Getting the configuration filename from the command-line arguments could of course be put somewhere else, so I'll just skip talking about that. The two main functions are appconfig and engine_from_config.

appconfig is from paste.deploy, which reads the configuration file and returns a dictionary-like object. engine_from_config is from SQLAlchemy and takes a dictionary-like object, retrieves SQLAlchemy-specific configurations, and uses the configurations to create an engine object.

Monday, August 10, 2009

Reusable SQLAlchemy Models

Recently looking at django, I took a liking to their idea of reusable apps. One thing that was common with most of these apps were some model that they would include and which you would call a function to automatically set up a relation to one of your models, thus having a "reusable model". An example would be any object that you'd like to have comments on automatically having a Comment model generated and relationships to your custom-built object automatically set up.

Out of mere curiosity, I wondered how difficult it would be to create a "reusable model" in SQLAlchemy. My end goal was to be able to do something like this...


@commentable
class Post(Base):
__tablename__ = 'posts'
id = sa.Column(sa.Integer, primary_key=True)
text = sa.Column(sa.String)


A class that was decorated as commentable would automatically have a relation defined to contain multiple comments. After trying out a few ideas, I wrote a test for what I would want the end result to look like...


class TestModels(SATestCase):
def test_make_comment(self):
p = Post()
p.text = 'SQLAlchemy is amazing!'

text = 'I agree!'
name = 'Mark'
url = 'http://www.sqlalchemy.org/'

c = Post.Comment()
c.text = text
c.name = name
c.url = url
p.add_comment(c)
Session.add(p)

p = self.reload(p)

self.assertEquals(len(p.comments), 1)
c = p.comments[0]
self.assertEquals(c.text, text)
self.assertEquals(c.name, name)
self.assertEquals(c.url, url)



(As a little note, self.reload() is a helper function that will force a complete reload of the session and return the objects you passed in after the session is opened again)

First off, a little bit about how the table structure would work. AFAIK, django stores comments in one large table and has what you might call a discriminator field to determine the object type that the row relates to. In my case, every type of commentable object (Post, NewItem, etc.) would instead get it's own comment class (PostComment, NewsItemComment) as well as their own table (post_comments, news_items_comments). No real reason to do it this way, I just thought it'd be easier.

In the end, it was actually pretty easy. Here is what the initial results look like...


class BaseComment(object):
pass

def build_comment_model(clazz):
class_table_name = str(class_mapper(clazz).local_table)
metadata = clazz.metadata

comment_class_name = clazz.__name__ + 'Comment'
comment_class = type(comment_class_name, (BaseComment,), {})

comment_table_name = class_table_name + '_comments'
comment_table = sa.Table(comment_table_name, metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column(class_table_name + '_id',
sa.Integer,
sa.ForeignKey(class_table_name + '.id')),

sa.Column('text', sa.String),
sa.Column('name', sa.String(100)),
sa.Column('url', sa.String(255)),
)

mapper(comment_class, comment_table)

return comment_class, comment_table

def commentable(clazz):
comment_class, comment_table = build_comment_model(clazz)

clazz.Comment = comment_class
setattr(clazz, 'comments', relation(comment_class))

def add_comment(self, comment):
self.comments.append(comment)

setattr(clazz, 'add_comment', add_comment)

return clazz


First off, we have the BaseComment class. This is just an empty class, but you could easily see there being logic on here.

In the build_comment_model() function, you see where the creation of the table's metadata takes place. It'll first create a new class that will be used for the new comment model (in the case of a Post model being commentable, it's creating a PostComment class that inherits from BaseComment). Using SQLAlchemy information that we find on the mapper of the class we're decorating, we can determine things such as the name of the table so that we can create our own table (the Post model's table name is "posts", so we'll create a "posts_comments" table).

Finally, the commentable() function finishes by setting the Comment attribute on the model class we're decorating, as well as adding the relation and a class method. The Comment attribute allows us to easily get at the new class that we created, the relation allows us to easily work with the items, and the add_comment is an example of how we can also add extra methods to the model.

Currently, there are some pitfalls:

1.) It assumes that the class has been mapped before running. Because mapping of a class must happen after the class is instantiated, you couldn't use the commentable() function as a decorator for a non-declarative style mapping, since it wouldn't be able to find the mapper. The solution would be to just call the commentable function after the class is mapped. I guess it's not really a pitfall per se, although I haven't actually tried it, so I'm just guessing that it works :)

2.) Right now, it assumes that the foreign key for the Comment object would be a single column that corresponds to the column named "id" on whatever model it's decorating. I still need to work on this, since you might have a different name, different primary key type, or even a multi-column primary key. Basically when the new comment table's metadata is being laid out, it needs to look at the decorated model's primary keys to determine how it should construct it's foreign keys, rather than just jumping to the conclusions that it does now.

Monday, August 3, 2009

Shortening bash prompts inside a virtualenv

This post will describe a way to shorten your bash prompt in Linux so that inside the inevitable multiple directories caused by using virtualenv, you can still have a small prompt.

I wanted to take an existing library and split it into it's own library. On my computer, I was going to work wit this library at ~/projects/sc/kespa_scrape.

Of course, that kespa_scrape directory is actually the directory holding the virtual_env...


kespa_scrape/
|-- bin
| |-- activate
| |-- activate_this.py
| |-- easy_install
| |-- easy_install-2.6
| |-- nosetests
| |-- nosetests-2.6
| |-- python
| |-- testall.sh
| `-- to3.sh
|-- include
| `-- python2.6 -> /usr/include/python2.6
|-- kespa_scrape
| `-- kespa_scrape
`-- lib
`-- python2.6


This is what the kespa_scrape directory looks like. You can see it has a kespa_scrape directory inside it as well. This directory is the root to the actual project. This directory will probably also have a kespa_scrape directory which would be the actual library package directory. Obviously, there is redundancy, but it's necessary redundancy to keep all the parts where I want them.

Some people complain about virtualenv because then you need to cd through a whole bunch of directories to start working. I make this easier with a script. This is how it is used...


gobo@gobo:~$ cd projects/sc/
gobo@gobo:~/projects/sc$ activate_project kespa_scrape
(kespa_scrape)gobo@gobo:~/projects/sc/kespa_scrape/kespa_scrape$


As you can see, I change to the directory where the project is, enter "activate_project", and it will put me into the virtual environment as well as change to the root directory of the project.

The problem I still had with this is that now my prompt is huge. I would typically change directories once more into the next kespa_scrape package, and with a large enough library name, have a prompt that easily took up half the screen.

At first, I made sure to choose short library names, but now I've come up with a better solution. It rewrites the bash prompt to replace the virtual environment directory with two tildes, and looks like this...


gobo@gobo:~$ cd projects/sc
gobo@gobo:~/projects/sc$ activate_project kespa_scrape
(kespa_scrape)gobo@gobo:~~$


Check out the difference for yourself...


(kespa_scrape)gobo@gobo:~/projects/sc/kespa_scrape/kespa_scrape$
(kespa_scrape)gobo@gobo:~~$


Here's the full script...


#!/bin/bash

# Remove possible slash at end.
DIR=${1%/}

ACTIVATE_FILE=$1/bin/activate

if [ -z "$DIR" ]; then
echo "usage: $0 directory"
return 1
fi

if [ ! -d "$DIR" ]; then
echo "Directory not found: $DIR"
return 1
fi

if [ ! -d "$DIR/$DIR" ]; then
echo "Child directory not found: $DIR/$DIR"
return 1
fi

if [ ! -e "$DIR/bin/activate" ]; then
echo "Activate file not found. Are you sure this is a virtualenv directory?"
return 1
fi


cd $DIR
source bin/activate
cd $DIR

CUR_DIR_CMD='pwd | sed -e '\''s!'$VIRTUAL_ENV/$DIR'!~~!g'\'' | sed -e '\''s!'$HOME'!~!g'\'
PS1=${PS1//\\w/$\($CUR_DIR_CMD\)}

alias cdp='cd '$VIRTUAL_ENV/$DIR


After checking to make sure all the files exist, it sets a variable called CUR_DIR_CMD. This is basically a variable that contains a bash command to take the current directory, and replace any instances of the virtual directory with ~~. Since virtualenv sets the VIRTUAL_ENV variable, it basically looks for "/projects/sc/kespa_scrape/kespa_scrape" and replaces it with ~~. It will also take the result of that and replace instances of the home directory with ~. This is so that if I change directories to something outside of the virtualenv, I can still have '~' instead of '/home/gobo'

Next, I need to replace all instances of \w in the prompt string with this expression. \w is the special string you'd use when setting your bash prompt to give you the full directory that you're in. Replacing it with my custom command allows me to replace the area where the directory should go with what I want, but leave the rest of the prompt (including virtualenv's addition to the beginning) intact.

The entire process also does pretty well when you change directories to outside the virtualenv, since you're just left with the directory...


(kespa_scrape)gobo@gobo:~~$ cd
(kespa_scrape)gobo@gobo:~$ cd projects/
(kespa_scrape)gobo@gobo:~/projects$


The only thing I want to fix up on it is to allow activate_project to work from ANY directory. Currently, it assumes that the project directory is right in front of you. This is good enough for now though.

----

Edit: Feb 18, 2010

Also, one thing to keep in mind, for those who don't really deal much with bash scripts, is that typically when you launch a script the script runs inside it's own shell, and when it completes, the shell closes. This means you lose things such as the new alias or the changes to the directory. So, you can't run the script in any of these ways:


sh /home/mark/bin/activate_project project_name
/home/mark/bin/activate_project project_name


If you did this, the script would run, but you would not see anything changed, since all the changes would be done in the shell created for the script, which exits without changing the shell that ran the script. To ensure that you run the script on the shell you run the script with, you need to use the source or dot operator.


. /home/mark/bin/activate_project project_name
source /home/mark/bin/activate_project project_name


As a side note, now you really know what that source bin/activate line does! Anyway, you don't want to deal with this hassle, so set up an alias...


alias activate_project="source /home/mark/bin/activate_project"

Friday, July 10, 2009

Starcraft Professional VOD Search Engine: Introduction

This is an introduction to a series of posts designed to give you a tour of the project I've been working on for the past month or so, "SC Engine".

The entire series is linked below:

- Introduction
- Part One: System Overview
- Part Two: System Overview: Messages and Applications
- Part Three: Screen Scraping
- Part Four: YouTube Parsing
- Part Five: Linking the Video to the Game
- Part Six: Messaging Middleware
- Part Seven: The Console
- Part Eight: The Site


SC Engine is a series of programs and tools that indexes Professional Starcraft videos found on the net into one easy-to-use web site. This first post gives an idea of what the project is expected to do, with the rest of the posts going into the implementation.

For those not aware, Starcraft is a PC game that was created by Blizzard and originally released in 1998. It has withstood the test of time, and in South Korea even has a following that is as popular as Soccer or Pro Wrestling here in the US, if not greater. Starcraft in South Korea comes complete with their own professional leagues. E-Sports as a whole in South Korea has really taken off, gaining great sponsors and recognition. Starcraft is the game at the forefront of this new form of entertainment, drawing the largest crowds and best ratings.

Games are often cast on TV with commentators just like you'd see in any televised sporting event. Games of the more popular leagues are often uploaded to sites like YouTube or the league's own website for viewing, and often referred to as "VODS" ("Videos On Demand").

Some fans, wishing to view the games with commentators speaking their own language, began dubbing their own commentary over the Korean ones. Here's an example of a game in it's original version, and here's an example with an English commentator, Klazart.

SC Engine started coming together for a number of different reasons:


  1. I was out of work since January (although took some time until April just to relax, living off of savings) and unsure of how well I could express my ability as a python/web developer (much of what I did at my work was in .NET, and even later python stuff I wrote before being laid off never saw it's way into production). Thus, I was looking for a project to show to the world in code what I probably could not easily express in words.


  2. I wanted to work on SOMETHING. My job had me starting to burn out from coding, working at some pretty tedious objectives. After my break, I wanted something new that was interesting, not the same old "django blog" application. Asynchronous messaging via AMQP seemed very interesting.

  3. I enjoyed watching Professional Starcraft VODs, especially with the English commentators, but found it a pain trying to find games, then keep track of how all the games I was watching related to each-other. A person who only watched videos from one or two commentators could only watch a portion of the games, leaving them with gaps in the knowledge of how the league is progressing. Since all uploaders and commentators do this in their spare time, they can only cover so many videos.

  4. Watching games on Youtube, there was a chance of "spoiling" the results of the game. Games can take anywhere from a few minutes to an hour or more to play. Knowing beforehand the length of the game gives clues as to what will happen in the game as you're watching it. Also, because many games are in a "best of x" series, a commentator might upload only the games played, leaving a viewer to be able to easily determine the winner of a match through logic.

    Many uploaders will upload "anti-spoiler" videos, that are just small videos that seem like the next game is played, but when you view it just contains a quick message that the match was already decided. I feel like this isn't exactly a great solution: it's more work for the uploaders, and not all uploaders did it anyway.




The result was to create a website that tried to solve these problems.

In it's first interval, the home page would show you all of the leagues, and you would select matches in the league to watch. Then, you could watch each game in that match. Because the entire navigation of the page view games as a first-class citizen (as opposed to watching on YouTube, where the videos are a first-class citizen) you would have a visual guide as to where this game stands in terms of the league (knowing a game took place just before the end of the season, or in the playoffs, or what part of the playoffs, etc.). When actually choosing the game, you would get to pick which uploader's version of the game you want to watch. This allowed you to view more games than you might otherwise have had you just watched the videos of one or a few commentators. Also, since the Starcraft community (a few memebers, specifically) is pretty amazing at their ability to upload most every game played in large events, even if you can't find a commentary in your language you'll probably still have the option to view the original game, albeit with Korean commentary.

Also, I felt it'd be nice to see some other information on the game's page, such as:


  • The names of the players in the game.

  • The race that that player plays the game with. In Starcraft, there are one of three possible races to select, each with their own unique traits.

  • The kespa site occasionally puts the "starting position" of the players on their site, which let's you know where the player will be when you are watching the game.

  • An image of the "map". Each game takes place on a different map, each with different layouts and base locations to add variety to the game. Being able to see a picture of the entire map as you watch a game helps immensely with understanding how the game is developing, especially if you're not familiar the particular map.



Right now, work is complete on the first iteration. The site still needs a complete redesign (I just hacked some css) and will probably change a ton. You can see what it looks like here:

http://sc.markhildreth.webfactional.com/

There isn't much in terms of data, as I'm still working on the hardware that the engine will run on, but it gives you an idea of the type of things that you might be able to expect.

In any case, that's the background on the project. My remaining posts will dive into the aspects of software implementation.

Tuesday, July 7, 2009

Python virtual console race condition

Here's a fun drinking game you can play. Every time I introduce a bug because I forget to close() something, take a drink.

My last entry talked about how paramiko was giving me fits because I wasn't closing the connection. Today, I had another problem, this time using amqp-lib. I had two virtual consoles open, one with the process that would read from the queue in a tight loop and printing out what it was doing. Another console would run a script to send messages to the queue. I started noticing a problem where the script that would send 9 messages would complete without error, but if I switched over to the first terminal, I saw only a partial number of them would actually be acted on. Sometimes it was one, sometimes it was four or five, sometimes it was all of them. I opened up a third console to look at the queues in RabbitMQ and saw that there were no more messages left. Either it was picking the messages out from the queue and not acting on them correctly, or never being sent to the queue correctly.

Sometimes I could run the script a few times in a row, and they would all pass. Finally, I saw a way to reproduce it... before the script sending the messages ended, switch to the other terminal.

Here's what I imagine was happening..

When I switched from one console to the other on my (slow, old, reserved for use as a server) laptop, the slow ass virtual console didn't have to print out the printlines in the script I was debugging with. Because the script ran faster, it was able to finish before all the data was effectively sent from the socket to RabbitMQ. When the script finished, the garbage collector forced the connection closed, even if it was still working. By adding in the code to manually close the connection, I provided the connection those extra split seconds necessary to really close with everything complete.

Sunday, July 5, 2009

Paramiko hangs.

I was looking at paramiko for use in scripts and an application I was writing where I needed to do some secure ftp. Seems like I should've recognized this, but I was caught by surprise how it would handle closing the connection when python closed. I assumed the connection would close just like an open file (so that upon exiting the python shell, it would close). However, I found that the shell hung. At first I thought it was a bug in the library, but really it's just that paramiko needs you to explicitly close the connection.


def upload_file(host, user, key, remote_filename, data):
t = paramiko.Transport(host)
try:
t.start_client()
t.auth_publickey(user, key)
t.open_session()

sftp = paramiko.SFTPClient.from_transport(t)

with tempfile.NamedTemporaryFile() as f:
f.write(data)
f.flush()
sftp.put(f.name, remote_filename)
finally:
t.close()


Another one of those situations where the answer should be obvious...

Wednesday, June 24, 2009

Subclassing JSONEncoder and JSONDecoder

Today I was trying to write code to add the ability to encode datetime and timedelta objects as JSON. The idea is that two python applications would talk to eachother over an http request, with json as the content. It seemed simple, but I didn't realize that the decoder works a bit differently, and so it'll probably make you rethink how you write the encoder.

The json documentation shows an example of overriding the JSONEncoder:


class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
return [obj.real, obj.imag]
return json.JSONEncoder.default(self, obj)


Using this, I wrote my own encoder for datetimes...


class DateTimeAwareJSONEncoder(JSONEncoder):
"""
Converts a python object, where datetime and timedelta objects are converted
into strings that can be decoded using the DateTimeAwareJSONDecoder.
"""
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime('dt(%Y-%m-%dT%H:%M:%SZ)')
elif isinstance(obj, timedelta):
days = obj.days
seconds = obj.seconds
milliseconds = obj.microseconds / 1000
milliseconds += obj.seconds * 1000
milliseconds += obj.days * 24 * 60 * 60 * 1000

return 'td(%d)' % (milliseconds)
else:
return JSONEncoder.default(self, obj)



The default method is called with every object encountered, so whenever it finds a datetime or timedelta, it uses my custom code to convert it into a string that I should be able to pick out using a regular expression or some other means when decoding. Here's what I tried...


datetime_regex = re.compile('\"dt\((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\)\"')
timedelta_regex = re.compile('\"td\((\d+)\)\"')

class DateTimeAwareJSONDecoder(JSONDecoder):
"""
Converts a json string, where datetime and timedelta objects were converted
into strings using the DateTimeAwareJSONEncoder, into a python object.
"""
def decode(self, obj):
dt_result = datetime_regex.match(obj)

if dt_result:
year, month, day, hour, minute, second = map(lambda x: int(x), dt_result.groups())
return datetime(year, month, day, hour, minute, second)

td_result = timedelta_regex.match(obj)
if td_result:
milliseconds = int(td_result.groups()[0])
return timedelta(milliseconds=milliseconds)

return super(DateTimeAwareJSONDecoder, self).decode(obj)


This seemed to work, and it would so long as the data I input was just the datetime object...


decoder = DateTimeAwareJSONDecoder()

# Worked
decoder.decode('"dt(2009-04-01T23:51:23Z)"')

# Didn't work
decoder.decode('["dt(2009-04-01T23:51:23Z)"]')
decoder.decode('{"a": "dt(2009-04-01T23:51:23Z)"}')


It turns out that the "decode" method on the JSONDecoder does not work like the "default" method on the JSONEncoder. As soon as I passed the call to the JSONDecoder's "decode" method, it went ahead and decoded the whole thing before passing it back.

For awhile I was still pretty sure this is how I should do it, and I just wasn't using the "decode" method right, until I read an article about the json encoder and decoder by Doug Hellmann. Here he showed how you could create a decoder a bit differently. The code looks as such (some changed to make it more readable):


class MyDecoder(json.JSONDecoder):
def __init__(self):
json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)

def dict_to_object(self, d):
if '__class__' in d:
class_name = d.pop('__class__')
module_name = d.pop('__module__')
module = __import__(module_name)
class_ = getattr(module, class_name)
args = dict( (key.encode('ascii'), value) for key, value in d.items())
inst = class_(**args)
else:
inst = d
return inst


The important thing to see here is that rather than overriding a method, you're actually passing a method as a parameter to the parent constructor. This dict_to_object works a bit more like the "default" method, in that whenever you get an object this method is called to give you a chance to convert it. What's important to realize, however, is that it will only get called if the object in question is a json object. This sounded as if that would be okay with what I have, but remember that in json, the word "object" has a specific meaning. A string value is not an object, it's a string value. As such, it will get decoded from the json string to a python string, and your object hook method will not get to touch it.

The solution, then, is to drop the idea of converting the datetime and timedelta objects into strings, and instead convert them into objects. Here is the code...


class DateTimeAwareJSONEncoder(JSONEncoder):
"""
Converts a python object, where datetime and timedelta objects are converted
into objects that can be decoded using the DateTimeAwareJSONDecoder.
"""
def default(self, obj):
if isinstance(obj, datetime):
return {
'__type__' : 'datetime',
'year' : obj.year,
'month' : obj.month,
'day' : obj.day,
'hour' : obj.hour,
'minute' : obj.minute,
'second' : obj.second,
'microsecond' : obj.microsecond,
}

elif isinstance(obj, timedelta):
return {
'__type__' : 'timedelta',
'days' : obj.days,
'seconds' : obj.seconds,
'microseconds' : obj.microseconds,
}

else:
return JSONEncoder.default(self, obj)

class DateTimeAwareJSONDecoder(JSONDecoder):
"""
Converts a json string, where datetime and timedelta objects were converted
into objects using the DateTimeAwareJSONEncoder, back into a python object.
"""

def __init__(self):
JSONDecoder.__init__(self, object_hook=self.dict_to_object)

def dict_to_object(self, d):
if '__type__' not in d:
return d

type = d.pop('__type__')
if type == 'datetime':
return datetime(**d)
elif type == 'timedelta':
return timedelta(**d)
else:
# Oops... better put this back together.
d['__type__'] = type
return d


I convert the datetime and timedelta to objects, using the '__type__' attribute so that I can pick them out of a crowd more easily. Doing it this way makes a lot more sense than going with strings, regular expressions, and the such.