text: """Base reader class -- read from a URL in the background."""
  
  import sys
  import string
  from tkinter import *
  import urlparse
  import grailutil
  
  
  # Default tuning parameters
  # BUFSIZE = 8*1024                      # Buffer size for api.getdata()
  BUFSIZE = 512                           # Smaller size for better response
  SLEEPTIME = 100                         # Milliseconds between regular checks
  
  class BaseReader:
  
      """Base reader class -- read from a URL in the background.
  
      Given an API object, poll it until it completes or an
      unrecoverable error occurs.
  
      Derived classes are supposed to override the handle_*() methods to
      do something meaningful.
  
      The sequence of calls made to the stop and handle_* functions can
      be expressed by a regular expression:
  
      (meta data* stop (eof | error) | stop handle_error)
  
      """
  
      # Tuning parameters
      sleeptime = SLEEPTIME
  
      def __init__(self, context, api):
          self.context = context
          self.api = api
          self.callback = self.checkmeta
          self.poller = self.api.pollmeta
          self.bufsize = BUFSIZE
          
          # Stuff for status reporting
          self.nbytes = 0
          self.maxbytes = 0
          self.shorturl = ""
          self.message = "waiting for socket"
  
          self.context.addreader(self)
  
          self.fno = None   # will be assigned by start
          self.killed = None
  
          # Only http_access has delayed startup property.
          # Second argument would allow implementation of persistent
          # connections.
          try:
              self.api.register_reader(self.start, self.checkapi)
          except AttributeError:
              # if the protocol doesn't do that
              ok = 0
              try:
                  self.start()
                  ok = 1
              finally:
                  if not ok:
                      if self.context:
                          self.context.rmreader(self)
  
      def start(self):
          # when the protocol API is ready to go, it tells the reader
          # to get busy
          self.message = "awaiting server response"
          if self.killed:
              print "start() called after a kill"
              return
          self.fno = self.api.fileno()
          if TkVersion == 4.0 and sys.platform == 'irix5':
              if self.fno >= 20: self.fno = -1 # XXX for SGI Tk OPEN_MAX bug
  
          if self.fno >= 0:
              tkinter.createfilehandler(
                  self.fno, tkinter.READABLE, self.checkapi)
          else:
              # No fileno() -- check every 100 ms
              self.checkapi_regularly()
  
          # Delete pervious context local protocol handlers
          # We've gotten far enough into the next page without errors
          if self.context:
              self.context.remove_local_api_handlers()
  
      def __str__(self):
          if self.maxbytes:
              percent = self.nbytes*100/self.maxbytes
              status = "%d%% of %s read" % (percent,
                                            grailutil.nicebytes(self.maxbytes))
          elif not self.nbytes:
              status = self.message
          else:
              status = "%s read" % grailutil.nicebytes(self.nbytes)
          if self.api and self.api.iscached():
              status = status + " (cached)"
          if self.api and not self.shorturl:
              tuple = urlparse.urlparse(self.api._url_)
              path = tuple[2]
              i = string.rfind(path[:-1], '/')
              if i >= 0:
                  path = path[i+1:]
              self.shorturl = path or self.api._url_
          return "%s: %s" % (self.shorturl, status)
  
      def __repr__(self):
          return "%s(...%s)" % (self.__class__.__name__, self.api)
  
      def update_status(self):
          self.context.new_reader_status() # Will call our __str__() method
  
      def update_maxbytes(self, headers):
          self.maxbytes = 0
          if headers.has_key('content-length'):
              try:
                  self.maxbytes = string.atoi(headers['content-length'])
              except string.atoi_error:
                  pass
          self.update_status()
  
      def update_nbytes(self, data):
          self.nbytes = self.nbytes + len(data)
          self.update_status()
  
      def kill(self):
          self.killed = 1
          self.stop()
          self.handle_error(-1, "Killed", {})
  
      def stop(self):
          if self.fno >= 0:
              fno = self.fno
              self.fno = -1
              tkinter.deletefilehandler(fno)
  
          self.callback = None
          self.poller = None
  
          if self.api:
              self.api.close()
              self.api = None
  
          if self.context:
              self.context.rmreader(self)
              self.context = None
  
      def checkapi_regularly(self):
          if not self.callback:
  ##          print "*** checkapi_regularly -- too late ***"
              return
          self.callback()
          if self.callback:
              sleeptime = self.sleeptime
              if self.poller and self.poller()[1]: sleeptime = 0
              self.context.root.after(sleeptime, self.checkapi_regularly)
  
      def checkapi(self, *args):
          if not self.callback:
              print "*** checkapi -- too late ***"
              if self.fno >= 0:
                  fno = self.fno
                  self.fno = -1
                  tkinter.deletefilehandler(fno)
              return
          try:
              self.callback()                     # Call via function pointer
          except:
              if self.context and self.context.app:
                  app = self.context.app
              else:
                  app = grailutil.get_grailapp()
              app.exception_dialog("in BaseReader")
              self.kill()
  
      def checkmeta(self):
          self.message, ready = self.api.pollmeta()
          if ready:
              self.getapimeta()
  
      def checkdata(self):
          self.message, ready = self.api.polldata()
          if ready:
              self.getapidata()
  
      def getapimeta(self):
          errcode, errmsg, headers = self.api.getmeta()
          self.callback = self.checkdata
          self.poller = self.api.polldata
          if headers.has_key('content-type'):
              content_type = headers['content-type']
          else:
              content_type = None
          if headers.has_key('content-encoding'):
              content_encoding = headers['content-encoding']
          else:
              content_encoding = None
          self.content_type = content_type
          self.content_encoding = content_encoding
          self.update_maxbytes(headers)
          self.handle_meta(errcode, errmsg, headers)
          if self.callback:
              self.callback()             # XXX Handle httpAPI readahead
  
      def getapidata(self):
          data = self.api.getdata(self.bufsize)
          if not data:
              self.handle_eof()
              self.stop()
              return
          self.update_nbytes(data)
          self.handle_data(data)
  
      def geteverything(self):
          if self.api:
              if self.callback == self.checkmeta:
                  self.getapimeta()
              while self.api:
                  self.getapidata()
  
      # Derived classes are expected to override the following methods
  
      def handle_meta(self, errcode, errmsg, headers):
          # May call self.stop()
          self.update_maxbytes(headers)
          if errcode != 200:
              self.stop()
              self.handle_error(errcode, errmsg, headers)
  
      def handle_data(self, data):
          # May call self.stop()
          pass
  
      def handle_error(self, errcode, errmsg, headers):
          # Called after self.stop() has been called
          pass
  
      def handle_eof(self):
          # Called after self.stop() has been called
          pass
  