rpc, rpcreply, listener... worked out now; still to test
[ncclient] / ncclient / operations / rpc.py
1 # Copyright 2009 Shikhar Bhushan
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 from threading import Event, Lock
16 from uuid import uuid1
17
18 from ncclient.content import TreeBuilder, BASE_NS
19 from ncclient.glue import Listener
20
21 from . import logger
22 from reply import RPCReply
23
24
25 class RPC(object):
26     
27     def __init__(self, session, async=False):
28         self._session = session
29         self._id = uuid1().urn
30         self._listener = RPCReplyListener(session)
31         self._listener.register(self._id, self)
32         self._reply = None
33         self._reply_event = Event()
34     
35     def _build(self, op, encoding='utf-8'):
36         if isinstance(op, dict):
37             return self.build_from_spec(self._id, op, encoding)
38         else:
39             return self.build_from_string(self._id, op, encoding)
40     
41     def _request(self, op):
42         req = self._build(op)
43         self._session.send(req)
44         if async:
45             self._reply_event.wait()
46             self._reply.parse()
47             return self._reply
48     
49     def deliver(self, raw):
50         self._reply = RPCReply(raw)
51         self._reply_event.set()
52     
53     @property
54     def has_reply(self):
55         return self._reply_event.isSet()
56     
57     @property
58     def reply(self):
59         return self._reply
60     
61     @property
62     def id(self):
63         return self._id
64     
65     @property
66     def session(self):
67         return self._session
68     
69     @property
70     def reply_event(self):
71         return self._reply_event
72     
73     @staticmethod
74     def build_from_spec(msgid, opspec, encoding='utf-8'):
75         "TODO: docstring"
76         spec = {
77             'tag': _('rpc', BASE_NS),
78             'attributes': {'message-id': msgid},
79             'children': opspec
80             }
81         return TreeBuilder(spec).to_string(encoding)
82     
83     @staticmethod
84     def build_from_string(msgid, opstr, encoding='utf-8'):
85         "TODO: docstring"
86         decl = '<?xml version="1.0" encoding="%s"?>' % encoding
87         doc = (u'<rpc message-id="%s" xmlns="%s">%s</rpc>' %
88                (msgid, BASE_NS, opstr)).encode(encoding)
89         return '%s%s' % (decl, doc)
90
91
92 class RPCReplyListener(Listener):
93     
94     # TODO - determine if need locking
95     
96     # one instance per subject    
97     def __new__(cls, subject):
98         instance = subject.get_listener_instance(cls)
99         if instance is None:
100             instance = object.__new__(cls)
101             instance._id2rpc = WeakValueDictionary()
102             instance._errback = None
103             subject.add_listener(instance)
104         return instance
105     
106     def __str__(self):
107         return 'RPCReplyListener'
108     
109     def set_errback(self, errback):
110         self._errback = errback
111
112     def register(self, msgid, rpc):
113         self._id2rpc[msgid] = rpc
114     
115     def callback(self, root, raw):
116         tag, attrs = root
117         if __(tag) != 'rpc-reply':
118             return
119         for key in attrs:
120             if __(key) == 'message-id':
121                 id = attrs[key]
122                 try:
123                     rpc = self._id2rpc[id]
124                     rpc.deliver(raw)
125                 except:
126                     logger.warning('RPCReplyListener.callback: no RPC '
127                                    + 'registered for message-id: [%s]' % id)
128                 break
129         else:
130             logger.warning('<rpc-reply> without message-id received: %s' % raw)
131     
132     def errback(self, err):
133         logger.error('RPCReplyListener.errback: %r' % err)
134         if self._errback is not None:
135             self._errback(err)