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.
Files changed (61) hide show
  1. trigger/__init__.py +7 -0
  2. trigger/acl/__init__.py +32 -0
  3. trigger/acl/autoacl.py +70 -0
  4. trigger/acl/db.py +324 -0
  5. trigger/acl/dicts.py +357 -0
  6. trigger/acl/grammar.py +112 -0
  7. trigger/acl/ios.py +222 -0
  8. trigger/acl/junos.py +422 -0
  9. trigger/acl/models.py +118 -0
  10. trigger/acl/parser.py +168 -0
  11. trigger/acl/queue.py +296 -0
  12. trigger/acl/support.py +1431 -0
  13. trigger/acl/tools.py +746 -0
  14. trigger/bin/__init__.py +0 -0
  15. trigger/bin/acl.py +233 -0
  16. trigger/bin/acl_script.py +574 -0
  17. trigger/bin/aclconv.py +82 -0
  18. trigger/bin/check_access.py +93 -0
  19. trigger/bin/check_syntax.py +66 -0
  20. trigger/bin/fe.py +197 -0
  21. trigger/bin/find_access.py +191 -0
  22. trigger/bin/gnng.py +434 -0
  23. trigger/bin/gong.py +86 -0
  24. trigger/bin/load_acl.py +841 -0
  25. trigger/bin/load_config.py +18 -0
  26. trigger/bin/netdev.py +317 -0
  27. trigger/bin/optimizer.py +638 -0
  28. trigger/bin/run_cmds.py +18 -0
  29. trigger/changemgmt/__init__.py +352 -0
  30. trigger/changemgmt/bounce.py +57 -0
  31. trigger/cmds.py +1217 -0
  32. trigger/conf/__init__.py +94 -0
  33. trigger/conf/global_settings.py +674 -0
  34. trigger/contrib/__init__.py +7 -0
  35. trigger/exceptions.py +307 -0
  36. trigger/gorc.py +172 -0
  37. trigger/netdevices/__init__.py +1288 -0
  38. trigger/netdevices/loader.py +174 -0
  39. trigger/netscreen.py +1030 -0
  40. trigger/packages/__init__.py +6 -0
  41. trigger/packages/peewee.py +8084 -0
  42. trigger/rancid.py +463 -0
  43. trigger/tacacsrc.py +584 -0
  44. trigger/twister.py +2203 -0
  45. trigger/twister2.py +745 -0
  46. trigger/utils/__init__.py +88 -0
  47. trigger/utils/cli.py +349 -0
  48. trigger/utils/importlib.py +77 -0
  49. trigger/utils/network.py +157 -0
  50. trigger/utils/rcs.py +178 -0
  51. trigger/utils/templates.py +81 -0
  52. trigger/utils/url.py +78 -0
  53. trigger/utils/xmltodict.py +298 -0
  54. trigger-2.0.0.dist-info/METADATA +146 -0
  55. trigger-2.0.0.dist-info/RECORD +61 -0
  56. trigger-2.0.0.dist-info/WHEEL +5 -0
  57. trigger-2.0.0.dist-info/entry_points.txt +15 -0
  58. trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
  59. trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
  60. trigger-2.0.0.dist-info/top_level.txt +2 -0
  61. 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