commit 2acc860ab5da8380a9b79feab8d5b09ca11eaa5e
Author: Shikhar Bhushan <shikhar@schmizz.net>
Date:   Wed Apr 22 23:34:31 2009 +0000

    made content module object-oriented; lots of code organization changes overall
    
    
    
    git-svn-id: http://ncclient.googlecode.com/svn/trunk@41 6dbcf712-26ac-11de-a2f3-1373824ab735

diff --git a/ncclient/capabilities.py b/ncclient/capabilities.py
index 2165383..a446fb8 100644
--- a/ncclient/capabilities.py
+++ b/ncclient/capabilities.py
@@ -25,8 +25,11 @@ class Capabilities:
     def __contains__(self, key):
         return ( key in self._dict ) or ( key in self._dict.values() )
     
+    def __iter__(self):
+        return self._dict.keys().__iter__()
+    
     def __repr__(self):
-        return self.to_xml()
+        return repr(self._dict.keys())
     
     def add(self, uri, shorthand=None):
         if shorthand is None:
@@ -44,10 +47,6 @@ class Capabilities:
                     del self._dict[uri]
                     break
     
-    def to_xml(self):
-        elems = ['<capability>%s</capability>' % uri for uri in self._dict]
-        return ('<capabilities>%s</capabilities>' % ''.join(elems))
-    
     @staticmethod
     def guess_shorthand(uri):
         if uri.startswith('urn:ietf:params:netconf:capability:'):
@@ -69,5 +68,4 @@ CAPABILITIES = Capabilities([
     ])
 
 if __name__ == "__main__":
-    assert(':validate' in CAPABILITIES) # test __contains__
-    print CAPABILITIES # test __repr__
\ No newline at end of file
+    assert(':validate' in CAPABILITIES) # test __contains__
\ No newline at end of file
diff --git a/ncclient/content.py b/ncclient/content.py
index 861a885..a82426c 100644
--- a/ncclient/content.py
+++ b/ncclient/content.py
@@ -14,43 +14,110 @@
 
 import logging
 from xml.etree import cElementTree as ElementTree
+from cStringIO import StringIO
 
 logger = logging.getLogger('ncclient.content')
 
-BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
-NOTIFICATION_NS = 'urn:ietf:params:xml:ns:netconf:notification:1.0'
-
-def qualify(tag, ns=BASE_NS):
-    return '{%s}%s' % (ns, tag)
 
+def qualify(tag, ns=None):
+    if ns is None:
+        return tag
+    else:
+        return '{%s}%s' % (ns, tag)
 _ = qualify
 
-def make_hello(capabilities):
-    return '<hello xmlns="%s">%s</hello>' % (BASE_NS, capabilities)
-
-def make_rpc(id, op):
-    return '<rpc message-id="%s" xmlns="%s">%s</rpc>' % (id, BASE_NS, op)
-
-def parse_hello(raw):
-    from capabilities import Capabilities
-    id, capabilities = 0, Capabilities()
-    root = ElementTree.fromstring(raw)
-    if root.tag == _('hello'):
-        for child in root.getchildren():
-            if child.tag == _('session-id'):
-                id = int(child.text)
-            elif child.tag == _('capabilities'):
-                for cap in child.getiterator(_('capability')):
-                    capabilities.add(cap.text)
-    return id, capabilities
-
-def parse_message_root(raw):
-    from cStringIO import StringIO
-    fp = StringIO(raw)
-    for event, element in ElementTree.iterparse(fp, events=('start',)):
-        if element.tag == _('rpc'):
-            return element.attrib['message-id']
-        elif element.tag == _('notification', NOTIFICATION_NS):
-            return 'notification'
+
+class RootElementParser:
+    
+    '''Parse the root element of an XML document. The tag and namespace of
+    recognized elements, and attributes of interest can be customized.
+    
+    RootElementParser does not parse any sub-elements.
+    '''
+    
+    def __init__(self, recognize=[]):
+        self._recognize = recognize
+    
+    def recognize(self, element):
+        '''Specify an element that should be successfully parsed.
+        
+        element should be a string that represents a qualified name of the form
+        *{namespace}tag*.
+        '''
+        self._recognize.append((element, attrs))
+    
+    def parse(self, raw):
+        '''Parse the root element from a string representing an XML document.
+        
+        Returns a (tag, attributes) tuple. tag is a string representing
+        the qualified name of the recognized element. attributes is a
+        {'attr': value} dictionary.
+        '''
+        fp = StringIO(raw)
+        for event, element in ElementTree.iterparse(fp, events=('start',)):
+            for e in self._recognize:
+                if element.tag == e:
+                    return (element.tag, element.attrib)
+            break
+        return None
+
+
+###########
+
+class XMLBuilder:
+    
+    @staticmethod
+    def _element(node):
+        element = ElementTree.Element( _(node.get('tag'),
+                                         node.get('namespace', None)),
+                                      node.get('attributes', {}))
+        if node.has_key('children'):
+            for child in node['children']:
+                element.append(_make_element(child))
         else:
-            return None
\ No newline at end of file
+            return element
+    
+    @staticmethod
+    def _etree(tree_dict):
+        return ElementTree.ElementTree(XMLBuilder._element(tree_dict))
+    
+    @staticmethod
+    def to_xml(tree_dict, encoding='utf-8'):
+        fp = StringIO()
+        self._etree(tree_dict).write(fp, encoding)
+        return fp.get_value()
+
+
+### Hello exchange
+
+class Hello:
+    
+    NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
+    
+    @staticmethod
+    def build(capabilities, encoding='utf-8'):
+        hello = ElementTree.Element(_('hello', Hello.NS))
+        caps = ElementTree.Element('capabilities')
+        for uri in capabilities:
+            cap = ElementTree.Element('capability')
+            cap.text = uri
+            caps.append(cap)
+        hello.append(caps)
+        tree = ElementTree.ElementTree(hello)
+        fp = StringIO()
+        tree.write(fp, encoding)
+        return fp.getvalue()
+    
+    @staticmethod
+    def parse(raw):
+        'Returns tuple of (session-id, ["capability_uri", ...])'
+        id, capabilities = 0, []
+        root = ElementTree.fromstring(raw)
+        if root.tag == _('hello', Hello.NS):
+            for child in root.getchildren():
+                if child.tag == _('session-id', Hello.NS):
+                    id = int(child.text)
+                elif child.tag == _('capabilities', Hello.NS):
+                    for cap in child.getiterator(_('capability', Hello.NS)):
+                        capabilities.append(cap.text)
+        return id, capabilities
diff --git a/ncclient/listeners.py b/ncclient/listeners.py
index 3169746..096a46e 100644
--- a/ncclient/listeners.py
+++ b/ncclient/listeners.py
@@ -32,13 +32,12 @@ class SessionListener(object):
     def __init__(self):
         self._id2rpc = WeakValueDictionary()
         self._expecting_close = False
-        self._subscription = None
+        sself._notification_rpc_id = None
     
     def __str__(self):
         return 'SessionListener'
     
-    def set_subscription(self, id):   
-        self._subscription = id
+    
     
     def expect_close(self):
         self._expecting_close = True
@@ -68,6 +67,27 @@ class SessionListener(object):
             if not self._expecting_close:
                 raise err
 
+
+class HelloListener:
+    
+    def __str__(self):
+        return 'HelloListener'
+        
+    def __init__(self, session):
+        self._session = session
+    
+    def reply(self, data):
+        try:
+            id, capabilities = content.Hello.parse(data)
+            logger.debug('HelloListener: session_id: %s; capabilities: %s', id, capabilities)
+            self._session.initialize(id, capabilities)
+        except Exception as e:
+            self._session.initialize_error(e)
+    
+    def error(self, err):
+        self._session.initialize_error(err)
+
+
 class DebugListener:
     
     def __str__(self):
@@ -77,4 +97,4 @@ class DebugListener:
         logger.debug('DebugListener:reply:\n%s' % raw)
     
     def error(self, err):
-        logger.debug('DebugListener:error:\n%s' % err)
+        logger.debug('DebugListener:error:\n%s' % err)
\ No newline at end of file
diff --git a/ncclient/operations/__init__.py b/ncclient/operations/__init__.py
new file mode 100644
index 0000000..9229c58
--- /dev/null
+++ b/ncclient/operations/__init__.py
@@ -0,0 +1,34 @@
+# Copyright 2009 Shikhar Bhushan
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ncclient import content as content
+from ncclient.capabilities import CAPABILITIES
+
+from retrieve import Get, GetConfig
+from edit import EditConfig, DeleteConfig
+from session import CloseSession, KillSession
+from lock import Lock, Unlock
+from notification import CreateSubscription
+
+__all__ = [
+    'Get',
+    'GetConfig',
+    'EditConfig',
+    'DeleteConfig',
+    'Lock',
+    'Unlock',
+    'CloseSession',
+    'KillSession',
+    'CreateSubscription',
+    ]
\ No newline at end of file
diff --git a/ncclient/rpc.py b/ncclient/rpc.py
index c186bb0..2fe5ffb 100644
--- a/ncclient/rpc.py
+++ b/ncclient/rpc.py
@@ -20,6 +20,11 @@ from listeners import session_listener_factory
 
 class RPC:
     
+    metadata = {
+        'tag': 'rpc',
+        'xmlns': 'urn:ietf:params:xml:ns:netconf:base:1.0',
+        }
+    
     def __init__(self, session, async=False, parse=True):
         self._session = session
         self._async = async
@@ -36,6 +41,7 @@ class RPC:
     
     def _do_request(self, op):
         self._session.send(content.make_rpc(self._id, op))
+        # content.make(RPC, attrs={'message-id': self._id}, children=(op,))
         if not self._async:
             self._reply_event.wait()
         return self._reply
@@ -66,9 +72,8 @@ class RPC:
     def session(self):
         return self._session
 
-
 class RPCReply:
-    pass
-
-class RPCError:
-    pass
\ No newline at end of file
+    
+    class RPCError:
+        
+        pass
\ No newline at end of file
diff --git a/ncclient/session.py b/ncclient/session.py
index dbafb3b..9d8ae06 100644
--- a/ncclient/session.py
+++ b/ncclient/session.py
@@ -17,7 +17,7 @@ from threading import Thread, Event
 from Queue import Queue
 
 import content
-from capabilities import CAPABILITIES
+from capabilities import Capabilities, CAPABILITIES
 from error import ClientError
 from subject import Subject
 
@@ -29,7 +29,7 @@ class Session(Thread, Subject):
     
     def __init__(self):
         Thread.__init__(self, name='session')
-        Subject.__init__(self, listeners=[Session.HelloListener(self)])
+        Subject.__init__(self, listeners=[HelloListener(self)])
         self._client_capabilities = CAPABILITIES
         self._server_capabilities = None # yet
         self._id = None # session-id
@@ -42,7 +42,7 @@ class Session(Thread, Subject):
         # start the subclass' main loop
         self.start()
         # queue client's hello message for sending
-        self.send(content.make_hello(self._client_capabilities))
+        self.send(content.Hello.build(self._client_capabilities))
         # we expect server's hello message, wait for _init_event to be set by HelloListener
         self._init_event.wait()
         # there may have been an error
@@ -50,9 +50,15 @@ class Session(Thread, Subject):
             self._close()
             raise self._error
     
+    def initialize(self, id, capabilities):
+        self._id, self._capabilities = id, Capabilities(capabilities)
+        self._init_event.set()
+    
+    def initialize_error(self, err):
+        self._error = err
+        self._init_event.set()
+    
     def send(self, message):
-        message = (u'<?xml version="1.0" encoding="UTF-8"?>%s' %
-                   message).encode('utf-8')
         logger.debug('queueing message: \n%s' % message)
         self._q.put(message)
     
@@ -61,9 +67,15 @@ class Session(Thread, Subject):
 
     def run(self):
         raise NotImplementedError
+        
+    def capabilities(self, whose='client'):
+        if whose == 'client':
+            return self._client_capabilities
+        elif whose == 'server':
+            return self._server_capabilities
     
     ### Properties
-
+    
     @property
     def client_capabilities(self):
         return self._client_capabilities
@@ -79,31 +91,3 @@ class Session(Thread, Subject):
     @property
     def id(self):
         return self._id
-    
-    class HelloListener:
-        
-        def __str__(self):
-            return 'HelloListener'
-        
-        def __init__(self, session):
-            self._session = session
-        
-        def _done(self, err=None):
-            if err is not None:
-                self._session._error = err
-            self._session.remove_listener(self)
-            self._session._init_event.set()
-        
-        def reply(self, data):
-            err = None
-            try:
-                id, capabilities = content.parse_hello(data)
-                logger.debug('session_id: %s | capabilities: \n%s', id, capabilities)
-                self._session._id, self._session.capabilities = id, capabilities
-            except Exception as e:
-                err = e
-            finally:
-                self._done(err)
-        
-        def error(self, err):
-            self._done(err)
diff --git a/ncclient/ssh.py b/ncclient/ssh.py
index 7d4be34..9f64f90 100644
--- a/ncclient/ssh.py
+++ b/ncclient/ssh.py
@@ -15,9 +15,11 @@
 import logging
 from cStringIO import StringIO
 from os import SEEK_CUR
+import socket
 
 import paramiko
 
+
 from session import Session, SessionError
 
 logger = logging.getLogger('ncclient.ssh')
@@ -39,7 +41,7 @@ class SSHSession(Session):
     MSG_DELIM = ']]>]]>'
     
     def __init__(self, load_known_hosts=True,
-                 missing_host_key_policy=paramiko.RejectPolicy):
+                 missing_host_key_policy=paramiko.RejectPolicy()):
         Session.__init__(self)
         self._client = paramiko.SSHClient()
         self._channel = None
@@ -93,27 +95,30 @@ class SSHSession(Session):
         self._parsing_state = state
         self._parsing_pos = self._in_buf.tell()
 
-    def load_host_keys(self, filename):
-        self._client.load_host_keys(filename)
-    
-    def set_missing_host_key_policy(self, policy):
-        self._client.set_missing_host_key_policy(policy)
-    
-    # paramiko exceptions ok?
-    # user might be looking for ClientError
+    #def load_host_keys(self, filename):
+    #    self._client.load_host_keys(filename)
+    #
+    #def set_missing_host_key_policy(self, policy):
+    #    self._client.set_missing_host_key_policy(policy)
+    #
+    #def connect(self, hostname, port=830, username=None, password=None,
+    #            key_filename=None, timeout=None, allow_agent=True,
+    #            look_for_keys=True):
+    #    self._client.connect(hostname, port=port, username=username,
+    #                        password=password, key_filename=key_filename,
+    #                        timeout=timeout, allow_agent=allow_agent,
+    #                        look_for_keys=look_for_keys)    
+    #    transport = self._client.get_transport()
+    #    self._channel = transport.open_session()
+    #    self._channel.invoke_subsystem('netconf')
+    #    self._channel.set_name('netconf')
+    #    self._connected = True
+    #    self._post_connect()
+
     def connect(self, hostname, port=830, username=None, password=None,
                 key_filename=None, timeout=None, allow_agent=True,
                 look_for_keys=True):
-        self._client.connect(hostname, port=port, username=username,
-                            password=password, key_filename=key_filename,
-                            timeout=timeout, allow_agent=allow_agent,
-                            look_for_keys=look_for_keys)    
-        transport = self._client.get_transport()
-        self._channel = transport.open_session()
-        self._channel.invoke_subsystem('netconf')
-        self._channel.set_name('netconf')
-        self._connected = True
-        self._post_connect()
+        self._transport = paramiko.Transport()
     
     def run(self):
         chan = self._channel
