trigger 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trigger/__init__.py +7 -0
- trigger/acl/__init__.py +32 -0
- trigger/acl/autoacl.py +70 -0
- trigger/acl/db.py +324 -0
- trigger/acl/dicts.py +357 -0
- trigger/acl/grammar.py +112 -0
- trigger/acl/ios.py +222 -0
- trigger/acl/junos.py +422 -0
- trigger/acl/models.py +118 -0
- trigger/acl/parser.py +168 -0
- trigger/acl/queue.py +296 -0
- trigger/acl/support.py +1431 -0
- trigger/acl/tools.py +746 -0
- trigger/bin/__init__.py +0 -0
- trigger/bin/acl.py +233 -0
- trigger/bin/acl_script.py +574 -0
- trigger/bin/aclconv.py +82 -0
- trigger/bin/check_access.py +93 -0
- trigger/bin/check_syntax.py +66 -0
- trigger/bin/fe.py +197 -0
- trigger/bin/find_access.py +191 -0
- trigger/bin/gnng.py +434 -0
- trigger/bin/gong.py +86 -0
- trigger/bin/load_acl.py +841 -0
- trigger/bin/load_config.py +18 -0
- trigger/bin/netdev.py +317 -0
- trigger/bin/optimizer.py +638 -0
- trigger/bin/run_cmds.py +18 -0
- trigger/changemgmt/__init__.py +352 -0
- trigger/changemgmt/bounce.py +57 -0
- trigger/cmds.py +1217 -0
- trigger/conf/__init__.py +94 -0
- trigger/conf/global_settings.py +674 -0
- trigger/contrib/__init__.py +7 -0
- trigger/exceptions.py +307 -0
- trigger/gorc.py +172 -0
- trigger/netdevices/__init__.py +1288 -0
- trigger/netdevices/loader.py +174 -0
- trigger/netscreen.py +1030 -0
- trigger/packages/__init__.py +6 -0
- trigger/packages/peewee.py +8084 -0
- trigger/rancid.py +463 -0
- trigger/tacacsrc.py +584 -0
- trigger/twister.py +2203 -0
- trigger/twister2.py +745 -0
- trigger/utils/__init__.py +88 -0
- trigger/utils/cli.py +349 -0
- trigger/utils/importlib.py +77 -0
- trigger/utils/network.py +157 -0
- trigger/utils/rcs.py +178 -0
- trigger/utils/templates.py +81 -0
- trigger/utils/url.py +78 -0
- trigger/utils/xmltodict.py +298 -0
- trigger-2.0.0.dist-info/METADATA +146 -0
- trigger-2.0.0.dist-info/RECORD +61 -0
- trigger-2.0.0.dist-info/WHEEL +5 -0
- trigger-2.0.0.dist-info/entry_points.txt +15 -0
- trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
- trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
- trigger-2.0.0.dist-info/top_level.txt +2 -0
- twisted/plugins/trigger_xmlrpc.py +124 -0
trigger/utils/rcs.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides a CVS like wrapper for local RCS (Revision Control System) with common commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__author__ = "Jathan McCollum"
|
|
6
|
+
__maintainer__ = "Jathan McCollum"
|
|
7
|
+
__email__ = "jathan.mccollum@teamaol.com"
|
|
8
|
+
__copyright__ = "Copyright 2009-2011, AOL Inc."
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import commands
|
|
14
|
+
|
|
15
|
+
# Exports
|
|
16
|
+
__all__ = ("RCS",)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Classes
|
|
20
|
+
class RCS:
|
|
21
|
+
"""
|
|
22
|
+
Simple wrapper for CLI ``rcs`` command. An instance is bound to a file.
|
|
23
|
+
|
|
24
|
+
:param file: The filename (or path) to use
|
|
25
|
+
:param create: If set, create the file if it doesn't exist
|
|
26
|
+
|
|
27
|
+
>>> from trigger.utils.rcs import RCS
|
|
28
|
+
>>> rcs = RCS('foo')
|
|
29
|
+
>>> rcs.lock()
|
|
30
|
+
True
|
|
31
|
+
>>> f = open('foo', 'w')
|
|
32
|
+
>>> f.write('bar\\n')
|
|
33
|
+
>>> f.close()
|
|
34
|
+
>>> rcs.checkin('This is my commit message')
|
|
35
|
+
True
|
|
36
|
+
>>> print rcs.log()
|
|
37
|
+
RCS file: RCS/foo,v
|
|
38
|
+
Working file: foo
|
|
39
|
+
head: 1.2
|
|
40
|
+
branch:
|
|
41
|
+
locks: strict
|
|
42
|
+
access list:
|
|
43
|
+
symbolic names:
|
|
44
|
+
keyword substitution: kv
|
|
45
|
+
total revisions: 2; selected revisions: 2
|
|
46
|
+
description:
|
|
47
|
+
----------------------------
|
|
48
|
+
revision 1.2
|
|
49
|
+
date: 2011/07/08 21:01:28; author: jathan; state: Exp; lines: +1 -0
|
|
50
|
+
This is my commit message
|
|
51
|
+
----------------------------
|
|
52
|
+
revision 1.1
|
|
53
|
+
date: 2011/07/08 20:56:53; author: jathan; state: Exp;
|
|
54
|
+
first commit
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, filename, create=True):
|
|
58
|
+
self.locked = False
|
|
59
|
+
self.filename = filename
|
|
60
|
+
|
|
61
|
+
if not os.path.exists(filename):
|
|
62
|
+
if not create:
|
|
63
|
+
self.filename = None
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
f = open(self.filename, "w")
|
|
67
|
+
except:
|
|
68
|
+
return None
|
|
69
|
+
f.close()
|
|
70
|
+
if not self.checkin(initial=True):
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def checkin(self, logmsg="none", initial=False, verbose=False):
|
|
74
|
+
"""
|
|
75
|
+
Perform an RCS checkin. If successful this also unlocks the file, so
|
|
76
|
+
there is no need to unlock it afterward.
|
|
77
|
+
|
|
78
|
+
:param logmsg: The RCS commit message
|
|
79
|
+
:param initial: Initialize a new RCS file, but do not deposit any revision
|
|
80
|
+
:param verbose: Print command output
|
|
81
|
+
|
|
82
|
+
>>> rcs.checkin('This is my commit message')
|
|
83
|
+
True
|
|
84
|
+
"""
|
|
85
|
+
if initial:
|
|
86
|
+
cmd = f'ci -m"first commit" -t- -i {self.filename}'
|
|
87
|
+
else:
|
|
88
|
+
cmd = f'ci -u -m"{logmsg}" {self.filename}'
|
|
89
|
+
status, output = commands.getstatusoutput(cmd)
|
|
90
|
+
|
|
91
|
+
if verbose:
|
|
92
|
+
print(output)
|
|
93
|
+
|
|
94
|
+
if status > 0:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def lock(self, verbose=False):
|
|
100
|
+
"""
|
|
101
|
+
Perform an RCS checkout with lock. Returns boolean of whether lock
|
|
102
|
+
was sucessful.
|
|
103
|
+
|
|
104
|
+
:param verbose: Print command output
|
|
105
|
+
|
|
106
|
+
>>> rcs.lock()
|
|
107
|
+
True
|
|
108
|
+
"""
|
|
109
|
+
cmd = f"co -f -l {self.filename}"
|
|
110
|
+
status, output = commands.getstatusoutput(cmd)
|
|
111
|
+
|
|
112
|
+
if verbose:
|
|
113
|
+
print(output)
|
|
114
|
+
|
|
115
|
+
if status > 0:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
self.locked = True
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def unlock(self, verbose=False):
|
|
122
|
+
"""
|
|
123
|
+
Perform an RCS checkout with unlock (for cancelling changes).
|
|
124
|
+
|
|
125
|
+
:param verbose: Print command output
|
|
126
|
+
|
|
127
|
+
>>> rcs.unlock()
|
|
128
|
+
True
|
|
129
|
+
"""
|
|
130
|
+
cmd = f"co -f -u {self.filename}"
|
|
131
|
+
status, output = commands.getstatusoutput(cmd)
|
|
132
|
+
|
|
133
|
+
if verbose:
|
|
134
|
+
print(output)
|
|
135
|
+
|
|
136
|
+
if status > 0:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
self.locked = False
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def lock_loop(self, callback=None, timeout=5, verbose=False):
|
|
143
|
+
"""
|
|
144
|
+
Keep trying to lock the file until a lock is obtained.
|
|
145
|
+
|
|
146
|
+
:param callback: The function to call after lock is complete
|
|
147
|
+
:param timeout: How long to sleep between lock attempts
|
|
148
|
+
:param verbose: Print command output
|
|
149
|
+
|
|
150
|
+
Default:
|
|
151
|
+
>>> rcs.lock_loop(timeout=1)
|
|
152
|
+
Sleeping to wait for the lock on the file: foo
|
|
153
|
+
Sleeping to wait for the lock on the file: foo
|
|
154
|
+
|
|
155
|
+
Verbose:
|
|
156
|
+
>>> rcs.lock_loop(timeout=1, verbose=True)
|
|
157
|
+
RCS/foo,v --> foo
|
|
158
|
+
co: RCS/foo,v: Revision 1.2 is already locked by joe.
|
|
159
|
+
Sleeping to wait for the lock on the file: foo
|
|
160
|
+
RCS/foo,v --> foo
|
|
161
|
+
co: RCS/foo,v: Revision 1.2 is already locked by joe.
|
|
162
|
+
"""
|
|
163
|
+
while not self.lock(verbose=verbose):
|
|
164
|
+
print(f"Sleeping to wait for the lock on the file: {self.filename}")
|
|
165
|
+
time.sleep(timeout)
|
|
166
|
+
if callback:
|
|
167
|
+
callback()
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def log(self):
|
|
171
|
+
"""Returns the RCS log as a string (see above)."""
|
|
172
|
+
cmd = f"rlog {self.filename} 2>&1"
|
|
173
|
+
status, output = commands.getstatusoutput(cmd)
|
|
174
|
+
|
|
175
|
+
if status > 0:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return output
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Templating functions for unstructured CLI output.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__author__ = "Thomas Cuthbert"
|
|
6
|
+
__maintainer__ = "Thomas Cuthbert"
|
|
7
|
+
__email__ = "tcuthbert90@gmail.com"
|
|
8
|
+
__copyright__ = "Copyright 2016 Trigger Org"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from twisted.python import log
|
|
14
|
+
|
|
15
|
+
from trigger.conf import settings
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import textfsm
|
|
19
|
+
except ImportError:
|
|
20
|
+
print("""
|
|
21
|
+
Woops, looks like you're missing the textfsm library.
|
|
22
|
+
|
|
23
|
+
Try installing it like this::
|
|
24
|
+
|
|
25
|
+
>>> pip install textfsm
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Exports
|
|
30
|
+
__all__ = ("get_template_path", "load_cmd_template", "get_textfsm_object")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_template_path(cmd, dev_type=None):
|
|
34
|
+
"""
|
|
35
|
+
Return textfsm templates from the directory pointed to by the TEXTFSM_TEMPLATE_DIR trigger variable.
|
|
36
|
+
|
|
37
|
+
:param dev_type: Type of device ie cisco_ios, arista_eos
|
|
38
|
+
:type dev_type: str
|
|
39
|
+
:param cmd: CLI command to load template.
|
|
40
|
+
:type cmd: str
|
|
41
|
+
:returns: String template path
|
|
42
|
+
"""
|
|
43
|
+
t_dir = settings.TEXTFSM_TEMPLATE_DIR
|
|
44
|
+
return (
|
|
45
|
+
os.path.join(
|
|
46
|
+
t_dir, "{1}_{2}.template".format(t_dir, dev_type, cmd.replace(" ", "_"))
|
|
47
|
+
)
|
|
48
|
+
or None
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_cmd_template(cmd, dev_type=None):
|
|
53
|
+
"""
|
|
54
|
+
:param dev_type: Type of device ie cisco_ios, arista_eos
|
|
55
|
+
:type dev_type: str
|
|
56
|
+
:param cmd: CLI command to load template.
|
|
57
|
+
:type cmd: str
|
|
58
|
+
:returns: String template path
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
with open(get_template_path(cmd, dev_type=dev_type), "rb") as f:
|
|
62
|
+
return textfsm.TextFSM(f)
|
|
63
|
+
except:
|
|
64
|
+
log.msg(f"Unable to load template:\n{cmd} :: {dev_type}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_textfsm_object(re_table, cli_output):
|
|
68
|
+
"Returns structure object from TextFSM data."
|
|
69
|
+
from collections import defaultdict
|
|
70
|
+
|
|
71
|
+
rv = defaultdict(list)
|
|
72
|
+
keys = re_table.header
|
|
73
|
+
values = re_table.ParseText(cli_output)
|
|
74
|
+
l = []
|
|
75
|
+
for item in values:
|
|
76
|
+
l.extend(zip(map(lambda x: x.lower(), keys), item))
|
|
77
|
+
|
|
78
|
+
for k, v in l:
|
|
79
|
+
rv[k].append(v)
|
|
80
|
+
|
|
81
|
+
return dict(rv)
|
trigger/utils/url.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for parsing/handling URLs
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__author__ = "Jathan McCollum"
|
|
6
|
+
__maintainer__ = "Jathan McCollum"
|
|
7
|
+
__email__ = "jathan.mccollum@teamaol.com"
|
|
8
|
+
__copyright__ = "Copyright 2013, AOL Inc."
|
|
9
|
+
__version__ = "0.1"
|
|
10
|
+
|
|
11
|
+
from urllib.parse import parse_qsl, unquote, urlparse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_url(url):
|
|
15
|
+
"""
|
|
16
|
+
Guts for `~trigger.utils.url.parse_url`.
|
|
17
|
+
|
|
18
|
+
Based on Kombu's ``kombu.utils.url``.
|
|
19
|
+
Source: http://bit.ly/11UFcfH
|
|
20
|
+
"""
|
|
21
|
+
parts = urlparse(url)
|
|
22
|
+
scheme = parts.scheme
|
|
23
|
+
port = parts.port or None
|
|
24
|
+
hostname = parts.hostname
|
|
25
|
+
path = parts.path or ""
|
|
26
|
+
virtual_host = path[1:] if path and path[0] == "/" else path
|
|
27
|
+
return (
|
|
28
|
+
scheme,
|
|
29
|
+
unquote(hostname or "") or None,
|
|
30
|
+
port,
|
|
31
|
+
unquote(parts.username or "") or None,
|
|
32
|
+
unquote(parts.password or "") or None,
|
|
33
|
+
unquote(path or "") or None,
|
|
34
|
+
unquote(virtual_host or "") or None,
|
|
35
|
+
unquote(parts.query or "") or None,
|
|
36
|
+
dict(dict(parse_qsl(parts.query))),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_url(url):
|
|
41
|
+
"""
|
|
42
|
+
Given a ``url`` returns, a dict of its constituent parts.
|
|
43
|
+
|
|
44
|
+
Based on Kombu's ``kombu.utils.url``.
|
|
45
|
+
Source: http://bit.ly/11UFcfH
|
|
46
|
+
|
|
47
|
+
:param url:
|
|
48
|
+
Any standard URL. (file://, https://, etc.)
|
|
49
|
+
"""
|
|
50
|
+
scheme, host, port, user, passwd, path, vhost, qs, qs_dict = _parse_url(url)
|
|
51
|
+
return dict(
|
|
52
|
+
scheme=scheme,
|
|
53
|
+
hostname=host,
|
|
54
|
+
port=port,
|
|
55
|
+
username=user,
|
|
56
|
+
password=passwd,
|
|
57
|
+
path=path,
|
|
58
|
+
virtual_host=vhost,
|
|
59
|
+
query=qs,
|
|
60
|
+
**qs_dict,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
tests = (
|
|
66
|
+
"https://username:password@myhost.aol.com:12345?limit=10&vendor=cisco#develop",
|
|
67
|
+
"file:///usr/local/etc/netdevices.xml",
|
|
68
|
+
"/usr/local/etc/netdevices.xml",
|
|
69
|
+
"mysql://dbuser:dbpass@dbhost.com:3306/",
|
|
70
|
+
"http://jathan:password@api.foo.com/netdevices/?limit=10&device_type=switch&vendor=cisco&format=json",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
import pprint
|
|
74
|
+
|
|
75
|
+
for test in tests:
|
|
76
|
+
print(test)
|
|
77
|
+
pprint.pprint(parse_url(test))
|
|
78
|
+
print
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"Makes working with XML feel like you are working with JSON"
|
|
3
|
+
|
|
4
|
+
from xml.parsers import expat
|
|
5
|
+
from xml.sax.saxutils import XMLGenerator
|
|
6
|
+
from xml.sax.xmlreader import AttributesImpl
|
|
7
|
+
|
|
8
|
+
try: # pragma no cover
|
|
9
|
+
from cStringIO import StringIO
|
|
10
|
+
except ImportError: # pragma no cover
|
|
11
|
+
try:
|
|
12
|
+
from StringIO import StringIO
|
|
13
|
+
except ImportError:
|
|
14
|
+
from io import StringIO
|
|
15
|
+
try: # pragma no cover
|
|
16
|
+
from collections import OrderedDict
|
|
17
|
+
except ImportError: # pragma no cover
|
|
18
|
+
OrderedDict = dict
|
|
19
|
+
|
|
20
|
+
try: # pragma no cover
|
|
21
|
+
_basestring = basestring
|
|
22
|
+
except NameError: # pragma no cover
|
|
23
|
+
_basestring = str
|
|
24
|
+
try: # pragma no cover
|
|
25
|
+
_unicode = unicode
|
|
26
|
+
except NameError: # pragma no cover
|
|
27
|
+
_unicode = str
|
|
28
|
+
|
|
29
|
+
__author__ = "Martin Blech"
|
|
30
|
+
__version__ = "0.4.6"
|
|
31
|
+
__license__ = "MIT"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ParsingInterrupted(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _DictSAXHandler:
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
item_depth=0,
|
|
42
|
+
item_callback=lambda *args: True,
|
|
43
|
+
xml_attribs=True,
|
|
44
|
+
attr_prefix="@",
|
|
45
|
+
cdata_key="#text",
|
|
46
|
+
force_cdata=False,
|
|
47
|
+
cdata_separator="",
|
|
48
|
+
postprocessor=None,
|
|
49
|
+
dict_constructor=OrderedDict,
|
|
50
|
+
strip_whitespace=True,
|
|
51
|
+
):
|
|
52
|
+
self.path = []
|
|
53
|
+
self.stack = []
|
|
54
|
+
self.data = None
|
|
55
|
+
self.item = None
|
|
56
|
+
self.item_depth = item_depth
|
|
57
|
+
self.xml_attribs = xml_attribs
|
|
58
|
+
self.item_callback = item_callback
|
|
59
|
+
self.attr_prefix = attr_prefix
|
|
60
|
+
self.cdata_key = cdata_key
|
|
61
|
+
self.force_cdata = force_cdata
|
|
62
|
+
self.cdata_separator = cdata_separator
|
|
63
|
+
self.postprocessor = postprocessor
|
|
64
|
+
self.dict_constructor = dict_constructor
|
|
65
|
+
self.strip_whitespace = strip_whitespace
|
|
66
|
+
|
|
67
|
+
def startElement(self, name, attrs):
|
|
68
|
+
attrs = self.dict_constructor(zip(attrs[0::2], attrs[1::2]))
|
|
69
|
+
self.path.append((name, attrs or None))
|
|
70
|
+
if len(self.path) > self.item_depth:
|
|
71
|
+
self.stack.append((self.item, self.data))
|
|
72
|
+
if self.xml_attribs:
|
|
73
|
+
attrs = self.dict_constructor(
|
|
74
|
+
(self.attr_prefix + key, value) for (key, value) in attrs.items()
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
attrs = None
|
|
78
|
+
self.item = attrs or None
|
|
79
|
+
self.data = None
|
|
80
|
+
|
|
81
|
+
def endElement(self, name):
|
|
82
|
+
if len(self.path) == self.item_depth:
|
|
83
|
+
item = self.item
|
|
84
|
+
if item is None:
|
|
85
|
+
item = self.data
|
|
86
|
+
should_continue = self.item_callback(self.path, item)
|
|
87
|
+
if not should_continue:
|
|
88
|
+
raise ParsingInterrupted()
|
|
89
|
+
if len(self.stack):
|
|
90
|
+
item, data = self.item, self.data
|
|
91
|
+
self.item, self.data = self.stack.pop()
|
|
92
|
+
if self.strip_whitespace and data is not None:
|
|
93
|
+
data = data.strip() or None
|
|
94
|
+
if data and self.force_cdata and item is None:
|
|
95
|
+
item = self.dict_constructor()
|
|
96
|
+
if item is not None:
|
|
97
|
+
if data:
|
|
98
|
+
self.push_data(item, self.cdata_key, data)
|
|
99
|
+
self.item = self.push_data(self.item, name, item)
|
|
100
|
+
else:
|
|
101
|
+
self.item = self.push_data(self.item, name, data)
|
|
102
|
+
else:
|
|
103
|
+
self.item = self.data = None
|
|
104
|
+
self.path.pop()
|
|
105
|
+
|
|
106
|
+
def characters(self, data):
|
|
107
|
+
if not self.data:
|
|
108
|
+
self.data = data
|
|
109
|
+
else:
|
|
110
|
+
self.data += self.cdata_separator + data
|
|
111
|
+
|
|
112
|
+
def push_data(self, item, key, data):
|
|
113
|
+
if self.postprocessor is not None:
|
|
114
|
+
result = self.postprocessor(self.path, key, data)
|
|
115
|
+
if result is None:
|
|
116
|
+
return item
|
|
117
|
+
key, data = result
|
|
118
|
+
if item is None:
|
|
119
|
+
item = self.dict_constructor()
|
|
120
|
+
try:
|
|
121
|
+
value = item[key]
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
value.append(data)
|
|
124
|
+
else:
|
|
125
|
+
item[key] = [value, data]
|
|
126
|
+
except KeyError:
|
|
127
|
+
item[key] = data
|
|
128
|
+
return item
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse(xml_input, encoding="utf-8", *args, **kwargs):
|
|
132
|
+
"""Parse the given XML input and convert it into a dictionary.
|
|
133
|
+
|
|
134
|
+
`xml_input` can either be a `string` or a file-like object.
|
|
135
|
+
|
|
136
|
+
If `xml_attribs` is `True`, element attributes are put in the dictionary
|
|
137
|
+
among regular child elements, using `@` as a prefix to avoid collisions. If
|
|
138
|
+
set to `False`, they are just ignored.
|
|
139
|
+
|
|
140
|
+
Simple example::
|
|
141
|
+
|
|
142
|
+
>>> doc = xmltodict.parse(\"\"\"
|
|
143
|
+
... <a prop="x">
|
|
144
|
+
... <b>1</b>
|
|
145
|
+
... <b>2</b>
|
|
146
|
+
... </a>
|
|
147
|
+
... \"\"\")
|
|
148
|
+
>>> doc['a']['@prop']
|
|
149
|
+
u'x'
|
|
150
|
+
>>> doc['a']['b']
|
|
151
|
+
[u'1', u'2']
|
|
152
|
+
|
|
153
|
+
If `item_depth` is `0`, the function returns a dictionary for the root
|
|
154
|
+
element (default behavior). Otherwise, it calls `item_callback` every time
|
|
155
|
+
an item at the specified depth is found and returns `None` in the end
|
|
156
|
+
(streaming mode).
|
|
157
|
+
|
|
158
|
+
The callback function receives two parameters: the `path` from the document
|
|
159
|
+
root to the item (name-attribs pairs), and the `item` (dict). If the
|
|
160
|
+
callback's return value is false-ish, parsing will be stopped with the
|
|
161
|
+
:class:`ParsingInterrupted` exception.
|
|
162
|
+
|
|
163
|
+
Streaming example::
|
|
164
|
+
|
|
165
|
+
>>> def handle(path, item):
|
|
166
|
+
... print 'path:%s item:%s' % (path, item)
|
|
167
|
+
... return True
|
|
168
|
+
...
|
|
169
|
+
>>> xmltodict.parse(\"\"\"
|
|
170
|
+
... <a prop="x">
|
|
171
|
+
... <b>1</b>
|
|
172
|
+
... <b>2</b>
|
|
173
|
+
... </a>\"\"\", item_depth=2, item_callback=handle)
|
|
174
|
+
path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:1
|
|
175
|
+
path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:2
|
|
176
|
+
|
|
177
|
+
The optional argument `postprocessor` is a function that takes `path`, `key`
|
|
178
|
+
and `value` as positional arguments and returns a new `(key, value)` pair
|
|
179
|
+
where both `key` and `value` may have changed. Usage example::
|
|
180
|
+
|
|
181
|
+
>>> def postprocessor(path, key, value):
|
|
182
|
+
... try:
|
|
183
|
+
... return key + ':int', int(value)
|
|
184
|
+
... except (ValueError, TypeError):
|
|
185
|
+
... return key, value
|
|
186
|
+
>>> xmltodict.parse('<a><b>1</b><b>2</b><b>x</b></a>',
|
|
187
|
+
... postprocessor=postprocessor)
|
|
188
|
+
OrderedDict([(u'a', OrderedDict([(u'b:int', [1, 2]), (u'b', u'x')]))])
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
handler = _DictSAXHandler(*args, **kwargs)
|
|
192
|
+
parser = expat.ParserCreate()
|
|
193
|
+
parser.ordered_attributes = True
|
|
194
|
+
parser.StartElementHandler = handler.startElement
|
|
195
|
+
parser.EndElementHandler = handler.endElement
|
|
196
|
+
parser.CharacterDataHandler = handler.characters
|
|
197
|
+
try:
|
|
198
|
+
parser.ParseFile(xml_input)
|
|
199
|
+
except (TypeError, AttributeError):
|
|
200
|
+
if isinstance(xml_input, _unicode):
|
|
201
|
+
xml_input = xml_input.encode(encoding)
|
|
202
|
+
parser.Parse(xml_input, True)
|
|
203
|
+
return handler.item
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _emit(
|
|
207
|
+
key,
|
|
208
|
+
value,
|
|
209
|
+
content_handler,
|
|
210
|
+
attr_prefix="@",
|
|
211
|
+
cdata_key="#text",
|
|
212
|
+
root=True,
|
|
213
|
+
preprocessor=None,
|
|
214
|
+
):
|
|
215
|
+
if preprocessor is not None:
|
|
216
|
+
result = preprocessor(key, value)
|
|
217
|
+
if result is None:
|
|
218
|
+
return
|
|
219
|
+
key, value = result
|
|
220
|
+
if not isinstance(value, (list, tuple)):
|
|
221
|
+
value = [value]
|
|
222
|
+
if root and len(value) > 1:
|
|
223
|
+
raise ValueError("document with multiple roots")
|
|
224
|
+
for v in value:
|
|
225
|
+
if v is None:
|
|
226
|
+
v = OrderedDict()
|
|
227
|
+
elif not isinstance(v, dict):
|
|
228
|
+
v = _unicode(v)
|
|
229
|
+
if isinstance(v, _basestring):
|
|
230
|
+
v = OrderedDict(((cdata_key, v),))
|
|
231
|
+
cdata = None
|
|
232
|
+
attrs = OrderedDict()
|
|
233
|
+
children = []
|
|
234
|
+
for ik, iv in v.items():
|
|
235
|
+
if ik == cdata_key:
|
|
236
|
+
cdata = iv
|
|
237
|
+
continue
|
|
238
|
+
if ik.startswith(attr_prefix):
|
|
239
|
+
attrs[ik[len(attr_prefix) :]] = iv
|
|
240
|
+
continue
|
|
241
|
+
children.append((ik, iv))
|
|
242
|
+
content_handler.startElement(key, AttributesImpl(attrs))
|
|
243
|
+
for child_key, child_value in children:
|
|
244
|
+
_emit(
|
|
245
|
+
child_key,
|
|
246
|
+
child_value,
|
|
247
|
+
content_handler,
|
|
248
|
+
attr_prefix,
|
|
249
|
+
cdata_key,
|
|
250
|
+
False,
|
|
251
|
+
preprocessor,
|
|
252
|
+
)
|
|
253
|
+
if cdata is not None:
|
|
254
|
+
content_handler.characters(cdata)
|
|
255
|
+
content_handler.endElement(key)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def unparse(item, output=None, encoding="utf-8", **kwargs):
|
|
259
|
+
((key, value),) = item.items()
|
|
260
|
+
must_return = False
|
|
261
|
+
if output is None:
|
|
262
|
+
output = StringIO()
|
|
263
|
+
must_return = True
|
|
264
|
+
content_handler = XMLGenerator(output, encoding)
|
|
265
|
+
content_handler.startDocument()
|
|
266
|
+
_emit(key, value, content_handler, **kwargs)
|
|
267
|
+
content_handler.endDocument()
|
|
268
|
+
if must_return:
|
|
269
|
+
value = output.getvalue()
|
|
270
|
+
try: # pragma no cover
|
|
271
|
+
value = value.decode(encoding)
|
|
272
|
+
except AttributeError: # pragma no cover
|
|
273
|
+
pass
|
|
274
|
+
return value
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__": # pragma: no cover
|
|
278
|
+
import marshal
|
|
279
|
+
import sys
|
|
280
|
+
|
|
281
|
+
(item_depth,) = sys.argv[1:]
|
|
282
|
+
item_depth = int(item_depth)
|
|
283
|
+
|
|
284
|
+
def handle_item(path, item):
|
|
285
|
+
marshal.dump((path, item), sys.stdout)
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
root = parse(
|
|
290
|
+
sys.stdin,
|
|
291
|
+
item_depth=item_depth,
|
|
292
|
+
item_callback=handle_item,
|
|
293
|
+
dict_constructor=dict,
|
|
294
|
+
)
|
|
295
|
+
if item_depth == 0:
|
|
296
|
+
handle_item([], root)
|
|
297
|
+
except KeyboardInterrupt:
|
|
298
|
+
pass
|