[Checkins] SVN: zc.ngi/trunk/ Initial version

Jim Fulton jim at zope.com
Fri Aug 11 08:55:35 EDT 2006


Log message for revision 69400:
  Initial version
  

Changed:
  A   zc.ngi/trunk/bootstrap.py
  A   zc.ngi/trunk/buildout.cfg
  A   zc.ngi/trunk/setup.py
  A   zc.ngi/trunk/src/
  A   zc.ngi/trunk/src/zc/
  A   zc.ngi/trunk/src/zc/__init__.py
  A   zc.ngi/trunk/src/zc/ngi/
  A   zc.ngi/trunk/src/zc/ngi/README.txt
  A   zc.ngi/trunk/src/zc/ngi/__init__.py
  A   zc.ngi/trunk/src/zc/ngi/adapters.py
  A   zc.ngi/trunk/src/zc/ngi/adapters.txt
  A   zc.ngi/trunk/src/zc/ngi/async.py
  A   zc.ngi/trunk/src/zc/ngi/async.txt
  A   zc.ngi/trunk/src/zc/ngi/interfaces.py
  A   zc.ngi/trunk/src/zc/ngi/message.py
  A   zc.ngi/trunk/src/zc/ngi/message.txt
  A   zc.ngi/trunk/src/zc/ngi/notes.txt
  A   zc.ngi/trunk/src/zc/ngi/testing.py
  A   zc.ngi/trunk/src/zc/ngi/tests.py
  A   zc.ngi/trunk/src/zc/ngi/wordcount.py

-=-
Added: zc.ngi/trunk/bootstrap.py
===================================================================
--- zc.ngi/trunk/bootstrap.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/bootstrap.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                     ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+    os.P_WAIT, sys.executable, sys.executable,
+    '-c', 'from setuptools.command.easy_install import main; main()',
+    '-mqNxd', tmpeggs, 'zc.buildout',
+    {'PYTHONPATH':
+     ws.find(pkg_resources.Requirement.parse('setuptools')).location
+     },
+    ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)


Property changes on: zc.ngi/trunk/bootstrap.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/buildout.cfg
===================================================================
--- zc.ngi/trunk/buildout.cfg	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/buildout.cfg	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,7 @@
+[buildout]
+develop = .
+parts = test
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ngi


Property changes on: zc.ngi/trunk/buildout.cfg
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/setup.py
===================================================================
--- zc.ngi/trunk/setup.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/setup.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+
+name = 'ngi'
+setup(
+    name = name,
+    version = "0.1",
+    author = "Jim Fulton",
+    author_email = "jim#zope.com",
+    description = "Network Gateway Interface",
+    license = "ZPL 2.1",
+    keywords = "network",
+    url='http://svn.zope.org/ngi',
+
+    packages = find_packages('src'),
+    include_package_data = True,
+    package_dir = {'':'src'},
+    namespace_packages = ['zc'],
+    install_requires = ['setuptools', 'zope.testing'],
+    )


Property changes on: zc.ngi/trunk/setup.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/__init__.py
===================================================================
--- zc.ngi/trunk/src/zc/__init__.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/__init__.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)


Property changes on: zc.ngi/trunk/src/zc/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/README.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/README.txt	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/README.txt	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,385 @@
+=========================
+Network Gateway Interface
+=========================
+
+Network programs are typically difficult to test because they require
+setting up network connections, clientts, and servers.  In addition,
+application code gets mixed up with networking code.
+
+The Network Gateway Interface (NGI) seeks to improve this situation by
+separating application code from network code.  This allows
+application and network code to be tested indepenndly and provides
+greater separation of concerns.
+
+There are several interfaces defined by the NGI:
+
+IConnection
+    Network connection implementation.  This is the core interface that
+    applications interact with,
+
+IConnectionHandler
+    Application component that handles network input.  
+
+IConnector
+    Create IConnection objects by making outgoing connections.
+
+IClientConnectHandler
+    Application callback that handles successful ot failed outgoing
+    connections.
+
+IListener
+    Listed for incoming connections.
+
+IServer
+    Callback to handle incoming connections.
+
+The interfaces are split between "implementation" and "application"
+interfaces.  An implementation of the NGI provides IConnection,
+IConnector, and IListener. An application provides IConnectionHandler
+and one or both of IClientConnectHandler and IServer.
+
+For more information, see interfaces.py.
+
+Testing Implementation
+======================
+
+These interface can have a number of implementations.  The simplest
+implementation is the testing implementation, which is used to test
+application code.
+
+    >>> import zc.ngi.testing
+
+The testing module provides IConnection, IConnector, and IListener
+implentations. We'll use this below to illustrate how application code
+is written.
+
+Implementing Network Clients
+============================
+
+Network clients make connections to and then use these connections to
+communicate with servers.  To do so, a client must be provided with an
+IConnector implemantation.  How this happens is outside the scope of
+the NGI.  An IConnector implementation could, for example, be provided
+via the Zope component architecture, or via package_resources entry
+points.
+
+Let's create a simple client that calls an echo server and verifies
+that the server properly echoes data sent do it.
+
+    >>> class EchoClient:
+    ...
+    ...     def __init__(self, connector):
+    ...         self.connector = connector
+    ...
+    ...     def check(self, addr, strings):
+    ...         self.strings = strings
+    ...         self.connector(addr, self)
+    ...
+    ...     def connected(self, connection):
+    ...         for s in self.strings:
+    ...             connection.write(s + '\n')
+    ...         self.input = ''
+    ...         connection.setHandler(self)
+    ...
+    ...     def failed_connect(self, reason):
+    ...         print 'failed connect:', reason
+    ...
+    ...     def handle_input(self, connection, data):
+    ...         print 'got input:', repr(data)
+    ...         self.input += data
+    ...         while '\n' in self.input:
+    ...             data, self.input = self.input.split('\n', 1)
+    ...             if self.strings:
+    ...                expected = self.strings.pop(0)
+    ...                if data == expected:
+    ...                    print 'matched:', data
+    ...                else:
+    ...                    print 'unmatched:', data
+    ...                if not self.strings:
+    ...                    connection.close()
+    ...             else:
+    ...                print 'Unexpected input', data
+    ...
+    ...     def handle_close(self, connection, reason):
+    ...         print 'closed:', reason
+    ...         if self.strings:
+    ...             print 'closed prematurely'    
+
+
+The client impements the IClientConnectHandler and IInputHandler
+interfaces.  More complex clients might implement these interfacs with
+separate classes.
+
+We'll instantiate our client using the testing connector:
+
+    >>> client = EchoClient(zc.ngi.testing.connector)
+
+Now we'll try to check a non-existent server:
+
+    >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+    failed connect: no such server
+
+Our client simply prints a message (and gives up) if a connection
+fails. More complex applications might retry, waiting between attemps,
+and so on.
+
+The testing connector always fails unless given a test connection
+ahead of time.  We'll create a testing connection and register it so a
+connection can suceed:
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> zc.ngi.testing.connectable(('localhost', 42), connection)
+
+We can register multiple connections with the same address:
+
+    >>> connection2 = zc.ngi.testing.Connection()
+    >>> zc.ngi.testing.connectable(('localhost', 42), connection2)
+
+The connections will be used in order.
+
+Now, our client should be able to connect to the first connection we
+created:
+
+    >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+    -> 'hello\n'
+    -> 'world\n'
+    -> 'how are you?\n'
+
+The test connection echoes data written to it, preceeded by "-> ".
+
+Active connections are true:
+
+    >>> bool(connection2)
+    True
+
+Test connections provide mehods generating test input and flow closing
+connections.  We can use these to simulate network events.  Let's
+generate some input for our client:
+
+    >>> connection.test_input('hello')
+    got input: 'hello'
+
+    >>> connection.test_input('\nbob\n')
+    got input: '\nbob\n'
+    matched: hello
+    unmatched: bob
+
+    >>> connection.test_close('done')
+    closed: done
+    closed prematurely
+
+    >>> client.check(('localhost', 42), ['hello'])
+    -> 'hello\n'
+
+    >>> connection2.test_input('hello\n')
+    got input: 'hello\n'
+    matched: hello
+    -> CLOSE
+
+    >>> bool(connection2)
+    False
+    
+
+Implementing network servers
+============================
+
+Implementing network servers is very similar to implementing clients,
+except that a listener, rather than a connector is used.  Let's
+implement a simple echo server:
+
+
+    >>> class EchoServer:
+    ...
+    ...     def __init__(self, connection):
+    ...         print 'server connected'
+    ...         self.input = ''
+    ...         connection.setHandler(self)
+    ...
+    ...     def handle_input(self, connection, data):
+    ...         print 'server got input:', repr(data)
+    ...         self.input += data
+    ...         if '\n' in self.input:
+    ...             data, self.input = self.input.split('\n', 1)
+    ...             connection.write(data + '\n')
+    ...             if data == 'Q':
+    ...                 connection.close()
+    ...
+    ...     def handle_close(self, connection, reason):
+    ...         print 'server closed:', reason
+
+Out EchoServer *class* provides IServer and implement IInputHandler.
+
+To use a server, we need a listener.  We'll use the use the testing
+listener:
+
+    >>> listener = zc.ngi.testing.listener(EchoServer)
+
+To simulate a client connection, we create a testing connection and
+call the listener's connect method:
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> listener.connect(connection)
+    server connected
+
+    >>> connection.test_input('hello\n')
+    server got input: 'hello\n'
+    -> 'hello\n'
+
+    >>> connection.test_close('done')
+    server closed: done
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> listener.connect(connection)
+    server connected
+
+    >>> connection.test_input('hello\n')
+    server got input: 'hello\n'
+    -> 'hello\n'
+
+    >>> connection.test_input('Q\n')
+    server got input: 'Q\n'
+    -> 'Q\n'
+    -> CLOSE
+
+Note that it is an error to write to a closed connection:
+
+    >>> connection.write('Hello')
+    Traceback (most recent call last):
+    ...
+    TypeError: Connection closed
+
+
+Server Control
+--------------
+
+The object returned from a listener is a IServerControl.  It provides
+access to the active connections:
+
+    >>> list(listener.connections())
+    []
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> listener.connect(connection)
+    server connected
+   
+    >>> list(listener.connections()) == [connection]
+    True
+
+    >>> connection2 = zc.ngi.testing.Connection()
+    >>> listener.connect(connection2)
+    server connected
+
+    >>> len(list(listener.connections()))
+    2
+    >>> connection in list(listener.connections())
+    True
+    >>> connection2 in list(listener.connections())
+    True
+
+Server connections have a control attribute that is the connections
+server control:
+
+    >>> connection.control is listener
+    True
+
+Server control objects provide a close method that allows a server to
+be shut down.  If the close method is called without arguments, then
+then all server connections are closed immediately and no more
+connections are accepted:
+
+    >>> listener.close()
+    server closed: stopped
+    server closed: stopped
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> listener.connect(connection)
+    Traceback (most recent call last):
+    ...
+    TypeError: Listener closed
+
+If a handler function is passed, then connections aren't closed
+immediately:
+
+    >>> listener = zc.ngi.testing.listener(EchoServer)
+    >>> connection = zc.ngi.testing.Connection()
+    >>> listener.connect(connection)
+    server connected
+    >>> connection2 = zc.ngi.testing.Connection()
+    >>> listener.connect(connection2)
+    server connected
+
+    >>> def handler(control):
+    ...     if control is listener:
+    ...        print 'All connections closed'
+
+    >>> listener.close(handler)
+
+But no more connections are accepted:
+
+    >>> connection3 = zc.ngi.testing.Connection()
+    >>> listener.connect(connection3)
+    Traceback (most recent call last):
+    ...
+    TypeError: Listener closed
+    
+And the handler will be called when all of the listener's connections
+are closed:
+
+    >>> connection.close()
+    -> CLOSE
+    >>> connection2.close()
+    -> CLOSE
+    All connections closed
+
+Long output
+===========
+
+Test requests output data written to them.  If output exceeds 50
+characters in length, it is wrapped by simply breakng the repr into 
+50-characters parts:
+
+    >>> connection = zc.ngi.testing.Connection()
+    >>> connection.write('hello ' * 50)
+    -> 'hello hello hello hello hello hello hello hello h
+    .> ello hello hello hello hello hello hello hello hel
+    .> lo hello hello hello hello hello hello hello hello
+    .>  hello hello hello hello hello hello hello hello h
+    .> ello hello hello hello hello hello hello hello hel
+    .> lo hello hello hello hello hello hello hello hello
+    .>  '
+
+END_OF_DATA
+===========
+
+Closing a connection closes it immediately, without sending any
+pending data.  An alternate way to close a connection is to write
+zc.ngi.END_OF_DATA. The connection will be automatically closed when
+zc.ngi.END_OF_DATA is encountered in the output stream.
+
+    >>> connection.write(zc.ngi.END_OF_DATA)
+    -> CLOSE
+
+    >>> connection.write('Hello')
+    Traceback (most recent call last):
+    ...
+    TypeError: Connection closed
+
+Peer connectors
+===============
+
+It is sometimes useful to connect a client handler and a server
+handler.  The zc.ngi.testing.peer function can be used to create a 
+connection to a peer handler. To illustrate, we'll set up an echo
+client that connects to our echo server:
+
+    >>> client = EchoClient(zc.ngi.testing.peer(('localhost', 42), EchoServer))
+    >>> client.check(('localhost', 42), ['hello', 'world', 'how are you?'])
+    server connected
+    server got input: 'hello\n'
+    server got input: 'world\n'
+    server got input: 'how are you?\n'
+    got input: 'hello\nworld\nhow are you?\n'
+    matched: hello
+    matched: world
+    matched: how are you?
+    server closed: closed


Property changes on: zc.ngi/trunk/src/zc/ngi/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/__init__.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/__init__.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/__init__.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,19 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Network Gateway Interface
+
+$Id$
+"""
+
+END_OF_DATA = object()


Property changes on: zc.ngi/trunk/src/zc/ngi/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/adapters.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/adapters.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/adapters.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,42 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""NGI connection adapters
+
+$Id$
+"""
+
+class Lines:
+
+    def __init__(self, connection):
+        self.connection = connection
+        self.close = connection.close
+        self.write = connection.write
+
+    def setHandler(self, handler):
+        self.handler = handler
+        self.input = ''
+        self.connection.setHandler(self)
+
+    def handle_input(self, connection, data):
+        self.input += data
+        data = self.input.split('\n')
+        self.input = data.pop()
+        for line in data:
+            self.handler.handle_input(self, line)
+
+    def handle_close(self, connection, reason):
+        self.handler.handle_close(self, reason)
+
+
+    


Property changes on: zc.ngi/trunk/src/zc/ngi/adapters.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/adapters.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/adapters.txt	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/adapters.txt	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,62 @@
+============
+NGI Adapters
+============
+
+The NGI is a fairly low-level event-based framework.  Adapters can be
+used to build higher-level sematics.  In this document, we'll describe
+some sample adapters that provide more examples of using the NGI and
+useful building blocks for other applications. The sound for these
+adapters can be found in the zc.ngi.adapters module.
+
+Lines
+=====
+
+The first adapter we'll look at collects input into lines. To
+illustrate this, we'll use a handler from zc.ngi.testing that simply
+prints its input:
+
+    >>> import zc.ngi.testing
+    >>> connection = zc.ngi.testing.Connection()
+    >>> handler = zc.ngi.testing.PrintingHandler(connection)
+ 
+This handler is used by by default as the peer
+of testing connections:
+
+    >>> connection.test_input('x' * 80)
+    -> 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+    .> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+
+    >>> connection.test_close('test')
+    -> CLOSE test
+
+Now, we'll use the lines adapter to break input into lines, separated
+by newlines.  We apply the adapter to a connection:
+
+    >>> import zc.ngi.adapters
+    >>> connection = zc.ngi.testing.Connection()
+    >>> adapter = zc.ngi.adapters.Lines(connection)
+    >>> handler = zc.ngi.testing.PrintingHandler(adapter)
+
+Now, when we provide input, it won't appear until lines are complete:
+
+    >>> connection.test_input('Hello world!')
+    >>> connection.test_input('\n')
+    -> 'Hello world!'
+
+    >>> connection.test_input('Hello\nWorld!\nHow are you')
+    -> 'Hello'
+    -> 'World!'
+
+Only input handling is affected.  Other methods of the adapter behave
+as would the underlying connnection:
+
+    >>> adapter.write('foo')
+    -> 'foo'
+
+    >>> connection.test_close('test')
+    -> CLOSE test
+
+The original connection is available in the connection attribute:
+
+    >>> adapter.connection is connection
+    True


Property changes on: zc.ngi/trunk/src/zc/ngi/adapters.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/async.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/async.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,449 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Asyncore-based implementation of the NGI
+
+$Id$
+"""
+
+import asyncore
+import errno
+import logging
+import os
+import select
+import socket
+import sys
+import threading
+import time
+
+import zc.ngi
+
+pid = os.getpid()
+
+_map = {}
+
+expected_socket_read_errors = {
+    errno.EWOULDBLOCK: 0,
+    errno.EAGAIN: 0,
+    errno.EINTR: 0,
+    }
+
+expected_socket_write_errors = {
+    errno.EAGAIN: 0,
+    errno.EWOULDBLOCK: 0,
+    errno.ENOBUFS: 0,
+    errno.EINTR: 0,
+    }
+
+class dispatcher(asyncore.dispatcher):
+
+    def __init__(self, sock, addr):
+        self.addr = addr
+        asyncore.dispatcher.__init__(self, sock, _map)
+
+    def handle_error(self):
+        reason = sys.exc_info()[1]
+        self.logger.exception('handle_error')
+        self.handle_close(reason)
+        self.close()
+
+    def close(self):
+        self.del_channel(_map)
+        self.socket.close()
+
+    def writable(self):
+        return False
+
+class _Connection(dispatcher):
+
+    control = None
+
+    def __init__(self, sock, addr, logger):
+        self.__connected = True
+        self.__closed = None
+        self.__handler = None
+        self.__output = []
+        self.__handler_lock = threading.Lock()
+        dispatcher.__init__(self, sock, addr)
+        self.logger = logger
+
+    def __nonzero__(self):
+        return self.__connected
+
+    def setHandler(self, handler):
+        if self.__handler is not None:
+            raise TypeError("Handler already set")
+
+        self.__handler_lock.acquire()
+        try:
+            self.__handler = handler
+            if self.__closed:
+                handler.handle_close(seld, self.__closed)
+        finally:
+            self.__handler_lock.release()
+        
+    def write(self, data):
+        if __debug__:
+            self.logger.debug('write %r', data)
+        self.__output.append(data)
+        notify_select()
+
+    def close(self):
+        self.__connected = False
+        dispatcher.close(self)
+        if self.control is not None:
+            self.control.closed(self)
+        notify_select()
+
+    def readable(self):
+        return self.__handler is not None
+
+    def writable(self):
+        return bool(self.__output)
+
+    def handle_read_event(self):
+        assert self.readable()
+        
+        while 1:
+            try:
+                d = self.recv(8192)
+            except socket.error, err:
+                if err[0] in expected_socket_read_errors:
+                    return
+                raise
+
+            if not d:
+                return
+
+            if __debug__:
+                self.logger.debug('input %r', d)
+            self.__handler.handle_input(self, d)
+            if len(d) < 8192:
+                break
+
+    def handle_write_event(self):
+        if __debug__:
+            self.logger.debug('handle_write_event')
+
+        while self.__output:
+            output = self.__output
+            v = output[0]
+            if v == zc.ngi.END_OF_DATA:
+                self.close()
+                return
+            
+            try:
+                n = self.send(v)
+            except socket.error, err:
+                if err[0] in expected_socket_write_errors:
+                    return # we couldn't write anything
+                raise
+            
+            if n == len(v):
+                output.pop(0)
+            else:
+                output[0] = v[n:]
+                return # can't send any more
+            
+    def handle_close(self, reason='end of input'):
+        if __debug__:
+            self.logger.debug('close %r', reason)
+        self.__handler_lock.acquire()
+        try:
+            if self.__handler is not None:
+                self.__handler.handle_close(self, reason)
+            else:
+                self.__closed = reason
+            self.close()
+        finally:
+            self.__handler_lock.release()
+
+    def handle_expt(self):
+        self.handle_close('expt')
+        
+
+class connector(dispatcher):
+
+    logger = logging.getLogger('zc.ngi.async.client')
+
+    # When trying to do a connect on a non-blocking socket, some outcomes
+    # are expected.  Set _CONNECT_IN_PROGRESS to the errno value(s) expected
+    # when an initial connect can't complete immediately.  Set _CONNECT_OK
+    # to the errno value(s) expected if the connect succeeds *or* if it's
+    # already connected (our code can attempt redundant connects).
+    if hasattr(errno, "WSAEWOULDBLOCK"):    # Windows
+        # Caution:  The official Winsock docs claim that WSAEALREADY should be
+        # treated as yet another "in progress" indicator, but we've never
+        # seen this.
+        _CONNECT_IN_PROGRESS = (errno.WSAEWOULDBLOCK,)
+        # Win98: WSAEISCONN; Win2K: WSAEINVAL
+        _CONNECT_OK          = (0, errno.WSAEISCONN, errno.WSAEINVAL)
+    else:                                   # Unix
+        _CONNECT_IN_PROGRESS = (errno.EINPROGRESS,)
+        _CONNECT_OK          = (0, errno.EISCONN)
+
+    def __init__(self, addr, handler):
+        self.__handler = handler
+        if isinstance(addr, str):
+            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        else:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+        if __debug__:
+            self.logger.debug('connecting to %s', addr)
+        dispatcher.__init__(self, sock, addr)
+        try:
+            if self.handle_write_event():
+                return
+        except:
+            self.handle_error()
+
+        notify_select()
+
+    def readable(self):
+        return False
+
+    def writable(self):
+        return True
+
+    def handle_close(reason):
+        if __debug__:
+            self.logger.debug('connector close %r', reason)
+        self.__handler.failed_connect(reason)
+        self.close()
+ 
+    def handle_write_event(self):
+        err = self.socket.connect_ex(self.addr)
+        if err in self._CONNECT_IN_PROGRESS:
+            return 
+
+        if err not in self._CONNECT_OK:
+            reason = errno.errorcode.get(err) or str(err)
+            self.logger.warning("error connecting to %s: %s", self.addr, reason)
+            self.handle_close(reason)
+            return
+
+        self.del_channel(_map)
+        if __debug__:
+            self.logger.debug('outgoing connected %r', self.addr)
+        self.__handler.connected(
+            _Connection(self.socket, self.addr, self.logger))
+        return
+
+class listener(asyncore.dispatcher):
+
+    logger = logging.getLogger('zc.ngi.async.server')
+
+    def __init__(self, addr, handler):
+        self.addr = addr
+        self.__handler = handler
+        self.__close_handler = None
+        self.__connections = {}
+        asyncore.dispatcher.__init__(self)
+        if isinstance(addr, str):
+            self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        else:
+            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.set_reuse_addr()
+        self.logger.info("listening on %s", self.addr)
+        self.bind(self.addr)
+        self.listen(5)
+        notify_select()
+
+    def handle_accept(self):
+        if not self.accepting:
+            return
+
+        try:
+            sock, addr = self.accept()
+        except socket.error, msg:
+            self.logger.exception("accepted failed: %s", msg)
+            return
+        if __debug__:
+            self.logger.debug('incoming connection %r', addr)
+
+        connection = _Connection(sock, addr, self.logger)
+        self.__connections[connection] = 1
+        connection.control = self
+        self.__handler(connection)
+
+    def connections(self):
+        return iter(self.__connections)
+
+    def closed(self, connection):
+        if connection in self.__connections:
+            del self.__connections[connection]
+            if not self.__connections and self.__close_handler:
+                self.__close_handler(self)
+
+    def close(self, handler=None):
+        self.accepting = False
+        self.del_channel(_map)
+        self.socket.close()
+        if handler is None:
+            for c in list(self._connections):
+                c.handle_close("stopped")
+        elif not self.__connections:
+            handler(self)
+        else:
+            self.__close_handler = handler
+
+    def add_channel(self):
+        # work around file-dispatcher bug
+        asyncore.dispatcher.add_channel(self, _map)
+
+    def handle_error(self):
+        reason = sys.exc_info()[1]
+        self.logger.exception('listener error')
+        self.close()
+    
+# The following trigger code is greatly simplified from the Medusa
+# trigger code.
+
+class _Triggerbase(object):
+    """OS-independent base class for OS-dependent trigger class."""
+
+    logger = logging.getLogger('zc.ngi.async.trigger')
+
+    def writable(self):
+        return 0
+
+    def handle_close(self):
+        self.close()
+
+    def handle_error(self):
+        self.logger.exception('trigger error %s', pid)
+        self.close()
+
+    def handle_read(self):
+        try:
+            self.recv(8192)
+        except socket.error:
+            return
+
+if os.name == 'posix':
+
+    class _Trigger(_Triggerbase, asyncore.file_dispatcher):
+        def __init__(self):
+            self.__readfd, self.__writefd = os.pipe()
+            asyncore.file_dispatcher.__init__(self, self.__readfd)
+
+        def close(self):
+            self.del_channel(_map)
+            os.close(self.__writefd)
+            os.close(self.__readfd)
+
+        def pull_trigger(self):
+            if __debug__:
+                self.logger.debug('pulled %s', pid)
+            os.write(self.__writefd, 'x')
+
+        def add_channel(self):
+            # work around file-dispatcher bug
+            asyncore.dispatcher.add_channel(self, _map)
+
+else:
+    # Windows version; uses just sockets, because a pipe isn't select'able
+    # on Windows.
+
+    class _Trigger(_Triggerbase, asyncore.dispatcher):
+        def __init__(self):
+
+            # Get a pair of connected sockets.  The trigger is the 'w'
+            # end of the pair, which is connected to 'r'.  'r' is put
+            # in the asyncore socket map.  "pulling the trigger" then
+            # means writing something on w, which will wake up r.
+
+            w = socket.socket()
+            # Disable buffering -- pulling the trigger sends 1 byte,
+            # and we want that sent immediately, to wake up asyncore's
+            # select() ASAP.
+            w.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+            count = 0
+            while 1:
+               count += 1
+               # Bind to a local port; for efficiency, let the OS pick
+               # a free port for us.
+               # Unfortunately, stress tests showed that we may not
+               # be able to connect to that port ("Address already in
+               # use") despite that the OS picked it.  This appears
+               # to be a race bug in the Windows socket implementation.
+               # So we loop until a connect() succeeds (almost always
+               # on the first try).  See the long thread at
+               # http://mail.zope.org/pipermail/zope/2005-July/160433.html
+               # for hideous details.
+               a = socket.socket()
+               a.bind(("127.0.0.1", 0))
+               connect_address = a.getsockname()  # assigned (host, port) pair
+               a.listen(1)
+               try:
+                   w.connect(connect_address)
+                   break    # success
+               except socket.error, detail:
+                   if detail[0] != errno.WSAEADDRINUSE:
+                       # "Address already in use" is the only error
+                       # I've seen on two WinXP Pro SP2 boxes, under
+                       # Pythons 2.3.5 and 2.4.1.
+                       raise
+                   # (10048, 'Address already in use')
+                   # assert count <= 2 # never triggered in Tim's tests
+                   if count >= 10:  # I've never seen it go above 2
+                       a.close()
+                       w.close()
+                       raise BindError("Cannot bind trigger!")
+                   # Close `a` and try again.  Note:  I originally put a short
+                   # sleep() here, but it didn't appear to help or hurt.
+                   a.close()
+
+            r, addr = a.accept()  # r becomes asyncore's (self.)socket
+            a.close()
+            self.trigger = w
+            asyncore.dispatcher.__init__(self, r, _map)
+
+        def close(self):
+            self.del_channel(_map)
+            # self.socket is r, and self.trigger is w, from __init__
+            self.socket.close()
+            self.trigger.close()
+
+        def pull_trigger(self):
+            if __debug__:
+                self.logger.debug('notify select %s', pid)
+            self.trigger.send('x')
+
+_trigger = _Trigger()
+
+notify_select = _trigger.pull_trigger
+
+def loop():
+    timeout = 30.0
+    map = _map
+    if hasattr(select, 'poll'):
+        poll_fun = asyncore.poll3
+    else:
+        poll_fun = asyncore.poll
+
+    logger = logging.getLogger('zc.ngi.async.loop')
+
+    while map:
+        try:
+            poll_fun(timeout, map)
+        except:
+            logger.exception('loop error')
+            raise
+
+_thread = threading.Thread(target=loop)
+_thread.setDaemon(True)
+_thread.start()
+


Property changes on: zc.ngi/trunk/src/zc/ngi/async.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/async.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/async.txt	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/async.txt	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,50 @@
+=================================
+asyncore-based NGI implementation
+=================================
+
+The async module provides an NGI implementation based on the Python
+standard asyncore framework.  It provides 2 objects to be invoked
+directly by applications:
+
+connector
+   an implementation of the NGI IConnector interface
+
+listener
+   an implementation of the NGI IListener interface
+
+The implementation creates a dedicated thread to run an asyncore main
+loop on import.
+
+There's nothing else to say about the implementation from a usage
+point of view.  The remainder of this document provides a
+demonstration (test) of using the impemantation to create a simple
+word-count server and client.
+
+Demonstration: wordcount
+========================
+
+The wordcount module has a simple word-count server and client
+implementation.  We'll run these using the async implementation.
+
+Let's start the wordcount server:
+
+    >>> import zc.ngi.wordcount
+    >>> import zc.ngi.async
+    >>> port = zc.ngi.wordcount.start_server_process(zc.ngi.async.listener)
+
+We passed the module and name of the listener to be used.
+
+Now, we'll start a number of threads that connect to the server and
+check word counts of some sample documents.  If all goes well, we
+shouldn't get any output.
+
+    >>> import threading
+    >>> addr = 'localhost', port
+    >>> threads = [threading.Thread(target=zc.ngi.wordcount.client_thread,
+    ...                             args=(zc.ngi.async.connector, addr))
+    ...            for i in range(200)]
+
+    >>> _ = [thread.start() for thread in threads]
+    >>> _ = [thread.join() for thread in threads]
+    
+    >>> zc.ngi.wordcount.stop_server_process(zc.ngi.async.connector, addr)


Property changes on: zc.ngi/trunk/src/zc/ngi/async.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/interfaces.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/interfaces.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/interfaces.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,190 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Network Gateway Interface (NGI)
+
+The interfaces are split between "implementation" and "application"
+interfaces.  An implementation of the NGI provides IConnection,
+IConnector, and IListener. An application provides IConnectionHandler
+and one or both of IClientConnectHandler and IServer.
+
+The NGI is an event-based framework in the sense that applications
+register handlers that respond to input events.  There are 3 kinds of
+handlers:
+
+- Input handlers recieve network input
+
+- Client-connect handlers respond to outbound connection events, and
+
+- Servers respond to incoming connection events.
+
+The interfaces are designed to allow single-threaded applications:
+
+- An implementation of the interfaces is not allowed to make multiple
+  simultanious calls to the same application handler.  (Note that this
+  requirement does not extend accross multiple implementations.
+  Theoretically, different implementations could call handlers at the
+  same time.
+
+- All handler calls that are associated with a connection include the
+  connection as a parameter,  This allows a single handler object to
+  respond to events from multiple connections.
+
+Applications may be multi-threaded.  This means that implementations
+must be thread safe.  This means that calls into the implementation
+could be made at any time.
+
+$Id$
+"""
+
+from zope.interface import Interface, Attribute
+
+class IConnection(Interface):
+    """Network connections
+  
+    Network connections support communication over a network
+    connection, or any connection having separate input and output
+    channels. 
+    """
+
+    def __nonzero__():
+        """Return the connection status
+        
+        True is returned if the connection is open/active and
+        False otherwise.
+        """
+
+    def setHandler(handler):
+        """Set the IConnectionHandler for a connection.
+
+        The handler can be set only once.
+        """
+
+    def write(data):
+        """Write output data to a connection.
+        
+        The write call is non-blocking.
+        """
+
+    def close():
+        """Close the connection
+        """
+
+class IServerConnection(IConnection):
+
+    control = Attribute("An IServerControl")
+
+class IInputHandler(Interface):
+    """Objects that can handle connection input-data events
+
+    The methods defined be this interface will never be called
+    simultaniously from separate threads, so implementation of the
+    methods needn't be concerned with thread safety with respect to
+    these methods.
+    """
+
+    def handle_input(connection, data):
+        """Handle input data from a connection
+        
+        The data is an 8-bit string.
+
+        Note that there are no promises about blocking.  There data
+        isn't necessarily record oriented.  For example, data could,
+        in theory be passed one character at a time.  It os up to
+        applications to organize data into records, if desired.
+        
+        """
+
+    def handle_close(connection, reason):
+        """Recieve notification that a connection has closed
+        
+        The reason argument can be converted to a string for logging
+        purposes.  It may have data useful for debugging, but this
+        is undefined.
+        
+        Notifications are received when the connection is closed
+        externally, for example, when the other side of the
+        connection is closed or in case of a network failure.  No
+        notification is given when the connection's close method is
+        called.      
+        """
+
+class IConnector(Interface):
+    """Create a connection to a server
+    """
+
+    def __call__(address, handler):
+        """Try to make a connection to the given address
+        
+        The handler is an IClientConnectHandler.  The handler
+        connected method will be called with an IConnection object
+        if and when the connection suceeds or failed_connect method
+        will be called if the connection fails.
+        """
+
+class IClientConnectHandler(Interface):
+    """Recieve notifications of connection results
+    """
+
+    def connected(connection):
+        """Recieve notification that a connection had been established
+        """
+        
+    def failed_connect(reason):
+        """Recieve notificantion that a connection could not be established
+
+        The reason argument can be converted to a string for logging
+        purposes.  It may have data useful for debugging, but this
+        is undefined.
+        """
+
+class IListener(Interface):
+    """Listed for incoming connections
+    """
+
+    def __call__(address, handler):
+        """Listen for incoming connections
+
+        When a connection is recieved, call the handler.
+
+        An IServerControl object is returned.
+        """
+
+class IServer(Interface):
+    """Handle server connections
+    """
+
+    def __call__(connection):
+        """Handle a connection from a client
+        """
+
+class IServerControl(Interface):
+    """Server information and close control
+    """
+
+    def connections():
+        """return an iterable of the current connections
+        """
+
+    def close(handler=None):
+        """Close the listener and all of it's connections
+
+        If no handler is passed, the listener and it's connections
+        are closed immediately without waiting for any pending input
+        to be handled or for pending output to be sent.
+
+        If a handler is passed, the listener will stop accepting new
+        connections and existing connections will be left open.  The
+        handler will be called when all of the existing connections
+        have been closed.
+        """


Property changes on: zc.ngi/trunk/src/zc/ngi/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/message.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/message.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/message.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,76 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample client that sends a single message and waits for a reply
+
+$Id$
+"""
+
+import threading
+
+class CouldNotConnect(Exception):
+    """Could not connect to a server
+    """
+
+class UnexpectedResponse(Exception):
+    """Got an unexpected response from a server
+    """
+
+class Message:
+
+    def __init__(self, message, expected, notify):
+        self.message = message
+        self.expected = expected
+        self.notify = notify
+        self.input = ''
+
+    def connected(self, connection):
+        connection.setHandler(self)
+        connection.write(self.message)
+
+    def failed_connect(self, reason):
+        self.notify(None, reason)
+
+    def handle_input(self, connection, data):
+        self.input += data
+        if self.expected is not None and self.expected(self.input):
+            connection.close()
+            self.handle_close(connection)
+
+    def handle_close(self, connection, reason=None):
+        self.notify(self.input, reason)
+
+
+def message(connector, addr, message, expected=None):
+    result = []
+    lock = threading.Lock()
+    lock.acquire()
+    def notify(*args):
+        if result:
+            return # already notified
+        result.extend(args)
+        lock.release()
+    connector(addr, Message(message, expected, notify))
+    lock.acquire()
+    data, reason = result
+
+    if reason is None:
+        return data
+
+    if data is None:
+        raise CouldNotConnect(reason)
+
+    if expected is not None and not expected(data):
+        raise UnexpectedResponse(data, reason)
+
+    return data


Property changes on: zc.ngi/trunk/src/zc/ngi/message.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/message.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/message.txt	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/message.txt	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,41 @@
+==============
+Message Client
+==============
+
+The message client is a simple NGI client that sends a single message
+and waits for a response. To illustrate, we'll use a simple echo
+server:
+
+    >>> class EchoServer:
+    ...
+    ...     def __init__(self, connection):
+    ...         self.input = ''
+    ...         connection.setHandler(self)
+    ...
+    ...     def handle_input(self, connection, data):
+    ...         self.input += data
+    ...         if '\n' in self.input:
+    ...             data, self.input = self.input.split('\n', 1)
+    ...             connection.write(data.upper() + '\n')
+    ...
+    ...     def handle_close(self, connection, reason):
+    ...         pass
+
+    >>> import zc.ngi.testing
+    >>> connector = zc.ngi.testing.peer('foo', EchoServer)
+
+and we'll use the message client to send it a message and get a
+response.
+        
+    >>> import zc.ngi.message
+    >>> import re
+    >>> expected = re.compile('\n').search
+    >>> zc.ngi.message.message(connector, 'foo', 'hello world!\n', expected)
+    'HELLO WORLD!\n'
+
+If we give an invalid address, we'll get an exception:
+
+    >>> zc.ngi.message.message(connector, 'bar', 'hello world!\n', expected)
+    Traceback (most recent call last):
+    ...
+    CouldNotConnect: connection refused


Property changes on: zc.ngi/trunk/src/zc/ngi/message.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/notes.txt
===================================================================
--- zc.ngi/trunk/src/zc/ngi/notes.txt	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/notes.txt	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,11 @@
+=====
+Notes
+=====
+
+Timeout
+=======
+
+It's tempting to try to add a timeout feature, however, this adds
+quite a bit of complication, or at least time calls to get this right.
+I think it makes more sense for applications to use threads to create
+alarms. 


Property changes on: zc.ngi/trunk/src/zc/ngi/notes.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/testing.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/testing.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/testing.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,149 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Testing NGI implementation
+
+$Id$
+"""
+
+import zc.ngi
+
+class PrintingHandler:
+
+    def __init__(self, connection):
+        connection.setHandler(self)
+
+    def handle_input(self, connection, data):
+        data = repr(data)
+        print '->', data[:50]
+        data = data[50:]
+        while data:
+            print '.>', data[:50]
+            data = data[50:]
+
+    def handle_close(self, connection, reason):
+        if reason != 'closed':
+            print '-> CLOSE', reason
+        else:
+            print '-> CLOSE'
+
+class Connection:
+
+    control = None
+
+    def __init__(self, peer=None, handler=PrintingHandler):
+        self.handler = None
+        self.closed = False
+        self.input = ''
+        if peer is None:
+            peer = Connection(self)
+            handler(peer)
+        self.peer = peer
+        
+    def __nonzero__(self):
+        return not self.closed
+
+    def close(self):
+        self.peer.test_close('closed')
+        if self.control is not None:
+            self.control.closed(self)
+        self.closed = True
+        def write(s):
+            raise TypeError("Connection closed")
+        self.write = write
+        
+    def setHandler(self, handler):
+        self.handler = handler
+        if self.input:
+            handler.handle_input(self, self.input)
+            self.input = ''
+
+        # Note is self.closed is True, we self closed and we
+        # don't want to call handle_close.
+        if self.closed and isinstance(self.closed, str):
+            handler.handle_close(self, self.closed)
+
+    def test_input(self, data):
+        if self.handler is not None:
+            self.handler.handle_input(self, data)
+        else:
+            self.input += data
+
+    def test_close(self, reason):
+        if self.control is not None:
+            self.control.closed(self)
+        self.closed = reason
+        if self.handler is not None:
+            self.handler.handle_close(self, reason)
+
+    def write(self, data):
+        if data is zc.ngi.END_OF_DATA:
+            return self.close()
+        self.peer.test_input(data)
+
+_connectable = {}
+
+def connector(addr, handler):
+    connections = _connectable.get(addr)
+    if connections:
+        handler.connected(connections.pop(0))
+    else:
+        handler.failed_connect('no such server')
+
+def connectable(addr, connection):
+    _connectable.setdefault(addr, []).append(connection)
+
+class listener:
+
+    def __init__(self, handler):
+        self._handler = handler
+        self._close_handler = None
+        self._connections = []
+
+    def connect(self, connection):
+        if self._handler is None:
+            raise TypeError("Listener closed")
+        self._connections.append(connection)
+        connection.control = self
+        self._handler(connection)
+
+    def connections(self):
+        return iter(self._connections)
+        
+    def close(self, handler=None):
+        self._handler = None
+        if handler is None:
+            while self._connections:
+                self._connections[0].test_close('stopped')
+        elif not self._connections:
+            handler(self)
+        else:
+            self._close_handler = handler
+
+    def closed(self, connection):
+        self._connections.remove(connection)
+        if not self._connections and self._close_handler:
+            self._close_handler(self)
+
+class peer:
+
+    def __init__(self, addr, handler):
+        self.addr = addr
+        self.handler = handler
+
+    def __call__(self, addr, handler):
+        if addr != self.addr:
+            handler.failed_connect('connection refused')
+        else:
+            handler.connected(Connection(None, self.handler))
+


Property changes on: zc.ngi/trunk/src/zc/ngi/testing.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/tests.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/tests.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/tests.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,31 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest
+import zc.ngi.async # start async thread before tests run
+
+def test_suite():
+    return doctest.DocFileSuite(
+        'README.txt',
+        'message.txt',
+        'async.txt',
+        'adapters.txt',
+        )
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: zc.ngi/trunk/src/zc/ngi/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.ngi/trunk/src/zc/ngi/wordcount.py
===================================================================
--- zc.ngi/trunk/src/zc/ngi/wordcount.py	2006-08-11 09:43:19 UTC (rev 69399)
+++ zc.ngi/trunk/src/zc/ngi/wordcount.py	2006-08-11 12:55:34 UTC (rev 69400)
@@ -0,0 +1,198 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample NGI server and client
+
+This file provides a sample NGI server and client that counts words.
+
+$Id$
+"""
+
+import errno
+import logging
+import os
+import random
+import socket
+import sys
+import threading
+import time
+
+import zc.ngi    
+import zc.ngi.message
+
+_lock = threading.Lock()
+_lock.acquire()
+exit = _lock.release
+run = _lock.acquire
+
+logger = logging.getLogger('zc.ngi.wordcount')
+
+class Server:
+    
+    def __init__(self, connection):
+        self.input = ''
+        connection.setHandler(self)
+
+    def handle_input(self, connection, data):
+        self.input += data
+        while '\0' in self.input:
+            data, self.input = self.input.split('\0', 1)
+            if data == 'Q':
+                connection.write('Q\n')
+                connection.write(zc.ngi.END_OF_DATA)
+                connection.control.close(lambda c: 1)
+                return
+            else:
+                cc = len(data)
+                lc = len(data.split('\n'))-1
+                wc = len(data.split())
+            connection.write("%s %s %s\n" % (lc, wc, cc))
+
+    def handle_close(self, connection, reason):
+        pass
+
+def serve():
+    mod, name, port = sys.argv[1:]
+    __import__(mod)
+    logger.info('serving')
+    getattr(sys.modules[mod], name)(('localhost', int(port)), Server)
+    run()
+
+def get_port():
+    """Return a port that is not in use.
+
+    Checks if a port is in use by trying to connect to it.  Assumes it
+    is not in use if connect raises an exception.
+
+    Raises RuntimeError after 10 tries.
+    """
+    for i in range(10):
+        port = random.randrange(20000, 30000)
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            try:
+                s.connect(('localhost', port))
+            except socket.error:
+                # Perhaps we should check value of error too.
+                return port
+        finally:
+            s.close()
+    raise RuntimeError("Can't find port")
+
+def wait(addr, up=True):
+    for i in range(120):
+        time.sleep(0.25)
+        try:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.connect(addr)
+            s.close()
+            if up:
+                break
+        except socket.error, e:
+            if e[0] not in (errno.ECONNREFUSED, errno.ECONNRESET):
+                raise
+            s.close()
+            if not up:
+                break
+    else:
+        if up:
+            print "Could not connect"
+        else:
+            print "Server still accepting connections"
+
+def start_server_process(listener):
+    """Start a server in a subprocess and return the port used
+    """
+    module = listener.__module__
+    name = listener.__name__
+    port = get_port()
+    env = dict(
+        os.environ,
+        PYTHONPATH=os.pathsep.join(sys.path),
+        )
+    os.spawnle(os.P_NOWAIT, sys.executable, sys.executable, __file__,
+               module, name, str(port), env)
+    addr = 'localhost', port
+    wait(addr)
+    return port
+
+def stop_server_process(connector, addr):
+    zc.ngi.message.message(connector, addr, 'Q\0', lambda s: s == 'Q\n')
+    wait(addr, up=False)
+
+sample_docs = [
+"""Hello world
+""",
+"""I give my pledge as an earthling
+to save and faithfully to defend from waste
+the natural resources of my planet
+its soils, minerals, forests, waters and wildlife.
+""",
+"""On my honor, I will do my best
+to do my duty to God and my country
+and to obey the Scout Law
+to always help others
+to keep myself physically strong, mentally awake, and morally straight.
+""",
+"""What we have here, is a failure to communicate.
+""",
+]
+
+class Client:
+
+    def __init__(self, docs=sample_docs, notify=None):
+        self.docs = list(docs)
+        self.notify = notify
+        self.input = ''
+
+    def connected(self, connection):
+        connection.write(self.docs[0]+'\0')
+        connection.setHandler(self)
+        
+    def failed_connect(self, reason):
+        print 'Failed to connect:', reason
+
+    def handle_input(self, connection, data):
+        self.input += data
+        if '\n' in self.input:
+            data, self.input = self.input.split('\n', 1)
+            doc = self.docs.pop(0)
+            cc = len(doc)
+            lc = len(doc.split('\n'))-1
+            wc = len(doc.split())
+            expected = "%s %s %s" % (lc, wc, cc)
+            if data != expected:
+                print '%r != %r' % (data, expected)
+            if self.docs:
+                connection.write(self.docs[0]+'\0')
+            else:
+                connection.close()
+                if self.notify is not None:
+                    self.notify()
+
+    def handle_close(self, connection, reason):
+        if self.docs:
+            print 'unexpected close', reason
+
+def client_thread(connector, addr):
+    logger.info('client started for %s', addr)
+    lock = threading.Lock()
+    lock.acquire()
+    client = Client(notify=lock.release)
+    connector(addr, client)
+    logger.info('client waiting')
+    lock.acquire() # wait till done
+    logger.info('client done')
+
+if __name__ == '__main__':
+    serve()


Property changes on: zc.ngi/trunk/src/zc/ngi/wordcount.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Checkins mailing list