"""A PostgreSQL client-side asynchronous notification handler.

pgnotify is a select()-based PostgreSQL client-side asynchronous
notification handler for Python. 

Copyright (c) 2001 Ng Pheng Siong. All rights reserved.

Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation.

NG PHENG SIONG PROVIDES THIS SOFTWARE ``AS IS'' AND ANY EXPRESSED OR 
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  
IN NO EVENT SHALL NG PHENG SIONG BE LIABLE FOR ANY DIRECT, INDIRECT, 
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

RCS_id = '$Id: pgnotify.py,v 2.3 2001/06/09 05:06:21 ngps Exp ngps $'

import select, thread, types


class pgnotifyError(Exception):
    """This error is impossible."""
    pass


class pgnotify:

    """A PostgreSQL client-side asynchronous notification handler.""" 

    def __init__(self, pgconn, event, callback, arg_dict={}, timeout=None):
        """
        pgconn   - PostgreSQL connection object.
        event    - Event to LISTEN for.
        callback - Event callback.
        arg_dict - A dictionary passed as the argument to the callback.
        timeout  - Timeout in seconds; a floating pointer number denotes
                    fractions of seconds. If it is absent or None, the
                    callers will never time out.
        """
        self.pgconn = pgconn
        self.event = event
        self.stop = 'stop_%s' % event
        self.callback = callback
        self.arg_dict = arg_dict
        self.timeout = timeout

    def __del__(self):
        try:
            self.pgconn.query('unlisten "%s"' % self.event)
            self.pgconn.query('unlisten "%s"' % self.stop)
        except pg.DatabaseError:
            pass

    def __call__(self):
        """
        Invoke the handler. The handler actually LISTENs for two NOTIFY messages:
        <event> and stop_<event>. 
        
        When either of these NOTIFY messages are received, its associated
        'pid' and 'event' are inserted into <arg_dict>, and the callback is
        invoked with <arg_dict>. If the NOTIFY message is stop_<event>, the
        handler UNLISTENs both <event> and stop_<event> and exits.

        """
        self.pgconn.query('listen "%s"' % self.event)
        self.pgconn.query('listen "%s"' % self.stop)
        _ilist = [self.pgconn.fileno()]
        while 1:
            ilist, olist, elist = select.select(_ilist, [], [], self.timeout)
            if ilist == []:
                # We timed out.
                self.pgconn.query('unlisten "%s"' % self.event)
                self.pgconn.query('unlisten "%s"' % self.stop)
                self.callback(None)
                return
            else:
                notice = self.pgconn.getnotify()  
                if notice is None:
                    pass
                event, pid = notice
                if event in (self.event, self.stop):
                    self.arg_dict['pid'] = pid
                    self.arg_dict['event'] = event
                    self.callback(self.arg_dict)
                    if event == self.stop:
                        self.pgconn.query('unlisten "%s"' % self.event)
                        self.pgconn.query('unlisten "%s"' % self.stop)
                        return
                else:
                    self.pgconn.query('unlisten "%s"' % self.event)
                    self.pgconn.query('unlisten "%s"' % self.stop)
                    raise pgnotifyError(
                        'listening for ("%s", "%s") but notified of "%s"' \
                        % (self.event, self.stop, event))



