rename content to xml_
[ncclient] / ncclient / xml_.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
16 """The :mod:`xml` module provides methods for creating XML documents, parsing
17 XML, and converting between different XML representations. It uses
18 :mod:`~xml.etree.ElementTree` internally.
19 """
20
21 from cStringIO import StringIO
22 from xml.etree import cElementTree as ET
23
24 from ncclient import NCClientError
25
26 class ContentError(NCClientError):
27     "Raised by methods of the :mod:`content` module in case of an error."
28     pass
29
30 ### Namespace-related
31
32 #: Base NETCONF namespace
33 BASE_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
34 #: ... and this is BASE_NS according to Cisco devices tested
35 CISCO_BS = 'urn:ietf:params:netconf:base:1.0'
36 #: namespace for Tail-f data model
37 TAILF_AAA_1_1 = 'http://tail-f.com/ns/aaa/1.1'
38 #: namespace for Tail-f data model
39 TAILF_EXECD_1_1 = 'http://tail-f.com/ns/execd/1.1'
40 #: namespace for Cisco data model
41 CISCO_CPI_10 = 'http://www.cisco.com/cpi_10/schema'
42
43 try:
44     register_namespace = ET.register_namespace
45 except AttributeError:
46     def register_namespace(prefix, uri):
47         from xml.etree import ElementTree
48         # cElementTree uses ElementTree's _namespace_map, so that's ok
49         ElementTree._namespace_map[uri] = prefix
50
51 register_namespace('netconf', BASE_NS)
52 register_namespace('aaa', TAILF_AAA_1_1)
53 register_namespace('execd', TAILF_EXECD_1_1)
54 register_namespace('cpi', CISCO_CPI_10)
55
56
57 qualify = lambda tag, ns=BASE_NS: tag if ns is None else '{%s}%s' % (ns, tag)
58
59 #: Deprecated
60 multiqualify = lambda tag, nslist=(BASE_NS, CISCO_BS): [qualify(tag, ns) for ns in nslist]
61
62 unqualify = lambda tag: tag[tag.rfind('}')+1:]
63
64 ### XML with Python data structures
65
66 class DictTree:
67
68     @staticmethod
69     def Element(spec):
70         """DictTree -> Element
71
72         :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
73
74         :rtype: :class:`~xml.etree.ElementTree.Element`
75         """
76         if iselement(spec):
77             return spec
78         elif isinstance(spec, basestring):
79             return XML.Element(spec)
80         if not isinstance(spec, dict):
81             raise ContentError("Invalid tree spec")
82         if 'tag' in spec:
83             ele = ET.Element(spec.get('tag'), spec.get('attrib', {}))
84             ele.text = spec.get('text', '')
85             ele.tail = spec.get('tail', '')
86             subtree = spec.get('subtree', [])
87             # might not be properly specified as list but may be dict
88             if not isinstance(subtree, list):
89                 subtree = [subtree]
90             for subele in subtree:
91                 ele.append(DictTree.Element(subele))
92             return ele
93         elif 'comment' in spec:
94             return ET.Comment(spec.get('comment'))
95         else:
96             raise ContentError('Invalid tree spec')
97
98     @staticmethod
99     def XML(spec, encoding='UTF-8'):
100         """DictTree -> XML
101
102         :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
103
104         :arg encoding: chraracter encoding
105
106         :rtype: string
107         """
108         return Element.XML(DictTree.Element(spec), encoding)
109
110 class Element:
111
112     @staticmethod
113     def DictTree(ele):
114         """DictTree -> Element
115
116         :type spec: :class:`~xml.etree.ElementTree.Element`
117         :rtype: :obj:`dict`
118         """
119         return {
120             'tag': ele.tag,
121             'attributes': ele.attrib,
122             'text': ele.text,
123             'tail': ele.tail,
124             'subtree': [ Element.DictTree(child) for child in ele.getchildren() ]
125         }
126
127     @staticmethod
128     def XML(ele, encoding='UTF-8'):
129         """Element -> XML
130
131         :type spec: :class:`~xml.etree.ElementTree.Element`
132         :arg encoding: character encoding
133         :rtype: :obj:`string`
134         """
135         xml = ET.tostring(ele, encoding)
136         if xml.startswith('<?xml'):
137             return xml
138         else:
139             return '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
140
141 class XML:
142
143     @staticmethod
144     def DictTree(xml):
145         """XML -> DictTree
146
147         :type spec: :obj:`string`
148         :rtype: :obj:`dict`
149         """
150         return Element.DictTree(XML.Element(xml))
151
152     @staticmethod
153     def Element(xml):
154         """XML -> Element
155
156         :type xml: :obj:`string`
157         :rtype: :class:`~xml.etree.ElementTree.Element`
158         """
159         return ET.fromstring(xml)
160
161 dtree2ele = DictTree.Element
162 dtree2xml = DictTree.XML
163 ele2dtree = Element.DictTree
164 ele2xml = Element.XML
165 xml2dtree = XML.DictTree
166 xml2ele = XML.Element
167
168 ### Other utility functions
169
170 iselement = ET.iselement
171
172 def find(ele, tag, nslist=[]):
173     """If *nslist* is empty, same as :meth:`xml.etree.ElementTree.Element.find`.
174     If it is not, *tag* is interpreted as an unqualified name and qualified
175     using each item in *nslist* (with a :const:`None` item in *nslit* meaning no
176     qualification is done). The first match is returned.
177
178     :arg nslist: optional list of namespaces
179     :type nslit: `string` `list`
180     """
181     if nslist:
182         for qname in multiqualify(tag):
183             found = ele.find(qname)
184             if found is not None:
185                 return found
186     else:
187         return ele.find(tag)
188
189 def parse_root(raw):
190     """Efficiently parses the root element of an XML document.
191
192     :arg raw: XML document
193     :type raw: string
194     :returns: a tuple of `(tag, attributes)`, where `tag` is the (qualified) name of the element and `attributes` is a dictionary of its attributes.
195     :rtype: `tuple`
196     """
197     fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd
198     for event, element in ET.iterparse(fp, events=('start',)):
199         return (element.tag, element.attrib)
200
201 def validated_element(rep, tags=None, attrs=None, text=None):
202     """Checks if the root element meets the supplied criteria. Returns a
203     :class:`~xml.etree.ElementTree.Element` instance if so, otherwise raises
204     :exc:`ContentError`.
205
206     :arg tags: tag name or a list of allowable tag names
207     :arg attrs: list of required attribute names, each item may be a list of allowable alternatives
208     :arg text: textual content to match
209     :type rep: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
210     """
211     ele = dtree2ele(rep)
212     err = False
213     if tags:
214         if isinstance(tags, basestring):
215             tags = [tags]
216         if ele.tag not in tags:
217             err = True
218     if attrs:
219         for req in attrs:
220             if isinstance(req, basestring): req = [req]
221             for alt in req:
222                 if alt in ele.attrib:
223                     break
224             else:
225                 err = True
226     if text and ele.text != text:
227         err = True
228     if err:
229         raise ContentError("Element [%s] does not meet requirements" % ele.tag)
230     return ele