Commits

Jannis Leidel committed 021e2d3

Initial checkin with setup.py

Comments (0)

Files changed (41)

+recursive-include docs *
+include apikey.txt
+include test_akismet.py
+# Version 0.1.5
+# 2006/02/05
+
+# Copyright Michael Foord 2005 - 2007
+# akismet.py
+# Python interface to the akismet API
+
+# http://www.voidspace.org.uk/python/modules.shtml
+# http://akismet.com
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# E-mail fuzzyman@voidspace.org.uk
+
+"""
+A python interface to the `Akismet <http://akismet.com>`_ 
+{acro;API;Application Programmers Interface}. This is a web service for
+blocking SPAM comments to blogs - or other online services.
+
+You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
+
+You should pass in the keyword argument 'agent' to the name of your program,
+when you create an Akismet instance. This sets the ``user-agent`` to a useful
+value.
+
+The default is : ::
+
+    Python Interface by Fuzzyman | akismet.py/0.1.3
+
+Whatever you pass in, will replace the *Python Interface by Fuzzyman* part.
+**0.1.2** will change with the version of this interface.
+
+"""
+import os, sys
+import urllib2
+from urllib import urlencode
+
+import socket
+if hasattr(socket, 'setdefaulttimeout'):
+    # Set the default timeout on sockets to 5 seconds
+    socket.setdefaulttimeout(5)
+
+isfile = os.path.isfile
+
+__version__ = '0.1.5'
+
+__all__ = (
+    '__version__',
+    'Akismet',
+    'AkismetError',
+    'APIKeyError',
+    )
+
+__author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
+
+__docformat__ = "restructuredtext en"
+
+user_agent = "%s | akismet.py/%s"
+DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
+
+class AkismetError(Exception):
+    """Base class for all akismet exceptions."""
+
+class APIKeyError(AkismetError):
+    """Invalid API key."""
+
+class Akismet(object):
+    """A class for working with the akismet API"""
+
+    baseurl = 'rest.akismet.com/1.1/'
+
+    def __init__(self, key=None, blog_url=None, agent=None):
+        """Automatically calls ``setAPIKey``."""
+        if agent is None:
+            agent = DEFAULTAGENT % __version__
+        self.user_agent = user_agent % (agent, __version__)
+        self.setAPIKey(key, blog_url)
+
+
+    def _getURL(self):
+        """
+        Fetch the url to make requests to.
+        
+        This comprises of api key plus the baseurl.
+        """
+        return 'http://%s.%s' % (self.key, self.baseurl)
+    
+    
+    def _safeRequest(self, url, data, headers):
+        try:
+            req = urllib2.Request(url, data, headers)
+            h = urllib2.urlopen(req)
+            resp = h.read()
+        except (urllib2.HTTPError, urllib2.URLError, IOError), e:
+            raise AkismetError(str(e))       
+        return resp
+
+
+    def setAPIKey(self, key=None, blog_url=None):
+        """
+        Set the wordpress API key for all transactions.
+        
+        If you don't specify an explicit API ``key`` and ``blog_url`` it will
+        attempt to load them from a file called ``apikey.txt`` in the current
+        directory.
+        
+        This method is *usually* called automatically when you create a new
+        ``Akismet`` instance.
+        """
+        if key is None and isfile('apikey.txt'):
+            the_file = [l.strip() for l in open('apikey.txt').readlines()
+                if l.strip() and not l.strip().startswith('#')]
+            try:
+                self.key = the_file[0]
+                self.blog_url = the_file[1]
+            except IndexError:
+                raise APIKeyError("Your 'apikey.txt' is invalid.")
+        else:
+            self.key = key
+            self.blog_url = blog_url
+
+
+    def verify_key(self):
+        """
+        This equates to the ``verify-key`` call against the akismet API.
+        
+        It returns ``True`` if the key is valid.
+        
+        The docs state that you *ought* to call this at the start of the
+        transaction.
+        
+        It raises ``APIKeyError`` if you have not yet set an API key.
+        
+        If the connection to akismet fails, it allows the normal ``HTTPError``
+        or ``URLError`` to be raised.
+        (*akismet.py* uses `urllib2 <http://docs.python.org/lib/module-urllib2.html>`_)
+        """
+        if self.key is None:
+            raise APIKeyError("Your have not set an API key.")
+        data = { 'key': self.key, 'blog': self.blog_url }
+        # this function *doesn't* use the key as part of the URL
+        url = 'http://%sverify-key' % self.baseurl
+        # we *don't* trap the error here
+        # so if akismet is down it will raise an HTTPError or URLError
+        headers = {'User-Agent' : self.user_agent}
+        resp = self._safeRequest(url, urlencode(data), headers)
+        if resp.lower() == 'valid':
+            return True
+        else:
+            return False
+
+    def _build_data(self, comment, data):
+        """
+        This function builds the data structure required by ``comment_check``,
+        ``submit_spam``, and ``submit_ham``.
+        
+        It modifies the ``data`` dictionary you give it in place. (and so
+        doesn't return anything)
+        
+        It raises an ``AkismetError`` if the user IP or user-agent can't be
+        worked out.
+        """
+        data['comment_content'] = comment
+        if not 'user_ip' in data:
+            try:
+                val = os.environ['REMOTE_ADDR']
+            except KeyError:
+                raise AkismetError("No 'user_ip' supplied")
+            data['user_ip'] = val
+        if not 'user_agent' in data:
+            try:
+                val = os.environ['HTTP_USER_AGENT']
+            except KeyError:
+                raise AkismetError("No 'user_agent' supplied")
+            data['user_agent'] = val
+        #
+        data.setdefault('referrer', os.environ.get('HTTP_REFERER', 'unknown'))
+        data.setdefault('permalink', '')
+        data.setdefault('comment_type', 'comment')
+        data.setdefault('comment_author', '')
+        data.setdefault('comment_author_email', '')
+        data.setdefault('comment_author_url', '')
+        data.setdefault('SERVER_ADDR', os.environ.get('SERVER_ADDR', ''))
+        data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', ''))
+        data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', ''))
+        data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', ''))
+        data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE',
+            ''))
+        data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
+            ''))
+        data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
+        data.setdefault('blog', self.blog_url)
+
+
+    def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
+        """
+        This is the function that checks comments.
+        
+        It returns ``True`` for spam and ``False`` for ham.
+        
+        If you set ``DEBUG=True`` then it will return the text of the response,
+        instead of the ``True`` or ``False`` object.
+        
+        It raises ``APIKeyError`` if you have not yet set an API key.
+        
+        If the connection to Akismet fails then the ``HTTPError`` or
+        ``URLError`` will be propogated.
+        
+        As a minimum it requires the body of the comment. This is the
+        ``comment`` argument.
+        
+        Akismet requires some other arguments, and allows some optional ones.
+        The more information you give it, the more likely it is to be able to
+        make an accurate diagnosise.
+        
+        You supply these values using a mapping object (dictionary) as the
+        ``data`` argument.
+        
+        If ``build_data`` is ``True`` (the default), then *akismet.py* will
+        attempt to fill in as much information as possible, using default
+        values where necessary. This is particularly useful for programs
+        running in a {acro;CGI} environment. A lot of useful information
+        can be supplied from evironment variables (``os.environ``). See below.
+        
+        You *only* need supply values for which you don't want defaults filled
+        in for. All values must be strings.
+        
+        There are a few required values. If they are not supplied, and
+        defaults can't be worked out, then an ``AkismetError`` is raised.
+        
+        If you set ``build_data=False`` and a required value is missing an
+        ``AkismetError`` will also be raised.
+        
+        The normal values (and defaults) are as follows : ::
+        
+            'user_ip':          os.environ['REMOTE_ADDR']       (*)
+            'user_agent':       os.environ['HTTP_USER_AGENT']   (*)
+            'referrer':         os.environ.get('HTTP_REFERER', 'unknown') [#]_
+            'permalink':        ''
+            'comment_type':     'comment' [#]_
+            'comment_author':   ''
+            'comment_author_email': ''
+            'comment_author_url': ''
+            'SERVER_ADDR':      os.environ.get('SERVER_ADDR', '')
+            'SERVER_ADMIN':     os.environ.get('SERVER_ADMIN', '')
+            'SERVER_NAME':      os.environ.get('SERVER_NAME', '')
+            'SERVER_PORT':      os.environ.get('SERVER_PORT', '')
+            'SERVER_SIGNATURE': os.environ.get('SERVER_SIGNATURE', '')
+            'SERVER_SOFTWARE':  os.environ.get('SERVER_SOFTWARE', '')
+            'HTTP_ACCEPT':      os.environ.get('HTTP_ACCEPT', '')
+        
+        (*) Required values
+        
+        You may supply as many additional 'HTTP_*' type values as you wish.
+        These should correspond to the http headers sent with the request.
+        
+        .. [#] Note the spelling "referrer". This is a required value by the
+            akismet api - however, referrer information is not always
+            supplied by the browser or server. In fact the HTTP protocol
+            forbids relying on referrer information for functionality in 
+            programs.
+        .. [#] The `API docs <http://akismet.com/development/api/>`_ state that this value
+            can be " *blank, comment, trackback, pingback, or a made up value*
+            *like 'registration'* ".
+        """
+        if self.key is None:
+            raise APIKeyError("Your have not set an API key.")
+        if data is None:
+            data = {}
+        if build_data:
+            self._build_data(comment, data)
+        url = '%scomment-check' % self._getURL()
+        # we *don't* trap the error here
+        # so if akismet is down it will raise an HTTPError or URLError
+        headers = {'User-Agent' : self.user_agent}
+        resp = self._safeRequest(url, urlencode(data), headers)
+        if DEBUG:
+            return resp
+        resp = resp.lower()
+        if resp == 'true':
+            return True
+        elif resp == 'false':
+            return False
+        else:
+            # NOTE: Happens when you get a 'howdy wilbur' response !
+            raise AkismetError('missing required argument.')
+
+
+    def submit_spam(self, comment, data=None, build_data=True):
+        """
+        This function is used to tell akismet that a comment it marked as ham,
+        is really spam.
+        
+        It takes all the same arguments as ``comment_check``, except for
+        *DEBUG*.
+        """
+        if self.key is None:
+            raise APIKeyError("Your have not set an API key.")
+        if data is None:
+            data = {}
+        if build_data:
+            self._build_data(comment, data)
+        url = '%ssubmit-spam' % self._getURL()
+        # we *don't* trap the error here
+        # so if akismet is down it will raise an HTTPError or URLError
+        headers = {'User-Agent' : self.user_agent}
+        self._safeRequest(url, urlencode(data), headers)
+
+
+    def submit_ham(self, comment, data=None, build_data=True):
+        """
+        This function is used to tell akismet that a comment it marked as spam,
+        is really ham.
+        
+        It takes all the same arguments as ``comment_check``, except for
+        *DEBUG*.
+        """
+        if self.key is None:
+            raise APIKeyError("Your have not set an API key.")
+        if data is None:
+            data = {}
+        if build_data:
+            self._build_data(comment, data)
+        url = '%ssubmit-ham' % self._getURL()
+        # we *don't* trap the error here
+        # so if akismet is down it will raise an HTTPError or URLError
+        headers = {'User-Agent' : self.user_agent}
+        self._safeRequest(url, urlencode(data), headers)
+
+"""
+
+USAGE
+=====
+
+.. raw:: html
+
+    {+coloring}
+    
+    api = Akismet(agent='Test Script')
+    # if apikey.txt is in place,
+    # the key will automatically be set
+    # or you can call ``api.setAPIKey()``
+    #
+    if api.key is None:
+        print "No 'apikey.txt' file."
+    elif not api.verify_key():
+        print "The API key is invalid."
+    else:
+        # data should be a dictionary of values
+        # They can all be filled in with defaults
+        # from a CGI environment
+        if api.comment_check(comment, data):
+            print 'This comment is spam.'
+        else:
+            print 'This comment is ham.'
+    
+    {-coloring}
+
+TODO
+====
+
+Make the timeout adjustable ?
+
+Should we fill in a default value for permalink ?
+
+What about automatically filling in the 'HTTP_*' values from os.environ ?
+
+"""
+# Lines starting with '#' are comments
+# The first non-blank, non-comment, line should be your api key
+# The second your blog URL
+#
+# You can get a wordpress API key from http://wordpress.com/
+SOME KEY
+SOME URL
+# Version 0.3.5
+# 2005/11/26
+
+# Copyright Michael Foord 2004 & 2005
+# cgiutils.py
+# Functions and constants useful for working with CGIs
+
+# http://www.voidspace.org.uk/python/modules.shtml
+
+# Released subject to the BSD License
+# Please see http://www.voidspace.org.uk/python/license.shtml
+
+# For information about bugfixes, updates and support, please join the Pythonutils mailing list.
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
+# E-mail fuzzyman@voidspace.org.uk
+
+import os
+import sys
+
+__version__ = '0.3.5'
+
+__all__ = (
+    'serverline',
+    'SENDMAIL',
+    'validchars',
+    'alphanums',
+    'getrequest',
+    'getform',
+    'getall',
+    'isblank',
+    'formencode',
+    'formdecode',
+    'mailme',
+    'sendmailme',
+    'createhtmlmail',
+    'environdata',
+    'validemail',
+    'cgiprint',
+    'ucgiprint',
+    'replace',
+    'error',
+    'makeindexline',
+    'istrue',
+    'randomstring',
+    'blacklisted',
+    '__version__',
+    )
+
+serverline = "Content-Type: text/html"
+
+# A common location of sendmail on servers
+SENDMAIL = "/usr/sbin/sendmail"
+validchars = 'abcdefghijklmnopqrstuvwxyz0123456789!-_*'
+alphanums = 'abcdefghijklmnopqrstuvwxyz0123456789'
+
+#######################################################
+# Some functions for dealing with CGI forms (instances of FieldStorage)
+
+def getrequest(valuelist=None, nolist=False):
+    """
+    Initialise the ``FieldStorage`` and return the specified list of values as 
+    a dictionary.
+    
+    If you don't specify a list of values, then *all* values will be returned.
+    
+    If you set ``nolist`` to ``True`` then any parameters supplied as lists 
+    will only have their first entry returned.
+    """
+    import cgi
+    form = cgi.FieldStorage()
+    if valuelist is not None:
+        return getform(valuelist, form, nolist=nolist)
+    else:
+        return getall(form, nolist=nolist)
+
+def getform(valuelist, theform, notpresent='', nolist=False):
+    """
+    This function, given a CGI form, extracts the data from it, based on
+    valuelist passed in. Any non-present values are set to '' - although this 
+    can be changed.
+    
+    It also takes a keyword argument 'nolist'. If this is True list values only 
+    return their first value.
+    
+    Returns a dictionary.
+    """
+    data = {}
+    for field in valuelist:
+        if not theform.has_key(field):
+            data[field] = notpresent
+        else:
+            if not isinstance(theform[field], list):
+                data[field] = theform[field].value
+            else:
+                if not nolist:
+                    # allows for list type values
+                    data[field] = [x.value for x in theform[field]]
+                else:
+                    # just fetch the first item
+                    data[field] = theform[field][0].value
+    return data
+
+def getall(theform, nolist=False):
+    """
+    Passed a form (FieldStorage instance) return all the values.
+    This doesn't take into account file uploads.
+    
+    Also accepts the 'nolist' keyword argument as ``getform``.
+    
+    Returns a dictionary.
+    """
+    data = {}
+    for field in theform.keys():
+        if not isinstance(theform[field], list):
+            data[field] = theform[field].value
+        else:
+            if not nolist:
+                # allows for list type values
+                data[field] = [x.value for x in theform[field]]
+            else:
+                # just fetch the first item
+                data[field] = theform[field][0].value
+    return data
+
+def isblank(indict):
+    """
+    Passed an indict of values it checks if any of the values are set.
+    
+    Returns ``True`` if every member of the indict is empty (evaluates as False).
+    
+    I use it on a form processed with getform to tell if my CGI has been 
+    activated without any values.
+    """
+    return not [val for val in indict.values() if val]
+
+def formencode(theform):
+    """
+    A version that turns a cgi form into a single string.
+    It only handles single and list values, not multipart.
+    This allows the contents of a form requested to be encoded into a single value as part of another request.
+    """
+    from urllib import urlencode, quote_plus
+    return quote_plus(urlencode(getall(theform)))
+
+def formdecode(thestring):
+    """Decode a single string back into a form like dictionary."""
+    from cgi import parse_qs
+    from urllib import unquote_plus 
+    return parse_qs(unquote_plus(thestring), True)
+
+
+#############################################################
+# Functions for handling emails
+#
+# Use mailme for sending email - specify a path to sendmail *or* a host, port etc (optionally username)
+
+
+def mailme(to_email, msg, email_subject=None, from_email=None,
+            host='localhost', port=25, username=None, password=None,
+            html=True, sendmail=None):
+    """
+    This function will send an email using ``sendmail`` or ``smtplib``, depending 
+    on what parameters you pass it.
+    
+    If you want to use ``sendmail`` to send the email then set 
+    ``sendmail='/path/to/sendmail'``. (The ``SENDMAIL`` value from Constants_ often 
+    works).
+    
+    If you aren't using sendmail then you will need to set ``host`` and ``port`` to 
+    the correct values. If your server requires authentication then you'll need to 
+    supply the correct ``username`` and ``password``. 
+    
+    ``to_email`` can be a single email address, *or* a list of addresses.
+    
+    ``mailme`` *assumes* you are sending an html email created by 
+    ``createhtmlmail``. If this isn't the case then set ``html=False``.
+    
+    Some servers won't let you send a message without supplying a ``from_email``.
+    """
+    if sendmail is not None:
+        # use sendmailme if specified
+        return sendmailme(to_email, msg, email_subject, from_email, 
+                            html, sendmail)
+    if not isinstance(to_email, list):
+        # if we have a single email then change it into a list
+        to_email = [to_email]
+    #
+    import smtplib
+    #
+    head = "To: %s\r\n" % ','.join(to_email) 
+    if from_email is not None:
+        head += ('From: %s\r\n' % from_email)
+    # subject is in the body of an html email
+    if not html and email_subject is not None:
+        head += ("Subject: %s\r\n\r\n" % email_subject)
+    msg = head + msg
+    #
+    server = smtplib.SMTP(host, port)
+    if username:
+        server.login(username, password)
+    server.sendmail(from_email, to_email, msg)
+    server.quit()
+
+ 
+def sendmailme(to_email, msg, email_subject=None, from_email=None, 
+                html=True, sendmail=SENDMAIL):
+    """
+    Quick and dirty, pipe a message to sendmail. Can only work on UNIX type systems 
+    with sendmail.
+    
+    Will need the path to sendmail - defaults to the 'SENDMAIL' constant.
+    
+    ``to_email`` can be a single email address, *or* a list of addresses.
+    
+    *Assumes* you are sending an html email created by ``createhtmlmail``. If this 
+    isn't the case then set ``html=False``.
+    """
+    if not isinstance(to_email, list):
+        to_email = [to_email]
+    o = os.popen("%s -t" %  sendmail,"w")
+    o.write("To: %s\r\n" %  ','.join(to_email))
+    if from_email:
+        o.write("From: %s\r\n" %  from_email)
+    if not html and email_subject:
+        o.write("Subject: %s\r\n" %  email_subject)
+    o.write("\r\n")
+    o.write("%s\r\n" % msg)
+    o.close()
+
+def createhtmlmail(subject, html, text=None):
+    """
+    Create a mime-message that will render as HTML or text as appropriate.
+    If no text is supplied we use htmllib to guess a text rendering. 
+    (so html needs to be well formed) 
+    
+    Adapted from recipe 13.5 from Python Cookbook 2
+    """
+    import MimeWriter, mimetools, StringIO
+    if text is None:
+        # produce an approximate text from the HTML input
+        import htmllib
+        import formatter
+        textout = StringIO.StringIO()
+        formtext = formatter.AbstractFormatter(formatter.DumbWriter(textout))
+        parser = htmllib.HTMLParser(formtext)
+        parser.feed(html)
+        parser.close()
+        text = textout.getvalue()
+        del textout, formtext, parser
+    out = StringIO.StringIO()       # output buffer for our message
+    htmlin = StringIO.StringIO(html)  # input buffer for the HTML
+    txtin = StringIO.StringIO(text)   # input buffer for the plain text
+    writer = MimeWriter.MimeWriter(out)
+    # Set up some basic headers. Place subject here because smtplib.sendmail
+    # expects it to be in the message, as relevant RFCs prescribe.
+    writer.addheader("Subject", subject)
+    writer.addheader("MIME-Version", "1.0")
+    # Start the multipart section of the message. Multipart/alternative seems
+    # to work better on some MUAs than multipart/mixed.
+    writer.startmultipartbody("alternative")
+    writer.flushheaders()
+    # the plain-text section: just copied through, assuming iso-8859-1  # XXXX always true ?
+    subpart = writer.nextpart()
+    pout = subpart.startbody("text/plain", [("charset", 'iso-8859-l')]) 
+    pout.write(txtin.read())
+    txtin.close()
+    # the HTML subpart of the message: quoted-printable, just in case
+    subpart = writer.nextpart()
+    subpart.addheader("Content-Transfer-Encoding", "quoted-printable")
+    pout = subpart.startbody("text/html", [("charset", 'us-ascii')])
+    mimetools.encode(htmlin, pout, 'quoted-printable')
+    htmlin.close()
+    # You're done; close your writer and return the message as a string
+    writer.lastpart()
+    msg = out.getvalue()
+    out.close()
+    return msg    
+
+def environdata():
+    """Returns some data about the CGI environment, in a way that can be mailed."""
+    ENVIRONLIST = [ 'REQUEST_URI','HTTP_USER_AGENT','REMOTE_ADDR','HTTP_FROM','REMOTE_HOST','REMOTE_PORT','SERVER_SOFTWARE','HTTP_REFERER','REMOTE_IDENT','REMOTE_USER','QUERY_STRING','DATE_LOCAL' ]  # XXX put this in template ??
+    environs = []
+    environs.append("\n\n---------------------------------------\n")
+    for x in ENVIRONLIST:
+        if os.environ.has_key(x):
+            environs.append("%s: %s\n" % (x, os.environ[x]))
+    environs.append("---------------------------------------\n")
+    return ''.join(environs)
+
+def validemail(email):
+    """
+    A quick function to do a basic email validation.
+    Returns False or the email address.
+    """
+    if ' ' in email:
+        return False
+    dot = email.rfind('.')
+    at = email.find('@')
+    if dot == -1 or at < 1 or at > dot:
+        return False
+    return email
+
+##########################################################
+
+def error(errorval=''):
+    """The generic error function."""
+    print serverline
+    print
+    print '''<html><head><title>An Error Has Occurred</title>
+    <body><center>
+    <h1>Very Sorry</h1>
+    <h2>An Error Has Occurred</h2>'''
+    if errorval:
+        print '<h3>%s</h3>' % errorval
+    print '</center></body></html>'
+    sys.exit()
+    
+#########################################################
+
+def makeindexline(url, startpage, total, numonpage=10, pagesonscreen=5):
+    """
+    Make a menu line for a given number of inputs, with a certain number per page.
+    Will look something like : ::
+    
+        First  Previous  22 23 24 25 26 27 28 29 30 31 32  Next  Last
+    
+    Each number or word will be a link to the relevant page.
+    
+    url should be in the format : ``'<a href="script.py?startpage=%s">%s</a>'`` - 
+    it will have the two ``%s`` values filled in by the function.
+    
+    The url will automatically be put between ``<strong></strong>`` tags. Your 
+    script needs to accepts a parameter ``start`` telling it which page to display.
+    
+    ``startpage`` is the page actually being viewed - which won't be a link.
+    
+    ``total`` is the number of total inputs.
+    
+    ``numonpage`` is the number of inputs per page - this tells makeindexline how 
+    many pages to divide the total into.
+    
+    The links shown will be some before startpage and some after. The amount of 
+    pages links are shown for is ``pagesonscreen``. (The actual total number shown 
+    will be *2 \* pagesonscreen + 1*).
+    
+    The indexes generated are *a bit* like the ones created by google. Unlike 
+    google however, next and previous jump you into the *middle* of the next set of 
+    links. i.e. If you are on page 27 next will take you to 33 and previous to 21. 
+    (assuming pagesonscreen is 5). This makes it possible to jump more quickly 
+    through a lot of links. Also - the current page will always be in the center of 
+    the index. (So you never *need* Next just to get to the next page).
+    """
+    b = '<strong>%s</strong>'
+    url = b % url
+    outlist = []
+    last = ''
+    next = ''
+    numpages = total//numonpage
+    if total%numonpage:
+        numpages += 1
+    if startpage - pagesonscreen > 1:
+        outlist.append(url % (1, 'First'))
+        outlist.append('&nbsp;')
+        outlist.append(url % (startpage-pagesonscreen-1, 'Previous'))
+        outlist.append('&nbsp;')
+    index = max(startpage - pagesonscreen, 1)
+    end = min(startpage+pagesonscreen, numpages)
+    while index <= end:
+        if index == startpage:
+            outlist.append(b % startpage)
+        else:
+            outlist.append(url % (index, index))
+        index += 1
+    outlist.append('&nbsp;')
+    if (startpage+pagesonscreen) < numpages:
+        outlist.append(url % (startpage+pagesonscreen+1, 'Next'))
+        outlist.append('&nbsp;')
+        outlist.append(url % (numpages, 'Last'))
+
+    return '&nbsp;'.join(outlist)    
+
+######################################
+
+def istrue(value):
+    """
+    Accepts a string as input.
+    
+    If the string is one of  ``True``, ``On``, ``Yes``, or ``1`` it returns 
+    ``True``.
+    
+    If the string is one of  ``False``, ``Off``, ``No``, or ``0`` it returns 
+    ``False``.
+    
+    ``istrue`` is not case sensitive.
+    
+    Any other input will raise a ``KeyError``. 
+    """
+    return {
+        'yes': True, 'no': False,
+        'on': True, 'off': False, 
+        '1': True, '0': False,
+        'true': True, 'false': False,
+        }[value.lower()]
+
+def randomstring(length):
+    """
+    Return a random string of length 'length'.
+    
+    The string is comprised only of numbers and lowercase letters.
+    """ 
+    import random
+    outstring = []
+    while length > 0:
+        length -= 1
+        outstring.append(alphanums[int(random.random()*36)])
+    return ''.join(outstring)
+
+##################################
+
+def cgiprint(inline='', unbuff=False, line_end='\r\n'):
+    """
+    Print to the ``stdout``.
+    
+    Set ``unbuff=True`` to flush the buffer after every write.
+    
+    It prints the inline you send it, followed by the ``line_end``. By default this 
+    is ``\r\n`` - which is the standard specified by the RFC for http headers.
+    """
+    sys.stdout.write(inline)
+    sys.stdout.write(line_end)
+    if unbuff:
+        sys.stdout.flush()
+
+def ucgiprint(inline='', unbuff=False, encoding='UTF-8', line_end='\r\n'):
+    """
+    A unicode version of ``cgiprint``. It allows you to store everything in your 
+    script as unicode and just do your encoding in one place.
+    
+    Print to the ``stdout``.
+    
+    Set ``unbuff=True`` to flush the buffer after every write.
+    
+    It prints the inline you send it, followed by the ``line_end``. By default this 
+    is ``\r\n`` - which is the standard specified by the RFC for http headers.
+    
+    ``inline`` should be a unicode string.
+    
+    ``encoding`` is the encoding used to encode ``inline`` to a byte-string. It 
+    defaults to ``UTF-8``, set it to ``None`` if you pass in ``inline`` as a byte 
+    string rather than a unicode string.
+    """
+    if encoding:
+        inline = inline.encode(encoding)
+        # don't need to encode the line endings
+    sys.stdout.write(inline)
+    sys.stdout.write(line_end)
+    if unbuff:
+        sys.stdout.flush()
+
+def replace(instring, indict):
+    """
+    This function provides a simple but effective template system for your html 
+    pages. Effectively it is a convenient way of doing multiple replaces in a 
+    single string.
+    
+    Takes a string and a dictionary of replacements. 
+    
+    This function goes through the string and replaces every occurrence of every 
+    dicitionary key with it's value.
+    
+    ``indict`` can also be a list of tuples instead of a dictionary (or anything 
+    accepted by the dict function).
+    """
+    indict = dict(indict)
+    if len(indict) > 40:
+        regex = re.compile("(%s)" % "|".join(map(re.escape, indict.keys())))
+        # For each match, look-up corresponding value in dictionary
+        return regex.sub(lambda mo: indict[mo.string[mo.start():mo.end()]],
+                                                                    instring)
+    for key in indict:
+        instring = instring.replace(key, indict[key])
+    return instring
+
+############################
+
+def blacklisted(ip, DNSBL_HOST='sbl-xbl.spamhaus.org'):
+    """
+    Returns ``True`` if ip address is a blacklisted IP (i.e. from a spammer).
+    
+    ip can also be a domain name - this raises ``socket.gaierror`` if the ip is
+    a domain name that cannot be resolved.
+    
+    The DNS blacklist host (``DNSBL_HOST``) defaults to *sbl-xbl.spamhaus.org*.
+    
+    Other ones you could use include :
+    
+    - 'relays.ordb.org'
+    - 'dns.rfc-ignorant.org'
+    - 'postmaster.rfc-ignorant.org'
+    - 'http.dnsbl.sorbs.net'
+    - 'misc.dnsbl.sorbs.net'
+    - 'spam.dnsbl.sorbs.net'
+    - 'bl.spamcop.net'
+    
+    Useful for vetting user added information posted to web applications.
+    """
+    # turn '1.2.3.4' into '4.3.2.1.sbl-xbl.spamhaus.org'
+    import socket
+    # convert domain name to IP
+    # raises an error if domain name can't be resolved
+    ip = socket.gethostbyname(ip)
+    iplist = ip.split('.')
+    iplist.reverse()
+    ip = '%s.%s' % ('.'.join(iplist), DNSBL_HOST)
+    try:
+        socket.gethostbyname(ip)
+        return True
+    except socket.gaierror:
+        return False
+
+############################
+
+if __name__ == '__main__':
+    print 'No tests yet - sorry'
+
+"""
+TODO/ISSUES
+===========
+
+The indexes generated by makeindexline use next to jump 10 pages. This is
+different to what people will expect if they are used to the 'Google' type
+index lines.
+
+createhtmlmail assumes iso-8859-1 input encoding for the html
+
+email functions to support 'cc' and 'bcc'
+
+Need doctests
+
+Changelog
+=========
+
+2005/11/26      Version 0.3.5
+-----------------------------
+
+Add the ``blacklisted`` function.
+
+Added ``__version__``
+
+
+2005/10/29      Version 0.3.4
+-----------------------------
+
+Shortened ``isblank``.
+
+
+2005/09/21      Version 0.3.3
+-----------------------------
+
+Fixed bug in ``getall`.
+
+Fixed bug in ``getrequest``.
+
+
+2005/08/27      Version 0.3.2
+-----------------------------
+
+Large dictionary replaces use a regex approach.
+
+
+2005/08/20      Version 0.3.1
+-----------------------------
+
+Improved istrue function.
+
+Added __all__.
+
+Various other code/doc improvements.
+
+
+2005/04/07      Version 0.3.0
+-----------------------------
+
+Changed the email functions, this may break things (but it's better this way)
+
+Added createhtmlemail, removed loginmailme
+
+mailme is now a wrapper for sendmailme, mailme, *and* the old loginmailme
+
+
+2005/03/20      Version 0.2.0
+-----------------------------
+
+Added ucgiprint and replace.
+
+
+2005/02/18      Version 0.1.0
+-----------------------------
+
+The first numbered version.
+"""

docs/Akismet API Docs.html

+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
+<!-- saved from url=(0035)http://akismet.com/development/api/ -->
+<HTML 
+xmlns="http://www.w3.org/1999/xhtml"><HEAD><TITLE>Development « Akismet</TITLE>
+<META http-equiv=Content-Type content="text/html; charset=utf-8">
+<STYLE type=text/css>@import url( /style.css );
+#promo {
+	BACKGROUND: url(i/days-1.jpg)
+}
+</STYLE>
+<LINK title="Akismet Blog and Updates" href="http://akismet.com/blog/feed/" 
+type=application/rss+xml rel=alternate>
+<SCRIPT src="Development « Akismet_files/urchin.js" 
+type=text/javascript></SCRIPT>
+
+<SCRIPT type=text/javascript>
+_uacct = "UA-52447-4";
+urchinTracker();
+</SCRIPT>
+
+<META content="MSHTML 6.00.2900.2769" name=GENERATOR></HEAD>
+<BODY>
+<DIV id=rap>
+<H1 id=logo><A href="http://akismet.com/">Akismet</A></H1>
+<UL id=menu>
+  <LI><A href="http://akismet.com/">About Akismet</A> </LI>
+  <LI><A href="http://akismet.com/download/">Download</A> </LI>
+  <LI><A href="http://akismet.com/faq/">FAQ</A> </LI>
+  <LI><A href="http://akismet.com/commercial/">Commercial Use</A> </LI>
+  <LI><A href="http://akismet.com/blog/">Blog</A> </LI>
+  <LI><A href="http://akismet.com/contact/">Contact Us</A> </LI></UL>
+<H1>Akismet API Documentation Version 1.1 </H1>
+<H2>About the Akismet Service</H2>
+<P>Akismet is basically a big machine that sucks up all the data it possibly 
+can, looks for patterns, and learns from its mistakes. Thus far it has been 
+highly effective at stopping spam and adapting to new techniques and attempts to 
+evade it, and time will tell how it stands up. I've tried to keep the API 
+interaction as simple as possible.</P>
+<H3>A Good Consumer</H3>
+<P>To interact fully with the Akismet API your program really should be putting 
+data back into the system as well as just taking it out. If it is at all 
+possible within the framework of your application you should have a way for your 
+users to submit missed spam and false positives, otherwise Akismet will never 
+learn from its mistakes.</P>
+<H2>User Agent</H2>
+<P>If it is at all possible, please modify the user agent string you request 
+with to be of the following format:</P><PRE>Application Name/Version | Plugin Name/Version</PRE>
+<P>So in the WordPress plugin this looks like:</P><PRE>$ksd_user_agent = "WordPress/$wp_version | Akismet/1.11";
+</PRE>
+<H2>Call Structure</H2>
+<P>All calls to Akismet are POST requests much like a web form would send. The 
+request variables should be constructed like a query string, 
+<CODE>key=value</CODE> and multiple variables separated by ampersands. Don't 
+forget to URL escape the values. </P>
+<P>In the WordPress plugin the POST part of things is abstracted out in this 
+function:</P><PRE>function ksd_http_post($request, $host, $path, $port = 80) {
+	global $ksd_user_agent;
+
+	$http_request  = "POST $path HTTP/1.0\r\n";
+	$http_request .= "Host: $host\r\n";
+	$http_request .= "Content-Type: application/x-www-form-urlencoded; charset=" . get_settings('blog_charset') . "\r\n";
+	$http_request .= "Content-Length: " . strlen($request) . "\r\n";
+	$http_request .= "User-Agent: $ksd_user_agent\r\n";
+	$http_request .= "\r\n";
+	$http_request .= $request;
+
+	$response = '';
+	if( false !== ( $fs = @fsockopen($host, $port, $errno, $errstr, 3) ) ) {
+		fwrite($fs, $http_request);
+		while ( !feof($fs) )
+			$response .= fgets($fs, 1160); // One TCP-IP packet
+		fclose($fs);
+		$response = explode("\r\n\r\n", $response, 2);
+	}
+	return $response;
+}  </PRE>
+<P>This sends a POST request to the specified host and port with a timeout of 3 
+seconds. The HTTP request is constructed with full headers. The response headers 
+are discarded and the function returns the body of the response. </P>
+<H2>API Key</H2>
+<P>Use of the Akismet API requires an API key, which are currently only being 
+provided along with accounts to <A 
+href="http://wordpress.com/">WordPress.com</A>. The API key is used as a 
+subdomain in the call, for example if you had the API key <CODE>aoeu1aoue</CODE> 
+you would make all API calls to <CODE>aoeu1aoue.rest.akismet.com</CODE>. The 
+only exception to this is the <A 
+href="http://akismet.com/development/api/#verify-key">verify-key</A> call, which 
+may be made to <CODE>rest.akismet.com</CODE> without an API key subdomain. </P>
+<H2 id=verify-key>Key Verification — <CODE>rest.akismet.com/1.1/verify 
+key</CODE> </H2>
+<P>The key verification call should be made before beginning to use the service. 
+It requires two variables, key and blog.</P>
+<DL>
+  <DT><CODE>key</CODE> (required) 
+  <DD>The API key being verified for use with the API 
+  <DT><CODE>blog</CODE> (required) 
+  <DD>The front page or home URL of the instance making the request. For a blog 
+  or wiki this would be the front page. </DD></DL>
+<P>The call returns "<CODE>valid</CODE>" if the key is valid. This is the one 
+call that can be made without the API key subdomain. Using our example function 
+from above, this is how the API key is verified in the WP plugin: <PRE>function akismet_verify_key( $key ) {
+	global $ksd_api_host, $ksd_api_port;
+	$blog = urlencode( get_option('home') );
+	$response = ksd_http_post("key=$key&amp;blog=$blog", 'rest.akismet.com', '/1.1/verify-key', $ksd_api_port);
+	if ( 'valid' == $response[1] )
+		return true;
+	else
+		return false;
+}</PRE>
+<H2 id=comment-check>Comment Check — 
+<CODE>api-key.rest.akismet.com/1.1/comment-check</CODE></H2>
+<P>This is basically the core of everything. This call takes a number of 
+arguments and characteristics about the submitted content and then returns a 
+thumbs up or thumbs down. Almost everything is optional, but performance can 
+drop dramatically if you exclude certain elements. I would recommend erring on 
+the side of too much data, as everything is used as part of the Akismet 
+signature.</P>
+<DL>
+  <DT><CODE>blog </CODE>(required) 
+  <DD>The front page or home URL of the instance making the request. For a blog 
+  or wiki this would be the front page. 
+  <DT><CODE>user_ip</CODE> (required) 
+  <DD>IP address of the comment submitter. 
+  <DT><CODE>user_agent</CODE> (required) 
+  <DD>User agent information. 
+  <DT><CODE>referrer</CODE> (note spelling) 
+  <DD>The content of the HTTP_REFERER header should be sent here. 
+  <DT><CODE>permalink</CODE> 
+  <DD>The permanent location of the entry the comment was submitted to. 
+  <DT><CODE>comment_type</CODE> 
+  <DD>May be blank, comment, trackback, pingback, or a made up value like 
+  "registration". 
+  <DT><CODE>comment_author</CODE> 
+  <DD>Submitted name with the comment 
+  <DT><CODE>comment_author_email</CODE> 
+  <DD>Submitted email address 
+  <DT><CODE>comment_author_url</CODE> 
+  <DD>Commenter URL. 
+  <DT><CODE>comment_content</CODE> 
+  <DD>The content that was submitted. 
+  <DT>Other server enviroment variables 
+  <DD>In PHP there is an array of enviroment variables called 
+  <CODE>$_SERVER</CODE> which contains information about the web server itself 
+  as well as a key/value for every HTTP header sent with the request. This data 
+  is highly useful to Akismet as how the submited content interacts with the 
+  server can be very telling, so please include as much information as possible. 
+  </DD></DL>
+<P>This call returns either "true" or "false" as the body content. True means 
+that the comment is spam and false means that it isn't spam. If you are having 
+trouble triggering you can send "viagra-test-123" as the author and it will 
+trigger a true response, always. </P>
+<H2 id=submit-spam>Submit Spam — 
+<CODE>api-key.rest.akismet.com/1.1/submit-spam</CODE></H2>
+<P>This call is for submitting comments that weren't marked as spam but should 
+have been. It takes identical arguments as comment check.</P>
+<H2 id=submit-ham>Submit Ham — 
+<CODE>api-key.rest.akismet.com/1.1/submit-ham</CODE></H2>
+<P>This call is intended for the marking of false positives, things that were 
+incorrectly marked as spam. It takes identical arguments as <A 
+href="http://akismet.com/development/api/#comment-check">comment check</A> and 
+submit spam.</P>
+<DL></DL>
+<DIV id=zeitgeist>
+<H2>Live Spam Zeitgeist</H2>
+<P>477,826 spams caught so far</P>
+<P>2,099 so far today</P>
+<P>81% of all comments are spam</P><!--  --></DIV>
+<DIV id=footer><A id=ap href="http://automattic.com/">An Automattic 
+Production</A> 
+<P><A href="http://akismet.com/privacy/">Privacy Policy</A> 
+</P></DIV></DIV></BODY></HTML>

docs/akismet_python.html

+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta name="generator" content="Docutils 0.4: http://docutils.sourceforge.net/" />
+<title>Akismet - The Python API</title>
+<meta name="author" content="Michael Foord" />
+<meta name="date" content="2007/02/05" />
+<link rel="stylesheet" href="stylesheets/voidspace_docutils.css" type="text/css" />
+</head>
+<body>
+<div class="document" id="akismet-the-python-api">
+<h1 class="title">Akismet - The Python API</h1>
+<h2 class="subtitle" id="stopping-comment-spam-with-akismet">Stopping Comment Spam with Akismet</h2>
+<table class="docinfo" frame="void" rules="none">
+<col class="docinfo-name" />
+<col class="docinfo-content" />
+<tbody valign="top">
+<tr><th class="docinfo-name">Author:</th>
+<td><a class="first last reference" href="http://www.voidspace.org.uk/python/index.shtml">Michael Foord</a></td></tr>
+<tr><th class="docinfo-name">Contact:</th>
+<td><a class="first last reference" href="mailto:fuzzyman&#37;&#52;&#48;voidspace&#46;org&#46;uk">fuzzyman<span>&#64;</span>voidspace<span>&#46;</span>org<span>&#46;</span>uk</a></td></tr>
+<tr><th class="docinfo-name">Version:</th>
+<td>0.1.5</td></tr>
+<tr><th class="docinfo-name">Date:</th>
+<td>2007/02/05</td></tr>
+<tr class="field"><th class="docinfo-name">License:</th><td class="field-body"><a class="reference" href="BSD-LICENSE.txt">BSD License</a> <a class="footnote-reference" href="#id5" id="id1" name="id1">[1]</a></td>
+</tr>
+<tr class="field"><th class="docinfo-name">Online Version:</th><td class="field-body"><a class="reference" href="http://www.voidspace.org.uk/python/akismet_python.html">akismet.py online</a></td>
+</tr>
+</tbody>
+</table>
+<div class="contents topic">
+<p class="topic-title first"><a id="the-akismet-api" name="the-akismet-api">The Akismet API</a></p>
+<ul class="simple">
+<li><a class="reference" href="#introduction" id="id9" name="id9">Introduction</a><ul>
+<li><a class="reference" href="#downloading" id="id10" name="id10">Downloading</a></li>
+</ul>
+</li>
+<li><a class="reference" href="#apikey-txt" id="id11" name="id11">apikey.txt</a></li>
+<li><a class="reference" href="#the-akismet-class" id="id12" name="id12">The Akismet Class</a><ul>
+<li><a class="reference" href="#creating-an-instance" id="id13" name="id13">Creating an Instance</a><ul>
+<li><a class="reference" href="#user-agent" id="id14" name="id14">User-Agent</a></li>
+<li><a class="reference" href="#example-1" id="id15" name="id15">Example 1</a></li>
+<li><a class="reference" href="#example-2" id="id16" name="id16">Example 2</a></li>
+<li><a class="reference" href="#example-3" id="id17" name="id17">Example 3</a></li>
+</ul>
+</li>
+<li><a class="reference" href="#setapikey" id="id18" name="id18">setAPIKey</a></li>
+<li><a class="reference" href="#akismet-methods" id="id19" name="id19">Akismet Methods</a><ul>
+<li><a class="reference" href="#verify-key" id="id20" name="id20">verify-key</a></li>
+<li><a class="reference" href="#comment-check" id="id21" name="id21">comment-check</a></li>
+<li><a class="reference" href="#submit-spam" id="id22" name="id22">submit-spam</a></li>
+<li><a class="reference" href="#submit-ham" id="id23" name="id23">submit-ham</a></li>
+</ul>
+</li>
+</ul>
+</li>
+<li><a class="reference" href="#error-classes" id="id24" name="id24">Error Classes</a><ul>
+<li><a class="reference" href="#akismeterror" id="id25" name="id25">AkismetError</a></li>
+<li><a class="reference" href="#apikeyerror" id="id26" name="id26">APIKeyError</a></li>
+</ul>
+</li>
+<li><a class="reference" href="#usage-example" id="id27" name="id27">Usage Example</a></li>
+<li><a class="reference" href="#akismet-test-cgi" id="id28" name="id28">Akismet Test CGI</a></li>
+<li><a class="reference" href="#todo" id="id29" name="id29">TODO</a></li>
+<li><a class="reference" href="#changelog" id="id30" name="id30">CHANGELOG</a><ul>
+<li><a class="reference" href="#version-0-1-5" id="id31" name="id31">2007/02/05      Version 0.1.5</a></li>
+<li><a class="reference" href="#version-0-1-4" id="id32" name="id32">2006/12/13      Version 0.1.4</a></li>
+<li><a class="reference" href="#version-0-1-3" id="id33" name="id33">2006/07/18      Version 0.1.3</a></li>
+<li><a class="reference" href="#version-0-1-2" id="id34" name="id34">2005/12/04      Version 0.1.2</a></li>
+<li><a class="reference" href="#version-0-1-1" id="id35" name="id35">2005/12/02      Version 0.1.1</a></li>
+<li><a class="reference" href="#version-0-1-0" id="id36" name="id36">2005/12/01      Version 0.1.0</a></li>
+</ul>
+</li>
+<li><a class="reference" href="#footnotes" id="id37" name="id37">Footnotes</a></li>
+</ul>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id9" id="introduction" name="introduction">Introduction</a></h1>
+<p><a class="reference" href="http://www.akismet.com">Akismet</a> is a web service for recognising spam
+comments. It promises to be almost 100% effective at catching comment spam.
+They say that currently 81% of all comments submitted to them are spam.</p>
+<p>It's designed to work with the <a class="reference" href="http://wordpress.org/">Wordpress Blog Tool</a>,
+but it's not restricted to that - so this is a Python interface to the
+<a class="reference" href="http://akismet.com/development/api/">Akismet API</a>.</p>
+<p>You'll need a <a class="reference" href="http://wordpress.com">Wordpress Key</a> to use it. This script
+will allow you to plug akismet into any CGI script or web application, and
+there are full docs in the code. It's extremely easy to use, because the folks
+at  akismet have implemented a nice and straightforward
+<acronym title="REpresentational State Transfer">REST</acronym>
+<acronym title="Application Programmers Interface">API</acronym>.</p>
+<div class="note">
+<p class="first admonition-title">Note</p>
+<p>If possible you should build into your program the ability to inform
+Akismet of false positives and false negatives.</p>
+<p>Informing Akismet helps makes the service more reliable.  <img src="smilies/smile.gif" alt="Smile" height="15" width="15" /> </p>
+<p class="last">To do this, use the <a class="reference" href="#submit-spam">submit-spam</a> and <a class="reference" href="#submit-ham">submit-ham</a> functionality.</p>
+</div>
+<p>Most of the work is done by the <a class="reference" href="#comment-check">comment-check</a> function.</p>
+<div class="section">
+<h2><a class="toc-backref" href="#id10" id="downloading" name="downloading">Downloading</a></h2>
+<p>You can download <strong>akismet.py</strong> from :</p>
+<ul class="simple">
+<li><a class="reference" href="http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=akismet.zip">akismet.zip (48k)</a></li>
+</ul>
+<p>This contains the docs, <strong>akismet.py</strong>, and a test <acronym title="Common Gateway Interface">CGI</acronym> called
+<em>test_akismet.py</em>.</p>
+</div>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id11" id="apikey-txt" name="apikey-txt">apikey.txt</a></h1>
+<p>The easiest way to use <em>akismet.py</em> is to provide the wordpress key and blog
+<acronym title="Uniform Resource Locator">URL</acronym> in a text file called <tt class="docutils literal"><span class="pre">apikey.txt</span></tt>.</p>
+<p>This should be in the current directory when you create your <tt class="docutils literal"><span class="pre">Akismet</span></tt>
+instance (or call <a class="reference" href="#setapikey">setAPIKey</a> with no arguments).</p>
+<p>The format for <tt class="docutils literal"><span class="pre">apikey.txt</span></tt> is simple (see the example one in the
+distribution.</p>
+<p>Lines that start with a <tt class="docutils literal"><span class="pre">#</span></tt> are comments. The first non-blank, non-comment
+line should be the API key. The second line should be the blog URL (or
+application URL) to use.</p>
+<pre class="literal-block">
+# Lines starting with '#' are comments
+# The first non-blank, non-comment, line should be your api key
+# The second your blog URL
+#
+# You can get a wordpress API key from http://wordpress.com/
+some_key
+some_blog_url
+</pre>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id12" id="the-akismet-class" name="the-akismet-class">The Akismet Class</a></h1>
+<p>The <a class="reference" href="http://www.akismet.com">akismet</a> API provides four functions. <em>akismet.py</em> gives you access to
+all of these through a single class.</p>
+<p>The four akismet functions are :</p>
+<ul class="simple">
+<li><a class="reference" href="#verify-key">verify-key</a>     - the <tt class="docutils literal"><span class="pre">verify_key</span></tt> method</li>
+<li><a class="reference" href="#comment-check">comment-check</a>  - the <tt class="docutils literal"><span class="pre">comment_check</span></tt> method</li>
+<li><a class="reference" href="#submit-spam">submit-spam</a>   - the <tt class="docutils literal"><span class="pre">submit_spam</span></tt> method</li>
+<li><a class="reference" href="#submit-ham">submit-ham</a>   - the <tt class="docutils literal"><span class="pre">submit_ham</span></tt> method</li>
+</ul>
+<p>In addition to these, the Akismet class has the following user methods and
+attributes :</p>
+<ul class="simple">
+<li><a class="reference" href="#setapikey">setAPIKey</a>    - method</li>
+<li>key           - attribute</li>
+<li>blog_url      - attribute</li>
+</ul>
+<div class="section">
+<h2><a class="toc-backref" href="#id13" id="creating-an-instance" name="creating-an-instance">Creating an Instance</a></h2>
+<pre class="literal-block">
+Akismet(key=None, blog_url=None, agent=None)
+</pre>
+<p>To use the akismet web service you <em>need</em> an API key. There are three ways of
+telling the <tt class="docutils literal"><span class="pre">Akismet</span></tt> class what this is.</p>
+<ol class="arabic simple">
+<li>When you create a new <tt class="docutils literal"><span class="pre">Akismet</span></tt> instance you can pass in the API key and
+blog url.</li>
+<li>If you don't pass in a key, it will automatically look for <a class="reference" href="#apikey-txt">apikey.txt</a> in
+the current directory, and attempt to load it.</li>
+<li>You can set the <tt class="docutils literal"><span class="pre">key</span></tt> and <tt class="docutils literal"><span class="pre">blog_url</span></tt> Attributes manually, after creating
+your instance.</li>
+</ol>
+<div class="section">
+<h3><a class="toc-backref" href="#id14" id="user-agent" name="user-agent">User-Agent</a></h3>
+<p>As well as setting your key, you <em>ought</em> to pass in a string for <tt class="docutils literal"><span class="pre">Akismet</span></tt> to
+create a User-Agent header with. This is the <tt class="docutils literal"><span class="pre">agent</span></tt> argument.</p>
+<p>According to the <a class="reference" href="http://akismet.com/development/api/">API docs</a>, this ought
+to be in the form :</p>
+<pre class="literal-block">
+Program Name/Version
+</pre>
+<p><em>akismet.py</em> adds it's version number to this, to create a User-Agent in the
+form liked by akismet.</p>
+<p>The default User-Agent (if you don't pass in a values to <tt class="docutils literal"><span class="pre">agent</span></tt>) is :</p>
+<pre class="literal-block">
+Python Interface by Fuzzyman/0.1.5 | akismet.py/0.1.5
+</pre>
+</div>
+<div class="section">
+<h3><a class="toc-backref" href="#id15" id="example-1" name="example-1">Example 1</a></h3>
+<div class="pysrc"><span class="pycomment">#example 1<br />
+</span><span class="pytext">api</span> <span class="pyoperator">=</span> <span class="pytext">Akismet</span><span class="pyoperator">(</span><span class="pytext">api_key</span><span class="pyoperator">,</span> <span class="pytext">url</span><span class="pyoperator">,</span> <span class="pytext">agent</span><span class="pyoperator">=</span><span class="pystring">'Example/0.1'</span><span class="pyoperator">)</span><span class="pytext"></span></div></div>
+<div class="section">
+<h3><a class="toc-backref" href="#id16" id="example-2" name="example-2">Example 2</a></h3>
+<div class="pysrc"><span class="pycomment">#example 2<br />
+</span><span class="pykeyword">if</span> <span class="pytext">os</span><span class="pyoperator">.</span><span class="pytext">path</span><span class="pyoperator">.</span><span class="pytext">isfile</span><span class="pyoperator">(</span><span class="pystring">'apikey.txt'</span><span class="pyoperator">)</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pytext">api</span> <span class="pyoperator">=</span> <span class="pytext">Akismet</span><span class="pyoperator">(</span><span class="pytext">agent</span><span class="pyoperator">=</span><span class="pystring">'Example/0.2'</span><span class="pyoperator">)</span><br />
+<br />
+<span class="pycomment"># The key and URL are loaded from<br />
+</span><span class="pycomment"># 'apikey.txt'</span><span class="pytext"></span></div></div>
+<div class="section">
+<h3><a class="toc-backref" href="#id17" id="example-3" name="example-3">Example 3</a></h3>
+<div class="pysrc"><span class="pycomment">#example 2<br />
+</span><span class="pytext">url</span> <span class="pyoperator">=</span> <span class="pystring">'http://www.voidspace.org.uk/cgi-bin/voidspace/guestbook.py'</span><br />
+<span class="pytext">api_key</span> <span class="pyoperator">=</span> <span class="pystring">'0acdfg1fr'</span><br />
+<span class="pykeyword">if</span> <span class="pykeyword">not</span> <span class="pytext">os</span><span class="pyoperator">.</span><span class="pytext">path</span><span class="pyoperator">.</span><span class="pytext">isfile</span><span class="pyoperator">(</span><span class="pystring">'apikey.txt'</span><span class="pyoperator">)</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pytext">api</span> <span class="pyoperator">=</span> <span class="pytext">Akismet</span><span class="pyoperator">(</span><span class="pytext">agent</span><span class="pyoperator">=</span><span class="pystring">'Example/0.3'</span><span class="pyoperator">)</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pytext">api</span><span class="pyoperator">.</span><span class="pytext">key</span> <span class="pyoperator">=</span> <span class="pytext">api_key</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pytext">api</span><span class="pyoperator">.</span><span class="pytext">blog_url</span> <span class="pyoperator">=</span> <span class="pytext">url</span><br />
+<br />
+<span class="pycomment"># The key and URL are set manually</span><span class="pytext"></span></div></div>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id18" id="setapikey" name="setapikey">setAPIKey</a></h2>
+<pre class="literal-block">
+setAPIKey(key=None, blog_url=None)
+</pre>
+<p>Set the wordpress API key for all transactions.</p>
+<p>If you don't specify an explicit API <tt class="docutils literal"><span class="pre">key</span></tt> and <tt class="docutils literal"><span class="pre">blog_url</span></tt> it will
+attempt to load them from a file called <tt class="docutils literal"><span class="pre">apikey.txt</span></tt> in the current
+directory.</p>
+<p>This method is <em>usually</em> called automatically when you create a new <tt class="docutils literal"><span class="pre">Akismet</span></tt>
+instance.</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id19" id="akismet-methods" name="akismet-methods">Akismet Methods</a></h2>
+<p>These four methods equate to the four functions of the <a class="reference" href="http://akismet.com/development/api/">Akismet API</a>.</p>
+<div class="section">
+<h3><a class="toc-backref" href="#id20" id="verify-key" name="verify-key">verify-key</a></h3>
+<pre class="literal-block">
+verify_key()
+</pre>
+<p>This equates to the <tt class="docutils literal"><span class="pre">verify-key</span></tt> call against the akismet API.</p>
+<p>It returns <tt class="docutils literal"><span class="pre">True</span></tt> if the key is valid.</p>
+<p>The docs state that your program <em>ought</em> to call this at the start of the
+transaction.</p>
+<p>It raises <tt class="docutils literal"><span class="pre">APIKeyError</span></tt> if you have not yet set an API key.</p>
+<p>If the connection to akismet fails, it allows the normal <tt class="docutils literal"><span class="pre">HTTPError</span></tt>
+or <tt class="docutils literal"><span class="pre">URLError</span></tt> to be raised. (<em>akismet.py</em> uses
+<a class="reference" href="http://docs.python.org/lib/module-urllib2.html">urllib2</a>)</p>
+</div>
+<div class="section">
+<h3><a class="toc-backref" href="#id21" id="comment-check" name="comment-check">comment-check</a></h3>
+<pre class="literal-block">
+comment_check(comment, data=None, build_data=True, DEBUG=False)
+</pre>
+<p>This is the main function in the Akismet API. It checks comments.</p>
+<p>It returns <tt class="docutils literal"><span class="pre">True</span></tt> for spam and <tt class="docutils literal"><span class="pre">False</span></tt> for ham.</p>
+<p>If you set <tt class="docutils literal"><span class="pre">DEBUG=True</span></tt> then it will return the text of the response,
+instead of the <tt class="docutils literal"><span class="pre">True</span></tt> or <tt class="docutils literal"><span class="pre">False</span></tt> object.</p>
+<p>It raises <tt class="docutils literal"><span class="pre">APIKeyError</span></tt> if you have not yet set an API key.</p>
+<p>If the connection to Akismet fails then the <tt class="docutils literal"><span class="pre">HTTPError</span></tt> or
+<tt class="docutils literal"><span class="pre">URLError</span></tt> will be propogated.</p>
+<p>As a minimum it requires the body of the comment. This is the
+<tt class="docutils literal"><span class="pre">comment</span></tt> argument.</p>
+<p>Akismet requires some other arguments, and allows some optional ones.
+The more information you give it, the more likely it is to be able to
+make an accurate diagnosise.</p>
+<p>You supply these values using a mapping object (dictionary) as the
+<tt class="docutils literal"><span class="pre">data</span></tt> argument.</p>
+<p>If <tt class="docutils literal"><span class="pre">build_data</span></tt> is <tt class="docutils literal"><span class="pre">True</span></tt> (the default), then <em>akismet.py</em> will
+attempt to fill in as much information as possible, using default
+values where necessary. This is particularly useful for programs
+running in a <acronym title="Common Gateway Interface">CGI</acronym> environment. A lot of useful information
+can be supplied from evironment variables (<tt class="docutils literal"><span class="pre">os.environ</span></tt>). See below.</p>
+<p>You <em>only</em> need supply values for which you don't want defaults filled
+in for. All values must be strings.</p>
+<p>There are a few required values. If they are not supplied, and
+defaults can't be worked out, then an <tt class="docutils literal"><span class="pre">AkismetError</span></tt> is raised.</p>
+<p>If you set <tt class="docutils literal"><span class="pre">build_data=False</span></tt> and a required value is missing an
+<tt class="docutils literal"><span class="pre">AkismetError</span></tt> will also be raised.</p>
+<p>The normal values (and defaults) are as follows :</p>
+<ul class="simple">
+<li>'user_ip':          <tt class="docutils literal"><span class="pre">os.environ['REMOTE_ADDR']</span></tt>       (*)</li>
+<li>'user_agent':       <tt class="docutils literal"><span class="pre">os.environ['HTTP_USER_AGENT']</span></tt>   (*)</li>
+<li>'referrer':         <tt class="docutils literal"><span class="pre">os.environ.get('HTTP_REFERER',</span> <span class="pre">'unknown')</span></tt> <a class="footnote-reference" href="#id6" id="id3" name="id3">[2]</a></li>
+<li>'permalink':        ''</li>
+<li>'comment_type':     'comment' <a class="footnote-reference" href="#id7" id="id4" name="id4">[3]</a></li>
+<li>'comment_author':   ''</li>
+<li>'comment_author_email': ''</li>
+<li>'comment_author_url': ''</li>
+<li>'SERVER_ADDR':      <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_ADDR',</span> <span class="pre">'')</span></tt></li>
+<li>'SERVER_ADMIN':     <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_ADMIN',</span> <span class="pre">'')</span></tt></li>
+<li>'SERVER_NAME':      <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_NAME',</span> <span class="pre">'')</span></tt></li>
+<li>'SERVER_PORT':      <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_PORT',</span> <span class="pre">'')</span></tt></li>
+<li>'SERVER_SIGNATURE': <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_SIGNATURE',</span> <span class="pre">'')</span></tt></li>
+<li>'SERVER_SOFTWARE':  <tt class="docutils literal"><span class="pre">os.environ.get('SERVER_SOFTWARE',</span> <span class="pre">'')</span></tt></li>
+<li>'HTTP_ACCEPT':      <tt class="docutils literal"><span class="pre">os.environ.get('HTTP_ACCEPT',</span> <span class="pre">'')</span></tt></li>
+</ul>
+<p>(*) Required values</p>
+<p>You may supply as many additional <strong>'HTTP_*'</strong> type values as you wish.
+These should correspond to the http headers sent with the request.</p>
+</div>
+<div class="section">
+<h3><a class="toc-backref" href="#id22" id="submit-spam" name="submit-spam">submit-spam</a></h3>
+<pre class="literal-block">
+submit_spam(comment, data=None, build_data=True)
+</pre>
+<p>This function is used to tell akismet that a comment it marked as ham,
+is really spam.</p>
+<p>It takes all the same arguments as <tt class="docutils literal"><span class="pre">comment_check</span></tt>, except for
+<em>DEBUG</em>.</p>
+</div>
+<div class="section">
+<h3><a class="toc-backref" href="#id23" id="submit-ham" name="submit-ham">submit-ham</a></h3>
+<pre class="literal-block">
+submit_ham(self, comment, data=None, build_data=True)
+</pre>
+<p>This function is used to tell akismet that a comment it marked as spam,
+is really ham.</p>
+<p>It takes all the same arguments as <tt class="docutils literal"><span class="pre">comment_check</span></tt>, except for
+<em>DEBUG</em>.</p>
+</div>
+</div>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id24" id="error-classes" name="error-classes">Error Classes</a></h1>
+<p>In the course of using <em>akismet.py</em>, there are two possible errors you could
+see.</p>
+<div class="section">
+<h2><a class="toc-backref" href="#id25" id="akismeterror" name="akismeterror">AkismetError</a></h2>
+<p>This is for general Akismet errors. For example, if you didn't supply some of
+the required information.</p>
+<p>This error is a subclass of <tt class="docutils literal"><span class="pre">Exception</span></tt>.</p>
+<p>This error is also raised if there is a network connection error. This can happen when the Akismet
+service or domain goes down temporarily.</p>
+<p>Your code should trap this and handle it appropriately (either let the comment through or push it
+onto a moderation queue).</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id26" id="apikeyerror" name="apikeyerror">APIKeyError</a></h2>
+<p>If <em>apikey.txt</em> is invalid, or you attempt to call one of the <a class="reference" href="#akismet-methods">akismet methods</a>
+without setting a key, you will get an <tt class="docutils literal"><span class="pre">APIKeyError</span></tt>.</p>
+<p>This error is a subclass of <tt class="docutils literal"><span class="pre">AkismetError</span></tt>.</p>
+</div>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id27" id="usage-example" name="usage-example">Usage Example</a></h1>
+<p>A simple example that loads the key automatically, verifies the key, and then
+checks a comment.</p>
+<div class="pysrc"><span class="pytext">api</span> <span class="pyoperator">=</span> <span class="pytext">Akismet</span><span class="pyoperator">(</span><span class="pytext">agent</span><span class="pyoperator">=</span><span class="pystring">'Test Script'</span><span class="pyoperator">)</span><br />
+<span class="pycomment"># if apikey.txt is in place,<br />
+</span><span class="pycomment"># the key will automatically be set<br />
+</span><span class="pycomment"># or you can call ``api.setAPIKey()``<br />
+</span><span class="pycomment">#<br />
+</span><span class="pykeyword">if</span> <span class="pytext">api</span><span class="pyoperator">.</span><span class="pytext">key</span> <span class="pykeyword">is</span> <span class="pytext">None</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">print</span> <span class="pystring">"No 'apikey.txt' file."</span><br />
+<span class="pykeyword">elif</span> <span class="pykeyword">not</span> <span class="pytext">api</span><span class="pyoperator">.</span><span class="pytext">verify_key</span><span class="pyoperator">(</span><span class="pyoperator">)</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">print</span> <span class="pystring">"The API key is invalid."</span><br />
+<span class="pykeyword">else</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pycomment"># data should be a dictionary of values<br />
+</span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="pycomment"># They can all be filled in with defaults<br />
+</span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="pycomment"># from a CGI environment<br />
+</span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">if</span> <span class="pytext">api</span><span class="pyoperator">.</span><span class="pytext">comment_check</span><span class="pyoperator">(</span><span class="pytext">comment</span><span class="pyoperator">,</span> <span class="pytext">data</span><span class="pyoperator">)</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">print</span> <span class="pystring">'This comment is spam.'</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">else</span><span class="pyoperator">:</span><br />
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="pykeyword">print</span> <span class="pystring">'This comment is ham.'</span><span class="pytext"></span></div></div>
+<div class="section">
+<h1><a class="toc-backref" href="#id28" id="akismet-test-cgi" name="akismet-test-cgi">Akismet Test CGI</a></h1>
+<p>Included in the distribution is a file called <tt class="docutils literal"><span class="pre">test_akismet.py</span></tt>.</p>
+<p>This is a simple test CGI. It needs <a class="reference" href="http://www.voidspace.org.uk/python/recipebook.shtml#util">cgiutils</a>
+to run.</p>
+<p>When activated, it allows you to put a comment in and test it with akismet. It
+will tell you if the comment is marked as <em>ham</em>, or <em>spam</em>.</p>
+<p>To confirm that your setup is working; any post with <strong>viagra-test-123</strong> as the
+name, should be marked as spam.</p>
+<p>Obviously you will need an API key for this to work.</p>
+<p>You can try this online at :</p>
+<blockquote>
+<a class="reference" href="http://www.voidspace.org.uk/cgi-bin/akismet/test_akismet.py">Akismet Example CGI</a></blockquote>
+</div>
+<hr class="docutils" />
+<div class="section">
+<h1><a class="toc-backref" href="#id29" id="todo" name="todo">TODO</a></h1>
+<p>Make the timeout adjustable ?</p>
+<p>Should we fill in a default value for permalink ?</p>
+<p>What about automatically filling in the 'HTTP_*' values from os.environ ?</p>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id30" id="changelog" name="changelog">CHANGELOG</a></h1>
+<div class="section">
+<h2><a class="toc-backref" href="#id31" id="version-0-1-5" name="version-0-1-5">2007/02/05      Version 0.1.5</a></h2>
+<p>Fixed a typo/bug in <tt class="docutils literal"><span class="pre">submit_ham</span></tt>. Thanks to Ian Ozsvald for pointing this out.</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id32" id="version-0-1-4" name="version-0-1-4">2006/12/13      Version 0.1.4</a></h2>
+<p>Akismet now traps errors in connections. If there is a network error it raises an <tt class="docutils literal"><span class="pre">AkismetError</span></tt>.</p>
+<p>This can happen when the Akismet service or domain goes down temporarily.</p>
+<p>Your code should trap this and handle it appropriately (either let the comment through or push it onto a moderation
+queue).</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id33" id="version-0-1-3" name="version-0-1-3">2006/07/18      Version 0.1.3</a></h2>
+<p>Add the blog url to the data. Bugfix thanks to James Bennett.</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id34" id="version-0-1-2" name="version-0-1-2">2005/12/04      Version 0.1.2</a></h2>
+<p>Added the <tt class="docutils literal"><span class="pre">build_data</span></tt> argument to <tt class="docutils literal"><span class="pre">comment_check</span></tt>, <tt class="docutils literal"><span class="pre">submit_spam</span></tt>, and
+<tt class="docutils literal"><span class="pre">submit_ham</span></tt>.</p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id35" id="version-0-1-1" name="version-0-1-1">2005/12/02      Version 0.1.1</a></h2>
+<p>Corrected so that ham and spam are the right way round  <img src="smilies/smile.gif" alt="Smile" height="15" width="15" /> </p>
+</div>
+<div class="section">
+<h2><a class="toc-backref" href="#id36" id="version-0-1-0" name="version-0-1-0">2005/12/01      Version 0.1.0</a></h2>
+<p>Test version.</p>
+</div>
+</div>
+<div class="section">
+<h1><a class="toc-backref" href="#id37" id="footnotes" name="footnotes">Footnotes</a></h1>
+<table class="docutils footnote" frame="void" id="id5" rules="none">
+<colgroup><col class="label" /><col /></colgroup>
+<tbody valign="top">
+<tr><td class="label"><a class="fn-backref" href="#id1" name="id5">[1]</a></td><td>Online at <a class="reference" href="http://www.voidspace.org.uk/python/license.shtml">http://www.voidspace.org.uk/python/license.shtml</a></td></tr>
+</tbody>
+</table>
+<table class="docutils footnote" frame="void" id="id6" rules="none">
+<colgroup><col class="label" /><col /></colgroup>
+<tbody valign="top">
+<tr><td class="label"><a class="fn-backref" href="#id3" name="id6">[2]</a></td><td>Note the spelling &quot;referrer&quot;. This is a required value by the
+akismet api - however, referrer information is not always
+supplied by the browser or server. In fact the HTTP protocol
+forbids relying on referrer information for functionality in
+programs.</td></tr>
+</tbody>
+</table>
+<table class="docutils footnote" frame="void" id="id7" rules="none">
+<colgroup><col class="label" /><col /></colgroup>
+<tbody valign="top">
+<tr><td class="label"><a class="fn-backref" href="#id4" name="id7">[3]</a></td><td>The <a class="reference" href="http://akismet.com/development/api/">API docs</a> state that this value
+can be &quot; <em>blank, comment, trackback, pingback, or a made up value</em>
+<em>like 'registration'</em> &quot;.</td></tr>
+</tbody>
+</table>
+<div class="note">
+<p class="first admonition-title">Note</p>
+<p class="last">Rendering this document with docutils also needs the
+textmacros module and the <strong>PySrc</strong> CSS stuff. See
+<a class="reference" href="http://www.voidspace.org.uk/python/firedrop2/textmacros.shtml">http://www.voidspace.org.uk/python/firedrop2/textmacros.shtml</a></p>
+</div>
+<div align="center">
+    <a href="http://www.python.org">
+        <img src="images/powered_by_python.jpg" width="602" height="186" border="0" />
+    </a>
+    <a href="http://www.opensource.org">
+        <img src="images/osi-certified-120x100.gif" width="120" height="100" border="0" />
+        <br /><strong>Certified Open Source</strong>
+    </a>
+<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
+</script>
+<script type="text/javascript">
+_uacct = "UA-203625-1";
+urchinTracker();
+</script>
+</div></div>
+</div>
+<div class="footer">
+<hr class="footer" />
+<a class="reference" href="akismet_python.txt">View document source</a>.
+Generated on: 2007-02-05 20:47 UTC.
+Generated by <a class="reference" href="http://docutils.sourceforge.net/">Docutils</a> from <a class="reference" href="http://docutils.sourceforge.net/rst.html">reStructuredText</a> source.
+
+</div>
+</body>
+</html>

docs/akismet_python.txt

+==========================
+ Akismet - The Python API
+==========================
+------------------------------------
+ Stopping Comment Spam with Akismet
+------------------------------------
+
+:Author: `Michael Foord`_
+:Contact: fuzzyman@voidspace.org.uk
+:Version: 0.1.5
+:Date: 2007/02/05
+:License: `BSD License`_ [#]_
+:Online Version: `akismet.py online`_
+
+.. _`akismet.py online`: http://www.voidspace.org.uk/python/akismet_python.html
+.. _BSD License: BSD-LICENSE.txt
+
+.. contents:: The Akismet API
+
+
+Introduction
+============
+
+`Akismet <http://www.akismet.com>`_ is a web service for recognising spam
+comments. It promises to be almost 100% effective at catching comment spam. 
+They say that currently 81% of all comments submitted to them are spam.
+
+It's designed to work with the `Wordpress Blog Tool <http://wordpress.org/>`_,
+but it's not restricted to that - so this is a Python interface to the 
+`Akismet API <http://akismet.com/development/api/>`_.
+
+You'll need a `Wordpress Key <http://wordpress.com>`_ to use it. This script 
+will allow you to plug akismet into any CGI script or web application, and 
+there are full docs in the code. It's extremely easy to use, because the folks 
+at  akismet have implemented a nice and straightforward 
+{acro;REST;REpresentational State Transfer} 
+{acro;API;Application Programmers Interface}.
+
+.. note::
+
+    If possible you should build into your program the ability to inform
+    Akismet of false positives and false negatives.
+    
+    Informing Akismet helps makes the service more reliable. {sm;:-)}
+    
+    To do this, use the `submit-spam`_ and `submit-ham`_ functionality.
+
+Most of the work is done by the `comment-check`_ function.
+
+Downloading
+-----------
+
+You can download **akismet.py** from :
+
+* `akismet.zip (48k) <http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=akismet.zip>`_
+
+This contains the docs, **akismet.py**, and a test {acro;CGI} called 
+*test_akismet.py*.
+
+
+apikey.txt
+==========
+
+The easiest way to use *akismet.py* is to provide the wordpress key and blog
+{acro;URL} in a text file called ``apikey.txt``.
+
+This should be in the current directory when you create your ``Akismet``
+instance (or call setAPIKey_ with no arguments).
+
+The format for ``apikey.txt`` is simple (see the example one in the
+distribution.
+
+Lines that start with a ``#`` are comments. The first non-blank, non-comment
+line should be the API key. The second line should be the blog URL (or 
+application URL) to use.
+
+::
+
+    # Lines starting with '#' are comments
+    # The first non-blank, non-comment, line should be your api key
+    # The second your blog URL
+    #
+    # You can get a wordpress API key from http://wordpress.com/
+    some_key
+    some_blog_url
+
+
+The Akismet Class
+=================
+
+The akismet_ API provides four functions. *akismet.py* gives you access to
+all of these through a single class.
+
+The four akismet functions are :
+
+* `verify-key`_     - the ``verify_key`` method
+* `comment-check`_  - the ``comment_check`` method
+* `submit-spam`_   - the ``submit_spam`` method
+* `submit-ham`_   - the ``submit_ham`` method
+
+In addition to these, the Akismet class has the following user methods and
+attributes :
+
+* setAPIKey_    - method
+* key           - attribute
+* blog_url      - attribute
+
+
+Creating an Instance
+--------------------
+
+::
+
+    Akismet(key=None, blog_url=None, agent=None)
+
+To use the akismet web service you *need* an API key. There are three ways of
+telling the ``Akismet`` class what this is.
+
+1) When you create a new ``Akismet`` instance you can pass in the API key and
+   blog url.
+2) If you don't pass in a key, it will automatically look for apikey.txt_ in
+   the current directory, and attempt to load it.
+3) You can set the ``key`` and ``blog_url`` Attributes manually, after creating
+   your instance.
+
+
+User-Agent
+~~~~~~~~~~
+
+As well as setting your key, you *ought* to pass in a string for ``Akismet`` to
+create a User-Agent header with. This is the ``agent`` argument.
+
+According to the `API docs <http://akismet.com/development/api/>`_, this ought
+to be in the form : ::
+
+    Program Name/Version
+
+*akismet.py* adds it's version number to this, to create a User-Agent in the
+form liked by akismet.
+
+The default User-Agent (if you don't pass in a values to ``agent``) is : ::
+
+    Python Interface by Fuzzyman/0.1.5 | akismet.py/0.1.5
+
+
+Example 1
+~~~~~~~~~
+
+.. raw:: html
+
+    {+coloring}
+    
+    #example 1
+    api = Akismet(api_key, url, agent='Example/0.1') 
+    
+    {-coloring}
+
+Example 2
+~~~~~~~~~
+
+.. raw:: html
+
+    {+coloring}
+    
+    #example 2
+    if os.path.isfile('apikey.txt'):
+        api = Akismet(agent='Example/0.2') 
+    
+    # The key and URL are loaded from
+    # 'apikey.txt'
+    
+    {-coloring}
+
+Example 3
+~~~~~~~~~
+
+.. raw:: html
+
+    {+coloring}
+    
+    #example 2
+    url = 'http://www.voidspace.org.uk/cgi-bin/voidspace/guestbook.py'
+    api_key = '0acdfg1fr'
+    if not os.path.isfile('apikey.txt'):
+        api = Akismet(agent='Example/0.3') 
+        api.key = api_key
+        api.blog_url = url
+    
+    # The key and URL are set manually
+    
+    {-coloring}
+
+
+setAPIKey
+---------
+
+::
+
+    setAPIKey(key=None, blog_url=None)
+
+Set the wordpress API key for all transactions.
+
+If you don't specify an explicit API ``key`` and ``blog_url`` it will
+attempt to load them from a file called ``apikey.txt`` in the current
+directory.
+
+This method is *usually* called automatically when you create a new ``Akismet``
+instance.
+
+
+Akismet Methods
+---------------
+
+These four methods equate to the four functions of the `Akismet API <http://akismet.com/development/api/>`_.
+
+verify-key
+~~~~~~~~~~
+
+::
+
+    verify_key()
+
+This equates to the ``verify-key`` call against the akismet API.
+
+It returns ``True`` if the key is valid.
+
+The docs state that your program *ought* to call this at the start of the
+transaction.
+
+It raises ``APIKeyError`` if you have not yet set an API key.
+
+If the connection to akismet fails, it allows the normal ``HTTPError``
+or ``URLError`` to be raised. (*akismet.py* uses 
+`urllib2 <http://docs.python.org/lib/module-urllib2.html>`_)
+
+
+comment-check
+~~~~~~~~~~~~~
+
+::
+
+    comment_check(comment, data=None, build_data=True, DEBUG=False)
+
+This is the main function in the Akismet API. It checks comments.
+
+It returns ``True`` for spam and ``False`` for ham.
+
+If you set ``DEBUG=True`` then it will return the text of the response,
+instead of the ``True`` or ``False`` object.
+
+It raises ``APIKeyError`` if you have not yet set an API key.
+
+If the connection to Akismet fails then the ``HTTPError`` or
+``URLError`` will be propogated.
+
+As a minimum it requires the body of the comment. This is the
+``comment`` argument.
+
+Akismet requires some other arguments, and allows some optional ones.
+The more information you give it, the more likely it is to be able to
+make an accurate diagnosise.
+
+You supply these values using a mapping object (dictionary) as the
+``data`` argument.
+
+If ``build_data`` is ``True`` (the default), then *akismet.py* will
+attempt to fill in as much information as possible, using default
+values where necessary. This is particularly useful for programs
+running in a {acro;CGI} environment. A lot of useful information
+can be supplied from evironment variables (``os.environ``). See below.
+
+You *only* need supply values for which you don't want defaults filled
+in for. All values must be strings.
+
+There are a few required values. If they are not supplied, and
+defaults can't be worked out, then an ``AkismetError`` is raised.
+
+If you set ``build_data=False`` and a required value is missing an
+``AkismetError`` will also be raised.
+
+The normal values (and defaults) are as follows :
+
+*    'user_ip':          ``os.environ['REMOTE_ADDR']``       (*)
+*    'user_agent':       ``os.environ['HTTP_USER_AGENT']``   (*)
+*    'referrer':         ``os.environ.get('HTTP_REFERER', 'unknown')`` [#]_
+*    'permalink':        ''
+*    'comment_type':     'comment' [#]_
+*    'comment_author':   ''
+*    'comment_author_email': ''
+*    'comment_author_url': ''
+*    'SERVER_ADDR':      ``os.environ.get('SERVER_ADDR', '')``
+*    'SERVER_ADMIN':     ``os.environ.get('SERVER_ADMIN', '')``
+*    'SERVER_NAME':      ``os.environ.get('SERVER_NAME', '')``
+*    'SERVER_PORT':      ``os.environ.get('SERVER_PORT', '')``
+*    'SERVER_SIGNATURE': ``os.environ.get('SERVER_SIGNATURE', '')``
+*    'SERVER_SOFTWARE':  ``os.environ.get('SERVER_SOFTWARE', '')``
+*    'HTTP_ACCEPT':      ``os.environ.get('HTTP_ACCEPT', '')``
+
+(*) Required values
+
+You may supply as many additional **'HTTP_*'** type values as you wish.
+These should correspond to the http headers sent with the request.
+
+
+
+submit-spam
+~~~~~~~~~~~
+
+::
+
+    submit_spam(comment, data=None, build_data=True)
+
+This function is used to tell akismet that a comment it marked as ham,
+is really spam.
+
+It takes all the same arguments as ``comment_check``, except for
+*DEBUG*.
+
+
+submit-ham
+~~~~~~~~~~
+
+::
+
+    submit_ham(self, comment, data=None, build_data=True)
+
+This function is used to tell akismet that a comment it marked as spam,
+is really ham.
+
+It takes all the same arguments as ``comment_check``, except for
+*DEBUG*.
+
+
+Error Classes
+=============
+
+In the course of using *akismet.py*, there are two possible errors you could
+see.
+
+AkismetError
+------------
+
+This is for general Akismet errors. For example, if you didn't supply some of
+the required information.
+
+This error is a subclass of ``Exception``.
+
+This error is also raised if there is a network connection error. This can happen when the Akismet
+service or domain goes down temporarily.
+
+Your code should trap this and handle it appropriately (either let the comment through or push it
+onto a moderation queue).
+
+
+APIKeyError
+-----------
+
+If *apikey.txt* is invalid, or you attempt to call one of the `akismet methods`_
+without setting a key, you will get an ``APIKeyError``.
+
+This error is a subclass of ``AkismetError``.
+
+
+Usage Example
+=============
+
+A simple example that loads the key automatically, verifies the key, and then
+checks a comment.
+
+.. raw:: html
+
+    {+coloring}
+    
+    api = Akismet(agent='Test Script')
+    # if apikey.txt is in place,
+    # the key will automatically be set
+    # or you can call ``api.setAPIKey()``
+    #
+    if api.key is None:
+        print "No 'apikey.txt' file."
+    elif not api.verify_key():
+        print "The API key is invalid."
+    else:
+        # data should be a dictionary of values
+        # They can all be filled in with defaults
+        # from a CGI environment
+        if api.comment_check(comment, data):
+            print 'This comment is spam.'
+        else:
+            print 'This comment is ham.'
+    
+    {-coloring}
+
+
+Akismet Test CGI
+================
+
+Included in the distribution is a file called ``test_akismet.py``.
+
+This is a simple test CGI. It needs `cgiutils <http://www.voidspace.org.uk/python/recipebook.shtml#util>`_
+to run.
+
+When activated, it allows you to put a comment in and test it with akismet. It
+will tell you if the comment is marked as *ham*, or *spam*.
+
+To confirm that your setup is working; any post with **viagra-test-123** as the
+name, should be marked as spam.
+
+Obviously you will need an API key for this to work.
+
+You can try this online at :
+
+    `Akismet Example CGI <http://www.voidspace.org.uk/cgi-bin/akismet/test_akismet.py>`_
+
+
+---------------------
+
+
+TODO
+====
+
+Make the timeout adjustable ?
+
+Should we fill in a default value for permalink ?
+
+What about automatically filling in the 'HTTP_*' values from os.environ ?
+
+CHANGELOG
+=========
+
+2007/02/05      Version 0.1.5
+-----------------------------
+
+Fixed a typo/bug in ``submit_ham``. Thanks to Ian Ozsvald for pointing this out.
+
+2006/12/13      Version 0.1.4
+-----------------------------
+
+Akismet now traps errors in connections. If there is a network error it raises an ``AkismetError``.
+
+This can happen when the Akismet service or domain goes down temporarily.
+
+Your code should trap this and handle it appropriately (either let the comment through or push it onto a moderation
+queue).
+
+2006/07/18      Version 0.1.3
+-----------------------------
+
+Add the blog url to the data. Bugfix thanks to James Bennett.
+
+2005/12/04      Version 0.1.2
+-----------------------------
+
+Added the ``build_data`` argument to ``comment_check``, ``submit_spam``, and