<?xml version="1.0"?>
<!-- RSS generated by Radio UserLand v8.0.8 on Tue, 15 Jun 2004 20:12:01 GMT -->
<rss version="2.0">
	<channel>
		<title>Michael Kent: Python Projects</title>
		<link>http://radio.weblogs.com/0124960/categories/pythonProjects/</link>
		<description>Projects I&apos;m working on in the Python programming language.</description>
		<language>en-us</language>
		<copyright>Copyright 2004 Michael Kent</copyright>
		<lastBuildDate>Tue, 15 Jun 2004 20:12:01 GMT</lastBuildDate>
		<docs>http://backend.userland.com/rss</docs>
		<generator>Radio UserLand v8.0.8</generator>
		<managingEditor>mrmakent@cox.net</managingEditor>
		<webMaster>mrmakent@cox.net</webMaster>
		<category domain="http://www.weblogs.com/rssUpdates/changes.xml">rssUpdates</category> 
		<skipHours>
			<hour>23</hour>
			<hour>0</hour>
			<hour>1</hour>
			<hour>2</hour>
			<hour>3</hour>
			<hour>4</hour>
			<hour>5</hour>
			<hour>6</hour>
			</skipHours>
		<cloud domain="radio.xmlstoragesystem.com" port="80" path="/RPC2" registerProcedure="xmlStorageSystem.rssPleaseNotify" protocol="xml-rpc"/>
		<ttl>60</ttl>
		<item>
			<title>The Observer Pattern in Python</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/06/15.html#a30</link>
			<description>&lt;H3&gt;The Observer Pattern in Python&lt;/H3&gt;
&lt;P&gt;I find that the best way to get a deep understanding of a programming concept is to code it up yourself.&amp;nbsp; Here&apos;s my take on the standard Observer Pattern.&amp;nbsp; I wanted to fully understand it, because I expect to need it shortly.&amp;nbsp; This also gave me the opportunity to learn about the handy-dandy weakref module.&lt;/P&gt;
&lt;P&gt;This implementation is a bit different from the standard GoF one.&amp;nbsp; For one thing, I wanted a clean way for an Observer to be able to observe multiple Observables, while being able to easily know which Observable was sending it an update notification. That&apos;s why, when registering an Observer with an Observable, the Observer tells the Observable the name of the Observer&apos;s function to call for the update notification.&lt;/P&gt;
&lt;P&gt;Another difference has to do with the methods that the Observer base class provides.&amp;nbsp; Now, strictly speaking from a Python perspective, the Observer base class is unnecessary.&amp;nbsp; Any class that provides a method with the right signature can be used as an Observer.&amp;nbsp; But when I was talking out this design in my head, I kept using phrases like &quot;The observer tells the observable that it is interested in it.&quot;&amp;nbsp; This really seemed like an action performed by the Observer, so I wanted that behaviour in an Observer base class.&lt;/P&gt;
&lt;P&gt;The last big difference has to do with the way an Observer subscribes and unsubscribes to update notification by an Observable.&amp;nbsp; The standard GoF implementation has the Observable class having &apos;attach&apos; and &apos;detach&apos; methods.&amp;nbsp; I have an &apos;attach&apos; method (named addObserver), and a &apos;detach&apos; method (named removeObserver).&amp;nbsp; But I&apos;m also using weakref.WeakKeyDictionary as the way that an Observable keeps a list of its Observers.&amp;nbsp; This means that the fact that there&apos;s a reference to the Observer in the Observable does not keep the Observable alive if it goes out of scope or is deleted.&amp;nbsp; The weakref to the Observer will be automatically deleted from the Observable&apos;s list (I know, a dictionary is not a list.&amp;nbsp; I&apos;m using the word &apos;list&apos; here loosely.)&amp;nbsp; So, the &apos;removeObserver&apos; method is not strictly needed; just delete an Observer, and it gets removed from the list kept by any Observable it subscribed to.&lt;/P&gt;
&lt;P&gt;Here&apos;s the code.&amp;nbsp; It&amp;nbsp;should go in a file named observer.py:&lt;/P&gt;&lt;PRE&gt;import weakref&lt;BR&gt;import types&lt;/PRE&gt;&lt;PRE&gt;##&lt;BR&gt;# The Observer Pattern in Python&lt;BR&gt;#&lt;BR&gt;# Design goals:&lt;BR&gt;# 1. An Observer should be able to observe multiple Observables.&lt;BR&gt;# 2. An Observer should be able to tell an Observable what kinds of&lt;BR&gt;# events it is interested in observing.&lt;BR&gt;# 3. When an Observer is deleted, all Observables that it is observing&lt;BR&gt;# should be notified to remove that Observer.&lt;BR&gt;# 4. When an Observer is notified of an event,&lt;BR&gt;# it should be able to tell which Observable is calling it,&lt;BR&gt;# and which event occured.&lt;BR&gt;##&lt;/PRE&gt;&lt;PRE&gt;##&lt;BR&gt;# The abstract Observable Class.&lt;BR&gt;#&lt;/PRE&gt;&lt;PRE&gt;class Observable(object):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # A WeakKeyDictionary is one where, if the object used as the key&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # gets deleted, automatically removes that key from the&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # dictionary.&amp;nbsp; Thus, any Observers which get deleted will be&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # automatically removed from the observers dictionary, thus having&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # two effects:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # We won&apos;t have references to zombie objects in the dictionary, and&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # We won&apos;t have zombie objects, because the reference in this&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # dictionary won&apos;t stay around, and so won&apos;t keep the deleted object&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # alive.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._observers = weakref.WeakKeyDictionary()&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Add an observer to this Observable&apos;s notification list.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param observer The Observer to add.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param cbname The name (as a string) of the Observer&apos;s&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # method to call for an event notification, or None for the default&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # &quot;update&quot; method.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param events The events the Observer is interested in being&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # notified about.&amp;nbsp; None means all events.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def addObserver(self, observer, cbname=None, events=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if cbname is None:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cbname = &quot;update&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if events is not None and type(events) not in (types.TupleType, &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; types.ListType):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; events = (events,)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._observers[observer] = (cbname, events)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Remove an observer from this Observable&apos;s list of observers.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Note that this function is not strictly required, because when a&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # registered Observer is deleted, the weakref mechanism will cause&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # it to be removed from the notification list.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param observer the Observer to remove.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def removeObserver(self, observer):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if observer in self._observers:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; del self._observers[observer]&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Notify all currently-registered Observers.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Each observer must have an &apos;update&apos; method, which should take&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # three parameters (in addition to self): the Observable, an event,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # and a message.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # This method will be&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # called if the event is one that the Observer is interested in,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # or if event is &apos;None&apos;, or if the Observer is interested in all&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # events (it was registered with an event list of &apos;None&apos;).&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param event The event to notify the Observers about.&amp;nbsp; None&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # means no specific event.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param msg A reference to any data object that should be passed&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # to the Observers, or None.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def notifyObservers(self, event=None, msg=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for observer, data in self._observers.items():&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; #print &quot;data is&quot;, data&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cbname, events = data&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; #print &quot;cbname is&quot;, cbname&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; #print &quot;events is&quot;, events&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if events is None or event is None or event in events:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cb = getattr(observer, cbname, None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if cb is None:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise NotImplementedError, &quot;Observer has no %s method.&quot; % &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cbname&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cb(self, event, msg)&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;##&lt;BR&gt;# The abstract Observer Class&lt;BR&gt;# This class is a mix-in to add Observable registration methods to&lt;BR&gt;# a concrete Observer class.&amp;nbsp; It is not strictly required.&lt;BR&gt;#&lt;/PRE&gt;&lt;PRE&gt;class Observer(object):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param observable The Observable to observe.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param The events this Observer is interested in being&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # notified about.&amp;nbsp; This should be a tuple or list of events.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self, observable=None, cbname=None, events=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (observable is not None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.addObserver(self, cbname, events)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Inform an Observable that you would like to be notified when&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # an interesting event occurs in the Observable.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param observable The Observable this Observer would like&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # to observe.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param cbname The name (as a string) of the Observer&apos;s&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # method to call for an event notification, or None for the default&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # &quot;update&quot; method.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param events A tuple or list of events this Observer would like&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # to be notified of by the Observer, or None if it would like to&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # be notified of all events.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def subscribeToObservable(self, observable, cbname=None, events=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; assert observable is not None, &quot;Observable is None&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.addObserver(self, cbname, events)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ##&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Inform an observable that this Observer is no longer interested in it.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Note that this function is not strictly required, because when a&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # registered Observer is deleted, the weakref mechanism will cause&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # it to be removed from the Observable&apos;s notification list.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Use this function when you want to unsubscribe an Observer&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # without deleting it.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # @param observable The Observable that this Observer no longer wants&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # to observe.&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def unsubscribeToObservable(self, observable):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; assert observable is not None, &quot;Observable is None&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.removeObserver(self)&lt;/PRE&gt;
&lt;P&gt;Here&apos;s the code for the unit tests, which also serves as&amp;nbsp;usage examples.&amp;nbsp; It should go in a file named test_observer.py:&lt;/P&gt;&lt;PRE&gt;#! /usr/bin/env python&lt;/PRE&gt;&lt;PRE&gt;import unittest&lt;BR&gt;from observer import Observable, Observer&lt;/PRE&gt;&lt;PRE&gt;EVENT_FOO, EVENT_UPDATE = range(2)&lt;/PRE&gt;&lt;PRE&gt;class Stuff(Observable):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Observable.__init__(self)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = None&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setData(self, data):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = data&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.notifyObservers(None, data)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setDataWithUpdateEvent(self, data):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = data&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.notifyObservers(EVENT_UPDATE, data)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setDataWithFooEvent(self, data):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = data&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.notifyObservers(EVENT_FOO, data)&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;class StuffWatcher(Observer):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self, observable=None, cbname=None, events=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Observer.__init__(self, observable, cbname, events)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = None&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._updateData = None&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._reportData = None&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def update(self, observable, event, msg):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = msg&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._updateData = msg&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def report(self, observable, event, msg):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._data = msg&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._reportData = msg&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;class TestCase_01_Observer(unittest.TestCase):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_noMethod(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer1 = StuffWatcher(observable, &quot;foo&quot;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertRaises(NotImplementedError, observable.setData, 10)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_simple(self):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer1 = StuffWatcher(observable)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer2 = StuffWatcher(observable)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable._observers), 2)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setData(10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer1._data, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer2._data, 10)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setData(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer1._data, 20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer2._data, 20)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; del observer1&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable._observers), 1)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setData(30)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer2._data, 30)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer3 = StuffWatcher()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable._observers), 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer3.subscribeToObservable(observable)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable._observers), 2)&lt;BR&gt;&amp;nbsp;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setData(40)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer2._data, 40)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer3._data, 40)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; del observer2&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable._observers), 1)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setData(50)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer3._data, 50)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_03_specificEvents(self):&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer1 = StuffWatcher(observable, events=EVENT_UPDATE)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer2 = StuffWatcher(observable, events=EVENT_FOO)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setDataWithUpdateEvent(10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer1._data, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer2._data, None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer1 = StuffWatcher(observable, events=(EVENT_UPDATE, EVENT_FOO))&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setDataWithUpdateEvent(10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer1._data, 10)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable.setDataWithFooEvent(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer1._data, 20)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_03_multipleObservables(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable1 = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable2 = Stuff()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer = StuffWatcher()&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer.subscribeToObservable(observable1, &quot;update&quot;, EVENT_UPDATE)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observer.subscribeToObservable(observable2, &quot;report&quot;, EVENT_FOO)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable1._observers), 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(len(observable2._observers), 1)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable1.setDataWithUpdateEvent(10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._data, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._updateData, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._reportData, None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable1.setDataWithFooEvent(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._data, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._updateData, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._reportData, None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable2.setDataWithUpdateEvent(30)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._data, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._updateData, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._reportData, None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; observable2.setDataWithFooEvent(40)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._data, 40)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._updateData, 10)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(observer._reportData, 40)&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;if __name__ == &quot;__main__&quot;:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; unittest.main()&lt;BR&gt;&lt;/PRE&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/06/15.html#a30</guid>
			<pubDate>Tue, 15 Jun 2004 20:11:59 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=30&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F06%2F15.html%23a30</comments>
			</item>
		<item>
			<title>Rule for Refactoring Code</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/16.html#a29</link>
			<description>&lt;H3&gt;Rule for Refactoring Code&lt;/H3&gt;
&lt;P&gt;These are the rules I&apos;ve developed over the years that I follow when refactoring code.&amp;nbsp; They&apos;re rules in the sense that I always regret it if I fail to follow them for any reason.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Unit Testing: Don&apos;t Even Start Without It.&lt;/STRONG&gt;&amp;nbsp; You won&apos;t know your refactoring didn&apos;t break the code unless you have unit test coverage of the affected code.&amp;nbsp; If you don&apos;t have&amp;nbsp;that coverage already, add new tests.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Keep Each Refactoring Narrowly-Focused.&lt;/STRONG&gt;&amp;nbsp; Don&apos;t combine unrelated changes, refactor each one separately.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Keep Your Unit Tests Fine-Grained.&lt;/STRONG&gt;&amp;nbsp; If&amp;nbsp;a refactoring&amp;nbsp;involves multiple functions, you&apos;re best off having unit tests for each function.&amp;nbsp; If you depend on a single unit test&amp;nbsp;for a function&amp;nbsp;that then calls the refactored functions, a test failure leaves you not knowing which refactored function failed.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Use Many Small Refactorings, Even When It Seems Inefficient.&lt;/STRONG&gt;&amp;nbsp;&amp;nbsp;I prefer&amp;nbsp;making a small refactoring even when I know that a subsequent refactoring will change that same code yet again.&amp;nbsp; This is critical for&amp;nbsp;refactoring tangled, poorly-written code.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Unit Test Each Refactoring.&lt;/STRONG&gt;&amp;nbsp; Don&apos;t wait.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Refactor Separate Functionality&amp;nbsp;Into Separate Functions.&lt;/STRONG&gt;&amp;nbsp;&amp;nbsp;If I encounter code that&amp;nbsp;combines too much distinct functionality into one function, I try to break it apart into separate, individually-testable functions.&amp;nbsp; With unit tests for each new function.&lt;/LI&gt;&lt;/UL&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/16.html#a29</guid>
			<pubDate>Sun, 16 May 2004 21:47:12 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=29&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F05%2F16.html%23a29</comments>
			</item>
		<item>
			<title>Sun-Relative Time Events Using Python</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/13.html#a28</link>
			<description>&lt;H3&gt;Sun-Relative Time Events Using Python&lt;/H3&gt;
&lt;P&gt;For my current X10 home automation project, I want the ability to schedule events to occur at sun-relative times like &apos;sunrise&apos; and &apos;sunset&apos;.&amp;nbsp; Now, you can do that&amp;nbsp;by using&amp;nbsp;an X10 light sensor and watching for the &apos;on&apos; and &apos;off&apos; commands it will send.&amp;nbsp; But will the sensor be triggered by stray light, like from a passing car&apos;s headlights?&amp;nbsp; I don&apos;t know.&amp;nbsp; Perhaps with careful placement of the sensor, you could avoid accidental triggering.&amp;nbsp; But there&apos;s another way to handle the problem.&lt;/P&gt;
&lt;P&gt;Given your position on the world&amp;nbsp;by latitude and longitude and today&apos;s date, sunrise and sunset can be calculated accurately using spherical geometry.&amp;nbsp; It&apos;s what astromomers do.&amp;nbsp; You can google around for existing software and source code that does these calculations.&amp;nbsp; I wanted a Python solution, and so I settled for &lt;A href=&quot;http://kortis.to/radix/python/code/Sun.py&quot;&gt;Sun.py&lt;/A&gt; by Henrik H&amp;auml;rk&amp;ouml;nen.&amp;nbsp; This is code for a Python class that calculates sunrise and sunset, as well as three flavors of &apos;twilight&apos;.&amp;nbsp; The code is a direct translation of someone else&apos;s C code, even to the point of preserving the original C comments, but hey, it works well.&amp;nbsp; It calculates all times in UTC, so you&apos;ll need to apply the correct offset for your timezone.&lt;/P&gt;
&lt;P&gt;With this code, I can now translate an X10 event scheduled for sunrise today into the actual sunrise time for today.&amp;nbsp; That&apos;s one more item on my wish-list for X10 home automation I can check off.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/13.html#a28</guid>
			<pubDate>Thu, 13 May 2004 15:31:38 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=28&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F05%2F13.html%23a28</comments>
			</item>
		<item>
			<title>Python and X10 Home Automation, Part 1.1</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/10.html#a27</link>
			<description>&lt;H3&gt;Python and X10 Home Automation, Part 1.1&lt;/H3&gt;
&lt;P&gt;I wrote previously about Project WiSH, which gives you access to X10 computer interface controllers via device drivers.&amp;nbsp; I also wrote about uprading my home system to Mandrake 10.&amp;nbsp; Mandrake 10 has the new 2.6 kernel.&amp;nbsp; Sadly, WiSH does not (yet) work with that kernel.&amp;nbsp; So, I had to find an alternative.&lt;/P&gt;
&lt;P&gt;From googling around, and comments from readers, I found the &apos;&lt;A href=&quot;http://mlug.missouri.edu/~tymm/&quot;&gt;bottlerocket&lt;/A&gt;&apos; software, for accessing the CM-17a &apos;Firecracker&apos; X10 computer interface.&amp;nbsp; This gives you a simple command-line program for issuing X10 commands via the CM-17a.&amp;nbsp; It lacks the ability to watch for an X10 command coming in from a sensor, or querying the status of a status-capable X10 controller, &lt;S&gt;so it&apos;s not a complete replacement for WiSH&lt;/S&gt; (but that functionality is not supported by the CM-17a anyway, so you don&apos;t get it with WiSH either), but it will do me for now.&lt;/P&gt;
&lt;P&gt;When you are doing time-based automation under Linux, it makes sense to leverage Linux&apos;s built-in abilities.&amp;nbsp; The &apos;cron&apos; system lets you schedule tasks for execution based on date/time, either periodically on a period of your choosing, or at one particular data/time.&amp;nbsp; The &apos;at&apos; command lets you schedule a command for execution at&amp;nbsp;one particular time in the furture.&amp;nbsp; For example, typing the following command at the Linux command-line prompt will schedule the running of the command &apos;/usr/local/bin/br a2 on&apos; for 8:00pm today (&apos;br&apos; is the bottlerocket command):&lt;/P&gt;&lt;PRE&gt;at 8:00pm today &amp;lt;&amp;lt;EOF&lt;BR&gt;/usr/local/bin/br a2 on&lt;BR&gt;EOF&lt;/PRE&gt;
&lt;P&gt;To test the usability of &apos;at&apos; for X10 home automation, I wrote a simple shell script that issues two &apos;at&apos; commands -- one to turn on my driveway lights at dusk, and one to turn them off at 10:30pm.&amp;nbsp; I then set up a &apos;cron&apos; job to run this shell script ever day at 00:05 in the morning.&amp;nbsp; Thus, each day the two &apos;at&apos; jobs are reissued for the current day.&amp;nbsp; It&apos;s necessary to reissue the commands each day because an &apos;at&apos; job is a one-shot deal, while &apos;cron&apos; is what you use to run commands at regular intervals.&lt;/P&gt;
&lt;P&gt;This&amp;nbsp;system of &apos;at&apos;, shell scripts, and &apos;cron&apos;&amp;nbsp;works fine, and demonstrates the simplest Linux/X10 home automation setup.&amp;nbsp; For your needs, this may be all that you require.&amp;nbsp; For me, this was just baby&apos;s first steps.&lt;/P&gt;
&lt;P&gt;What I want is a more capable solution to the X10 home automation problem.&amp;nbsp; I want to be able to schedule both periodic and one-time events.&amp;nbsp; I want a system that understands how to&amp;nbsp;deal with times like&amp;nbsp;&apos;sunset&apos; and &apos;sunrise&apos;.&amp;nbsp; I want to be able to alias the X10 house/unit code that controls my driveway lights as &apos;driveway lights&apos;.&amp;nbsp; I want to be able to create macros that trigger multiple commands, so that I can execute macro &apos;wakeup&apos;, and have the commands sent to turn on the TV, the coffee maker, and the window blinds opener(insert images of George Jetson being ejected from bed like toast from a toaster).&amp;nbsp; I want to have a web interface to all of this.&amp;nbsp; And, of course, I want to do it all with Python.&lt;/P&gt;
&lt;P&gt;Sure there are several other pre-existing Linux solutions for X10 home automation.&amp;nbsp; For fun and learning, stay tuned as I put together my own solution using Python.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/10.html#a27</guid>
			<pubDate>Mon, 10 May 2004 18:03:02 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=27&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F05%2F10.html%23a27</comments>
			</item>
		<item>
			<title>Handling CSV Strings, Redux</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/05.html#a25</link>
			<description>&lt;H3&gt;Handling CSV Strings, Redux&lt;/H3&gt;
&lt;P&gt;I&apos;ve written several times about the Python csv module and its limited API.&amp;nbsp; I presented an adaptor class for use when you want to parse a CSV string, without being locked into reading sequentially from a file.&amp;nbsp; Here&apos;s another&amp;nbsp;way of doing it, using Python&apos;s standard &apos;I have a string, but I need a file-like object&apos; adapter, the StringIO module.&lt;/P&gt;&lt;PRE&gt;import StringIO&amp;nbsp;&amp;nbsp;&amp;nbsp; # Or use cStringIO, it&apos;s faster&lt;BR&gt;import csv&lt;/PRE&gt;&lt;PRE&gt;# First, create the StringIO object, then the csv reader object&lt;BR&gt;# using the StringIO object.&lt;BR&gt;sf = StringIO.StringIO()&lt;BR&gt;csvReader = csv.reader(sf)&lt;/PRE&gt;&lt;PRE&gt;# Now, write a CSV string out to the StringIO object.&lt;BR&gt;csvData = &apos;1,2,three,&quot;four,five&quot;,6&apos;&lt;BR&gt;sf.write(csvData)&lt;/PRE&gt;&lt;PRE&gt;# We have to seek the StringIO pseudo-file back to its start.&lt;BR&gt;sf.seek(0,0)&lt;/PRE&gt;&lt;PRE&gt;# Now read in the same data via the csv reader object.&lt;BR&gt;parsedData = csvReader.next()&lt;/PRE&gt;
&lt;P&gt;When we look at what &lt;FONT face=&quot;Courier, Monospace&quot;&gt;parsedData&lt;/FONT&gt; contains, we see a list like so:&lt;/P&gt;
&lt;P&gt;&lt;FONT face=&quot;Courier, Monospace&quot;&gt;[ &apos;1&apos;, &apos;2&apos;, &apos;three&apos;, &apos;four,five&apos;, &apos;6&apos; ]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Verdana,Geneva,Arial,Helvetica,Sans-Serif&gt;Using StringIO rather than the adaptor class I presented earlier is a somewhat more heavy-weight way of solving the problem, but might fit your use better.&lt;/FONT&gt;&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/05.html#a25</guid>
			<pubDate>Thu, 06 May 2004 01:07:14 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=25&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F05%2F05.html%23a25</comments>
			</item>
		<item>
			<title>Python and X10 Home Automation, Part 1</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/05.html#a24</link>
			<description>&lt;H4&gt;Python and X10 Home Automation, Part 1&lt;/H4&gt;
&lt;P&gt;I recently saw an ad from &lt;A href=&quot;http://www.x10.com&quot;&gt;x10.com&lt;/A&gt; for a free (you pay shipping) &lt;A href=&quot;http://www.x10.com/home/offer.cgi?!IFC22,../slickdealstxt.htm&quot;&gt;X10 starter kit&lt;/A&gt;, including a &apos;Firecracker&apos; computer interface.&amp;nbsp; That was a deal I couldn&apos;t pass up, so I ordered it through their web site, and 3 days later, the kit arrived.&lt;/P&gt;
&lt;P&gt;The kit consists of the CM-17a &apos;Firecracker&apos; serial computer interface, which transmits via radio, a transceiver module which receives the radio commands from the Firecracker and retransmits them via the X10 protocol over your house wiring, a lamp modules for controlling... lamps, and a PalmPilot-sized hand-held remote control that lets you manually do what the computer interface does.&amp;nbsp; Oh, and the transceiver module also double as an appliance module, allowing you to control appliances of up to 500 watts.&lt;/P&gt;
&lt;P&gt;With the hand-held controller, you can control any X10 modules you have, either the ones that come with the kit, or any add-on modules you may want to buy.&amp;nbsp; You could go wild, like many do, and completely automate your home -- lights, appliances, garage door, pool heater, ferret feeder, whatever.&lt;/P&gt;
&lt;P&gt;But with the computer interface, things get much more interesting.&amp;nbsp; You can, for example, download from x10.com a free application that duplicates the appearance and functionality of the hand-held controller on your computer screen.&amp;nbsp; Or, you can download, for $20, an application that fully utilizes your computer and the x10 interface to do full automation.&amp;nbsp; Want your hot-tub to turn on at a certain time every day?&amp;nbsp; No problem.&amp;nbsp; Want your lights to simulate an occupied house while you are on vacation?&amp;nbsp; Easy.&lt;/P&gt;
&lt;P&gt;Naturally, hand an X10 computer interface to a Python programmer, and he&apos;ll immediately start writing code for it.&amp;nbsp; Or that was my intent, anyway.&amp;nbsp; The first thing I did was google around for any existing Python projects for X10. I found two, &lt;A href=&quot;http://sourceforge.net/projects/pyxal/&quot;&gt;Pyxal&lt;/A&gt;&amp;nbsp;and &lt;A href=&quot;http://pyx10.sourceforge.net/&quot;&gt;Pyx10&lt;/A&gt;.&amp;nbsp; Both projects seem to be unmaintaned.&amp;nbsp; Pyxal is pure Python, and does not support the recent X10 controllers, like the Firecracker.&amp;nbsp; Pyx10 uses a wrapper to turn the &lt;A href=&quot;http://xal.sourceforge.net/&quot;&gt;XAL library&lt;/A&gt; into a Python extention module.&amp;nbsp; It supports recent X10 controllers, including the Firecracker.&lt;/P&gt;
&lt;P&gt;I downloaded and examined both.&amp;nbsp; Pyxal was right out, as it has no Firecracker support (why not add it yourself, you ask?&amp;nbsp; I&apos;ll get to that in a moment...).&amp;nbsp; Pyx10 and XAL looked good.&amp;nbsp; After compiling and installing XAL (a snap), I tried compiling Pyx10.&amp;nbsp; Nope.&amp;nbsp; The wrapper code for XAL would not compile.&amp;nbsp; From a quick exam, it looked like it was out-of-sync with XAL.&lt;/P&gt;
&lt;P&gt;I could have continued hacking at it to get it to work, but further googling (the trademark police are gonna get me), I found &lt;A href=&quot;http://wish.sourceforge.net&quot;&gt;Project WiSH&lt;/A&gt;, a project for turning X10 device drivers into... well, Linux device drivers.&amp;nbsp; Super!&amp;nbsp; Instead of having to do low-level device handling from my code, I can simply open a linux device driver and write commands to it, just like I was writing text to a file.&amp;nbsp; And WiSH was a snap to compile and install.&amp;nbsp; Just make sure you have your kernel source loaded on your machine.&amp;nbsp; (For the CM-17a &apos;Firecracker&apos;, be sure to download the 1.6.10 version of WiSH.&amp;nbsp; The later 2.0.1 version does not yet support it.&amp;nbsp; But both versions support the CM-11a, which is the other modern popular X10 computer interface controller.)&lt;/P&gt;
&lt;P&gt;Now, I do my work under Linux, so this is just what the code doctor ordered.&amp;nbsp; Actually, it&apos;s even better than it sounds.&amp;nbsp; You see, there&apos;s this little bit of info about that Firecracker X10 controller...&lt;/P&gt;
&lt;P&gt;If you look at one of the other X10 computer interfaces, say the CM-11a that comes with another of the home automation intro packages that x10.com sells, you will see that it is controlled via the computer in a manner rather like an external serial modem.&amp;nbsp; Connect it to your serial port, and send it strings of ASCII characters.&amp;nbsp; Not so with the CM-17a &apos;Firecracker&apos;.&amp;nbsp; This little guy is a serial pass-thru &apos;dongle&apos;, very small.&amp;nbsp; From what I can tell from my Google &lt;A href=&quot;http://mywebpages.comcast.net/ncherry/common/cm17a.html&quot;&gt;research&lt;/A&gt;, you must directly control the radio transmitter in it via bit-tiddling the RTS and DTR lines of the serial port.&amp;nbsp; You must assemble a 5-byte command via bit masking, then bit-shift it out to the CM-17a by directly controlling the states of the RTS and DTR lines, doing the timing yourself.&amp;nbsp; There are no smarts.&amp;nbsp; Ouch.&amp;nbsp; No wonder this is the bargain-basement controller.&lt;/P&gt;
&lt;P&gt;The CM-11a controller has another advantage, too.&amp;nbsp; It&apos;s smart, it&amp;nbsp;has its own processor.&amp;nbsp; So you don&apos;t even need to leave your computer on to do real-time home automation.&amp;nbsp; Use the scheduling software to send it commands, like &apos;turn on my security light at local-time dusk, and turn it off at dawn&apos;, and the CM-11a will do it, all by itself.&lt;/P&gt;
&lt;P&gt;But I don&apos;t have the CM-11a.&amp;nbsp; I have a CM-17a and a Linux box.&amp;nbsp; Add in the device drivers from Project WiSH, and &lt;STRONG&gt;from a Linux command line&lt;/STRONG&gt;, I can execute &apos;&lt;EM&gt;echo &quot;on&quot; &amp;gt;&amp;gt;/dev/x10/a1&lt;/EM&gt;&apos;, and send the &apos;on&apos; command to the X10 device at house code &apos;A&apos;, unit code &apos;1&apos;.&amp;nbsp; How cool is that?&lt;/P&gt;
&lt;P&gt;OK, how can we combine equal portions of X10, Project WiSH, Linux, Python, and fun?&amp;nbsp; (OK, fun gets a bigger portion.)&lt;/P&gt;
&lt;P&gt;Here&apos;s the deal.&amp;nbsp; I work for a major software house.&amp;nbsp; We do automated nightly compiles of our code on all of the platforms we support (Linux, various flavors of UNIX, Windoze).&amp;nbsp; The last thing you want is for some code change you made that day to &apos;break the build&apos;.&amp;nbsp; The automated process sends out email giving that night&apos;s&amp;nbsp;build status.&amp;nbsp; If you broke the build, it&apos;s supposed to be your first priority to fix it.&lt;/P&gt;
&lt;P&gt;I keep forgetting to check my email.&amp;nbsp; I have many projects, they grab my attention, and it may be hours before I check my mail.&amp;nbsp; Yes, I have a little task bar thingie that tells me if I get new mail.&amp;nbsp; I don&apos;t look at it if I&apos;m concentrating on a problem.&lt;/P&gt;
&lt;P&gt;Python and X10 to the rescue!&amp;nbsp; (This is a fun solution looking for a problem.)&amp;nbsp; I now have a Python script that is run via cron every 10 minutes.&amp;nbsp; It uses the poplib and email modules to grab and parse my email, looking for the specific patterns that a &apos;you broke the build&apos; message will contain.&amp;nbsp; If it finds such a message, it opens and writes an &apos;on&apos; command to the proper X10 device driver, which then turns on the &lt;STRONG&gt;BIG RED ROTATING LIGHT&lt;/STRONG&gt;.&amp;nbsp; I kid you not.&lt;/P&gt;
&lt;P&gt;This is so much fun!&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/05/05.html#a24</guid>
			<pubDate>Wed, 05 May 2004 18:34:14 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=24&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F05%2F05.html%23a24</comments>
			</item>
		<item>
			<title>The Python CSV Module and Legacy Data</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/04/23.html#a23</link>
			<description>&lt;H3&gt;The Python CSV Module and Legacy Data&lt;/H3&gt;
&lt;P&gt;When you work with csv files as much as I do, particularly with csv files created by legacy applications, you tend to run into the odd problems.&amp;nbsp; Consider the following legacy csv data (a real example):&lt;/P&gt;
&lt;P&gt;&quot;this is&quot;,&quot;an, example&quot;,&quot;10&quot;,&quot;of problem data&quot;,20&lt;/P&gt;
&lt;P&gt;The reader of Python&apos;s csv&amp;nbsp;module will turn this into a list like so:&lt;/P&gt;
&lt;P&gt;[ &quot;this is&quot;, &quot;an, example&quot;, &quot;10&quot;, &quot;of problem data&quot;,20 ]&lt;/P&gt;
&lt;P&gt;No problem so far.&amp;nbsp; Now, let&apos;s use the csv writer to turn this same data back into csv data again, round trip.&amp;nbsp; Without taking any special precautions, we would get:&lt;/P&gt;
&lt;P&gt;this is,&quot;an, example&quot;,10,of problem data,20&lt;/P&gt;
&lt;P&gt;What happened here?&amp;nbsp; Well, the csv writer will normally only quote data when it contains the field separator.&amp;nbsp; We can get closer to what we want (that is, recreating the original csv data) by using the QUOTE_NONNUMERIC parameter to the writer.&amp;nbsp; When we do, we get:&lt;/P&gt;
&lt;P&gt;&quot;this is&quot;,&quot;an, example&quot;,10,&quot;of problem data&quot;,20&lt;/P&gt;
&lt;P&gt;Closer, but the third field, which was quoted in the original data, is not.&amp;nbsp; We could try using the QUOTE_ALL parameter, which would give us the third field quoted, but unfortunately we&apos;d also get the fifth field quoted, which was not the way the original data had it.&lt;/P&gt;
&lt;P&gt;What I need is a way of controlling the quoting of fields on a field by field basis.&amp;nbsp; Sadly, Python&apos;s csv module doesn&apos;t give me that level of control over field quoting.&amp;nbsp; So when I have to deal with legacy csv data like that above, I&apos;m forced to bypass the csv module for writing, and roll my own.&amp;nbsp; I can still use the csv module for reading.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2004/04/23.html#a23</guid>
			<pubDate>Sat, 24 Apr 2004 03:29:11 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=23&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2004%2F04%2F23.html%23a23</comments>
			</item>
		<item>
			<title>Working With Fixed Record Length CSV Files </title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/23.html#a22</link>
			<description>&lt;H4&gt;Working With Fixed Record Length CSV Files&lt;/H4&gt;
&lt;P&gt;Yesterday I wrote about working with fixed record length files, and presented a class for making it easy.&amp;nbsp; Today I&apos;ll extend that idea to handle records containing csv data.&lt;/P&gt;
&lt;P&gt;The new &lt;EM&gt;CSVRecordFile&lt;/EM&gt; class inherits from the &lt;EM&gt;RecordFile&lt;/EM&gt; class, overriding the &lt;EM&gt;read&lt;/EM&gt; and &lt;EM&gt;write&lt;/EM&gt; methods to add csv parsing and formatting.&amp;nbsp; This allows you to read and write fixed length csv data records in random order.&amp;nbsp; The &lt;EM&gt;StringCSVAdaptor&lt;/EM&gt; class (presented earlier) is used to enable us to use Python2.3&apos;s &lt;EM&gt;csv&lt;/EM&gt; module with strings.&amp;nbsp; This is necessary since the csv modules reader and writer functions expect to work with interables, such as a file-like object or a sequence.&lt;/P&gt;
&lt;P&gt;Matt Goodall took me up on the need for the &lt;EM&gt;StringCSVAdaptor&lt;/EM&gt; class, rightly pointing out a simpler way of handling the problem (thanks, Matt!&amp;nbsp; I think you are reader #4 of this weblog, and the first person to leave me a comment!)&amp;nbsp; Sadly, Matt&apos;s suggestion does not fit with the problem domain I&apos;m using &lt;EM&gt;CSVRecordFile&lt;/EM&gt; for.&amp;nbsp; For one thing, I need both a csv reader and writer.&amp;nbsp; I only want to create these objects &lt;U&gt;once&lt;/U&gt; per &lt;EM&gt;CSVRecordFile&lt;/EM&gt; instance, and then&amp;nbsp;use them to parse/format many records in random (not sequential) order.&amp;nbsp; Matt&apos;s solution, while useful for simple one-shot csv needs, looks to me to require the creation of the reader and writer for each record that is to be parsed (because he wraps the string to be parsed in a list to make it an iterable).&amp;nbsp; You can read Matt&apos;s comments from yesterday&apos;s post.&lt;/P&gt;
&lt;P&gt;Put this code in a file called &lt;EM&gt;csvrecfile.py&lt;/EM&gt;:&lt;/P&gt;&lt;PRE&gt;&quot;&quot;&quot;This file contains the CSVRecordFile class, for working with fixed length&lt;BR&gt;record files, where the records contain csv data.&quot;&quot;&quot;&lt;/PRE&gt;&lt;PRE&gt;__author__ = &quot;Mike Kent&quot;&lt;BR&gt;__version__ = &quot;$Id$&quot;.split()[-2:][0]&lt;/PRE&gt;&lt;PRE&gt;import recfile&lt;BR&gt;import csv&lt;BR&gt;import csvadaptor&lt;/PRE&gt;&lt;PRE&gt;class CSVRecordFileFmtError(Exception): pass&lt;/PRE&gt;&lt;PRE&gt;class CSVRecordFile(recfile.RecordFile):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;This class provides a standard way to handle files which are layed out&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; as fixed-length records containing csv data, where each record is padded &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; to its proper length with a padding character, and may be optionally &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; terminated with a record terminator string.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self, filename, mode, reclen, recpad=&apos;&apos;, recterm=None,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; **csvKwParams):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recfile.RecordFile.__init__(self, filename, mode, reclen, recpad,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recterm)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.csvAdaptor = csvadaptor.StringCSVAdaptor()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.csvReader = csv.reader(self.csvAdaptor, **csvKwParams)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.csvWriter = csv.writer(self.csvAdaptor, **csvKwParams)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def read(self, recNum):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Read a record containing csv data by record number, and return a&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; list of strings.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Record numbers start a 1.&amp;nbsp; An empty list will returned on end of&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; file.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.csvAdaptor.data = recfile.RecordFile.read(self, recNum)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; try:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = self.csvReader.next()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; except csv.Error:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise CSVRecordFileFmtError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return rec&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def write(self, recNum, valueList):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Write a list of mixed-type values to a record, in csv format,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; by record number.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Record numbers start with 1.&amp;nbsp; The record will be&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; padded to the correct length using the padding character, and&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; optionally terminated by the record terminator string.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; You can seek to, and write, records beyond EOF.&amp;nbsp; However, to&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; append a new record to the current actual EOF, give a record number&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; of 0.&amp;nbsp; This function returns the actual record number written to.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; try:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.csvWriter.writerow(valueList)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; except csv.Error:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise CSVRecordFileFmtError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return recfile.RecordFile.write(self, recNum, self.csvAdaptor.data)&lt;BR&gt;&lt;/PRE&gt;
&lt;P&gt;Here are the unit tests.&amp;nbsp; Put this code in a file called &lt;EM&gt;test_csvrecfile.py&lt;/EM&gt;:&lt;/P&gt;&lt;PRE&gt;#! /usr/bin/env python&lt;/PRE&gt;&lt;PRE&gt;import sys&lt;BR&gt;import unittest&lt;BR&gt;import csv&lt;BR&gt;import csvrecfile&lt;/PRE&gt;&lt;PRE&gt;class TestCases_01_RecordFile(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_instantiate(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_02_RecordFileWriteAdd(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_writeAddOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 1, 2, &quot;three&quot; ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &apos;1,2,&quot;three&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; r\n&apos;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_writeAddSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 1, 2, &quot;Record %d&quot; % (count + 1) ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &apos;1,2,&quot;Record %d&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp; r\n&apos; % (count + 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_03_RecordFileWriteRandom(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setUp(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 1, 2, &quot;Record %d&quot; % (count + 1) ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(count + 1, rec)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.close()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_writeRandomOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recNum = 2&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 3, 4, &quot;New record %d&quot; % recNum ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(recNum, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &apos;3,4,&quot;New record %d&quot;r\n&apos; % recNum&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj.seek(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_writeRandomSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recNumList = [ 1, 5, 3, 2, 4 ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for recNum in recNumList:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 1, 2, &quot;New record %d&quot; % recNum ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(recNum, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &apos;1,2,&quot;New record %d&quot;r\n&apos; % recNumList[count]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj.seek((recNumList[count] - 1) * 20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_04_RecordFileReadRandom(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setUp(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = [ 1, 2, &quot;Record %d&quot; % (count + 1) ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(count + 1, rec)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.close()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_readRandomOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = recFileObj.read(3)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = [ &quot;1&quot;, &quot;2&quot;, &quot;Record 3&quot; ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(rec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_readRandomSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = csvrecfile.CSVRecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &apos;r\n&apos;, lineterminator=&apos;&apos;, quoting=csv.QUOTE_NONNUMERIC)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recNumList = [ 1, 5, 3, 1, 2, 2, 4 ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for recNum in recNumList:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = recFileObj.read(recNum)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = [ &quot;1&quot;, &quot;2&quot;, &quot;Record %d&quot; % recNum ]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(rec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;if __name__ == &quot;__main__&quot;:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; unittest.main()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; sys.exit(0)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&lt;/PRE&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/23.html#a22</guid>
			<pubDate>Tue, 23 Sep 2003 17:04:57 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=22&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F09%2F23.html%23a22</comments>
			</item>
		<item>
			<title>Working With Fixed Record Length Files</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/22.html#a21</link>
			<description>&lt;H4&gt;Working With Fixed Record Length Files&lt;/H4&gt;
&lt;P&gt;I&apos;m often called upon to work with data files that contain records composed of CSV data, where all of the records in the file have one&amp;nbsp;fixed record length.&amp;nbsp; Here&apos;s a small class I wrote to make the handling of fixed record length files easy.&amp;nbsp; It does not handle the parsing of CSV data itself;&amp;nbsp; for that, use my CSV Adaptor from my previous article.&lt;/P&gt;
&lt;P&gt;Put this code in a file named &lt;EM&gt;recfile.py&lt;/EM&gt;:&lt;/P&gt;&lt;PRE&gt;&quot;&quot;&quot;This file contains the RecordFile class, for working with fixed length&lt;BR&gt;record files.&quot;&quot;&quot;&lt;/PRE&gt;&lt;PRE&gt;__author__ = &quot;Mike Kent&quot;&lt;BR&gt;__version__ = &quot;$Id$&quot;.split()[-2:][0]&lt;/PRE&gt;&lt;PRE&gt;class RecordFileOpenError(Exception): pass&lt;BR&gt;class RecordFileReadError(Exception): pass&lt;BR&gt;class RecordFileTruncError(Exception): pass&lt;BR&gt;class RecordFileWriteError(Exception): pass&lt;/PRE&gt;&lt;PRE&gt;class RecordFile:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;This class provides a standard way to handle files which are layed out&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; as fixed-length records, where each record is padded to its proper length&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; with a padding character, and may be optionally terminated with a record&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; terminator string.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self, filename, mode, reclen, recpad=&apos; &apos;, recterm=None):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;The default record padding string is a single space.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; The record terminator defaults to None.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; try:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file = file(filename, mode)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; except IOError:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileOpenError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.recLen = reclen&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.recPad = recpad&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.recTerm = recterm&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.lenRecTerm = recterm and len(recterm) or 0&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def close(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if hasattr(self, &quot;_file&quot;):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file.close()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; __del__ = close&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def flush(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def read(self, recNum):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Read a record by number, and return a string.&amp;nbsp; Record numbers&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; start a 1.&amp;nbsp; The resulting string will have any record terminator or&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; padding specified on class initialization stripped.&amp;nbsp; An empty string&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; will be returned on EOF.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if recNum &amp;lt; 1:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileReadError&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; try:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file.seek((recNum - 1) * self.recLen)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = self._file.read(self.recLen)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; except IOError:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileReadError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lenRec = len(rec)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If we got a record...&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if lenRec &amp;gt; 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If what we read was too short, or it&apos;s supposed to have a record&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # terminator, but it&apos;s not there...&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(lenRec &amp;lt; self.recLen or &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (self.lenRecTerm and not rec.endswith(self.recTerm))):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileReadError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If it is supposed to have a record terminator, and it does,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # strip it.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if self.lenRecTerm and rec.endswith(self.recTerm):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = rec[:-self.lenRecTerm]&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If there is padding present, strip it.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if len(self.recPad):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = rec.rstrip(self.recPad)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return rec&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def write(self, recNum, data):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Write a string to a record by record number.&amp;nbsp; Record&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; numbers start with 1.&amp;nbsp; The record will be&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; padded to the correct length using the padding character, and&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; optionally terminated by the record terminator string.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; You can seek to, and write, records beyond EOF.&amp;nbsp; However, to&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; append a new record to the current actual EOF, give a record number of 0.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; This function returns the actual record number written to.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRecNum = recNum&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lenData = len(data)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Calculate the amount of padding needed.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; paddingNeeded = self.recLen - (lenData + self.lenRecTerm)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If that amount is negative, the record data is too long to fit.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if paddingNeeded &amp;lt; 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileTruncError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If padding is needed, append it to the record data.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if paddingNeeded &amp;gt; 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; data += self.recPad * paddingNeeded&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If a record terminator is wanted, append it to the record data.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if self.lenRecTerm:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; data += self.recTerm&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If the record number is zero, we want to seek to the current&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # end of file...&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if recNum == 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; offset = 0&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; whence = 2&amp;nbsp; # Seek relative to the end&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Else we want to seek to the beginning of the specified record.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; offset = (recNum - 1) * self.recLen&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; whence = 0&amp;nbsp; # Seek relative to the beginning&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; try:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file.seek(offset, whence)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # If we are writing to the current end of file,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # calculate what that record number is.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if recNum == 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRecNum = (self._file.tell() / self.recLen) + 1&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self._file.write(data)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; except IOError:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise RecordFileWriteError&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Return the actual record number written to.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return newRecNum&lt;/PRE&gt;
&lt;P&gt;Although you might not think so from my previous posts to this weblog, I&apos;m a firm believer in unit testing, so here are the tests for the above code.&lt;/P&gt;
&lt;P&gt;Put this code in a file named &lt;EM&gt;test_recfile.py&lt;/EM&gt;:&lt;/P&gt;&lt;PRE&gt;#! /usr/bin/env python&lt;/PRE&gt;&lt;PRE&gt;import sys&lt;BR&gt;import unittest&lt;BR&gt;import recfile&lt;/PRE&gt;&lt;PRE&gt;class TestCases_01_RecordFile(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_instantiate(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_02_RecordFileWriteAdd(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_writeAddOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;this is a test&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;this is a test&amp;nbsp;&amp;nbsp;&amp;nbsp; \r\n&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_writeAddSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;Record %d&quot; % (count + 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;Record %d&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; \r\n&quot; % (count + 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_03_RecordFileWriteRandom(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setUp(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;Record %d&quot; % (count + 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.close()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_writeRandomOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;this is a test&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(2, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;this is a test&amp;nbsp;&amp;nbsp;&amp;nbsp; \r\n&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj.seek(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_writeRandomSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recNumList = [ 1, 5, 3, 2, 4 ]&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for recNum in recNumList:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;New record %d&quot; % recNum&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(recNum, rec)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj = file(&quot;test.txt&quot;, &quot;rb&quot;)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;New record %d&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; \r\n&quot; % recNumList[count]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; testFileObj.seek((recNumList[count] - 1) * 20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; newRec = testFileObj.read(20)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(newRec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;class TestCases_04_RecordFileReadRandom(unittest.TestCase):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def setUp(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;w+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assert_(recFileObj is not None)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for count in range(5):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = &quot;Record %d&quot; % (count + 1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.write(0, rec)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj.close()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_01_readRandomOne(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = recFileObj.read(3)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;Record 3&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(rec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def test_02_readRandomSeveral(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recFileObj = recfile.RecordFile(&quot;test.txt&quot;, &quot;r+b&quot;, 20, &apos; &apos;, &apos;r\n&apos;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recNumList = [ 1, 5, 3, 1, 2, 2, 4 ]&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; for recNum in recNumList:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rec = recFileObj.read(recNum)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; expected = &quot;Record %d&quot; % recNum&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.assertEqual(rec, expected)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&lt;BR&gt;if __name__ == &quot;__main__&quot;:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; unittest.main()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; sys.exit(0)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&lt;/PRE&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/22.html#a21</guid>
			<pubDate>Mon, 22 Sep 2003 20:47:19 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=21&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F09%2F22.html%23a21</comments>
			</item>
		<item>
			<title>Python 2.3 and the csv module</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/12.html#a20</link>
			<description>&lt;H4&gt;Python 2.3 and the &lt;EM&gt;csv&lt;/EM&gt; module&lt;/H4&gt;
&lt;P&gt;&lt;FONT size=1&gt;[Edited on 05/06/04 to fix the class name, and add the &lt;EM&gt;write&lt;/EM&gt; method]&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Another new module we get with Python 2.3 that I&apos;ve been itching to get my hands on is the &lt;EM&gt;csv&lt;/EM&gt; module, which gives us a general-purpose way of parsing &apos;comma separated values&apos; data created by a wide range of applications.&amp;nbsp; While I don&apos;t have a need for that kind of capability in the current project being developed here, I do need it for other projects I work on.&lt;/P&gt;
&lt;P&gt;Once I actually started working with the csv module, I quickly discovered to my surprise just how limited its API is.&amp;nbsp; Basically, if what you want it to iterate over lines in a file from start to end, parsing out and returning csv data, it&apos;s easy.&amp;nbsp; But if, like me, you need to randomly seek around in a fixed-record-length file of csv data, reading in a record and then parsing that record string...&lt;/P&gt;
&lt;P&gt;The csv readers provided by the module take a file-like object that follows the iterator protocol and supplies a &lt;EM&gt;next&lt;/EM&gt; method.&amp;nbsp; Period.&amp;nbsp; There is no provision for parsing a string.&amp;nbsp; To me, parsing a string containing csv data is such a common use case for a csv parser that I&apos;m astounded it&apos;s not provided.&lt;/P&gt;
&lt;P&gt;That doesn&apos;t mean you &lt;EM&gt;can&apos;t&lt;/EM&gt; parse a csv string.&amp;nbsp; You just have to write an adaptor, heehee.&amp;nbsp; Like so:&lt;/P&gt;&lt;PRE&gt;import sys&lt;BR&gt;import csv&lt;/PRE&gt;&lt;PRE&gt;class StringCSVAdaptor:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;This adaptor let&apos;s us use a string where the csv parser wants&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; a file-like iterable object.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.data = &quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __iter__(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return self&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def next(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return self.data&lt;/PRE&gt;&lt;PRE&gt;    def write(self, data):&lt;/PRE&gt;&lt;PRE&gt;        self.data = data&lt;/PRE&gt;&lt;PRE&gt;def main():&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Create our adaptor object.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; adaptor = StringCSVAdaptor()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Create our csv parser object.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; csvReader = csv.reader(adaptor)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Set the adaptor&apos;s data to the string we want to parse.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; adaptor.data = &apos;now,is,&quot;the, time&quot;,,for&apos;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Parse our string of csv data.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; parsedData = csvReader.next()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; print &quot;data is&quot;, parsedData&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return 0&lt;/PRE&gt;&lt;PRE&gt;if __name__ == &quot;__main__&quot;:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; sys.exit(main())&lt;/PRE&gt;
&lt;P&gt;So, we can get where we want.&amp;nbsp; I just wish we didn&apos;t have to jump over the barrel to get there.&lt;/P&gt;
&lt;P&gt;BTW, as I was writing this up, I discovered that Hans Nowak over at &lt;A href=&quot;http://zephyrfalcon.org/weblog/arch_d7_2003_09_06.html#e335&quot;&gt;Tao of the Machine&lt;/A&gt; has just posted an article, where he details his problems with the limited API of the csv module.&amp;nbsp; So I&apos;m not the only one...&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/12.html#a20</guid>
			<pubDate>Fri, 12 Sep 2003 15:24:26 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=20&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F09%2F12.html%23a20</comments>
			</item>
		<item>
			<title>Python 2.3 and the datetime module</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/11.html#a19</link>
			<description>&lt;H4&gt;Python 2.3 and the &lt;EM&gt;datetime&lt;/EM&gt; module&lt;/H4&gt;
&lt;P&gt;One of the new modules that Python 2.3 gives us is &lt;EM&gt;datetime&lt;/EM&gt;, which I&apos;ve been itching to get my hands on.&amp;nbsp; Now, M.-A. Lemburg&apos;s mxDateTime module has given us Pythonistas date and time handling for some time, but it&apos;s always struck me as an 800-lb gorilla.&amp;nbsp; So I was happy to have the trimmed-down alternative.&lt;/P&gt;
&lt;P&gt;In addition to all the standard date handling stuff, one capability of mxDateTime I definitely need is the ability to parse a date/time string and create a date/time object from it.&amp;nbsp; For my current project, I need to read in existing schedule information so that I can detect appointment conflicts.&amp;nbsp; So I was concerned that the new datetime module has no string parser.&lt;/P&gt;
&lt;P&gt;Upon digging further, though, I found that the module provides a factory function that takes the number of seconds since the epoch (like what is returned by time.time), and returns a datetime object.&amp;nbsp; This didn&apos;t give me what I wanted, but it was interesting.&lt;/P&gt;
&lt;P&gt;Another interesting addition in Python 2.3 is the new time.strptime function, which parses a date/time string and returns a special sequence called a &lt;STRONG&gt;struct_time&lt;/STRONG&gt;.&amp;nbsp; So here we have the string parsing I want, just not the value I need.&amp;nbsp; This is where an adapter would come in handy, and what do you know, there&apos;s time.mktime, which takes a struct_time, and returns the corresponding time in seconds since the epoch.&lt;/P&gt;
&lt;P&gt;Putting this all together, we have:&lt;/P&gt;&lt;PRE&gt;import time&lt;BR&gt;import datetime&lt;/PRE&gt;&lt;PRE&gt;aDateString = &quot;9/11/2003 12:30&quot;&lt;BR&gt;dateParseFormat = &quot;%m/%d/%Y %H:%M&quot;&lt;/PRE&gt;&lt;PRE&gt;aDateObj = datetime.datetime.fromtimestamp(time.mktime( &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; time.strptime(aDateString, dateParseFormat)))&lt;BR&gt;&lt;/PRE&gt;
&lt;P&gt;Now &lt;STRONG&gt;this&lt;/STRONG&gt; gives me what I want.&amp;nbsp; I&apos;d rather not have had to&amp;nbsp;do this much work&amp;nbsp;to get it, but hey.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/09/11.html#a19</guid>
			<pubDate>Fri, 12 Sep 2003 00:04:07 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=19&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F09%2F11.html%23a19</comments>
			</item>
		<item>
			<title>&quot;I&apos;m not dead yet!&quot;</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/08/21.html#a18</link>
			<description>&lt;H2&gt;&quot;I&apos;m not dead yet!&quot;&lt;/H2&gt;
&lt;P&gt;My appologies for the &quot;near death&quot; appearance of my weblog.&amp;nbsp; I&apos;ve been kinda busy.&lt;/P&gt;
&lt;P&gt;I&apos;ve switched over to development using Python 2.3.&amp;nbsp; That required me to wait for the effbot to provide a Win32 installer for his ElementTidy extension to ElementTree.&amp;nbsp; That&apos;s now done.&lt;/P&gt;
&lt;P&gt;I decided to try using Eclipse and the TruStudio plugin for Python.&amp;nbsp; Looks good so far, but it&apos;s still early and I don&apos;t know enough about Eclipse yet to be truely productive.&lt;/P&gt;
&lt;P&gt;Maybe I need to make fewer simultaneous changes to my development process?&amp;nbsp; :-)&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/08/21.html#a18</guid>
			<pubDate>Thu, 21 Aug 2003 19:56:43 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=18&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F08%2F21.html%23a18</comments>
			</item>
		<item>
			<title>mxDateTime Update, and Stuff</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/21.html#a17</link>
			<description>&lt;H4&gt;mxDateTime Update, and Stuff&lt;/H4&gt;
&lt;P&gt;M.-A. Lemburg&amp;nbsp;responded to my email, in which I reported&amp;nbsp;my problem with the mxDateTime string parser.&amp;nbsp; He informed me that this problem was already fixed in the 2.0 beta 5 version.&amp;nbsp; Unfortunately, I can&apos;t find a link to it on his &lt;A href=&quot;http://www.egenix.com/&quot;&gt;web site&lt;/A&gt;...&lt;/P&gt;
&lt;P&gt;I see that Python 2.3 has reached the release candidate stage.&amp;nbsp; Goodie...&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/21.html#a17</guid>
			<pubDate>Mon, 21 Jul 2003 14:47:45 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=17&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F07%2F21.html%23a17</comments>
			</item>
		<item>
			<title>Quest for Massage, Part 8</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/17.html#a16</link>
			<description>&lt;H4&gt;Quest for Massage, Part 8&lt;/H4&gt;
&lt;P&gt;The &lt;A href=&quot;http://radio.weblogs.com/0124960/stories/2003/07/17/iwannamassagepyV05.html&quot;&gt;latest version&lt;/A&gt; of the project shows improvements in two areas.&amp;nbsp; First, I&apos;ve added in a (rather primative) method for getting my list of appoinment time spans to exclude.&amp;nbsp; Appointment time spans to be excluded are now listed in a file named &quot;conflicts.txt.&amp;nbsp; Each line in the file has the form &apos;date&amp;lt;comma&amp;gt;start time&amp;lt;comma&amp;gt;end time&apos;.&amp;nbsp; For example:&lt;/P&gt;&lt;PRE&gt;07/15/2003,12:30,13:30&lt;/PRE&gt;
&lt;P&gt;Times are given in 24-hour military-style time.&amp;nbsp; There can be any number of such lines in the file, which is automatically read upon the robot&apos;s startup.&lt;/P&gt;
&lt;P&gt;This method of defining appointment exclusion times is a stand-in for what I&apos;ll eventually shoot for, which is a way of pulling the scheduling conflicts out of a PIM.&lt;/P&gt;
&lt;P&gt;The second area of improvement involves the use of mxDateTime.&amp;nbsp; The &lt;FONT face=&quot;Courier, Monospace&quot;&gt;slotToDateTime&lt;/FONT&gt; method of the &lt;FONT face=&quot;Courier, Monospace&quot;&gt;ApptSched&lt;/FONT&gt; class did a lot of work to convert the dates and times given in the web page&apos;s appointment schedule table into a form that could be used to instantiate a DateTime object:&lt;/P&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def slotToDateTime(self, timeIndex, dateIndex):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Given the indexes into a two-dimension array that identifies a&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; slot, this method converts the date and time for that slot into a form&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; that can be used to instantiate an mx.DateTime object, and returns&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; that object.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; timeRaw = self.times[timeIndex]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; dateRaw = self.dates[dateIndex]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; dateSplit = dateRaw.split()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # The appointment schedule dates have no year info. For now, assume&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 2003.&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; year = 2003&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; month = self.monthNameToNumMap[dateSplit[1][:3].lower()]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; day = int(dateSplit[2])&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; timeSplit1 = timeRaw.split()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; timeSplit2 = timeSplit1[0].split(&quot;:&quot;)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; hour = int(timeSplit2[0])&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if timeSplit1[1].lower() == &quot;pm&quot;:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; hour += 12&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; minute = int(timeSplit2[1])&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return DateTime.DateTime(year, month, day, hour, minute)&lt;BR&gt;&lt;/PRE&gt;
&lt;P&gt;Now, the mxDateTime package comes with a parser which is supposed to be able to take&amp;nbsp;a date and time in the form of a string and return a DateTime object. However, I wasn&apos;t able to get it to work properly, and so had to resort to the above extra work.&amp;nbsp; Then I added the new scheduling conflict code, that needs to read in dates and times from a file and create DateTime object.&amp;nbsp; I ran into the same problems with the mxDateTime string parser, and had to investigate it all again.&amp;nbsp; What I found was interesting.&amp;nbsp; It seems that the mxDateTime parser function &lt;FONT face=&quot;Courier, Monospace&quot;&gt;DateTime.Parser.DateTimeFromString()&lt;/FONT&gt;&lt;FONT face=&quot;Times New Roman,Times,Serif&quot;&gt;&amp;nbsp;&lt;/FONT&gt;generates a &apos;time out of range&apos; error when parsing the time string &quot;12:15 PM&quot;.&amp;nbsp; Once I dug into it, I discovered that upon seeing the &apos;PM&apos;, the code adds 12 to the hour to convert into 24-hour format time.&amp;nbsp; This results in an hour of 24, when the legal range is 0-23.&amp;nbsp; Obviously, you should only add 12 hours for hour values of 1 PM and greater.&amp;nbsp; Interestingly, the above code I wrote has the same problem.&amp;nbsp; I was able to fix the mxDateTime code by hacking in a test that enforced that rule.&amp;nbsp; I&apos;ll have to follow up on that with M.A. Lemburg.&lt;/P&gt;
&lt;P&gt;Once I put in the mxDateTime parser fix, I was able to greatly simplify &lt;FONT face=&quot;Courier, Monospace&quot;&gt;slotToDateTime() &lt;/FONT&gt;by using the parser rather than all of the code I&apos;d had to write to do the parsing myself.&amp;nbsp; This reduced the method down to:&lt;/P&gt;&lt;PRE&gt; def slotToDateTime(self, timeIndex, dateIndex):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;Given the indexes into a two-dimension array that identifies a&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; slot, this method converts the date and time for that slot into a form&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; that can be used to instantiate an mx.DateTime object, and returns&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; that object.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; timeRaw = self.times[timeIndex]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; dateRaw = self.dates[dateIndex]&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; fullDateTimeString = &quot; &quot;.join([dateRaw.split(&quot;,&quot;)[1].strip(), currYear,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; timeRaw])&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; dateTimeObj = DateTime.Parser.DateTimeFromString(fullDateTimeString)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return dateTimeObj&lt;BR&gt;&lt;/PRE&gt;
&lt;P&gt;A third area I&apos;d hope to make substantial improvement in has to do with the verision of HTML Tidy I&apos;m using.&amp;nbsp; The effbot &lt;A href=&quot;http://online.effbot.org/2003_07_01_archive.htm#elementtidy-10a1&quot;&gt;announced&lt;/A&gt; that an HTML parser for ElementTree using the library-ized version of HTML Tidy was now available.&amp;nbsp; Of course, I downloaded it immediately.&amp;nbsp; Unfortunately, there seems to be a problem with the Windows installer for the package, so I&apos;m unable to try it out as of yet.&amp;nbsp; I&apos;ll have to follow up with the effbot on that.&amp;nbsp; Stay tuned...&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/17.html#a16</guid>
			<pubDate>Fri, 18 Jul 2003 02:43:40 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=16&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F07%2F17.html%23a16</comments>
			</item>
		<item>
			<title>Quest for Massage, Project Status Update</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/10.html#a15</link>
			<description>&lt;H4&gt;Quest for Massage, Project Status Update&lt;/H4&gt;
&lt;P&gt;Now that Trivial Thoughts is listed on the &lt;A href=&quot;http://mechanicalcat.net/pyblagg.html&quot;&gt;Python Programmers Weblogs&lt;/A&gt; list, I thought I&apos;d give a give a quick summary for new readers (if any!)&amp;nbsp; Trival Thoughts is a weblog mostly dedicated to thoughts and discussion on programming projects using the Python language.&amp;nbsp; The current project, half done, involves designing and writing a specialized robot that accesses an appointment schedule posted on a web page, finds the first available slot not in conflict with my other appointments, and posts and appointment reservation.&amp;nbsp; In the process, much is learned about grabbing web pages, processing HTML and XML, doing date comparisons, and more.&amp;nbsp; The full code for each evolution of the project is given, with discussion of many interesting code snippets.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/10.html#a15</guid>
			<pubDate>Thu, 10 Jul 2003 19:18:12 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=15&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F07%2F10.html%23a15</comments>
			</item>
		<item>
			<title>ElementTree and Tidy</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/07.html#a14</link>
			<description>&lt;H4&gt;ElementTree and Tidy&lt;/H4&gt;
&lt;P&gt;Way back in Part 4, I discussed the fact that I was not able to get ElementTree&apos;s build-in Tidy functionality to work for me.&amp;nbsp; It depended on having an executable Tidy command, that it would run via os.system().&amp;nbsp; Therefore, I switched to M.-A. Lemburg&apos;s mxTidy, which used a library-ized version of Tidy.&amp;nbsp; However, using mxTidy lead me down the whole path of needing to create the FileAdapter class so that I could use mxTidy as part of a chain of file-like objects...&lt;/P&gt;
&lt;P&gt;Now comes an &lt;A href=&quot;http://effbot.org/zone/element-tidylib.htm&quot;&gt;announcement&lt;/A&gt; from Mr. ElementTree himself (the effbot), that the new 1.2 (final) version of ElementTree will use a (different) library-ized version of Tidy, and that the new TidyHTMLTreeBuilder parser will use this version of Tidy to directly build an element tree from HTML.&amp;nbsp; Woot!&amp;nbsp; This will allow me to greatly simplify my appointment-making robot.&amp;nbsp; I will eagerly await the release of ElementTree 1.2 (final).&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;(Upon further investigation, I discovered that both ElementTree and mxTidy are in fact using the same library-ized version of Dave Raggett&apos;s &lt;/EM&gt;&lt;A href=&quot;http://tidy.sourceforge.net/&quot;&gt;&lt;EM&gt;HTML Tidy&lt;/EM&gt;&lt;/A&gt;&lt;EM&gt; program.)&lt;/EM&gt;&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/07.html#a14</guid>
			<pubDate>Mon, 07 Jul 2003 19:46:21 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=14&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F07%2F07.html%23a14</comments>
			</item>
		<item>
			<title>Quest for Massage, Part 7</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/01.html#a13</link>
			<description>&lt;H4&gt;Quest for Massage, Part 7&lt;/H4&gt;
&lt;P&gt;In Part 6, I developed the project to the point where the program could successfully find the first available appointment slot in the website&apos;s appointment schedule.&amp;nbsp; But I needed the ability to &lt;EM&gt;exclude&lt;/EM&gt; available slots during time periods when I&apos;m not available.&amp;nbsp; This means being able to do date and time comparisons.&amp;nbsp; Python 2.3 will have date and time classes as part of its &apos;batteries included&apos;, but I&apos;m still using Python 2.2.&amp;nbsp; So, M.-A. Lemburg&apos;s &lt;A href=&quot;http://www.egenix.com/files/python/mxDateTime.html&quot;&gt;mxDateTime&lt;/A&gt; to the rescue.&lt;/P&gt;
&lt;P&gt;The &lt;A href=&quot;http://radio.weblogs.com/0124960/stories/2003/07/01/iwannamassagepyV04.html&quot;&gt;new version&lt;/A&gt;&amp;nbsp;of the program changes the &lt;FONT face=&quot;Courier, Monospace&quot;&gt;ApptSched.getNextAvail&lt;/FONT&gt;&lt;FONT face=&quot;Times New Roman,Times,Serif&quot;&gt; &lt;/FONT&gt;generator method so that, instead of returning the date and time of the next available appointment slot as a string, it now returns a tuple of indexes.&amp;nbsp; The first index is an index into the list of dates shown on the appointment schedule, while the second is an index into the list of times.&lt;/P&gt;
&lt;P&gt;We pass this tuple of indexes to a new method, &lt;FONT face=&quot;Courier, Monospace&quot;&gt;slotToDateTime&lt;/FONT&gt;.&amp;nbsp; This method uses the indexes to get the date and time strings, then parses out the date and time portions and uses this information to instantiate a DateTime object, which the method returns.&lt;/P&gt;
&lt;P&gt;The &lt;FONT face=&quot;Courier, Monospace&quot;&gt;MassageAppt&lt;/FONT&gt; class has been reorganized.&amp;nbsp; It now has an &lt;FONT face=&quot;Courier, Monospace&quot;&gt;addExcludeRange&lt;/FONT&gt; method, which takes a tuple composed of a pair of DateTime objects.&amp;nbsp; These two objects specify the start and end of the date/time range to be excluded from desired appointment slots.&amp;nbsp; The tuple is added to a list of exclude ranges.&amp;nbsp;&amp;nbsp;The &lt;FONT face=&quot;Courier, Monospace&quot;&gt;findSlot&lt;/FONT&gt; method repeatedly calls &lt;FONT face=&quot;Courier, Monospace&quot;&gt;ApptSched.getNextSlot&lt;/FONT&gt;&lt;FONT face=&quot;Times New Roman,Times,Serif&quot;&gt;, &lt;/FONT&gt;testing to see if the slot found falls within any of the date/time ranges to be excluded, until either a non-excluded slot is found, or no more slots are available.&lt;/P&gt;
&lt;P&gt;There&apos;s another important area of reorganization in the &lt;FONT face=&quot;Courier, Monospace&quot;&gt;ApptSched&lt;/FONT&gt; class.&amp;nbsp; &lt;A href=&quot;http://effbot.org/zone/element-xpath.htm&quot;&gt;ElementTree 1.2a&lt;/A&gt; adds limited support for &lt;A href=&quot;http://www.w3.org/TR/xpath.html&quot;&gt;XPath&lt;/A&gt;, which is a method for specifying a particular part of an XML document via a URL-like address.&amp;nbsp; In the previous version of the project, finding the ElementTree element that contained the appointment schedule table required this (more verbose than necessary) code:&lt;/P&gt;&lt;PRE&gt;treeRoot = treeObj.getroot()&lt;BR&gt;bodyElem = treeRoot[1]&lt;BR&gt;outerTableElem = bodyElem[0]&lt;BR&gt;outerTBodyElem = outerTableElem[0]&lt;BR&gt;secondTrElem = outerTBodyElem[1]&lt;BR&gt;firstTdElem = secondTrElem[0]&lt;BR&gt;secondInnerTableElem = firstTdElem[3]&lt;BR&gt;secondTBodyElem = secondInnerTableElem[0]&lt;/PRE&gt;
&lt;P&gt;Using ElementTree&apos;s XPath, that code is reduced to:&lt;/P&gt;&lt;PRE&gt;elem = treeObj.findall(&quot;.//tbody&quot;)[-1]&lt;/PRE&gt;
&lt;P&gt;The string &lt;FONT face=&quot;Courier, Monospace&quot;&gt;&quot;.//tbody&quot;&lt;/FONT&gt; is an XPath address, which the &lt;FONT face=&quot;Courier, Monospace&quot;&gt;findall&lt;/FONT&gt; method now understands.&amp;nbsp; It tells &lt;FONT face=&quot;Courier, Monospace&quot;&gt;findall&lt;/FONT&gt; to search for all elements with the tag &apos;tbody&apos; throughout the tree of XML elements, and return them as a list.&amp;nbsp; Our problem-domain-specific knowledge of our web page tells us that we want the very last &apos;tbody&apos; element in the list.&amp;nbsp; XPath is good!&lt;/P&gt;
&lt;P&gt;Our project has now reached the point where it can find the first available appointment slot that is not in conflict with our own schedule.&amp;nbsp; But we need a better way for the program to know what that schedule is.&amp;nbsp; Stay tuned for Part 8, where we explore how to synchronize our massage appointment scheduler with our personal schedule.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/07/01.html#a13</guid>
			<pubDate>Tue, 01 Jul 2003 19:22:59 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=13&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F07%2F01.html%23a13</comments>
			</item>
		<item>
			<title>XMLSPY</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/26.html#a12</link>
			<description>&lt;H4&gt;XMLSPY&lt;/H4&gt;
&lt;P&gt;I&apos;ve been using XML quite a bit lately, both for work-related stuff and my personal projects.&amp;nbsp; While not ideal for everything, there are many areas where&amp;nbsp;XML is well-suited.&amp;nbsp; Up to now, I&apos;ve just used VIM for editing XML files.&amp;nbsp; No more.&amp;nbsp; Now I use &lt;A href=&quot;http://www.xmlspy.com/products_ide.html&quot;&gt;XMLSPY&lt;/A&gt;, a fantasitic... I was going to say &apos;XML editor&apos;, but in reality, it&apos;s an entire XML development environment.&amp;nbsp; It can create a DTD automatically from an XML file, it validates, it slices, dices, and purees.&amp;nbsp; Highly recommended.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/26.html#a12</guid>
			<pubDate>Thu, 26 Jun 2003 15:14:16 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=12&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F06%2F26.html%23a12</comments>
			</item>
		<item>
			<title>Crunch Time</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/25.html#a11</link>
			<description>&lt;H4&gt;Crunch Time&lt;/H4&gt;
&lt;P&gt;Well, I&apos;m back from vacation.&amp;nbsp; Sadly, it&apos;s crunch time here at work, so I&apos;ll still not be able to put as much time into the on-going project as I&apos;d like to.&amp;nbsp; Stay tuned...&lt;/P&gt;
&lt;P&gt;I did come across something interesting recently, though.&amp;nbsp; In the current project, I found it necessary to create a adapter to turn a file-like object into a true file object.&amp;nbsp; &lt;A href=&quot;http://peak.telecommunity.com/PyProtocols.html&quot;&gt;PyProtocols&lt;/A&gt; is an entire system for writing adapters, and I&apos;m certainly going to check it out more thoroughly.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/25.html#a11</guid>
			<pubDate>Wed, 25 Jun 2003 20:51:31 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=11&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F06%2F25.html%23a11</comments>
			</item>
		<item>
			<title>Quest for Massage, Part 6</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/18.html#a10</link>
			<description>&lt;H4&gt;Quest for Massage, Part 6&lt;/H4&gt;
&lt;P&gt;My little robot is starting to grow up.&amp;nbsp; &lt;A href=&quot;http://radio.weblogs.com/0124960/stories/2003/06/18/iwannamassagepyV03.html&quot;&gt;Here&lt;/A&gt; is the latest version, which now knows how to extract the appointment schedule from the web page, and find the first available non-taken appointment slot.&amp;nbsp; I&apos;ve also done some refactoring, turning the main code into its own class, which is the way I prefer to write code.&lt;/P&gt;
&lt;P&gt;I like Python&apos;s list comprehensions.&amp;nbsp; It&apos;s still true that I often have to write the code more verbosely, then turn it into a comprehension, but I&apos;m getting better at it.&amp;nbsp; This piece of code, from the ApptSched class,&amp;nbsp;is the key to extracting the appointment schedule data from the ElementTree tree of element objects:&lt;/P&gt;&lt;PRE&gt;rows = secondTBodyElem.getchildren()&lt;/PRE&gt;&lt;PRE&gt;skipCols = (0, 4, 8)&lt;BR&gt;self.dates = [ dateElem[0].text for index, dateElem in enumerate(rows[0]) &lt;BR&gt; if index not in skipCols ]&lt;/PRE&gt;&lt;PRE&gt;self.times = [ row[0].text for row in rows[1:] ]&lt;/PRE&gt;&lt;PRE&gt;self.slots = [ [ col.get(&quot;class&quot;) for colIndex, col in enumerate(row.getchildren()) &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if colIndex not in skipCols ] for row in rows[1:] ]&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/PRE&gt;
&lt;P&gt;&lt;FONT face=&quot;Courier, Monospace&quot;&gt;self.slots&lt;/FONT&gt; is bound to a two-dimensional array (a list of lists), containing the data from each appointment slot.&amp;nbsp; If the data for a slot is &quot;avail&quot;, then it&apos;s an available slot.&amp;nbsp; The two-dimensional array is generated by using a list comprehension inside another list comprehension.&amp;nbsp; Some of the columns in the table that the data is extracted from are labels for the rows, so we use &lt;FONT face=&quot;Courier, Monospace&quot;&gt;skipCols&lt;/FONT&gt;&lt;FONT face=&quot;Times New Roman,Times,Serif&quot;&gt; &lt;/FONT&gt;to skip over them.&amp;nbsp; The &lt;FONT face=&quot;Courier, Monospace&quot;&gt;enumerate&lt;/FONT&gt; function (not shown in the code) is my implementation of Python 2.3&apos;s new function for iteration over a sequence while at the same time numbering (enumerating) each element of the sequence.&amp;nbsp; It&apos;s easy to write your own function using a generator, for use with versions of Python prior to 2.3.&lt;/P&gt;
&lt;P&gt;So far, I&apos;ve successfully parsed out the appointment schedule info from the web page, and I can find the first available slot.&amp;nbsp; However, this won&apos;t be sufficient, because I need to be able to restrict my slot search to those slots where &lt;EM&gt;I&apos;m &lt;/EM&gt;available.&amp;nbsp; That is, when searching for an appointment slot, I need to be able to exclude certain ranges of slots (like when I&apos;m in a meeting, or at lunch).&amp;nbsp; This requires being able to do date comparisons.&amp;nbsp; For that, if I were using Python2.3, I&apos;d use its new &apos;batteries included&apos; datetime class.&amp;nbsp; However, since I&apos;m using 2.2 now, it&apos;s time to investigate mxDateTime.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/18.html#a10</guid>
			<pubDate>Wed, 18 Jun 2003 17:17:51 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=10&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F06%2F18.html%23a10</comments>
			</item>
		<item>
			<title>Quest for Massage, Part 5</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/15.html#a9</link>
			<description>&lt;H4&gt;Quest for Massage, Part 5&lt;/H4&gt;
&lt;P&gt;&lt;EM&gt;(I&apos;m on vacation, so updates will be infrequent for a bit...)&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;Now that I understand how to write a filter that works like a file-like object, that can be linked into a chain of other file-like objects, &lt;A href=&quot;http://radio.weblogs.com/0124960/stories/2003/06/15/iwannamassagepyV02.html&quot;&gt;here&lt;/A&gt;&amp;nbsp;is the new version of the project that wraps up mx.tidy.Tidy into a file-like filter object.&amp;nbsp; &lt;EM&gt;(Oops... problems with that... fixed it.)&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;This might be overkill for this little project at this point, but it sure makes the main code cleaner, and I think it will scale well.&amp;nbsp; I like it a lot.&lt;/P&gt;
&lt;P&gt;Next, I need to look at pulling out the appointment schedule table from the parsed XML.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/15.html#a9</guid>
			<pubDate>Mon, 16 Jun 2003 00:11:37 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=9&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F06%2F15.html%23a9</comments>
			</item>
		<item>
			<title>More Thoughts About Filters</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/02.html#a8</link>
			<description>&lt;H4&gt;More Thoughts About Filters&lt;/H4&gt;
&lt;P&gt;(For some reason, I can&apos;t seem to get this article to show up on my home page -- it shows up on the Python Projects page fine... working on it...)&lt;/P&gt;
&lt;P&gt;I have a confession to make, right up front.&amp;nbsp; For some reason, I had a lot of trouble getting my mind around the filter concept.&amp;nbsp; I think it speaks of my procedural-language background.&amp;nbsp; But I finally made the conceptual breakthrough, and understood what others were trying to tell me.&lt;/P&gt;
&lt;P&gt;My problem was partly with what &lt;EM&gt;wasn&apos;t&lt;/EM&gt; being said.&amp;nbsp; So, to cement my own understanding, and ensure others do not have the same problem, let me spew forth what I now believe I understand about writing filters for file-like input in Python.&amp;nbsp; Please, if I get something wrong, poke me.&lt;/P&gt;
&lt;P&gt;I&apos;ll start with an analogy.&amp;nbsp; My appologies in advance.&lt;/P&gt;
&lt;P&gt;There are three workers in a quarry, each operating a simple machine.&amp;nbsp; Larry&apos;s machine takes in big rocks, and turns them into gravel.&amp;nbsp; Next, Curly&apos;s machine takes in gravel, and turns it into pea-gravel.&amp;nbsp; Lastly, Moe&apos;s machine takes in pea-gravel, and turns it into sand.&amp;nbsp; Larry&apos;s machine is fed from a big hopper of rocks.&amp;nbsp; Moe&apos;s machine outputs its sand to a big sand pit.&lt;/P&gt;
&lt;P&gt;Moe&apos;s boss, we&apos;ll call him Shemp, says to Moe &quot;The sand pit&apos;s getting low!&amp;nbsp; We need more sand!&quot;&amp;nbsp; Moe says to Curly &quot;Hey, moron, I need more pea-gravel!&quot;&amp;nbsp; Curly says to Larry &quot;Hey Larry!&amp;nbsp; Send me some of that gravel!&quot;&amp;nbsp; Larry pulls the handle on the hopper, dumping big rocks into his machine (one lands on his foot, he starts hopping).&amp;nbsp; Larry&apos;s machine grinds for a while, and dumps out gravel.&amp;nbsp; Larry says to Curly, &quot;Ouch!&amp;nbsp; Here!&amp;nbsp; Ouch!&quot;&amp;nbsp; Curly shovels the gravel into his machine (poking Moe in the gut with the handle), and pushes the button.&amp;nbsp; It grinds for a while, and dumps out pea-gravel.&amp;nbsp; Curly says, &quot;Hey, Moe!&amp;nbsp; Lookie what I made!&quot;&amp;nbsp; Moe whacks Curly on the head with his shovel, then loads his machine with the pea-gravel and pushes the button.&amp;nbsp; It grinds for a while, and dumps sand into the pit.&amp;nbsp; Moe says to Shemp, &quot;There you go, boss!&quot;&amp;nbsp; The lunch whistle blows.&amp;nbsp; Larry, still hopping, backs over the edge of the sand pit and falls into the soft sand...&lt;/P&gt;
&lt;P&gt;What&apos;s gone on here (other than classic slapstick)?&amp;nbsp; We have a chain consisting of a source, three filters, and a sink.&amp;nbsp; The initial command that began the process came from the end of the chain.&amp;nbsp; This is called &apos;pull filtering&apos;.&amp;nbsp; Each stage in the chain pulls data from the stage before it.&amp;nbsp; When you have a &quot;pull&quot; chain of filters working on file-like objects, each filter needs only a publicly-accessible file-like read interface.&amp;nbsp; At a minimum, a &apos;read&apos; method.&amp;nbsp; Each filter object gets initialized with the filter object that preceeds it.&amp;nbsp; You call the &apos;read&apos; method of the last filter in the chain.&amp;nbsp; It needs data, so it calls the &apos;read&apos; method of the next filter up the chain.&amp;nbsp; And so on up the chain until the &apos;read&apos; method of an actual data source is called.&amp;nbsp; This gives the first filter in the chain something to work on. It then passes the filtered data on via the return value of its &apos;read&apos; method.&amp;nbsp; And so on down the chain.&amp;nbsp; Notice, no &apos;write&apos; method was needed at all.&lt;/P&gt;
&lt;P&gt;It was the lack of a &apos;write&apos; method that was blowing my mind.&amp;nbsp; Then this rock fell on my foot, and I got it...&lt;/P&gt;
&lt;P&gt;Now, there&apos;s another kind of filtering that does use a &apos;write&apos; method, and it&apos;s called &apos;push filtering&apos;.&amp;nbsp; In this case, the initial command that starts the process comes from the source, rather than the sink, by writing the data down the chain to the next stage.&amp;nbsp; For my purposes, I&apos;m going to be concentrating on &apos;pull&apos; filtering.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/06/02.html#a8</guid>
			<pubDate>Mon, 02 Jun 2003 19:31:41 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=8&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F06%2F02.html%23a8</comments>
			</item>
		<item>
			<title>Thinking About Filters</title>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/28.html#a7</link>
			<description>&lt;H4&gt;Thinking About Filters&lt;/H4&gt;
&lt;P&gt;As I mentioned before, the Tidy function from the mx extensions acts as a filter, taking in possibly-malformed HTML, and outputting well-formed XHTML.&amp;nbsp; What I&apos;d really like to do is use the filter metaphor more concisely in my code.&amp;nbsp; Never one to re-invent the wheel, I googled for &apos;file-like filter pattern&apos; and behold, found &lt;A href=&quot;http://groups.google.com/groups?hl=en&amp;amp;lr=&amp;amp;ie=UTF-8&amp;amp;threadm=3E0050D3.43D5622A%40alcyone.com&amp;amp;rnum=2&amp;amp;prev=/groups%3Fq%3Dfile-like%2Bgroup:comp.lang.python.*%2Bgroup:comp.lang.python.*%2Bgroup:comp.lang.python.*%26hl%3Den%26lr%3D%26ie%3DUTF-8%26group%3Dcomp.lang.python.*%26selm%3D3E0050D3.43D5622A%2540alcyone.com%26rnum%3D2&quot;&gt;this interesting thread&lt;/A&gt; on the &lt;A href=&quot;http://groups.google.com/groups?hl=en&amp;amp;lr=&amp;amp;ie=UTF-8&amp;amp;group=comp.lang.python&quot;&gt;comp.lang.python&lt;/A&gt; newsgroup.&lt;/P&gt;
&lt;P&gt;This is spot-on for what I want to do.&amp;nbsp; I&apos;m going to take some time and study this concept.&amp;nbsp; It may actually be overkill for my simple use, but hey, this is a learning experience.&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/28.html#a7</guid>
			<pubDate>Thu, 29 May 2003 01:59:45 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=7&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F05%2F28.html%23a7</comments>
			</item>
		<item>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/25.html#a5</link>
			<description>&lt;H4&gt;Quest for Massage, Part 4.1&lt;/H4&gt;
&lt;P&gt;As I noted, the &apos;tidy&apos; function in the mx extentions requires a true file object as its input. What I want to give it is the results of urlopen.&amp;nbsp; My initial solution was to use a temporary file.&amp;nbsp; It worked, but I didn&apos;t like the explicit steps I had to go through.&lt;/P&gt;
&lt;P&gt;When you have an X, and you need a Y, you are looking at a use for the &lt;A href=&quot;http://c2.com/cgi/wiki?AdapterPattern&quot;&gt;Adapter Pattern&lt;/A&gt;.&amp;nbsp; So I started working on a adaptor that could take a file-like object, and give me a true file object.&amp;nbsp; Here&apos;s my initial look at it:&lt;/P&gt;&lt;PRE&gt;import types&lt;BR&gt;import os&lt;/PRE&gt;&lt;PRE&gt;class FileAdapter:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;&quot;&quot;A FileAdapter instance takes a &apos;file-like&apos; object having at least a &apos;read&apos; method&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; and, via the file method, returns a true file object.&quot;&quot;&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def __init__(self, fileObj):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.fileObj = fileObj&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.chunksize = 1024 * 10&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if type(self.fileObj) != types.FileType:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if not hasattr(fileObj, &quot;read&quot;):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise ValueError, &quot;not a file-like object&quot;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.tmpFileObj = os.tmpfile()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; while True:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; data = fileObj.read(self.chunksize)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if len(data) == 0:&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.tmpFileObj.write(data)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; del data&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.tmpFileObj.flush()&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.tmpFileObj.seek(0, 0)&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.fileObj = self.tmpFileObj&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return&lt;/PRE&gt;&lt;PRE&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def file(self):&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return self.fileObj&lt;/PRE&gt;
&lt;P&gt;&lt;BR&gt;Note that I&apos;m still using a temporary file, but now my interface to it is much cleaner.&amp;nbsp; I can use it like this:&lt;/P&gt;&lt;PRE&gt;realFileObj = FileAdapter(urlopen(&quot;&lt;A href=&apos;http://www.cnn.com&quot;)).file&apos;&gt;&lt;a href=&quot;http://www.cnn.com&quot;&gt;http://www.cnn.com&lt;/a&gt;&quot;)).file&lt;/A&gt;()&lt;/PRE&gt;
&lt;P&gt;This takes the file-like object returned by urlopen, reads in all of its data to a temporary file, and returns that file as the real file object.&lt;/P&gt;
&lt;P&gt;This gives me what I need to make a url file-like object play nice with tidy.&amp;nbsp; But you know, maybe I can do better.&amp;nbsp; It&apos;s pretty obvious that tidy works like a filter.&amp;nbsp; I&apos;d like to treat it like one, from within my program.&amp;nbsp; That bears thought.&lt;BR&gt;&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/25.html#a5</guid>
			<pubDate>Mon, 26 May 2003 01:06:50 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=5&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F05%2F25.html%23a5</comments>
			</item>
		<item>
			<link>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/25.html#a4</link>
			<description>&lt;H4&gt;Quest for Massage, Part 4&lt;/H4&gt;
&lt;P&gt;I left off talking about how ElementTree has provisions for running messy HTML through the standard &apos;Tidy&apos; command to get well-formed HTML.&amp;nbsp; After some investigation, I still haven&apos;t gotten it to work.&amp;nbsp; So...&lt;/P&gt;
&lt;P&gt;M.-A. Lemburg&apos;s &lt;A href=&quot;http://www.egenix.com/files/python/eGenix-mx-Extensions.html&quot;&gt;mx extensions&lt;/A&gt; for Python includes a whole bunch of handy stuff - like a version of the Tidy program turned into a Python extention module.&amp;nbsp; To get it, download the &lt;A href=&quot;http://www.egenix.com/files/python/eGenix-mx-Extensions.html#mxEXPERIMENTAL&quot;&gt;&apos;experimental&apos;&lt;/A&gt; package.&lt;/P&gt;
&lt;P&gt;Here&apos;s how I&apos;m using it:&lt;/P&gt;&lt;PRE&gt;from mx.Tidy import Tidy&lt;BR&gt;nerrors, nwarnings, outputdata, errordata = &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Tidy.tidy(input, output=None, errors=None, output_xhtml=1)&lt;/PRE&gt;
&lt;P&gt;Tidy takes a bunch of keyword options (see the docs).&amp;nbsp; Here, I&apos;m telling it to output XHTML. &apos;input&apos; can be either an open file object, or a string.&amp;nbsp; If output is specified, it must be an open file object, which the output will be written to.&amp;nbsp; If not specified, output will be written as a string to the return value tuple element &apos;outputdata&apos;.&amp;nbsp; The same kind of thing happens for error output - if the &apos;errors&apos; parameter is set to an open file object, error output is written to it.&amp;nbsp; Otherwise, error output is written as a string to the return value tuple element &apos;errordata&apos;.&lt;/P&gt;
&lt;P&gt;Sadly, experimenting with it had proven that Tidy will only work with actual file objects, not &apos;file-like&apos; objects.&amp;nbsp; This means, for example, that I can&apos;t give tidy an object returned by urlopen.&amp;nbsp; I must read in the web page, write it out to a temporary file, pass this temporary file to tidy and collect its output in a temporary file, then pass this temporary file to ElementTree.&lt;/P&gt;
&lt;P&gt;Putting all of this together, I get an actual, working HTML-to-ElementTree parser.&amp;nbsp; Here&apos;s the working code:&lt;/P&gt;
&lt;P&gt;&lt;A href=&quot;http://radio.weblogs.com/0124960/stories/2003/05/23/iwannamassagepyV01.html&quot;&gt;&lt;a href=&quot;http://radio.weblogs.com/0124960/stories/2003/05/23/iwannamassagepyV01.html&quot;&gt;http://radio.weblogs.com/0124960/stories/2003/05/23/iwannamassagepyV01.html&lt;/a&gt;&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;Kinda crude, and I know I can improve on this.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
			<guid>http://radio.weblogs.com/0124960/categories/pythonProjects/2003/05/25.html#a4</guid>
			<pubDate>Sun, 25 May 2003 16:59:26 GMT</pubDate>
			<comments>http://radiocomments2.userland.com/comments?u=124960&amp;amp;p=4&amp;amp;link=http%3A%2F%2Fradio.weblogs.com%2F0124960%2F2003%2F05%2F25.html%23a4</comments>
			</item>
		</channel>
	</rss>
