<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xml:base="http://whijo.net" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
 <title>python</title>
 <link>http://whijo.net/taxonomy/term/29/feed</link>
 <description>The taxonomy view with a depth of 0.</description>
 <language>en</language>
<item>
 <title>Periodic Status Update (exclaimation, exclaimation, one, exclaimation)</title>
 <link>http://whijo.net/blog/brad/2008/05/02/periodic-status-update-exclaimation-exclaimation-one-exclaimation.html</link>
 <description>&lt;p&gt;Because Blogging has become a secondary industry to Having a Family, and Working, and Living In Cape Town, I figured it was time for a generic status update, for those of you who dont use &lt;a href=&quot;http://twitter.com&quot;&gt;Twitter&lt;/a&gt;, and/or live near me. It is a hodge-podge, so apologies to any random internet search victims who think this page contained what they were looking for.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;We moved to Cape Town&lt;/strong&gt; at the beginning of the year, for me to take up a position as a &lt;strong&gt;Web Architect&lt;/strong&gt; at &lt;a href=&quot;http://www.openvoice.co.za&quot;&gt;openVOICE&lt;/a&gt;, and for the whijos to start living the Cape Town dream. About a month after I joined OV the decision was taken to split the company into different parts, to maintain focus. I was offered a position in the still to be formed company (which is now called nvent, and for a short time was called Richtrau no. 201). So, the company split, and I started to work for the new company (April 1st). In the run up to the split I became a bit unhappy with the uncertainty involved, and I ran into the problem (which I have had for a while) of being &quot;the Web Guy&quot;, and, while talented and handsome, I am very much keen to be part of a web team, in a web company, to see what it feels like, to have peers who worry about the same things I do, etc. So I put my CV out there, hoping to find a web firm that does &lt;a href=&quot;http://www.python.org&quot;&gt;python&lt;/a&gt; (my one true programming love after pascal). I didn´t find that, but I did find a very nice PHP web firm called &lt;a href=&quot;http://www.persuasionlab.com/&quot;&gt;Persuasion Lab&lt;/a&gt;. So, I interviewed with them, and decided to quit my position at OV/nvent, and take up a position in &lt;acronym title=&quot;Persuasion Lab&quot;&gt;PL&lt;/acronym&gt;.&lt;/li&gt;
&lt;li&gt;Now, &lt;strong&gt;I was not convinced of PL at first&lt;/strong&gt;, on paper, since they specialised in...Online Dating. I chuckled to myself, and decided to give them an interview, since it was either going to be new material at parties, or something genuinely intriguing. They won points out the box because they are located within walking distance of our home. To be honest I was expecting red velvet, and a handful of programmers wearing black silk shirts with long, slicked back pony tails. The former was true, the latter was devastatingly not. I interviewed with the directors of the company, David Burstein (there is only one n in burstein), and Duncan Forrest. They were nice, and I enjoyed the interview (I was unusually nervous, &lt;acronym title=&quot;For no apparent reason&quot;&gt;FNAR&lt;/acronym&gt;). I did a &lt;a href=&quot;http://validator.w3.org/check?uri=http%3A%2F%2Fwww.datingbuzz.co.za%2F&amp;amp;charset=(detect+automatically)&amp;amp;doctype=Inline&amp;amp;group=0&quot;&gt;small amount of research&lt;/a&gt; before I went to the interview, and was satisfied by the info that turned up (even if it is not 100%, 4 errors is better than this web page). So I was pleasantly surprised. They have the online dating game wrapped up in SA, powering most of the newspaper dating sites, including ¨Date Your Destiny¨ IOL dating (which always makes me smile when I read an IOL story). They seem to have grown, or split, out of Ogilvy Interactive (information courtesy of some retrospective facebook research), they have been going since 2000 (which is dog years in the web game), and Duncan seems to have done a lot of the development, and currently still dives into the code (which is something that makes me happy). They have a good number of employees, split over varying roles (which also makes me happy), and they also have a London office, for their British clients. I think I stand to learn a lot there, and hopefully pick up the skills and understanding I will need when I build my own empire.&lt;/li&gt;
&lt;li&gt;We visited JHB for two weeks to go to &lt;strong&gt;Pippa and Vaughn´s not-a-wedding-wedding&lt;/strong&gt;, which was frankly amazing, and, to date, is one of the most heartfelt and enjoyable weddings I have ever been to, but maybe it is just because they invited all our friends. The time we did spend with them we used productively to encourage them to get pregnant, so Fin has friends. We stayed with Pippa and Vaughn, Hannah and Mau, and saw a lot of Marc Bradley Lewis and &lt;abbr title=&quot;Marlise Richter&quot;&gt;Bokkie/Sweetie&lt;/abbr&gt;, Alison Green, caught up with old school friends Marc Patrick Lewis, and Breton VDB, among others. Unfortunately we did not see everyone I would have liked to, but JHB is a big place hey, and, with traffic, two weeks goes by quick.&lt;/li&gt;
&lt;li&gt;In May we are going to visit Grahamstown, to attend the wedding of Mamello Wheee Thinyane, and Dr. Hannah Barbie Sparkles Slay, and I did not want to fork out for the plane trips, so Mandy said we could drive if we had a new car (I am sorry Telburt, I loved you like a man loves a car, and It Was Her Idea). I really wanted a Toyota Prius, but they are PRICY, and still the first generation of hybrids, so I am a bit cautious. Instead, after narrowing down the choice between a low mileage VW Polo Classic, and a low mileage Golf 5, we went with the Golf. The Polo Classic came with mostly the same features, but the Golf FEELS incredible. I find the fittings and interior of the Polos to be quite plasticy, and sensible. So, parked outside our house is possibly the most expensive toy we have ever bought, but shew, it´s nice. I have taken every opportunity to drive it over the long weekend (we recieved it on Wednesday, when I traded Telburt in), including an airport run for Russell and Les. It feels like heaven to drive, and features like being able to wind down the electic windows using the remote blows my mind.&lt;/li&gt;
&lt;li&gt;We bought a new &lt;strong&gt;VW Golf 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Over the past few months I have steadily been attending more Geek things, like &lt;a href=&quot;http://clug.org.za&quot;&gt;CLUG&lt;/a&gt; (actually, the first CLUG talk I attended was the one given by &lt;a href=&quot;http://vhata.net&quot;&gt;Jonathan&lt;/a&gt; and Me). Attended two[&lt;a href=&quot;http://wiki.geekdinner.org.za/wiki/Cape_Town_January_2008&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;http://wiki.geekdinner.org.za/wiki/Cape_Town_March_2008&quot;&gt;2&lt;/a&gt;] &lt;a href=&quot;http://geekdinner.org.za/&quot;&gt;Geek Dinner(s)&lt;/a&gt;, during the second of which I did slideshow karaoke, and designed the &lt;a href=&quot;http://wiki.geekdinner.org.za/wiki/Image:Garrulousgrape.120.png&quot;&gt;random grape graphic&lt;/a&gt; (&lt;a href=&quot;http://wiki.geekdinner.org.za/wiki/Image:Garrulous_Grape.svg&quot;&gt;SVG here&lt;/a&gt;, needs gradients and blur filters) using &lt;a href=&quot;http://inkscape.org&quot;&gt;inkscape&lt;/a&gt;. There are some blog posts about GeekDinner on &lt;a href=&quot;http://planet.geekdinner.org.za/&quot;&gt;Planet Geekdinner&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I upgraded to Ubuntu 8.04 ´Hardy Heron´, and I am in love with Linux once again (every six months Ubuntu re-ignites the spark)&lt;/li&gt;
&lt;li&gt;I have accounts on, and I am going to write for, &lt;a href=&quot;http://ilovemylinux.com&quot; title=&quot;I love my linux&quot;&gt;ILML&lt;/a&gt; (the brainchild of my brother Kyle, and his mates), and the still-to-be-launched &lt;a href=&quot;http://techleader.co.za&quot;&gt;TechLeader&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That should cover it for now, thanks for your patience and I hope you enjoyed your stay.&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2008/05/02/periodic-status-update-exclaimation-exclaimation-one-exclaimation.html#comments</comments>
 <category domain="http://whijo.net/tags/current-status">current status</category>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/tags/golf-v">Golf V</category>
 <category domain="http://whijo.net/geek-tags/ilml">ilml</category>
 <category domain="http://whijo.net/tags/life">life</category>
 <category domain="http://whijo.net/geek-tags/linux">linux</category>
 <category domain="http://whijo.net/tags/peopleware">Peopleware</category>
 <category domain="http://whijo.net/tags/persuasion-lab">Persuasion lab</category>
 <category domain="http://whijo.net/geek-tags/php">php</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/tech-leader">tech leader</category>
 <category domain="http://whijo.net/tags/whijos">whijos</category>
 <pubDate>Fri, 02 May 2008 19:59:39 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">433 at http://whijo.net</guid>
</item>
<item>
 <title>Intrepid Ibex</title>
 <link>http://whijo.net/blog/brad/2008/02/21/intrepid-ibex.html</link>
 <description>&lt;p&gt;&lt;span class=&quot;inline&quot;&gt;&lt;a href=&quot;http://whijo.net/files/nubianibex.jpg&quot; class=&quot;inline-image-link&quot; title=&quot;View: A Nubian Ibex, from http://commons.wikimedia.org/wiki/Image:Capra_ibex_ibex_%E2%80%93_02.jpg&quot; rel=&quot;lightbox[gp_inline]&quot;&gt;&lt;img src=&quot;http://whijo.net/files/imagecache/inline_resize/files/nubianibex.jpg&quot; alt=&quot;A Nubian Ibex, from http://commons.wikimedia.org/wiki/Image:Capra_ibex_ibex_%E2%80%93_02.jpg&quot; title=&quot;A Nubian Ibex, from http://commons.wikimedia.org/wiki/Image:Capra_ibex_ibex_%E2%80%93_02.jpg&quot;  class=&quot;inline&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;
In celebration of the &lt;a href=&quot;https://lists.ubuntu.com/archives/ubuntu-devel/2008-February/025136.html&quot;&gt;newly announced&lt;/a&gt; &quot;Intrepid Ibex&quot; (or Ubuntu 8.10), I once again bring your attention to my &lt;a href=&quot;http://whijo.net/node/116&quot;&gt;python script which generates alternative Ubuntu names&lt;/a&gt;. I brute forced some names starting with I and came up with these alternatives (which suggests maybe Intrepid Impala would be a nicer name for 8.10). So, &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;radbrad@renton:~/python/ubuntu/choose_ubuntu$ while ((1));do python namer.py |grep ^I; done;
Inspirational Insect
Insolent Imp
Inexpensive Imp
Illustrious Ichthyosaur
Ingenious Indri
Incredible Impala
Immense Inkanyamba
Insurgent Ibis
Indefatigable Insect
Inescapable Ichthyosaur
Impressive Ichthyosaur
Inquisitive Imp
Ignited Impala
Ineffable Imp
Inevitable Insect
Inquisitive Ibis
Immodest Imp
Incomparable Ibex
Incredible Ichthyosaur
Ill Insect
Impressive Ibex
Indefatigable Ichthyosaur
Icky Iceweasel
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
Like the kubuntu, xubuntu, gubuntu names &lt;a href=&quot;http://www.tectonic.co.za/?p=2053&quot;&gt;confuse newbies and make Ubuntu difficult to market&lt;/a&gt;. I dig the naming scheme, just not the names that come out of it.&lt;/p&gt;
&lt;p&gt;Also, &lt;a href=&quot;http://www.theregister.co.uk/2008/02/20/ubuntu_intrepid_ibex/&quot;&gt;the register points out&lt;/a&gt; that:&lt;/p&gt;
&lt;blockquote src=&quot;http://www.theregister.co.uk/2008/02/20/ubuntu_intrepid_ibex/&quot;&gt;&lt;p&gt;We&#039;re informed that a male ibex will try and tempt a female into sex by licking and sniffing the lass and by issuing a low scream. It&#039;s suspected that Bono responds to a similar ritual. Sometimes the female will demonstrate her interest by urinating or by ramming the male with her horns. The male then returns the favor through the flehmen response, which is a curling of the upper lip. This goes on for about 30 minutes until Bono shows up or the ibexes get it on.&lt;/p&gt;
&lt;/p&gt;&lt;/blockquote&gt;
</description>
 <comments>http://whijo.net/blog/brad/2008/02/21/intrepid-ibex.html#comments</comments>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/linux">linux</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/ubuntu">ubuntu</category>
 <pubDate>Thu, 21 Feb 2008 10:09:42 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">426 at http://whijo.net</guid>
</item>
<item>
 <title>The Zen of Python, by Tim Peters</title>
 <link>http://whijo.net/blog/brad/2007/12/05/zen-python-tim-peters.html</link>
 <description>&lt;pre&gt;&lt;code&gt;
radbrad@Cylon:~$ python
Python 2.5.1 (r251:54863, May  2 2007, 16:56:35) 
[GCC 4.1.2 (Ubuntu 4.1.2-0ubuntu4)] on linux2
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
&amp;gt;&amp;gt;&amp;gt; import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren&#039;t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you&#039;re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it&#039;s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let&#039;s do more of those!
&lt;/code&gt;&lt;/pre&gt;</description>
 <comments>http://whijo.net/blog/brad/2007/12/05/zen-python-tim-peters.html#comments</comments>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/poem">poem</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <pubDate>Wed, 05 Dec 2007 11:18:02 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">422 at http://whijo.net</guid>
</item>
<item>
 <title>Hardy Heron</title>
 <link>http://whijo.net/blog/brad/2007/09/02/hardy-heron.html</link>
 <description>&lt;div&gt;&lt;a href=&quot;http://www.flickr.com/photos/locutis/146001589&quot; title=&quot;Hadeda&quot;&gt;&lt;img src=&quot;http://farm1.static.flickr.com/56/146001589_13cb17e347_m.jpg&quot; alt=&quot;&quot; title=&quot;&quot;  class=&quot; flickr-photo-img&quot; height=&quot;160&quot; width=&quot;240&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;In celebration of the &lt;a href=&quot;http://www.jonobacon.org/?p=1017&quot;&gt;newly announced&lt;/a&gt; &quot;Hardy Heron&quot; version of Ubuntu (8.04) I built a script to randomly generate a name for Ubuntu releases. &lt;del&gt;I did not have a huge corpus of animal names, and I tried to find as many adjectives as I could find, but&lt;/del&gt; &lt;ins&gt;I used the adjectives and animals from &lt;a href=&quot;https://wiki.ubuntu.com/DevelopmentCodeNames&quot;&gt;the ubuntu wiki&lt;/a&gt;, and&lt;/ins&gt; here is the script in all it&#039;s glory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re
from random import randrange

def get_adjectives():
        f = open(&#039;adjectives&#039;,&#039;r&#039;)
        adjectives = f.readlines()
        f.close()
        return adjectives

def get_animals():
        f = open(&#039;animals&#039;,&#039;r&#039;)
        animals = f.readlines()
        f.close()
        return animals

def choose_adjective(adjectives):
        return adjectives[randrange(len(adjectives))].strip()

def choose_ubuntu(adjectives,animals):
        adj = choose_adjective(adjectives)
        letter = &#039;^%s&#039; % adj[0]
        letter = re.compile(letter)
        animal = [i for i in animals if letter.match(i)]
        if animal == []:
                return choose_ubuntu(adjectives,animals)
        if len(animal)&amp;gt;1:
                animal = animal[randrange(len(animal))].strip()
        else:
                animal = animal[0].strip()
        return [adj,animal]

adjectives = get_adjectives()
animals = get_animals()
ubuntu = choose_ubuntu(adjectives,animals)

print(&#039;%s %s&#039; % (ubuntu[0],ubuntu[1]))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
The script and the animal and adjective corpusesses (not sure of the plural of corpus) are attached in a zipfile.&lt;/p&gt;
&lt;p&gt;P.S. I would have preferred a name like Happy Hummingbird, or something that doesn&#039;t sound so much like &lt;a href=&quot;http://images.google.com/images?q=hadeda+ibis&quot;&gt;Hadeda&lt;/a&gt; (the reference to a bird does not help it&#039;s case). Surely in light of Hoary Hedgehog they should skip H entirely?&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2007/09/02/hardy-heron.html#comments</comments>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/linux">linux</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/ubuntu">ubuntu</category>
 <enclosure url="http://whijo.net/files/choose_ubuntu.zip" length="6029" type="application/zip" />
 <pubDate>Sun, 02 Sep 2007 15:37:04 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">116 at http://whijo.net</guid>
</item>
<item>
 <title>Statistics logging for Django - part 2</title>
 <link>http://whijo.net/blog/brad/2007/07/29/statistics-logging-django-part-2.html</link>
 <description>&lt;p&gt;In &lt;a href=&quot;http://whijo.net/blog/brad/2007/07/19/statistics-logging-django.html&quot;&gt;part 1&lt;/a&gt; I explained how to build middleware and an associated model to capture page accesses, and tie them to a user session. Now that we have all this useful info logged we need to do something with it, like, display it. Unfortunately Django doesn&#039;t have a facility for using GROUP BY with mysql, so you have two major choices (there are more but we can ignore them): implement a custom request in a &lt;a href=&quot;http://www.djangoproject.com/documentation/model-api/#managers&quot;&gt;custom Manager&lt;/a&gt; (see &lt;a href=&quot;http://www.djangosnippets.org/snippets/236/&quot;&gt;snippet&lt;/a&gt; and &lt;a href=&quot;http://www.djangosnippets.org/snippets/1/&quot;&gt;snippet&lt;/a&gt;, or &lt;a href=&quot;http://www.djangosnippets.org/tags/group-by/&quot;&gt;tagged snippets&lt;/a&gt;), or exploit a &lt;a href=&quot;&quot;&gt;mysql view&lt;/a&gt; and model it in Django. Now for me I prefer the latter because it means my custom sql becomes a mysql customisation and as far as Django is concerned it is dealing with a normal table (but don&#039;t tell Django that it is read only), and thus the model code works, so subsequent queries and manipulations can exploit the &lt;acronym title=&quot;Object Relational Manager&quot;&gt;ORM&lt;/acronym&gt; easily. My subjective and non-scientific experience is that using views is a lot more efficient/quick than using custom queries in the manager (it probably has to do with whatever optimisations exist with views, and the fact that you only fetch items when Django decides you need to fetch a row). So, how the hell do we do it?&lt;/p&gt;
&lt;p&gt;First I created a model that describes what information I want to deal with (something which maps neatly on to our other model):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserActivity(models.Model):
        session = models.OneToOneField(Session,
                                        db_index=True, 
                                        null=True,blank=True,
                                        primary_key=True)
        user = models.ForeignKey(User,null=True,blank=True)
        date = models.DateTimeField(
                       help_text=&quot;Date Request started processing&quot;,
                       auto_now_add=True,
                       db_index=True)
        processing_time = models.IntegerField(
                       help_text=&quot;Total time spent on this user&quot;)
        requests = models.IntegerField(
                       help_text=&quot;Total Requests in this session&quot;)
        stats = UserActivityManager()
        def __str__(self):
                return &#039;%s: %s %s - %s - %s&#039; % (self.user,self.session,self.date,self.processing_time,self.requests)
        class Admin:
                list_display= (&#039;user&#039;,&#039;session&#039;,&#039;date&#039;,&#039;processing_time&#039;,&#039;requests&#039;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
The nice thing about this set up is when we aggregate our activity logs we can pull out random stuff like total processing time for requests for a user/session, along with number of requests/user/session (and thus average request time)&lt;/p&gt;
&lt;p&gt;But that is just our model, we still need the magic. To implement the magic nicely I put some custom initial SQL into the sql directory of my application (in my case the housing application for this is called accounts, so I make a file called accounts/sql/useractivity.sql), you can read more about initial data &lt;a href=&quot;http://www.djangoproject.com/documentation/model-api/#providing-initial-sql-data&quot;&gt;here&lt;/a&gt;, &lt;a href=&quot;http://www.djangoproject.com/documentation/models/fixtures/&quot;&gt;Django fixtures&lt;/a&gt;).My SQL looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE accounts_useractivity;
CREATE OR REPLACE VIEW accounts_useractivity AS 
SELECT i.session_id,
       i.user_id,
       MAX(i.date) as date,
       sum(i.request_time) AS processing_time, 
       count(*) AS requests 
FROM accounts_activitylog i 
GROUP BY 1 
ORDER BY NULL;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So first I tell mysql to drop the table that django just created (accounts_useractivity), and create a view in it&#039;s place. The view is very simple, in that it just GROUP BY the session_id. The real hair puller for me was figuring out that I needed to use the MAX(i.date) (see more about &lt;a href=&quot;http://dev.mysql.com/doc/refman/5.0/en/group-by-functions.html&quot;&gt;aggregate functions&lt;/a&gt;) to get the most recent access to float to the top when it normalises the data (otherwise the GROUP BY normally ORDER BY the session_id, which helps no one), the ORDER BY NULL is &lt;a href=&quot;http://dev.mysql.com/doc/refman/5.0/en/group-by-optimization.html&quot;&gt;an optimisation&lt;/a&gt; to tell GROUP BY not to ORDER BY. I am hoping that because date is an INDEX (from our logging model) it shouldn&#039;t cost too much to do a MAX. (I would like someone with Much MYSQL-fu to point out any further optimisations to this, or even alternative approaches to the whole thing).&lt;/p&gt;
&lt;p&gt;So now we have an aggregating VIEW which Django maps using it&#039;s ORM, so that to figure out sessions which have been active in the last x minutes (where x is a datetime.timedelta object) we simply do a:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserActivity.objects.get_query_set().filter(date__gte=datetime.now()-x)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
I wrote a custom manager for getting recent sessions etc., but that is an exercise for the reader. What I did include in my model is something which returns a stepped &quot;request_weight&quot; i.e. session requests / largest session request x steps, which in my case defaults to 6. This means I can style my users like one would a &quot;&lt;a href=&quot;http://en.wikipedia.org/wiki/Tag_cloud&quot;&gt;tag cloud&lt;/a&gt;&quot;, so very active sessions will grow bigger than less active sessions. I needed to implement a helper function in the custom manager to return the session with the most requests.&lt;/p&gt;
&lt;p&gt;The final tip is to use a &lt;a href=&quot;http://www.djangoproject.com/documentation/templates_python/#subclassing-context-requestcontext&quot;&gt;context processor&lt;/a&gt; to make the information available to all your templates, although you could do it with middleware (maybe middleware is the proper way to do it?).&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2007/07/29/statistics-logging-django-part-2.html#comments</comments>
 <category domain="http://whijo.net/geek-tags/django">django</category>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/middleware">middleware</category>
 <category domain="http://whijo.net/geek-tags/mysql">mysql</category>
 <category domain="http://whijo.net/geek-tags/mysql-views">mysql views</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/statistics">statistics</category>
 <pubDate>Sun, 29 Jul 2007 21:52:25 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">110 at http://whijo.net</guid>
</item>
<item>
 <title>Hey Rhythmbox, go pause yourself!</title>
 <link>http://whijo.net/blog/brad/2007/07/26/hey-rhythmbox-go-pause-yourself.html</link>
 <description>&lt;p&gt;A while back &lt;a href=&quot;http://nxsy.org/blog/archives/2007/03/20/getting-amarok-to-pause-when-the-screen-locks-using-python-of-course&quot;&gt;NBM wrote a script to pause Amarokk when his screensaver turned on&lt;/a&gt;. Since I get competitive I felt I could do it better, and do it for the media player that I love, &lt;a href=&quot;http://www.gnome.org/projects/rhythmbox/&quot;&gt;Rhythmbox&lt;/a&gt;. If you are wondering what purpose this really serves, imagine you are away from your desk for a long time, you want to make sure the music is the same when you come back, and you don&#039;t want to bug any co-workers. It also serves the purpose of deepening my understanding of python, and scratches the itch of customising desktops (and feeling better than windows people). So...&lt;/p&gt;
&lt;p&gt;I started out writing a daemonisd script like the one &lt;acronym title=&quot;Neil Blakey-Milner&quot;&gt;nbm&lt;/acronym&gt; built, which listens on the d-bus (the Linux/Freedesktop inter-process messaging/control bus), and connects to Rhythmbox on the same bus to get in control. &lt;a href=&quot;http://nxsy.org/blog/archives/2007/03/20/getting-amarok-to-pause-when-the-screen-locks-using-python-of-course#comment-5167&quot;&gt;Taking his script and changing the message passing&lt;/a&gt; to point to &lt;acronym title=&quot;Rhythmbox&quot;&gt;RB&lt;/acronym&gt; instead of Amarokk was trivial (but a mission due to lack of documentation) and I threw in some improvements.&lt;/p&gt;
&lt;p&gt;Then I got itchy and figured I could write a plugin for RB and push control of the functionality to RB (where it belongs), and drop the need for a daemon script. So I opened up the artdisplay plugin for Rhythmbox (since it is a python based plugin) to get an idea of what is involved. After browsing through the plugin, and reading the &lt;a href=&quot;http://live.gnome.org/RhythmboxPlugins/WritingGuide&quot;&gt;Rhythmbox plugins writing guide&lt;/a&gt; I rolled my own.&lt;/p&gt;
&lt;p&gt;I call my plugin dbusevents because while it pauses when the screen saver is on it could be extended to support multiple events and actions (maybe you want the volume turned up louder when you first plug in your mouse so you can party). So, a Rhythmbox plugin inhabits a directory and has atleast two parts: A descriptor file, and the plugin.&lt;/p&gt;
&lt;p&gt;You can download the whole thing: &lt;a href=&quot;http://whijo.net/files/dbusevents.tar.gz&quot; title=&quot;Download: dbusevents.tar.gz (1.42 KB)&quot;&gt;dbusevents.tar.gz&lt;/a&gt;. To use it drop the extracted directory in /usr/lib/rhythmbox/plugins/&lt;/p&gt;
&lt;p&gt;My descriptor file looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[RB Plugin]
Loader=python
Module=dbusevents
IAge=1
Name=DBUS Events
Name[en_GB]=DBUS Events
Description=Connects Rhythmbox actions to DBUS events (for e.g. pausing when the screensaver is on)
Description[en_GB]=Connects Rhythmbox actions to DBUS events (for e.g. pausing when the screensaver is on)
Authors=Bradley Whittington 
Copyright=Copyright © 2007 Bradley Whittington
Website=http://www.rhythmbox.org
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Pretty straightforward stuff eh? The net result is:&lt;br /&gt;
&lt;span class=&quot;inline&quot;&gt;&lt;a href=&quot;http://whijo.net/files/rhythmbox-dbus-config.png&quot; class=&quot;inline-image-link&quot; title=&quot;View: rhythmbox-dbus-config.png&quot; rel=&quot;lightbox[gp_inline]&quot;&gt;&lt;img src=&quot;http://whijo.net/files/imagecache/inline_resize/files/rhythmbox-dbus-config.png&quot; alt=&quot;rhythmbox-dbus-config.png&quot; title=&quot;rhythmbox-dbus-config.png&quot;  class=&quot;inline&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The meat of the plugin is contained in dbusevents/__init__.py which implements a Rhythmbox plugin, which means it inherits from rb.plugin (import rb), implements at least __init__, activate, and deactivate.&lt;/p&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DbusEventsPlugin (rb.Plugin):
#Initialisation 
        def __init__ (self):
                rb.Plugin.__init__ (self)

        def activate (self, shell):
                self.shell = shell
                self.rb = shell.get_player ()
                self.session_bus = dbus.SessionBus()
                self.load_events()
                self.connect_events()

        def deactivate (self, shell):
                self.disconnect_events()
                self.save_events()
                self.shell = None
                self.rb = None
                self.session_bus = None
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
When my plugin is told to activate it assigns self.rb to an instance of the Rhythmbox shell, makes a connection to the D-BUS session bus, and assigns the instance to self.session_bus, then it calls load_events (which could load events and actions from a config file), and finally connect_events which tells the session_bus which functions to call when certain events happen. The deactivate function does everything, in reverse ;). Ultimately I would like to make a configuration dialogue which allows you to have your custom event-action set up, but I havent found anything more beyond pausing when the screensaver is on.&lt;/p&gt;
&lt;p&gt;The magic in connect_events and disconnect_events is pretty basic, at the moment it walks a dictionary with relevant information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        events = {
                        &#039;Screensaver Play/Pause&#039;:{
                                &#039;__doc__&#039;:&#039;Pauses Rhythmbox when the Screensaver is running&#039;,
                                &#039;handler&#039;:&#039;playPause&#039;,
                                &#039;signal&#039;:(&#039;SessionIdleChanged&#039;,&#039;org.gnome.ScreenSaver&#039;),
                                &#039;enabled&#039;:True,
                        }
                }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The functions use self.session_bus.add_signal_receiver and self.session_bus.remove_signal_receiver to attach events to actions. I have only defined a PlayPause function at the moment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        def playPause(self,state):
                if not state:
                        if self.wasplaying:
                                self.rb.play()
                else:
                        self.wasplaying = self.rb.get_playing()
                        self.rb.pause()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which checks if it is playing when the screensaver is turned on, and pauses if it is, and unpauses when the screensaver turns off, if it was paused when the screensaver went on.&lt;/p&gt;
&lt;p&gt;I would appreciate feedback, so drop me a comment if you have any!&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2007/07/26/hey-rhythmbox-go-pause-yourself.html#comments</comments>
 <category domain="http://whijo.net/geek-tags/d-bus">d-bus</category>
 <category domain="http://whijo.net/geek-tags/gnome-screensaver">gnome-screensaver</category>
 <category domain="http://whijo.net/geek-tags/linux">linux</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/rhythmbox">rhythmbox</category>
 <category domain="http://whijo.net/geek-tags/xscreensaver">xscreensaver</category>
 <enclosure url="http://whijo.net/files/dbusevents.tar.gz" length="1453" type="application/x-gzip" />
 <pubDate>Thu, 26 Jul 2007 16:41:49 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">109 at http://whijo.net</guid>
</item>
<item>
 <title>Statistics logging for Django</title>
 <link>http://whijo.net/blog/brad/2007/07/19/statistics-logging-django.html</link>
 <description>&lt;p&gt;Last night I built some middleware/models for a django application to log visitor/user activity on the site. The intention is to be able to do better user tracking, and build more comprehensive statistics stored in the mysql db (obviously I am also logging everything with apache). The current set up still needs some periodical scripts to conflate data into statistics. I was thinking of doing a daily-weekly-monthly routine (i.e. once a day stats are conflated for yesterday&#039;s stats, and once a week they are turned into weekly stats, and once a month they are minimised into a monthly overview. It was actually really simple to implement, but I butted my head against some django issues (more at the end).&lt;/p&gt;
&lt;p&gt;So, first we build a model to represent a request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class UserActivity(models.Model):
        user = models.ForeignKey(
                      User,
                      null=True, blank=True,
                      db_index=True
               )
        session = models.ForeignKey(
                      Session,
                      db_index=True,
                      null=True, blank=True
                  )
        date = models.DateTimeField(
                      help_text=&quot;Date Request started processing&quot;,
                      auto_now_add=True,
                      db_index=True)
        request_time = models.IntegerField(
                              help_text=&quot;Processing time (in ms)&quot;,
                              null=True, blank=True)
        request_url = models.CharField(maxlength=800,db_index=True)
        referer_url = models.URLField(
                              verify_exists=False,
                              db_index=True,
                              blank=True, null=True)
        client_address = models.IPAddressField(
                              blank=True,null=True)
        client_host = models.CharField(
                              maxlength=256,
                              blank=True,null=True)
        browser_info = models.TextField(null=True,blank=True)
        error = models.TextField(null=True,blank=True)
        def set_request_time(self):
                from datetime import datetime
                self.request_time = (
                                      datetime.now() - 
                                      self.date 
                                    ).microseconds
                self.save()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(download &lt;a href=&quot;http://whijo.net/files/models.py_.txt&quot; title=&quot;Download: models.py_.txt (1.17 KB)&quot;&gt;models.py_.txt&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I think the model captures all the relevant info (we tie a request to a session and user, we have the time they made the request (and using middleware we can calculate how long the request took), the referer, and some info about the client).&lt;/p&gt;
&lt;p&gt;Most of the fields can be blank/null because we are not always going to have a session (see below), etc.&lt;/p&gt;
&lt;p&gt;The function set_request_time is called by the outgoing middleware function (process_response) and just notes how long the request took, and saves the object.&lt;/p&gt;
&lt;p&gt;Next we need some middleware to handle the object creation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from datetime import datetime
from django.conf import settings
from my_app.models import UserActivity

class Activity(object):
        def process_request(self,request):
                if request.META.has_key(&#039;HTTP_REFERER&#039;):
                        referer = request.META[&#039;HTTP_REFERER&#039;]
                else:
                        referer = &#039;&#039;

                self.activity = UserActivity(
                        user = request.user,
                        session = request.session,
                        date = datetime.now(),
                        request_url = request.META[&#039;PATH_INFO&#039;],
                        referer_url = referer,
                        client_address = request.META[&#039;REMOTE_ADDR&#039;],
                        client_host = request.META[&#039;REMOTE_HOST&#039;],
                        browser_info = request.META[&#039;HTTP_USER_AGENT&#039;]
                )

        def process_exception(self,request,exception):
                self.activity.error = exception
                self.activity.save()

        def process_response(self,request,response):
                self.activity.set_request_time()
                return response
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(download &lt;a href=&quot;http://whijo.net/files/middleware.py_.txt&quot; title=&quot;Download: middleware.py_.txt (825 bytes)&quot;&gt;middleware.py_.txt&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;You may (or may not) have noticed that we only actually save our model on the outgoing response, so we only have one db write per request. The middleware system is very easy to build for, and is &lt;a href=&quot;http://www.djangoproject.com/documentation/middleware/&quot;&gt;documented here&lt;/a&gt;. The nice thing is the process_exception will keep a record of the exception (but I am not sure if this could be done so it stores more information than just the exception.__str__()?)&lt;/p&gt;
&lt;p&gt;To install this you would need to have your model within in an app that is &quot;installed&quot; and &quot;syncdb&quot;. The middleware needs to be placed after the session middleware, for e.g. in settings.py (in MIDDLEWARE_CLASSES):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    
    &quot;django.middleware.common.CommonMiddleware&quot;,
    &quot;django.contrib.sessions.middleware.SessionMiddleware&quot;,
    &quot;django.contrib.auth.middleware.AuthenticationMiddleware&quot;,
    &quot;league.middleware.Activity&quot;,
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
The next step is to build a context_processor that will include some useful stats like who is logged in etc. but that will need a more models, or mysql view or UserActivityManager that does a custom sql request with some &quot;group by&quot; magic. I have not built those parts yet, so I won&#039;t speak about them yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My gripes about this implementation&lt;/strong&gt; doing regular user activity stats is a relatively costly request (you need to do a SELECT COUNT(*) WHERE date&amp;gt;now()-(20 minutes) GROUP BY user). This could be cheapened by having a OneToOne join table with the user table which just has an indexed recent_activity field against a User which is touched every request from that user. To get anonymous user activity we can only really rely on ip addresses, since sessions are not set until a user logs in/logs out, so we would need to do a similar system to the user OneToOne table, and use the REPLACE syntax of mysql (not sure if this is possible using django).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My gripes about the session middleware&lt;/strong&gt; is that users do not get sessions until they log in/log out. This is good because once of visitor etc. do not get sent a cookie, and you don&#039;t allocate them a session in the DB, but it means unique sessions are more difficult to track because anonymous, first time visitors are only unique by their IP address, and nothing else. I can obviously change this, by setting any session variable for visitors without a session in the process_request of the activity middleware. This is neat because it is an opt in db hit, but after wrestling for ages with session middleware appreciating opt in is something to be done in the sober light of day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My gripes about Django&#039;s ORM&lt;/strong&gt; are that there is no neat way to do custom sql requests (the nicest group by sql snippet I have seen is &lt;a href=&quot;http://www.djangosnippets.org/snippets/1/&quot;&gt;this one&lt;/a&gt; because it uses django&#039;s _meta to get the table names). Newer changes in Django introduced the &lt;a href=&quot;http://www.djangoproject.com/documentation/db-api/#extra-select-none-where-none-params-none-tables-none&quot;&gt;extra&lt;/a&gt; parameter, which means less completely custom sql (i.e. you can just append your customisations to the existing sql statement), but it still doesn&#039;t allow you to use very specific stuff like GROUP BY (which not all DBs support). The way to remedy this is to figure out some way you can still send sanitised sql to a db server in an extra statement, while allowing more appended customisations for developers. The alternative is to build group_by functions which either translate to DB specific requests, or do it virtually (much like the transactions infrastructure). I prefer the latter solution because I think GROUP BY is very relevant and very useful, but the latter solution does mean that if your DB doesnt support it, then it could be a very costly operation in python-space.&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2007/07/19/statistics-logging-django.html#comments</comments>
 <category domain="http://whijo.net/tags/development">development</category>
 <category domain="http://whijo.net/geek-tags/django">django</category>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/logging">logging</category>
 <category domain="http://whijo.net/geek-tags/middleware">middleware</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/statistics">statistics</category>
 <enclosure url="http://whijo.net/files/middleware.py_.txt" length="825" type="text/plain" />
 <pubDate>Thu, 19 Jul 2007 12:05:13 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">108 at http://whijo.net</guid>
</item>
<item>
 <title>Python and cybersmart Caps</title>
 <link>http://whijo.net/blog/brad/2007/06/24/python-and-cybersmart-caps.html</link>
 <description>&lt;p&gt;I recently got DSL at home (&lt;a href=&quot;http://nml.ru.ac.za/blog/brad/2007/05/24/one-goes-out-telkom.html&quot;&gt;after 22 working days&lt;/a&gt;), and signed up with a local ISP (&lt;a href=&quot;http://www.cybersmart.co.za&quot;&gt;cybersmart&lt;/a&gt; - you can tell them &lt;a href=&quot;mailto:bwhittington@cybersmart.co.za&quot;&gt;bwhittington@cybersmart.co.za&lt;/a&gt; referred you) because it was the &lt;a href=&quot;http://www.hellkom.co.za/ispprices/adsl-3gb.php&quot;&gt;cheapest 3 Gig cap&lt;/a&gt; available. What I also like about them is they provide a very simple web page that helps you keep track of your usage. I knew a bit of python, and I wanted to further my skills, so I decided to tap out a python script to bring my cap usage to the desktop. The result:&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;inline&quot;&gt;&lt;a href=&quot;http://whijo.net/files/cybersmart-cap.png&quot; class=&quot;inline-image-link&quot; title=&quot;View: cybersmart-cap.png&quot; rel=&quot;lightbox[gp_inline]&quot;&gt;&lt;img src=&quot;http://whijo.net/files/imagecache/inline_resize/files/cybersmart-cap.png&quot; alt=&quot;cybersmart-cap.png&quot; title=&quot;cybersmart-cap.png&quot;  class=&quot;inline&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;What this re-iterated for me is that python is awesome, linux is awesome, gnome is awesome, and python is awesome. You can get python wrappers for just about everything, and more and more programs include python as an avenue to build extensions (my first non-django, python project was a rhythmbox plugin that pauses the music when your screensaver turns on). &lt;/p&gt;
&lt;p&gt;So I started out on the problem. It was a pretty straightforward issue: download the html from cybersmart, walk the &lt;abbr title=&quot;Document Object Model&quot;&gt;DOM&lt;/abbr&gt;, grab the elements with the relevant data, typecast the data to a floating point number, and, for cream, calculate daily averages (per day usage, per day remaining). Finally, display the data. For this little project I used urllib, pyxml, pynotify (a thin wrapper around libnotify), datetime and time (you can download the full file as an attachment to this blog post).&lt;/p&gt;
&lt;p&gt;We start with the data, the cybersmart page looks like this:&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;inline&quot;&gt;&lt;a href=&quot;http://whijo.net/files/cap-summary.png&quot; class=&quot;inline-image-link&quot; title=&quot;View: cap-summary.png&quot; rel=&quot;lightbox[gp_inline]&quot;&gt;&lt;img src=&quot;http://whijo.net/files/imagecache/inline_resize/files/cap-summary.png&quot; alt=&quot;cap-summary.png&quot; title=&quot;cap-summary.png&quot;  class=&quot;inline&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The relevant bits of information are the &quot;Used for this month&quot; and &quot;Total left for month&quot;. Luckily these are stored in elements with class names that are relevant (although they do reuse the same class name for multiple things, instead of doing a class=&quot;used usedTotal&quot; and a class=&quot;remaining remainingTotal&quot;, and styling the relevant classes, while getting the semantic meaning, and visual identity across). &lt;/p&gt;
&lt;p&gt;So, we download the file (sending relevant log in details as a POST), and send it to the HtmlReader class of pyxml:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;Downloading HTML&quot;)
data = urllib.urlencode(
  {   &quot;accountName&quot;:&quot;account@cybersmart.co.za&quot;,
      &quot;password&quot;:&quot;password&quot;,
      &quot;login&quot;:&quot;login&quot;})
f = urllib.urlopen(
      &quot;http://www.cybersmart.co.za/getAccountDetails.cgi&quot;,
      data
    )
doc = f.read()
f.close()
reader= HtmlLib.Reader()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
Then we parse the document, and walk the DOM tree, pulling out relevant info (sidebar: I couldn&#039;t do this more efficiently than pulling out all the td&#039;s and then checking each class entry, the getElementByClass doesn&#039;t seem to work out properly?? I also cheat because there are multiple savedTotal elements, but I am only worried about the last one):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;Parsing Document&quot;)
dom = reader.fromString(doc)

print(&quot;Walking the tree&quot;)
td_elements = dom.getElementsByTagName(&quot;td&quot;)
remaining=&#039;&#039;
used=&#039;&#039;
for td in td_elements:
        temp = td.attributes.getNamedItem(&#039;class&#039;)
        if temp:
                if temp.value == &quot;savedTotal&quot;:
                        remaining=collect_text(td)
                if temp.value == &quot;usedTotal&quot;:
                        used=collect_text(td)

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
Now we have strings in our &quot;remaining&quot; and &quot;used&quot; variables which we can easily typecast to floats and process them further. I used some helper functions that I found on the net for dealing with number of days past and remaining in the month, I didn&#039;t note where they came from, so apologies to the author.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;Caluclating averages&quot;)
daysleft = 
    mkLastOfMonth(datetime.datetime.now())-datetime.datetime.now()
daysleft = daysleft.days
remaining_float = float(remaining)
used_float = -float(used)
days = int(datetime.datetime.now().strftime(&quot;%d&quot;))
&lt;/pre&gt;&lt;p&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, the magic...displaying the data as a notification bubble is flipping simple, you just import pynotify, initialise it with your namespace, and tell it what to display. You can get wrappers that allow you more flexibility for your notification (like images etc.), but I didn&#039;t need it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;Displaying data&quot;)
usage=
   &quot;%.2f Gb used in %s days\nUsage average of %.2f Mb&quot; % 
   (used_float,days,(used_float*1000)/days)

projected=
   &quot;\n\n%s days left to use %s Gb\ndaily allowance of %.2f Mb&quot; % 
   (daysleft,remaining,(remaining_float*1000)/daysleft)

content = &#039;&#039;.join([usage,projected])
pynotify.init(&quot;cybersmart&quot;)
n = pynotify.Notification(&quot;Cybersmart Remaining Cap&quot;,content)
n.show()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;
It is not the most wonderful, or extensible code ever, but it works for me, and hopefully this can help someone else do something else. I would like to find a platform agnostic way to do notifications, and I have found some code to build a platform agnostic systray icon (which could keep track of you usage daily as a graph, and pop up a detailed notification when clicked). Splitting the code into classes would allow it to become more extensible in that multiple ISPs could be catered for using different approaches, with the same display code. &lt;/p&gt;
&lt;p&gt;I am stoked with how easy it was to roll it together (POSTing, downloading, parsing, processing and display in just over 30 lines), how easy it was to hook into libnotify (the freedesktop notification lib) for display. I have upgraded from edgy to feisty (which bumped python from 2.4 to 2.5) and it wasn&#039;t bothered either. You will need to install python-xml, and python-notify. The nice thing is that this is also an appropriate area for python, since it is just message passing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update 24/06/2007 - &lt;a href=&quot;http://vhata.rucus.net&quot;&gt;Jonathan&lt;/a&gt; pointed out &lt;a href=&quot;http://www.crummy.com/software/BeautifulSoup/&quot;&gt;BeautifulSoup&lt;/a&gt; as a more appropriate alternative to htmllib&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a side note, I fell in love with &lt;a href=&quot;http://www.python.org&quot;&gt;python&lt;/a&gt; when I learnt it so I could program in &lt;a href=&quot;http://www.djangoproject.com&quot;&gt;Django&lt;/a&gt;. I have slowly been trying to spread my python wings beyond simply writing web apps, because it is a flipping rad language, with awesome readability, and crazy power. I suppose you could say I script in python, because python is interpreted at run time, then all the java programmers are scriptors too, so ne ner ne ner ne ner.&lt;/p&gt;
</description>
 <comments>http://whijo.net/blog/brad/2007/06/24/python-and-cybersmart-caps.html#comments</comments>
 <category domain="http://whijo.net/tags/cybersmart">cybersmart</category>
 <category domain="http://whijo.net/tags/geek">geek</category>
 <category domain="http://whijo.net/geek-tags/gnome">gnome</category>
 <category domain="http://whijo.net/tags/internet">internet</category>
 <category domain="http://whijo.net/tags/isp">ISP</category>
 <category domain="http://whijo.net/geek-tags/lazyweb">lazyweb</category>
 <category domain="http://whijo.net/geek-tags/libnotify">libnotify</category>
 <category domain="http://whijo.net/geek-tags/linux">linux</category>
 <category domain="http://whijo.net/geek-tags/python">python</category>
 <category domain="http://whijo.net/geek-tags/snippet">snippet</category>
 <category domain="http://whijo.net/geek-tags/tutorial">tutorial</category>
 <enclosure url="http://whijo.net/files/cybersmart.py_.txt" length="2433" type="text/plain" />
 <pubDate>Sun, 24 Jun 2007 10:07:17 +0200</pubDate>
 <dc:creator>brad</dc:creator>
 <guid isPermaLink="false">99 at http://whijo.net</guid>
</item>
</channel>
</rss>
