1. Szymon Wróblewski
  2. pygnetic

Commits

Szymon Wróblewski  committed a286001

updated Client, Server, Connection and adapters

  • Participants
  • Parent commits 2173dac
  • Branches default

Comments (0)

Files changed (10)

File doc/api/index.rst

View file
 
 .. automodule:: pygnetic
 
-   .. autofunction:: init([events[, event_val[, logging_lvl[, n_module[, s_module]]]]])
-   
-   .. autofunction:: register(name[, field_names[, **kwargs]])
-   
-      .. note:: It uses :data:`.message.message_factory`
-      
+   .. autofunction:: init([events, event_val, logging_lvl, n_adapter, s_adapter])
+
+   .. autofunction:: register([name, field_names, **kwargs])
+
    .. autoclass:: Client
    
    .. autoclass:: Server
    
-   .. class :: Handler
-   
+   .. class:: Handler
+
       :class:`handler.Handler` binding.
    
    
 
 .. automodule:: pygnetic.client
     
-   .. autoclass:: Client([conn_limit, [*args, **kwargs]])
+   .. autoclass:: Client([conn_limit, message_factory, *args, **kwargs])
     
       Example::
       
          while True:
             client.update()
             
-      .. data:: message_factory
+      .. attribute:: message_factory
          
-         Default :class:`~.message.MessageFactory` object used for new
+         :class:`~.message.MessageFactory` instance used for new
          connections. (default: :data:`.message.message_factory`)
       
-      .. automethod:: connect(host, port[, message_factory[, **kwargs]])
+      .. automethod:: connect(host, port[, message_factory, **kwargs])
       
       .. automethod:: update(self[, timeout])
 
 .. automodule:: pygnetic.connection
     
    .. autoclass:: Connection(parent, conn_obj, message_factory)
+
+      .. note::
+         It's created by :class:`~.client.Client` or :class:`~.server.Server`
+         and shouldn't be created manually.
+
+      Sending is possible in two ways:
+
+      * using :meth:`net_message_name` methods, where ``message_name``
+        is name of message registered in :class:`~.message.MessageFactory`
+      * using :meth:`send` method with message as argument
       
       Example::
       
       
       .. automethod:: disconnect([*args])
       
-      .. method:: net_message_name([\*args, \*\*kwargs])
+      .. method:: net_message_name([*args, **kwargs])
 
          Send ``message_name`` messagge to remote host.
          
    
       .. attribute:: connection
       
-         :func:`Proxy <weakref.proxy>` to :class:`~.connection.Connection`
-         derived class instance
+         :func:`Proxy <weakref.proxy>` to instance of
+         :class:`~.connection.Connection` derived class
          
       .. attribute:: server
       
-         :func:`Proxy <weakref.proxy>` to :class:`~.server.Server`
-         derived class instance   
+         :func:`Proxy <weakref.proxy>` to instance of
+         :class:`~.server.Server` derived class
       
       .. method:: net_message_name(message[, **kwargs])
       
       
       .. automethod:: pack
       
-      .. automethod:: register(name[, field_names[, **kwargs]])
+      .. automethod:: register(name[, field_names, **kwargs])
       
       .. automethod:: reset_context
       
 
 .. automodule:: pygnetic.server
     
-   .. autoclass:: Server([host[, port[, conn_limit[, *args, **kwargs]]]])
+   .. autoclass:: Server([host, port, conn_limit, handler, message_factory, *args, **kwargs])
       
       .. attribute:: address
          
          Server address.
+
+      .. attribute:: handler
+
+         Class derived from :class:`~.handler.Handler`
+         used to handle incoming messages.
+         New instance is created for every new connection.
+
+      .. attribute:: message_factory
+
+         :class:`.MessageFactory` instance used for new
+         connections. (default: :data:`.message.message_factory`)
          
       .. automethod:: connections([exclude])
       
    The only drawback of this method is the need to register the same messages
    in the same order in client and server.
 
-**Why order of registration messages is important?**
+**Why order of registration of messages is important?**
    As You may noticed in previous example, there is no string with type of
    message in packed data. That's because type is encoded as integer,
    depending on order of registration.
 
+**How can I change MessageFactory used in Client or Server?**
+   Class scope - every class instance will use it::
+
+      import pygnetic as net
+
+      mf = net.message.MessageFactory()
+      mf.register(...)
+
+      class Client(net.Client):
+         message_factory = mf
+
+      net.init()
+      client = Client()
+
+   Instance scope - only one instance will use it::
+
+      import pygnetic as net
+
+      mf = net.message.MessageFactory()
+      mf.register(...)
+
+      net.init()
+      client = net.Client(message_factory=mf)
+
+**How can I change adapter used to create Client or Server?**
+   Class scope - every class instance will use it::
+
+      import pygnetic as net
+
+      class Client(net.Client):
+         adapter = 'socket'
+
+      net.init()
+      client = Client()
+
+   Instance scope - only one instance will use it::
+
+      import pygnetic as net
+
+      net.init()
+      client = net.Client(adapter = 'enet')
+
 
 Glossary
 ========

File pygnetic/__init__.py

View file
 
 __version__ = '1.0'
 _logger = logging.getLogger(__name__)
-register = message.message_factory.register
 
 
 def init(events=False, event_val=None, logging_lvl=logging.INFO,
-         n_module=('enet', 'socket'), s_module=('msgpack', 'json')):
+         n_adapter=('enet', 'socket'), s_adapter=('msgpack', 'json')):
     """Initialize network library.
 
     :param events: allow sending Pygame events (default False)
         level of logging messages (default :const:`logging.INFO`
         (see: :ref:`logging-basic-tutorial`), None to skip initializing
         logging module)
-    :param n_module:
+    :param n_adapter:
         name(s) of network library, first available will be used
         (default: ['enet', 'socket'])
-    :type n_module: string or list of strings
-    :param s_module:
+    :type n_adapter: string or list of strings
+    :param s_adapter:
         name(s) of serialization library, first available will be used
         (default: ['msgpack', 'json'])
-    :type s_module: string or list of strings
+    :type s_adapter: string or list of strings
     :return: True if initialization ended with success
-
-    .. note::
-        Because of the dynamic loading of network library adapter,
-        :class:`Client` and :class:`Server` classes will only be
-        available after initialization.
     """
     global _network_module, _serialization_module
     if logging_lvl is not None:
         logging.basicConfig(level=logging_lvl,
                             format='%(asctime)-8s %(levelname)-8s %(message)s',
                             datefmt='%H:%M:%S')
-    network.select_adapter(n_module)
+    network.select_adapter(n_adapter)
     if network.selected_adapter is not None:
         _logger.info("Using %s",
             network.selected_adapter.__name__.split('.')[-1])
     else:
         _logger.critical("Can't find any network module")
         return False
-    serialization.select_adapter(s_module)
+    serialization.select_adapter(s_adapter)
     if serialization.selected_adapter is not None:
         _logger.info("Using %s",
             serialization.selected_adapter.__name__.split('.')[-1])
         _logger.info("Enabling pygame events")
         event.init(event_val)
     return True
+
+
+def register(*args, **kwargs):
+    """Register new message type in :data:`.message.message_factory`.
+
+    :param name: name of message class
+    :param field_names: list of names of message fields
+    :param kwargs: additional keyword arguments for send method
+    :return: message class (namedtuple)
+    """
+    return message.message_factory.register(*args, **kwargs)

File pygnetic/_utils.py

View file
 
 _logger = logging.getLogger(__name__)
 
+
 def find_adapter(a_type, names):
     """Return first found adapter
 
             return import_module('.'.join((a_type, a_name)))
         except ImportError as e:
             _logger.debug("%s: %s", e.__class__.__name__, e.message)
+
+
+class lazyproperty(object):
+    """Decorator for properties calculated only once"""
+    def __init__(self, calculate_function):
+        self._calculate = calculate_function
+
+    def __get__(self, obj, _=None):
+        if obj is None:
+            return self
+        value = self._calculate(obj)
+        setattr(obj, self._calculate.func_name, value)
+        return value

File pygnetic/client.py

View file
 
 import logging
 import message
+import network
 
 _logger = logging.getLogger(__name__)
 
 
 class Client(object):
-    """Class representing network client.
+    """Base class representing network client.
 
     :param int con_limit: maximum amount of created connections (default: 1)
+    :param message_factory: custom instance of :class:`~.message.MessageFactory`
+       (default: :attr:`.Client.message_factory`)
     :param args: additional arguments for :term:`network adapter`
     :param kwargs: additional keyword arguments for :term:`network adapter`
     """
     message_factory = message.message_factory
 
-    def __init__(self, conn_limit=1, *args, **kwargs):
+    def __init__(self, conn_limit=1, message_factory=None, *args, **kwargs):
         super(Client, self).__init__(*args, **kwargs)
         self.conn_map = {}
+        if message_factory is not None:
+            self.message_factory = message_factory
         _logger.info('Client created, connections limit: %d', conn_limit)
 
     def connect(self, host, port, message_factory=None, **kwargs):
             message_factory = self.message_factory
         _logger.info('Connecting to %s:%d', host, port)
         message_factory.set_frozen()
-        connection, c_id = self._create_connection(host, port,
+        connection, c_key = self._create_connection(host, port,
             message_factory, **kwargs)
-        self.conn_map[c_id] = connection
+        self.conn_map[c_key] = connection
+        connection._key = c_key
         return connection
 
     def update(self, timeout=0):
             waiting time for network events in milliseconds
             (default: 0 - no waiting)
         """
-        pass
+        raise NotImplementedError('Should be implemented by adapter class')
 
-    def _connect(self, c_id):
-        self.conn_map[c_id]._connect()
+    def _get(self, c_key):
+        return self.conn_map[c_key]
 
-    def _disconnect(self, c_id):
-        self.conn_map[c_id]._disconnect()
-        del self.conn_map[c_id]
+    def _remove(self, c_key):
+        del self.conn_map[c_key]
 
-    def _receive(self, c_id, data, **kwargs):
-        self.conn_map[c_id]._receive(data, **kwargs)
+    def _create_connection(self, host, port, message_factory, **kwargs):
+        raise NotImplementedError('Should be implemented by adapter class')

File pygnetic/connection.py

View file
 import logging
 from weakref import proxy
 from functools import partial
-import message
 import event
 
 _logger = logging.getLogger(__name__)
     :param parent: parent :class:`~.client.Client` or :class:`~.server.Server`
     :param conn_obj: connection object
     :param message_factory: :class:`~.message.MessageFactory` object
-
-    .. note::
-        It's created by :class:`~.client.Client` or :class:`~.server.Server`
-        and shouldn't be created manually.
-
-    Sending is possible in two ways:
-
-    * using :meth:`net_message_name` methods, where ``message_name``
-      is name of message registered in :class:`~.message.MessageFactory`
-    * using :meth:`send` method with message as argument
     """
-    address = ('', '')
+    address = ('', '') # \
+    connected = False  # / default values, should be overridden by adapter class
+    __id_cnt = 0
 
     def __init__(self, parent, conn_obj, message_factory, *args, **kwargs):
         super(Connection, self).__init__(*args, **kwargs)
         self.data_received = 0
         self.messages_sent = 0
         self.messages_received = 0
+        self.id = self.__class__.__id_cnt = self.__id_cnt + 1
+        self._key = None
 
     def __getattr__(self, name):
         parts = name.split('_', 1)
             raise TypeError('%s takes exactly %d arguments (%d given)' %
                 (message.__doc__, int(e) - 1, int(f) - 1))
         data = self.message_factory.pack(message_)
-        _logger.info('Sent %s message to %s', name, self.address)
+        _logger.info('#%s Sent %s message', self.id, name)
         self.data_sent += len(data)
         self.messages_sent += 1
         return self._send_data(data, **params)
 
+    def _send_data(self, data, channel=0, **kwargs):
+        raise NotImplementedError('Should be implemented by adapter class')
+
     def _receive(self, data, **kwargs):
         self.data_received += len(data)
         for message in self.message_factory.unpack_all(data, self):
             self.messages_received += 1
             name = message.__class__.__name__
-            _logger.info('Received %s message from %s', name, self.address)
+            _logger.info('#%s Received %s message', self.id, name)
             event.received(self, message)
             for h in self.handlers:
                 getattr(h, 'net_' + name, h.on_recive)(message, **kwargs)
 
     def _connect(self):
-        _logger.info('Connected to %s', self.address)
+        _logger.info('#%s Connected to %s', self.id, self.address)
         event.connected(self)
         for h in self.handlers:
             h.on_connect()
 
     def _disconnect(self):
-        _logger.info('Disconnected from %s', self.address)
+        _logger.info('#%s Disconnected from %s', self.id, self.address)
         event.disconnected(self)
         for h in self.handlers:
             h.on_disconnect()
+        self.parent._remove(self._key)
 
     def disconnect(self, *args):
         """Request a disconnection.
 
         :param args: additional arguments for :term:`network adapter`
         """
-        pass
+        raise NotImplementedError('Should be implemented by adapter class')
 
     def add_handler(self, handler):
         """Add new Handler to handle messages.

File pygnetic/network/enet_adapter.py

View file
 import logging
 import enet
 from .. import connection, server, client
+from .._utils import lazyproperty
 
 _logger = logging.getLogger(__name__)
 
         """Connection state."""
         return self.peer.state == enet.PEER_STATE_CONNECTED
 
-    @property
+    @lazyproperty
     def address(self):
         """Connection address."""
         address = self.peer.address
 
 
 class Server(server.Server):
-    def __init__(self, host='', port=0, conn_limit=4, *args, **kwargs):
-        super(Server, self).__init__(host, port, conn_limit, *args, **kwargs)
+    def __init__(self, host='', port=0, conn_limit=4, handler=None, message_factory=None, *args, **kwargs):
+        super(Server, self).__init__(host, port, conn_limit, handler, message_factory, *args, **kwargs)
         host = enet.Address(host, port)
         self.host = enet.Host(host, conn_limit, *args, **kwargs)
-        self._peer_cnt = 0
+
+    def _create_connection(self, peer, message_factory):
+        connection = Connection(self, peer, message_factory)
+        peer_id = peer.data = str(connection.id)
+        return connection, peer_id
 
     def update(self, timeout=0):
         host = self.host
         event = host.service(timeout)
         while event is not None:
             if event.type == enet.EVENT_TYPE_CONNECT:
-                peer_id = str(self._peer_cnt + 1)
-                if self._accept(Connection, event.peer, event.peer.address,
-                                peer_id, event.data):
-                    event.peer.data = peer_id
-                    self._peer_cnt += 1
-                else:
+                if not self._accept(event.peer, event.peer.address, event.data):
                     event.peer.disconnect_now()
             elif event.type == enet.EVENT_TYPE_DISCONNECT:
-                self._disconnect(event.peer.data)
+                self._get(event.peer.data)._disconnect()
             elif event.type == enet.EVENT_TYPE_RECEIVE:
-                self._receive(event.peer.data, event.packet.data,
-                    channel=event.channelID)
+                self._get(event.peer.data)._receive(event.packet.data,
+                        channel=event.channelID)
             event = host.check_events()
 
-    @property
+    @lazyproperty
     def address(self):
-        """Server address."""
         address = self.host.address
         return address.host, address.port
 
     def __init__(self, conn_limit=1, *args, **kwargs):
         super(Client, self).__init__(*args, **kwargs)
         self.host = enet.Host(None, conn_limit)
-        self._peer_cnt = 0
 
     def _create_connection(self, host, port, message_factory, channels=1,
                            **kwargs):
         host = enet.Address(host, port)
-        peer_id = self._peer_cnt = self._peer_cnt + 1
-        peer_id = str(peer_id)
         peer = self.host.connect(host, channels, message_factory.get_hash())
-        peer.data = peer_id
         connection = Connection(self, peer, message_factory)
+        peer_id = peer.data = str(connection.id)
         return connection, peer_id
 
     def update(self, timeout=0):
         event = host.service(timeout)
         while event is not None:
             if event.type == enet.EVENT_TYPE_CONNECT:
-                self._connect(event.peer.data)
+                self._get(event.peer.data)._connect()
             elif event.type == enet.EVENT_TYPE_DISCONNECT:
-                self._disconnect(event.peer.data)
+                self._get(event.peer.data)._disconnect()
             elif event.type == enet.EVENT_TYPE_RECEIVE:
-                self._receive(event.peer.data, event.packet.data,
-                              channel=event.channelID)
+                self._get(event.peer.data)._receive(event.packet.data,
+                                                    channel=event.channelID)
             event = host.check_events()

File pygnetic/network/socket_adapter.py

View file
 import asyncore
 from collections import deque
 from .. import connection, server, client
+from .._utils import lazyproperty
 
 _logger = logging.getLogger(__name__)
 _connect_struct = struct.Struct('!I')
 #            return
 
     def handle_connect(self):
-        self.addr = self.socket.getpeername()
         self._connect()
 
     def handle_close(self):
-        self.parent._disconnect(self.socket.fileno())
+        self._disconnect()
         self.close()
 
     def log_info(self, message, type='info'):
         return getattr(_logger, type)(message)
 
     def disconnect(self, *args):
-        self.parent._disconnect(self.socket.fileno())
+        self._disconnect()
         self.close()
 
-    @property
+    @lazyproperty
     def address(self):
-        return self.addr
+        return self.socket.getpeername()
 
 
 class Server(server.Server, Dispacher):
     connection = Connection
 
-    def __init__(self, host='', port=0, con_limit=4, *args, **kwargs):
+    def __init__(self, host='', port=0, conn_limit=4, handler=None, message_factory=None, *args, **kwargs):
         super(Server, self).__init__(
-            host, port, con_limit,
+            host, port, conn_limit,  handler, message_factory,
             None, None,
             *args, **kwargs)
         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
         self.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
         self.set_reuse_addr()
         self.bind((host, port))
-        self.listen(con_limit)
+        self.listen(conn_limit)
+
+    def _create_connection(self, socket, message_factory):
+        connection = Connection(self, socket, message_factory)
+        return connection, socket.fileno()
 
     def handle_accept(self):
         pair = self.accept()
         if pair is not None:
             sock, addr = pair
-            sock.settimeout(0.5)  # TODO: sth better?
-            mf_hash = _connect_struct.unpack(sock.recv(4))[0]
-            sock.setblocking(0)
-            if not self._accept(Connection, sock, addr, self._fileno, mf_hash):
-                sock.close()
+            # TODO: sth better?
+            sock.settimeout(0.5) # enable blocking read with 0.5s timeout
+            try:
+                data = sock.recv(4) # wait for hash data
+                mf_hash = _connect_struct.unpack(data)[0]
+                sock.setblocking(0) # disable blocking read
+                if self._accept(sock, addr, mf_hash):
+                    return # end if hash is correct
+            except socket.timeout:
+                _logger.info('Connection with %s refused, MessageFactory'
+                                ' hash not received', addr)
+            sock.shutdown(socket.SHUT_RDWR)
+            sock.close()
 
     def update(self, timeout=0):
         asyncore.loop(timeout / 1000.0, False, None, 1)
 
+    @lazyproperty
+    def address(self):
+        return self.socket.getsockname()
+
 
 class Client(client.Client):
     def __init__(self, conn_limit=0, *args, **kwargs):
         self._sock_cnt = 0
 
     def _create_connection(self, host, port, message_factory, **kwargs):
-        conn = Connection(self, None, message_factory)
-        conn.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        connection = Connection(self, None, message_factory)
+        connection.create_socket(socket.AF_INET, socket.SOCK_STREAM)
         # disable Nagle buffering algorithm
-        conn.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        conn.connect((host, port))
-        conn._send_data(_connect_struct.pack(message_factory.get_hash()))
-        return conn, conn.socket.fileno()
+        connection.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+        connection.connect((host, port))
+        connection._send_data(_connect_struct.pack(message_factory.get_hash()))
+        return connection, connection.socket.fileno()
 
     def update(self, timeout=0):
-        asyncore.loop(timeout / 1000.0, False, self.conn_map, 1)
+        asyncore.poll(timeout / 1000.0, self.conn_map)

File pygnetic/server.py

View file
     :param string host: IP address or name of host (default: "" - any)
     :param int port: port of host (default: 0 - any)
     :param int con_limit: maximum amount of created connections (default: 4)
+    :param handler: custom :class:`~.handler.Handler` derived class
+       (default: :attr:`.Server.handler`)
+    :param message_factory: custom instance of :class:`~.message.MessageFactory`
+       (default: :attr:`.Server.message_factory`)
     :param args: additional arguments for :term:`network adapter`
     :param kwargs: additional keyword arguments for :term:`network adapter`
     """
+    address = ('', '')
     message_factory = message.message_factory
     handler = None
 
-    def __init__(self, host='', port=0, conn_limit=4, *args, **kwargs):
+    def __init__(self, host='', port=0, conn_limit=4, handler=None,
+                 message_factory=None, *args, **kwargs):
         super(Server, self).__init__(*args, **kwargs)
         _logger.info('Server created %s:%d, connections limit: %d',
                      host, port, conn_limit)
         self.message_factory.set_frozen()
         self.conn_map = {}
         self.conn_limit = conn_limit
+        if handler is not None:
+            self.handler = handler
+            _logger.debug("Using %s handler", handler.__name__)
+        if message_factory is not None:
+            self.message_factory = message_factory
 
     def update(self, timeout=0):
         """Process network traffic and update connections.
             waiting time for network events in milliseconds
             (default: 0 - no waiting)
         """
-        pass
+        raise NotImplementedError('Should be implemented by adapter class')
 
-    def _accept(self, connection, socket, address, c_id, mf_hash):
+    def _create_connection(self, socket, message_factory):
+        raise NotImplementedError('Should be implemented by adapter class')
+
+    def _accept(self, socket, address, mf_hash):
         if mf_hash == self.message_factory.get_hash():
             _logger.info('Connection with %s accepted', address)
-            conn = connection(self, socket, self.message_factory)
+            connection, c_key = self._create_connection(socket,
+                    self.message_factory)
             if self.handler is not None and issubclass(self.handler, Handler):
                 handler = self.handler()
                 handler.server = proxy(self)
-                conn.add_handler(handler)
-            self.conn_map[c_id] = conn
+                connection.add_handler(handler)
+            else:
+                _logger.error('xxx') # TODO
+            self.conn_map[c_key] = connection
+            connection._key = c_key
             event.accepted(self)
-            conn._connect()
+            connection._connect()
             return True
         else:
-            _logger.info('Connection with %s refused, MessageFactory'\
+            _logger.info('Connection with %s refused, MessageFactory'
                             ' hash incorrect', address)
             return False
 
-    def _disconnect(self, c_id):
-        conn = self.conn_map.get(c_id)
-        if conn is not None:
-            conn._disconnect()
-            del self.conn_map[c_id]
+    def _get(self, c_key):
+        return self.conn_map[c_key]
 
-    def _receive(self, c_id, data, **kwargs):
-        self.conn_map[c_id]._receive(data, **kwargs)
+    def _remove(self, c_key):
+        del self.conn_map[c_key]
 
     def connections(self, exclude=None):
         """Returns iterator over connections.
         else:
             return (c.handlers[0] for c in self.conn_map.itervalues()
                     if c not in exclude)
-
-    @property
-    def address(self):
-        """Server address."""
-        return None, None

File test_client_2.py

View file
     def net_echo(self, message, **kwargs):
         logging.info('Received message: %s', message)
         self.in_counter += 1
-        if self.in_counter == 10:
-            self.connection.disconnect()
 
     def update(self):
         if self.out_counter < 10 and self.connection.connected:
 
 
 def main():
-    net.init(logging_lvl=logging.DEBUG)
+    net.init(logging_lvl=logging.DEBUG, n_adapter='enet')
     net.register('echo', ('msg', 'msg_id'))
-    client = net.Client()
+    client = net.Client(n_adapter='socket')
     connection = client.connect("localhost", 1337)
     handler = EchoHandler()
     connection.add_handler(handler)
             handler.update()
     except KeyboardInterrupt:
         pass
-
+    finally:
+        connection.disconnect()
+        client.update()
 
 if __name__ == '__main__':
     main()

File test_server.py

View file
 
 
 def main():
-    net.init(logging_lvl=logging.DEBUG)
+    net.init(logging_lvl=logging.DEBUG, n_adapter='enet')
     net.register('echo', ('msg', 'msg_id'))
-    server = net.Server(port=1337)
+    #server = net.Server(port=1337, handler=EchoHandler)
+    server = net.Server(port=1337, n_adapter='socket')
     server.handler = EchoHandler
     logging.info('Listening')
     try: