Repository: aboutcode-org/tracecode-toolkit-strace (language: python)

Issue:
Failed to process trace
```
$ $(which strace) -ff -y -ttt -qq -a1 -o /tmp/outputs/ls- ls -lah /tmp/
...<ls output>...
$ ls /tmp/outputs
ls-.1301978

$ tracecode parse /tmp/outputs /tmp/parsed_outputs
INFO:tracecode:Processing traces with cwd: None, input_dir: '/tmp/outputs', output_dir: '/tmp/parsed_outputs'.
INFO:tracecode:validate_traces: Found 1 traces with root pid: 1301978.
INFO:tracecode:validate_traces: Oldest pid: 1301978.
INFO:tracecode:validate_traces: Smallest pid: 1301978.
INFO:tracecode:Queuing trace of pid 1301978 for parsing. Left to do: 0
INFO:tracecode:All 1 traces queued for parsing in 0.26 seconds.
INFO:tracecode:Filtering and saving cleaned traces to '/tmp/parsed_outputs'.
INFO:tracecode:Applied filters to 1 traces in 0.00 seconds.
INFO:tracecode:Filtered 0 empty traces from '/tmp/parsed_outputs' with 1 cycles in 0.00 seconds.
INFO:tracecode:Processing completed in 0.60 seconds. All 1 traces parsed and saved to: "/tmp/parsed_outputs".
INFO:tracecode:Completed in 0.60 seconds.

$ ls /tmp/parsed_outputs
1301978.pickle
$ ls /tmp/a
$ ls /tmp/b
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b/writes
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/a/reads'
$ tracecode list /tmp/parsed_outputs /tmp/a /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
IsADirectoryError: [Errno 21] Is a directory: '/tmp/a'
$ touch /tmp/a/reads
$ tracecode list /tmp/parsed_outputs /tmp/a /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
IsADirectoryError: [Errno 21] Is a directory: '/tmp/a'
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2318, in save_sorted
    fo.write("\n".join(sorted(seq)))
io.UnsupportedOperation: not writable
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b
$ touch /tmp/a/writes
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b/writes
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2318, in save_sorted
    fo.write("\n".join(sorted(seq)))
io.UnsupportedOperation: not writable
```
Failed to process trace
```
$ $(which strace) -ff -y -ttt -qq -a1 -o /tmp/outputs/ls- ls -lah /tmp/
...<ls output>...
$ ls /tmp/outputs
ls-.1301978

$ tracecode parse /tmp/outputs /tmp/parsed_outputs
INFO:tracecode:Processing traces with cwd: None, input_dir: '/tmp/outputs', output_dir: '/tmp/parsed_outputs'.
INFO:tracecode:validate_traces: Found 1 traces with root pid: 1301978.
INFO:tracecode:validate_traces: Oldest pid: 1301978.
INFO:tracecode:validate_traces: Smallest pid: 1301978.
INFO:tracecode:Queuing trace of pid 1301978 for parsing. Left to do: 0
INFO:tracecode:All 1 traces queued for parsing in 0.26 seconds.
INFO:tracecode:Filtering and saving cleaned traces to '/tmp/parsed_outputs'.
INFO:tracecode:Applied filters to 1 traces in 0.00 seconds.
INFO:tracecode:Filtered 0 empty traces from '/tmp/parsed_outputs' with 1 cycles in 0.00 seconds.
INFO:tracecode:Processing completed in 0.60 seconds. All 1 traces parsed and saved to: "/tmp/parsed_outputs".
INFO:tracecode:Completed in 0.60 seconds.

$ ls /tmp/parsed_outputs
1301978.pickle
$ ls /tmp/a
$ ls /tmp/b
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b/writes
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/a/reads'
$ tracecode list /tmp/parsed_outputs /tmp/a /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
IsADirectoryError: [Errno 21] Is a directory: '/tmp/a'
$ touch /tmp/a/reads
$ tracecode list /tmp/parsed_outputs /tmp/a /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2317, in save_sorted
    with open(file_out) as fo:
IsADirectoryError: [Errno 21] Is a directory: '/tmp/a'
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2318, in save_sorted
    fo.write("\n".join(sorted(seq)))
io.UnsupportedOperation: not writable
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b
$ touch /tmp/a/writes
$ tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b/writes
INFO:tracecode:Loading traces ...
INFO:tracecode:Building file lists ...
INFO:tracecode:Saving file lists ...
Traceback (most recent call last):
  File "tracecode/tracecode-toolkit-strace/venv/bin/tracecode", line 8, in <module>
    sys.exit(cli())
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2606, in cli
    main(arguments)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2547, in main
    save_file_lists(input_dir, reads_file, writes_file, debug=debug)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2255, in save_file_lists
    save_sorted(reads_file, all_reads)
  File "tracecode/tracecode-toolkit-strace/src/tracecode/tracecode.py", line 2318, in save_sorted
    fo.write("\n".join(sorted(seq)))
io.UnsupportedOperation: not writable
```


Discussion / hints:

Fix patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 6ca19c4..8eaddf4 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -9,19 +9,26 @@ jobs:
 
     - template: etc/ci/azure-posix.yml
       parameters:
-          job_name: ubuntu18_cpython
-          image_name: ubuntu-18.04
-          python_versions: ['3.6', '3.7', '3.8', '3.9', '3.10']
+          job_name: ubuntu22_cpython
+          image_name: ubuntu-22.04
+          python_versions: ['3.8', '3.9', '3.10']
           test_suites:
-              all: venv/bin/pytest -n 2 -vvs
+              all: |
+                sudo apt-get -y update
+                sudo apt-get -y install build-essential strace graphviz
+                venv/bin/pytest -n 2 -vvs
+
 
     - template: etc/ci/azure-posix.yml
       parameters:
           job_name: ubuntu20_cpython
           image_name: ubuntu-20.04
-          python_versions: ['3.6', '3.7', '3.8', '3.9', '3.10']
+          python_versions: ['3.8', '3.9', '3.10']
           test_suites:
-              all: venv/bin/pytest -n 2 -vvs
+              all: |
+                sudo apt-get -y update
+                sudo apt-get -y install build-essential strace graphviz
+                venv/bin/pytest -n 2 -vvs
 
     - template: etc/ci/azure-posix.yml
       parameters:
@@ -39,6 +46,14 @@ jobs:
           test_suites:
               all: venv/bin/pytest -n 2 -vvs
 
+    - template: etc/ci/azure-posix.yml
+      parameters:
+          job_name: macos12_cpython
+          image_name: macos-12
+          python_versions: ['3.8', '3.9', '3.10']
+          test_suites:
+              all: venv/bin/pytest -n 2 -vvs
+
     - template: etc/ci/azure-win.yml
       parameters:
           job_name: win2019_cpython
diff --git a/requirements.txt b/requirements.txt
index e69de29..4fd4628 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+#altgraph==0.17.3
+#docopt==0.6.2 
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 1b4564a..5d7e2f3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -44,9 +44,9 @@ setup_requires = setuptools_scm[toml] >= 4
 
 python_requires = >=3.6.*
 
-install_requires =
-    altgraph
-    docopt
+#install_requires =
+#    altgraph
+#    docopt
 
 [options.packages.find]
 where = src
@@ -58,6 +58,7 @@ testing =
     pytest-xdist >= 2
     aboutcode-toolkit >= 6.0.0
     black
+	vendorize
 
 docs =
     Sphinx >= 3.3.1
diff --git a/src/tracecode/__init__.py b/src/tracecode/__init__.py
index aacb54c..0a8e97d 100644
--- a/src/tracecode/__init__.py
+++ b/src/tracecode/__init__.py
@@ -7,4 +7,4 @@
 # See https://aboutcode.org for more information about nexB OSS projects.
 #
 
-__version__ = "0.10.0"
+__version__ = "0.20.0"
diff --git a/src/tracecode/_vendor/__init__.py b/src/tracecode/_vendor/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/INSTALLER b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/INSTALLER
@@ -0,0 +1,1 @@
+pip
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/LICENSE b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/LICENSE
new file mode 100644
index 0000000..6013a21
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2004 Istvan Albert unless otherwise noted.
+Copyright (c) 2006-2010 Bob Ippolito
+Copyright (2) 2010-2020 Ronald Oussoren, et. al.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/METADATA b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/METADATA
new file mode 100644
index 0000000..01cfb21
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/METADATA
@@ -0,0 +1,300 @@
+Metadata-Version: 2.1
+Name: altgraph
+Version: 0.17.3
+Summary: Python graph (network) package
+Home-page: https://altgraph.readthedocs.io
+Author: Ronald Oussoren
+Author-email: ronaldoussoren@mac.com
+Maintainer: Ronald Oussoren
+Maintainer-email: ronaldoussoren@mac.com
+License: MIT
+Download-URL: http://pypi.python.org/pypi/altgraph
+Keywords: graph
+Platform: any
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Scientific/Engineering :: Mathematics
+Classifier: Topic :: Scientific/Engineering :: Visualization
+Description-Content-Type: text/x-rst; charset=UTF-8
+License-File: LICENSE
+Project-URL: Documentation, https://altgraph.readthedocs.io/en/latest/
+Project-URL: Issue tracker, https://github.com/ronaldoussoren/altgraph/issues
+Project-URL: Repository, https://github.com/ronaldoussoren/altgraph
+
+altgraph is a fork of graphlib: a graph (network) package for constructing
+graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with
+graphviz output.
+
+altgraph includes some additional usage of Python 2.6+ features and
+enhancements related to modulegraph and macholib.
+
+CI status
+---------
+
+.. image:: https://github.com/ronaldoussoren/altgraph/workflows/Lint/badge.svg
+.. image:: https://github.com/ronaldoussoren/altgraph/workflows/Test/badge.svg
+
+Project links
+-------------
+
+* `Documentation <https://altgraph.readthedocs.io/en/latest/>`_
+
+* `Issue Tracker <https://github.com/ronaldoussoren/altgraph/issues>`_
+
+* `Repository <https://github.com/ronaldoussoren/altgraph/>`_
+
+
+Release history
+===============
+
+0.17.3
+------
+
+* Update classifiers for Python 3.11
+
+0.17.2
+------
+
+* Change in setup.py to fix the sidebar links on PyPI
+
+0.17.1
+------
+
+* Explicitly mark Python 3.10 as supported in wheel metadata.
+
+0.17
+----
+
+* Explicitly mark Python 3.8 as supported in wheel metadata.
+
+* Migrate from Bitbucket to GitHub
+
+* Run black on the entire repository
+
+0.16.1
+------
+
+* Explicitly mark Python 3.7 as supported in wheel metadata.
+
+0.16
+----
+
+* Add LICENSE file
+
+0.15
+----
+
+* ``ObjectGraph.get_edges``, ``ObjectGraph.getEdgeData`` and ``ObjectGraph.updateEdgeData``
+  accept *None* as the node to get and treat this as an alias for *self* (as other
+  methods already did).
+
+0.14
+----
+
+- Issue #7: Remove use of ``iteritems`` in altgraph.GraphAlgo code
+
+0.13
+----
+
+- Issue #4: Graph._bfs_subgraph and back_bfs_subgraph return subgraphs with reversed edges
+
+  Fix by "pombredanne" on bitbucket.
+
+
+0.12
+----
+
+- Added ``ObjectGraph.edgeData`` to retrieve the edge data
+  from a specific edge.
+
+- Added ``AltGraph.update_edge_data`` and ``ObjectGraph.updateEdgeData``
+  to update the data associated with a graph edge.
+
+0.11
+----
+
+- Stabilize the order of elements in dot file exports,
+  patch from bitbucket user 'pombredanne'.
+
+- Tweak setup.py file to remove dependency on distribute (but
+  keep the dependency on setuptools)
+
+
+0.10.2
+------
+
+- There where no classifiers in the package metadata due to a bug
+  in setup.py
+
+0.10.1
+------
+
+This is a bugfix release
+
+Bug fixes:
+
+- Issue #3: The source archive contains a README.txt
+  while the setup file refers to ReadMe.txt.
+
+  This is caused by a misfeature in distutils, as a
+  workaround I've renamed ReadMe.txt to README.txt
+  in the source tree and setup file.
+
+
+0.10
+-----
+
+This is a minor feature release
+
+Features:
+
+- Do not use "2to3" to support Python 3.
+
+  As a side effect of this altgraph now supports
+  Python 2.6 and later, and no longer supports
+  earlier releases of Python.
+
+- The order of attributes in the Dot output
+  is now always alphabetical.
+
+  With this change the output will be consistent
+  between runs and Python versions.
+
+0.9
+---
+
+This is a minor bugfix release
+
+Features:
+
+- Added ``altgraph.ObjectGraph.ObjectGraph.nodes``, a method
+  yielding all nodes in an object graph.
+
+Bugfixes:
+
+- The 0.8 release didn't work with py2app when using
+  python 3.x.
+
+
+0.8
+-----
+
+This is a minor feature release. The major new feature
+is a extensive set of unittests, which explains almost
+all other changes in this release.
+
+Bugfixes:
+
+- Installing failed with Python 2.5 due to using a distutils
+  class that isn't available in that version of Python
+  (issue #1 on the issue tracker)
+
+- ``altgraph.GraphStat.degree_dist`` now actually works
+
+- ``altgraph.Graph.add_edge(a, b, create_nodes=False)`` will
+  no longer create the edge when one of the nodes doesn't
+  exist.
+
+- ``altgraph.Graph.forw_topo_sort`` failed for some sparse graphs.
+
+- ``altgraph.Graph.back_topo_sort`` was completely broken in
+  previous releases.
+
+- ``altgraph.Graph.forw_bfs_subgraph`` now actually works.
+
+- ``altgraph.Graph.back_bfs_subgraph`` now actually works.
+
+- ``altgraph.Graph.iterdfs`` now returns the correct result
+  when the ``forward`` argument is ``False``.
+
+- ``altgraph.Graph.iterdata`` now returns the correct result
+  when the ``forward`` argument is ``False``.
+
+
+Features:
+
+- The ``altgraph.Graph`` constructor now accepts an argument
+  that contains 2- and 3-tuples instead of requireing that
+  all items have the same size. The (optional) argument can now
+  also be any iterator.
+
+- ``altgraph.Graph.Graph.add_node`` has no effect when you
+  add a hidden node.
+
+- The private method ``altgraph.Graph._bfs`` is no longer
+  present.
+
+- The private method ``altgraph.Graph._dfs`` is no longer
+  present.
+
+- ``altgraph.ObjectGraph`` now has a ``__contains__`` methods,
+  which means you can use the ``in`` operator to check if a
+  node is part of a graph.
+
+- ``altgraph.GraphUtil.generate_random_graph`` will raise
+  ``GraphError`` instead of looping forever when it is
+  impossible to create the requested graph.
+
+- ``altgraph.Dot.edge_style`` raises ``GraphError`` when
+  one of the nodes is not present in the graph. The method
+  silently added the tail in the past, but without ensuring
+  a consistent graph state.
+
+- ``altgraph.Dot.save_img`` now works when the mode is
+  ``"neato"``.
+
+0.7.2
+-----
+
+This is a minor bugfix release
+
+Bugfixes:
+
+- distutils didn't include the documentation subtree
+
+0.7.1
+-----
+
+This is a minor feature release
+
+Features:
+
+- Documentation is now generated using `sphinx <http://pypi.python.org/pypi/sphinx>`_
+  and can be viewed at <http://packages.python.org/altgraph>.
+
+- The repository has moved to bitbucket
+
+- ``altgraph.GraphStat.avg_hops`` is no longer present, the function had no
+  implementation and no specified behaviour.
+
+- the module ``altgraph.compat`` is gone, which means altgraph will no
+  longer work with Python 2.3.
+
+
+0.7.0
+-----
+
+This is a minor feature release.
+
+Features:
+
+- Support for Python 3
+
+- It is now possible to run tests using 'python setup.py test'
+
+  (The actual testsuite is still very minimal though)
+
+
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/RECORD b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/RECORD
new file mode 100644
index 0000000..b82e58c
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/RECORD
@@ -0,0 +1,22 @@
+altgraph-0.17.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+altgraph-0.17.3.dist-info/LICENSE,sha256=bBlNbbDGTUVTXRDJUUK5sM2nt9zH8d3uMCs9U289vkY,1002
+altgraph-0.17.3.dist-info/METADATA,sha256=su1fAc5Kx9YoIY4_t-re_vyX_xTMvUacCSa-6NUtMgA,7395
+altgraph-0.17.3.dist-info/RECORD,,
+altgraph-0.17.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+altgraph-0.17.3.dist-info/WHEEL,sha256=WzZ8cwjh8l0jtULNjYq1Hpr-WCqCRgPr--TX4P5I1Wo,110
+altgraph-0.17.3.dist-info/top_level.txt,sha256=HEBeRWf5ItVPc7Y9hW7hGlrLXZjPoL4by6CAhBV_BwA,9
+altgraph-0.17.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
+altgraph/Dot.py,sha256=fHS-GozpcEKyWxW2v110JaFMS68iIc0oYFlFDuNQgOQ,9901
+altgraph/Graph.py,sha256=6b6fSHLA5QSqMDnSHIO7_WJnBYIdq3K5Bt8VipRODwg,20788
+altgraph/GraphAlgo.py,sha256=Uu9aTjSKWi38iQ_e9ZrwCnzQaI1WWFDhJ6kfmu0jxAA,5645
+altgraph/GraphStat.py,sha256=vj3VqCOkzpAKggxVFLE_AlMIfPm1WN17DX4rbZjXAx4,1890
+altgraph/GraphUtil.py,sha256=1T4DJc2bJn6EIU_Ct4m0oiKlXWkXvqcXE8CGL2K9en8,3990
+altgraph/ObjectGraph.py,sha256=o7fPJtyBEgJSXAkUjzvj35B-FOY4uKzfLGqSvTitx8c,6490
+altgraph/__init__.py,sha256=YtY-rHf6X_lYk8820da2uVZT-C-B9KGpGXvBg1oZ0Fc,5015
+altgraph/__pycache__/Dot.cpython-39.pyc,,
+altgraph/__pycache__/Graph.cpython-39.pyc,,
+altgraph/__pycache__/GraphAlgo.cpython-39.pyc,,
+altgraph/__pycache__/GraphStat.cpython-39.pyc,,
+altgraph/__pycache__/GraphUtil.cpython-39.pyc,,
+altgraph/__pycache__/ObjectGraph.cpython-39.pyc,,
+altgraph/__pycache__/__init__.cpython-39.pyc,,
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/REQUESTED b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/WHEEL b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/WHEEL
new file mode 100644
index 0000000..b733a60
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
+
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/top_level.txt b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/top_level.txt
new file mode 100644
index 0000000..5ad6b8a
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/top_level.txt
@@ -0,0 +1,1 @@
+altgraph
diff --git a/src/tracecode/_vendor/altgraph-0.17.3.dist-info/zip-safe b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph-0.17.3.dist-info/zip-safe
@@ -0,0 +1,1 @@
+
diff --git a/src/tracecode/_vendor/altgraph/Dot.py b/src/tracecode/_vendor/altgraph/Dot.py
new file mode 100644
index 0000000..ae3bc25
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/Dot.py
@@ -0,0 +1,321 @@
+"""
+altgraph.Dot - Interface to the dot language
+============================================
+
+The :py:mod:`~altgraph.Dot` module provides a simple interface to the
+file format used in the
+`graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
+program. The module is intended to offload the most tedious part of the process
+(the **dot** file generation) while transparently exposing most of its
+features.
+
+To display the graphs or to generate image files the
+`graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
+package needs to be installed on the system, moreover the :command:`dot` and
+:command:`dotty` programs must be accesible in the program path so that they
+can be ran from processes spawned within the module.
+
+Example usage
+-------------
+
+Here is a typical usage::
+
+    from altgraph import Graph, Dot
+
+    # create a graph
+    edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ]
+    graph = Graph.Graph(edges)
+
+    # create a dot representation of the graph
+    dot = Dot.Dot(graph)
+
+    # display the graph
+    dot.display()
+
+    # save the dot representation into the mydot.dot file
+    dot.save_dot(file_name='mydot.dot')
+
+    # save dot file as gif image into the graph.gif file
+    dot.save_img(file_name='graph', file_type='gif')
+
+Directed graph and non-directed graph
+-------------------------------------
+
+Dot class can use for both directed graph and non-directed graph
+by passing ``graphtype`` parameter.
+
+Example::
+
+    # create directed graph(default)
+    dot = Dot.Dot(graph, graphtype="digraph")
+
+    # create non-directed graph
+    dot = Dot.Dot(graph, graphtype="graph")
+
+Customizing the output
+----------------------
+
+The graph drawing process may be customized by passing
+valid :command:`dot` parameters for the nodes and edges. For a list of all
+parameters see the `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
+documentation.
+
+Example::
+
+    # customizing the way the overall graph is drawn
+    dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75)
+
+    # customizing node drawing
+    dot.node_style(1, label='BASE_NODE',shape='box', color='blue' )
+    dot.node_style(2, style='filled', fillcolor='red')
+
+    # customizing edge drawing
+    dot.edge_style(1, 2, style='dotted')
+    dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90')
+    dot.edge_style(4, 5, arrowsize=2, style='bold')
+
+
+.. note::
+
+   dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to
+   display all graphics styles. To verify the output save it to an image file
+   and look at it that way.
+
+Valid attributes
+----------------
+
+    - dot styles, passed via the :py:meth:`Dot.style` method::
+
+        rankdir = 'LR'   (draws the graph horizontally, left to right)
+        ranksep = number (rank separation in inches)
+
+    - node attributes, passed via the :py:meth:`Dot.node_style` method::
+
+        style = 'filled' | 'invisible' | 'diagonals' | 'rounded'
+        shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle'
+
+    - edge attributes, passed via the :py:meth:`Dot.edge_style` method::
+
+        style     = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold'
+        arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none'
+            | 'tee' | 'vee'
+        weight    = number (the larger the number the closer the nodes will be)
+
+    - valid `graphviz colors
+        <http://www.research.att.com/~erg/graphviz/info/colors.html>`_
+
+    - for more details on how to control the graph drawing process see the
+      `graphviz reference
+        <http://www.research.att.com/sw/tools/graphviz/refs.html>`_.
+"""
+import os
+import warnings
+
+from ..altgraph import GraphError
+
+
+class Dot(object):
+    """
+    A  class providing a **graphviz** (dot language) representation
+    allowing a fine grained control over how the graph is being
+    displayed.
+
+    If the :command:`dot` and :command:`dotty` programs are not in the current
+    system path their location needs to be specified in the contructor.
+    """
+
+    def __init__(
+        self,
+        graph=None,
+        nodes=None,
+        edgefn=None,
+        nodevisitor=None,
+        edgevisitor=None,
+        name="G",
+        dot="dot",
+        dotty="dotty",
+        neato="neato",
+        graphtype="digraph",
+    ):
+        """
+        Initialization.
+        """
+        self.name, self.attr = name, {}
+
+        assert graphtype in ["graph", "digraph"]
+        self.type = graphtype
+
+        self.temp_dot = "tmp_dot.dot"
+        self.temp_neo = "tmp_neo.dot"
+
+        self.dot, self.dotty, self.neato = dot, dotty, neato
+
+        # self.nodes: node styles
+        # self.edges: edge styles
+        self.nodes, self.edges = {}, {}
+
+        if graph is not None and nodes is None:
+            nodes = graph
+        if graph is not None and edgefn is None:
+
+            def edgefn(node, graph=graph):
+                return graph.out_nbrs(node)
+
+        if nodes is None:
+            nodes = ()
+
+        seen = set()
+        for node in nodes:
+            if nodevisitor is None:
+                style = {}
+            else:
+                style = nodevisitor(node)
+            if style is not None:
+                self.nodes[node] = {}
+                self.node_style(node, **style)
+                seen.add(node)
+        if edgefn is not None:
+            for head in seen:
+                for tail in (n for n in edgefn(head) if n in seen):
+                    if edgevisitor is None:
+                        edgestyle = {}
+                    else:
+                        edgestyle = edgevisitor(head, tail)
+                    if edgestyle is not None:
+                        if head not in self.edges:
+                            self.edges[head] = {}
+                        self.edges[head][tail] = {}
+                        self.edge_style(head, tail, **edgestyle)
+
+    def style(self, **attr):
+        """
+        Changes the overall style
+        """
+        self.attr = attr
+
+    def display(self, mode="dot"):
+        """
+        Displays the current graph via dotty
+        """
+
+        if mode == "neato":
+            self.save_dot(self.temp_neo)
+            neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
+            os.system(neato_cmd)
+        else:
+            self.save_dot(self.temp_dot)
+
+        plot_cmd = "%s %s" % (self.dotty, self.temp_dot)
+        os.system(plot_cmd)
+
+    def node_style(self, node, **kwargs):
+        """
+        Modifies a node style to the dot representation.
+        """
+        if node not in self.edges:
+            self.edges[node] = {}
+        self.nodes[node] = kwargs
+
+    def all_node_style(self, **kwargs):
+        """
+        Modifies all node styles
+        """
+        for node in self.nodes:
+            self.node_style(node, **kwargs)
+
+    def edge_style(self, head, tail, **kwargs):
+        """
+        Modifies an edge style to the dot representation.
+        """
+        if tail not in self.nodes:
+            raise GraphError("invalid node %s" % (tail,))
+
+        try:
+            if tail not in self.edges[head]:
+                self.edges[head][tail] = {}
+            self.edges[head][tail] = kwargs
+        except KeyError:
+            raise GraphError("invalid edge  %s -> %s " % (head, tail))
+
+    def iterdot(self):
+        # write graph title
+        if self.type == "digraph":
+            yield "digraph %s {\n" % (self.name,)
+        elif self.type == "graph":
+            yield "graph %s {\n" % (self.name,)
+
+        else:
+            raise GraphError("unsupported graphtype %s" % (self.type,))
+
+        # write overall graph attributes
+        for attr_name, attr_value in sorted(self.attr.items()):
+            yield '%s="%s";' % (attr_name, attr_value)
+        yield "\n"
+
+        # some reusable patterns
+        cpatt = '%s="%s",'  # to separate attributes
+        epatt = "];\n"  # to end attributes
+
+        # write node attributes
+        for node_name, node_attr in sorted(self.nodes.items()):
+            yield '\t"%s" [' % (node_name,)
+            for attr_name, attr_value in sorted(node_attr.items()):
+                yield cpatt % (attr_name, attr_value)
+            yield epatt
+
+        # write edge attributes
+        for head in sorted(self.edges):
+            for tail in sorted(self.edges[head]):
+                if self.type == "digraph":
+                    yield '\t"%s" -> "%s" [' % (head, tail)
+                else:
+                    yield '\t"%s" -- "%s" [' % (head, tail)
+                for attr_name, attr_value in sorted(self.edges[head][tail].items()):
+                    yield cpatt % (attr_name, attr_value)
+                yield epatt
+
+        # finish file
+        yield "}\n"
+
+    def __iter__(self):
+        return self.iterdot()
+
+    def save_dot(self, file_name=None):
+        """
+        Saves the current graph representation into a file
+        """
+
+        if not file_name:
+            warnings.warn(DeprecationWarning, "always pass a file_name")
+            file_name = self.temp_dot
+
+        with open(file_name, "w") as fp:
+            for chunk in self.iterdot():
+                fp.write(chunk)
+
+    def save_img(self, file_name=None, file_type="gif", mode="dot"):
+        """
+        Saves the dot file as an image file
+        """
+
+        if not file_name:
+            warnings.warn(DeprecationWarning, "always pass a file_name")
+            file_name = "out"
+
+        if mode == "neato":
+            self.save_dot(self.temp_neo)
+            neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo)
+            os.system(neato_cmd)
+            plot_cmd = self.dot
+        else:
+            self.save_dot(self.temp_dot)
+            plot_cmd = self.dot
+
+        file_name = "%s.%s" % (file_name, file_type)
+        create_cmd = "%s -T%s %s -o %s" % (
+            plot_cmd,
+            file_type,
+            self.temp_dot,
+            file_name,
+        )
+        os.system(create_cmd)
diff --git a/src/tracecode/_vendor/altgraph/Graph.py b/src/tracecode/_vendor/altgraph/Graph.py
new file mode 100644
index 0000000..039471b
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/Graph.py
@@ -0,0 +1,682 @@
+"""
+altgraph.Graph - Base Graph class
+=================================
+
+..
+  #--Version 2.1
+  #--Bob Ippolito October, 2004
+
+  #--Version 2.0
+  #--Istvan Albert June, 2004
+
+  #--Version 1.0
+  #--Nathan Denny, May 27, 1999
+"""
+
+from collections import deque
+
+from ..altgraph import GraphError
+
+
+class Graph(object):
+    """
+    The Graph class represents a directed graph with *N* nodes and *E* edges.
+
+    Naming conventions:
+
+    - the prefixes such as *out*, *inc* and *all* will refer to methods
+      that operate on the outgoing, incoming or all edges of that node.
+
+      For example: :py:meth:`inc_degree` will refer to the degree of the node
+      computed over the incoming edges (the number of neighbours linking to
+      the node).
+
+    - the prefixes such as *forw* and *back* will refer to the
+      orientation of the edges used in the method with respect to the node.
+
+      For example: :py:meth:`forw_bfs` will start at the node then use the
+      outgoing edges to traverse the graph (goes forward).
+    """
+
+    def __init__(self, edges=None):
+        """
+        Initialization
+        """
+
+        self.next_edge = 0
+        self.nodes, self.edges = {}, {}
+        self.hidden_edges, self.hidden_nodes = {}, {}
+
+        if edges is not None:
+            for item in edges:
+                if len(item) == 2:
+                    head, tail = item
+                    self.add_edge(head, tail)
+                elif len(item) == 3:
+                    head, tail, data = item
+                    self.add_edge(head, tail, data)
+                else:
+                    raise GraphError("Cannot create edge from %s" % (item,))
+
+    def __repr__(self):
+        return "<Graph: %d nodes, %d edges>" % (
+            self.number_of_nodes(),
+            self.number_of_edges(),
+        )
+
+    def add_node(self, node, node_data=None):
+        """
+        Adds a new node to the graph.  Arbitrary data can be attached to the
+        node via the node_data parameter.  Adding the same node twice will be
+        silently ignored.
+
+        The node must be a hashable value.
+        """
+        #
+        # the nodes will contain tuples that will store incoming edges,
+        # outgoing edges and data
+        #
+        # index 0 -> incoming edges
+        # index 1 -> outgoing edges
+
+        if node in self.hidden_nodes:
+            # Node is present, but hidden
+            return
+
+        if node not in self.nodes:
+            self.nodes[node] = ([], [], node_data)
+
+    def add_edge(self, head_id, tail_id, edge_data=1, create_nodes=True):
+        """
+        Adds a directed edge going from head_id to tail_id.
+        Arbitrary data can be attached to the edge via edge_data.
+        It may create the nodes if adding edges between nonexisting ones.
+
+        :param head_id: head node
+        :param tail_id: tail node
+        :param edge_data: (optional) data attached to the edge
+        :param create_nodes: (optional) creates the head_id or tail_id
+            node in case they did not exist
+        """
+        # shorcut
+        edge = self.next_edge
+
+        # add nodes if on automatic node creation
+        if create_nodes:
+            self.add_node(head_id)
+            self.add_node(tail_id)
+
+        # update the corresponding incoming and outgoing lists in the nodes
+        # index 0 -> incoming edges
+        # index 1 -> outgoing edges
+
+        try:
+            self.nodes[tail_id][0].append(edge)
+            self.nodes[head_id][1].append(edge)
+        except KeyError:
+            raise GraphError("Invalid nodes %s -> %s" % (head_id, tail_id))
+
+        # store edge information
+        self.edges[edge] = (head_id, tail_id, edge_data)
+
+        self.next_edge += 1
+
+    def hide_edge(self, edge):
+        """
+        Hides an edge from the graph. The edge may be unhidden at some later
+        time.
+        """
+        try:
+            head_id, tail_id, edge_data = self.hidden_edges[edge] = self.edges[edge]
+            self.nodes[tail_id][0].remove(edge)
+            self.nodes[head_id][1].remove(edge)
+            del self.edges[edge]
+        except KeyError:
+            raise GraphError("Invalid edge %s" % edge)
+
+    def hide_node(self, node):
+        """
+        Hides a node from the graph.  The incoming and outgoing edges of the
+        node will also be hidden.  The node may be unhidden at some later time.
+        """
+        try:
+            all_edges = self.all_edges(node)
+            self.hidden_nodes[node] = (self.nodes[node], all_edges)
+            for edge in all_edges:
+                self.hide_edge(edge)
+            del self.nodes[node]
+        except KeyError:
+            raise GraphError("Invalid node %s" % node)
+
+    def restore_node(self, node):
+        """
+        Restores a previously hidden node back into the graph and restores
+        all of its incoming and outgoing edges.
+        """
+        try:
+            self.nodes[node], all_edges = self.hidden_nodes[node]
+            for edge in all_edges:
+                self.restore_edge(edge)
+            del self.hidden_nodes[node]
+        except KeyError:
+            raise GraphError("Invalid node %s" % node)
+
+    def restore_edge(self, edge):
+        """
+        Restores a previously hidden edge back into the graph.
+        """
+        try:
+            head_id, tail_id, data = self.hidden_edges[edge]
+            self.nodes[tail_id][0].append(edge)
+            self.nodes[head_id][1].append(edge)
+            self.edges[edge] = head_id, tail_id, data
+            del self.hidden_edges[edge]
+        except KeyError:
+            raise GraphError("Invalid edge %s" % edge)
+
+    def restore_all_edges(self):
+        """
+        Restores all hidden edges.
+        """
+        for edge in list(self.hidden_edges.keys()):
+            try:
+                self.restore_edge(edge)
+            except GraphError:
+                pass
+
+    def restore_all_nodes(self):
+        """
+        Restores all hidden nodes.
+        """
+        for node in list(self.hidden_nodes.keys()):
+            self.restore_node(node)
+
+    def __contains__(self, node):
+        """
+        Test whether a node is in the graph
+        """
+        return node in self.nodes
+
+    def edge_by_id(self, edge):
+        """
+        Returns the edge that connects the head_id and tail_id nodes
+        """
+        try:
+            head, tail, data = self.edges[edge]
+        except KeyError:
+            head, tail = None, None
+            raise GraphError("Invalid edge %s" % edge)
+
+        return (head, tail)
+
+    def edge_by_node(self, head, tail):
+        """
+        Returns the edge that connects the head_id and tail_id nodes
+        """
+        for edge in self.out_edges(head):
+            if self.tail(edge) == tail:
+                return edge
+        return None
+
+    def number_of_nodes(self):
+        """
+        Returns the number of nodes
+        """
+        return len(self.nodes)
+
+    def number_of_edges(self):
+        """
+        Returns the number of edges
+        """
+        return len(self.edges)
+
+    def __iter__(self):
+        """
+        Iterates over all nodes in the graph
+        """
+        return iter(self.nodes)
+
+    def node_list(self):
+        """
+        Return a list of the node ids for all visible nodes in the graph.
+        """
+        return list(self.nodes.keys())
+
+    def edge_list(self):
+        """
+        Returns an iterator for all visible nodes in the graph.
+        """
+        return list(self.edges.keys())
+
+    def number_of_hidden_edges(self):
+        """
+        Returns the number of hidden edges
+        """
+        return len(self.hidden_edges)
+
+    def number_of_hidden_nodes(self):
+        """
+        Returns the number of hidden nodes
+        """
+        return len(self.hidden_nodes)
+
+    def hidden_node_list(self):
+        """
+        Returns the list with the hidden nodes
+        """
+        return list(self.hidden_nodes.keys())
+
+    def hidden_edge_list(self):
+        """
+        Returns a list with the hidden edges
+        """
+        return list(self.hidden_edges.keys())
+
+    def describe_node(self, node):
+        """
+        return node, node data, outgoing edges, incoming edges for node
+        """
+        incoming, outgoing, data = self.nodes[node]
+        return node, data, outgoing, incoming
+
+    def describe_edge(self, edge):
+        """
+        return edge, edge data, head, tail for edge
+        """
+        head, tail, data = self.edges[edge]
+        return edge, data, head, tail
+
+    def node_data(self, node):
+        """
+        Returns the data associated with a node
+        """
+        return self.nodes[node][2]
+
+    def edge_data(self, edge):
+        """
+        Returns the data associated with an edge
+        """
+        return self.edges[edge][2]
+
+    def update_edge_data(self, edge, edge_data):
+        """
+        Replace the edge data for a specific edge
+        """
+        self.edges[edge] = self.edges[edge][0:2] + (edge_data,)
+
+    def head(self, edge):
+        """
+        Returns the node of the head of the edge.
+        """
+        return self.edges[edge][0]
+
+    def tail(self, edge):
+        """
+        Returns node of the tail of the edge.
+        """
+        return self.edges[edge][1]
+
+    def out_nbrs(self, node):
+        """
+        List of nodes connected by outgoing edges
+        """
+        return [self.tail(n) for n in self.out_edges(node)]
+
+    def inc_nbrs(self, node):
+        """
+        List of nodes connected by incoming edges
+        """
+        return [self.head(n) for n in self.inc_edges(node)]
+
+    def all_nbrs(self, node):
+        """
+        List of nodes connected by incoming and outgoing edges
+        """
+        return list(dict.fromkeys(self.inc_nbrs(node) + self.out_nbrs(node)))
+
+    def out_edges(self, node):
+        """
+        Returns a list of the outgoing edges
+        """
+        try:
+            return list(self.nodes[node][1])
+        except KeyError:
+            raise GraphError("Invalid node %s" % node)
+
+    def inc_edges(self, node):
+        """
+        Returns a list of the incoming edges
+        """
+        try:
+            return list(self.nodes[node][0])
+        except KeyError:
+            raise GraphError("Invalid node %s" % node)
+
+    def all_edges(self, node):
+        """
+        Returns a list of incoming and outging edges.
+        """
+        return set(self.inc_edges(node) + self.out_edges(node))
+
+    def out_degree(self, node):
+        """
+        Returns the number of outgoing edges
+        """
+        return len(self.out_edges(node))
+
+    def inc_degree(self, node):
+        """
+        Returns the number of incoming edges
+        """
+        return len(self.inc_edges(node))
+
+    def all_degree(self, node):
+        """
+        The total degree of a node
+        """
+        return self.inc_degree(node) + self.out_degree(node)
+
+    def _topo_sort(self, forward=True):
+        """
+        Topological sort.
+
+        Returns a list of nodes where the successors (based on outgoing and
+        incoming edges selected by the forward parameter) of any given node
+        appear in the sequence after that node.
+        """
+        topo_list = []
+        queue = deque()
+        indeg = {}
+
+        # select the operation that will be performed
+        if forward:
+            get_edges = self.out_edges
+            get_degree = self.inc_degree
+            get_next = self.tail
+        else:
+            get_edges = self.inc_edges
+            get_degree = self.out_degree
+            get_next = self.head
+
+        for node in self.node_list():
+            degree = get_degree(node)
+            if degree:
+                indeg[node] = degree
+            else:
+                queue.append(node)
+
+        while queue:
+            curr_node = queue.popleft()
+            topo_list.append(curr_node)
+            for edge in get_edges(curr_node):
+                tail_id = get_next(edge)
+                if tail_id in indeg:
+                    indeg[tail_id] -= 1
+                    if indeg[tail_id] == 0:
+                        queue.append(tail_id)
+
+        if len(topo_list) == len(self.node_list()):
+            valid = True
+        else:
+            # the graph has cycles, invalid topological sort
+            valid = False
+
+        return (valid, topo_list)
+
+    def forw_topo_sort(self):
+        """
+        Topological sort.
+
+        Returns a list of nodes where the successors (based on outgoing edges)
+        of any given node appear in the sequence after that node.
+        """
+        return self._topo_sort(forward=True)
+
+    def back_topo_sort(self):
+        """
+        Reverse topological sort.
+
+        Returns a list of nodes where the successors (based on incoming edges)
+        of any given node appear in the sequence after that node.
+        """
+        return self._topo_sort(forward=False)
+
+    def _bfs_subgraph(self, start_id, forward=True):
+        """
+        Private method creates a subgraph in a bfs order.
+
+        The forward parameter specifies whether it is a forward or backward
+        traversal.
+        """
+        if forward:
+            get_bfs = self.forw_bfs
+            get_nbrs = self.out_nbrs
+        else:
+            get_bfs = self.back_bfs
+            get_nbrs = self.inc_nbrs
+
+        g = Graph()
+        bfs_list = get_bfs(start_id)
+        for node in bfs_list:
+            g.add_node(node)
+
+        for node in bfs_list:
+            for nbr_id in get_nbrs(node):
+                if forward:
+                    g.add_edge(node, nbr_id)
+                else:
+                    g.add_edge(nbr_id, node)
+
+        return g
+
+    def forw_bfs_subgraph(self, start_id):
+        """
+        Creates and returns a subgraph consisting of the breadth first
+        reachable nodes based on their outgoing edges.
+        """
+        return self._bfs_subgraph(start_id, forward=True)
+
+    def back_bfs_subgraph(self, start_id):
+        """
+        Creates and returns a subgraph consisting of the breadth first
+        reachable nodes based on the incoming edges.
+        """
+        return self._bfs_subgraph(start_id, forward=False)
+
+    def iterdfs(self, start, end=None, forward=True):
+        """
+        Collecting nodes in some depth first traversal.
+
+        The forward parameter specifies whether it is a forward or backward
+        traversal.
+        """
+        visited, stack = {start}, deque([start])
+
+        if forward:
+            get_edges = self.out_edges
+            get_next = self.tail
+        else:
+            get_edges = self.inc_edges
+            get_next = self.head
+
+        while stack:
+            curr_node = stack.pop()
+            yield curr_node
+            if curr_node == end:
+                break
+            for edge in sorted(get_edges(curr_node)):
+                tail = get_next(edge)
+                if tail not in visited:
+                    visited.add(tail)
+                    stack.append(tail)
+
+    def iterdata(self, start, end=None, forward=True, condition=None):
+        """
+        Perform a depth-first walk of the graph (as ``iterdfs``)
+        and yield the item data of every node where condition matches. The
+        condition callback is only called when node_data is not None.
+        """
+
+        visited, stack = {start}, deque([start])
+
+        if forward:
+            get_edges = self.out_edges
+            get_next = self.tail
+        else:
+            get_edges = self.inc_edges
+            get_next = self.head
+
+        get_data = self.node_data
+
+        while stack:
+            curr_node = stack.pop()
+            curr_data = get_data(curr_node)
+            if curr_data is not None:
+                if condition is not None and not condition(curr_data):
+                    continue
+                yield curr_data
+            if curr_node == end:
+                break
+            for edge in get_edges(curr_node):
+                tail = get_next(edge)
+                if tail not in visited:
+                    visited.add(tail)
+                    stack.append(tail)
+
+    def _iterbfs(self, start, end=None, forward=True):
+        """
+        The forward parameter specifies whether it is a forward or backward
+        traversal.  Returns a list of tuples where the first value is the hop
+        value the second value is the node id.
+        """
+        queue, visited = deque([(start, 0)]), {start}
+
+        # the direction of the bfs depends on the edges that are sampled
+        if forward:
+            get_edges = self.out_edges
+            get_next = self.tail
+        else:
+            get_edges = self.inc_edges
+            get_next = self.head
+
+        while queue:
+            curr_node, curr_step = queue.popleft()
+            yield (curr_node, curr_step)
+            if curr_node == end:
+                break
+            for edge in get_edges(curr_node):
+                tail = get_next(edge)
+                if tail not in visited:
+                    visited.add(tail)
+                    queue.append((tail, curr_step + 1))
+
+    def forw_bfs(self, start, end=None):
+        """
+        Returns a list of nodes in some forward BFS order.
+
+        Starting from the start node the breadth first search proceeds along
+        outgoing edges.
+        """
+        return [node for node, step in self._iterbfs(start, end, forward=True)]
+
+    def back_bfs(self, start, end=None):
+        """
+        Returns a list of nodes in some backward BFS order.
+
+        Starting from the start node the breadth first search proceeds along
+        incoming edges.
+        """
+        return [node for node, _ in self._iterbfs(start, end, forward=False)]
+
+    def forw_dfs(self, start, end=None):
+        """
+        Returns a list of nodes in some forward DFS order.
+
+        Starting with the start node the depth first search proceeds along
+        outgoing edges.
+        """
+        return list(self.iterdfs(start, end, forward=True))
+
+    def back_dfs(self, start, end=None):
+        """
+        Returns a list of nodes in some backward DFS order.
+
+        Starting from the start node the depth first search proceeds along
+        incoming edges.
+        """
+        return list(self.iterdfs(start, end, forward=False))
+
+    def connected(self):
+        """
+        Returns :py:data:`True` if the graph's every node can be reached from
+        every other node.
+        """
+        node_list = self.node_list()
+        for node in node_list:
+            bfs_list = self.forw_bfs(node)
+            if len(bfs_list) != len(node_list):
+                return False
+        return True
+
+    def clust_coef(self, node):
+        """
+        Computes and returns the local clustering coefficient of node.
+
+        The local cluster coefficient is proportion of the actual number of
+        edges between neighbours of node and the maximum number of edges
+        between those neighbours.
+
+        See "Local Clustering Coefficient" on
+        <http://en.wikipedia.org/wiki/Clustering_coefficient>
+        for a formal definition.
+        """
+        num = 0
+        nbr_set = set(self.out_nbrs(node))
+
+        if node in nbr_set:
+            nbr_set.remove(node)  # loop defense
+
+        for nbr in nbr_set:
+            sec_set = set(self.out_nbrs(nbr))
+            if nbr in sec_set:
+                sec_set.remove(nbr)  # loop defense
+            num += len(nbr_set & sec_set)
+
+        nbr_num = len(nbr_set)
+        if nbr_num:
+            clust_coef = float(num) / (nbr_num * (nbr_num - 1))
+        else:
+            clust_coef = 0.0
+        return clust_coef
+
+    def get_hops(self, start, end=None, forward=True):
+        """
+        Computes the hop distance to all nodes centered around a node.
+
+        First order neighbours are at hop 1, their neigbours are at hop 2 etc.
+        Uses :py:meth:`forw_bfs` or :py:meth:`back_bfs` depending on the value
+        of the forward parameter.  If the distance between all neighbouring
+        nodes is 1 the hop number corresponds to the shortest distance between
+        the nodes.
+
+        :param start: the starting node
+        :param end: ending node (optional). When not specified will search the
+            whole graph.
+        :param forward: directionality parameter (optional).
+            If C{True} (default) it uses L{forw_bfs} otherwise L{back_bfs}.
+        :return: returns a list of tuples where each tuple contains the
+            node and the hop.
+
+        Typical usage::
+
+            >> print (graph.get_hops(1, 8))
+            >> [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]
+            # node 1 is at 0 hops
+            # node 2 is at 1 hop
+            # ...
+            # node 8 is at 5 hops
+        """
+        if forward:
+            return list(self._iterbfs(start=start, end=end, forward=True))
+        else:
+            return list(self._iterbfs(start=start, end=end, forward=False))
diff --git a/src/tracecode/_vendor/altgraph/GraphAlgo.py b/src/tracecode/_vendor/altgraph/GraphAlgo.py
new file mode 100644
index 0000000..bc27ed8
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/GraphAlgo.py
@@ -0,0 +1,171 @@
+"""
+altgraph.GraphAlgo - Graph algorithms
+=====================================
+"""
+from ..altgraph import GraphError
+
+
+def dijkstra(graph, start, end=None):
+    """
+    Dijkstra's algorithm for shortest paths
+
+    `David Eppstein, UC Irvine, 4 April 2002
+        <http://www.ics.uci.edu/~eppstein/161/python/>`_
+
+    `Python Cookbook Recipe
+        <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/119466>`_
+
+    Find shortest paths from the  start node to all nodes nearer than or
+    equal to the end node.
+
+    Dijkstra's algorithm is only guaranteed to work correctly when all edge
+    lengths are positive.  This code does not verify this property for all
+    edges (only the edges examined until the end vertex is reached), but will
+    correctly compute shortest paths even for some graphs with negative edges,
+    and will raise an exception if it discovers that a negative edge has
+    caused it to make a mistake.
+
+    Adapted to altgraph by Istvan Albert, Pennsylvania State University -
+    June, 9 2004
+    """
+    D = {}  # dictionary of final distances
+    P = {}  # dictionary of predecessors
+    Q = _priorityDictionary()  # estimated distances of non-final vertices
+    Q[start] = 0
+
+    for v in Q:
+        D[v] = Q[v]
+        if v == end:
+            break
+
+        for w in graph.out_nbrs(v):
+            edge_id = graph.edge_by_node(v, w)
+            vwLength = D[v] + graph.edge_data(edge_id)
+            if w in D:
+                if vwLength < D[w]:
+                    raise GraphError(
+                        "Dijkstra: found better path to already-final vertex"
+                    )
+            elif w not in Q or vwLength < Q[w]:
+                Q[w] = vwLength
+                P[w] = v
+
+    return (D, P)
+
+
+def shortest_path(graph, start, end):
+    """
+    Find a single shortest path from the *start* node to the *end* node.
+    The input has the same conventions as dijkstra(). The output is a list of
+    the nodes in order along the shortest path.
+
+    **Note that the distances must be stored in the edge data as numeric data**
+    """
+
+    D, P = dijkstra(graph, start, end)
+    Path = []
+    while 1:
+        Path.append(end)
+        if end == start:
+            break
+        end = P[end]
+    Path.reverse()
+    return Path
+
+
+#
+# Utility classes and functions
+#
+class _priorityDictionary(dict):
+    """
+    Priority dictionary using binary heaps (internal use only)
+
+    David Eppstein, UC Irvine, 8 Mar 2002
+
+    Implements a data structure that acts almost like a dictionary, with
+    two modifications:
+
+        1. D.smallest() returns the value x minimizing D[x].  For this to
+           work correctly, all values D[x] stored in the dictionary must be
+           comparable.
+
+        2. iterating "for x in D" finds and removes the items from D in sorted
+           order. Each item is not removed until the next item is requested,
+           so D[x] will still return a useful value until the next iteration
+           of the for-loop.  Each operation takes logarithmic amortized time.
+    """
+
+    def __init__(self):
+        """
+        Initialize priorityDictionary by creating binary heap of pairs
+        (value,key).  Note that changing or removing a dict entry will not
+        remove the old pair from the heap until it is found by smallest()
+        or until the heap is rebuilt.
+        """
+        self.__heap = []
+        dict.__init__(self)
+
+    def smallest(self):
+        """
+        Find smallest item after removing deleted items from front of heap.
+        """
+        if len(self) == 0:
+            raise IndexError("smallest of empty priorityDictionary")
+        heap = self.__heap
+        while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
+            lastItem = heap.pop()
+            insertionPoint = 0
+            while 1:
+                smallChild = 2 * insertionPoint + 1
+                if (
+                    smallChild + 1 < len(heap)
+                    and heap[smallChild] > heap[smallChild + 1]
+                ):
+                    smallChild += 1
+                if smallChild >= len(heap) or lastItem <= heap[smallChild]:
+                    heap[insertionPoint] = lastItem
+                    break
+                heap[insertionPoint] = heap[smallChild]
+                insertionPoint = smallChild
+        return heap[0][1]
+
+    def __iter__(self):
+        """
+        Create destructive sorted iterator of priorityDictionary.
+        """
+
+        def iterfn():
+            while len(self) > 0:
+                x = self.smallest()
+                yield x
+                del self[x]
+
+        return iterfn()
+
+    def __setitem__(self, key, val):
+        """
+        Change value stored in dictionary and add corresponding pair to heap.
+        Rebuilds the heap if the number of deleted items gets large, to avoid
+        memory leakage.
+        """
+        dict.__setitem__(self, key, val)
+        heap = self.__heap
+        if len(heap) > 2 * len(self):
+            self.__heap = [(v, k) for k, v in self.items()]
+            self.__heap.sort()
+        else:
+            newPair = (val, key)
+            insertionPoint = len(heap)
+            heap.append(None)
+            while insertionPoint > 0 and newPair < heap[(insertionPoint - 1) // 2]:
+                heap[insertionPoint] = heap[(insertionPoint - 1) // 2]
+                insertionPoint = (insertionPoint - 1) // 2
+            heap[insertionPoint] = newPair
+
+    def setdefault(self, key, val):
+        """
+        Reimplement setdefault to pass through our customized __setitem__.
+        """
+        if key not in self:
+            self[key] = val
+        return self[key]
diff --git a/src/tracecode/_vendor/altgraph/GraphStat.py b/src/tracecode/_vendor/altgraph/GraphStat.py
new file mode 100644
index 0000000..577464b
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/GraphStat.py
@@ -0,0 +1,73 @@
+"""
+altgraph.GraphStat - Functions providing various graph statistics
+=================================================================
+"""
+
+
+def degree_dist(graph, limits=(0, 0), bin_num=10, mode="out"):
+    """
+    Computes the degree distribution for a graph.
+
+    Returns a list of tuples where the first element of the tuple is the
+    center of the bin representing a range of degrees and the second element
+    of the tuple are the number of nodes with the degree falling in the range.
+
+    Example::
+
+        ....
+    """
+
+    deg = []
+    if mode == "inc":
+        get_deg = graph.inc_degree
+    else:
+        get_deg = graph.out_degree
+
+    for node in graph:
+        deg.append(get_deg(node))
+
+    if not deg:
+        return []
+
+    results = _binning(values=deg, limits=limits, bin_num=bin_num)
+
+    return results
+
+
+_EPS = 1.0 / (2.0 ** 32)
+
+
+def _binning(values, limits=(0, 0), bin_num=10):
+    """
+    Bins data that falls between certain limits, if the limits are (0, 0) the
+    minimum and maximum values are used.
+
+    Returns a list of tuples where the first element of the tuple is the
+    center of the bin and the second element of the tuple are the counts.
+    """
+    if limits == (0, 0):
+        min_val, max_val = min(values) - _EPS, max(values) + _EPS
+    else:
+        min_val, max_val = limits
+
+    # get bin size
+    bin_size = (max_val - min_val) / float(bin_num)
+    bins = [0] * (bin_num)
+
+    # will ignore these outliers for now
+    for value in values:
+        try:
+            if (value - min_val) >= 0:
+                index = int((value - min_val) / float(bin_size))
+                bins[index] += 1
+        except IndexError:
+            pass
+
+    # make it ready for an x,y plot
+    result = []
+    center = (bin_size / 2) + min_val
+    for i, y in enumerate(bins):
+        x = center + bin_size * i
+        result.append((x, y))
+
+    return result
diff --git a/src/tracecode/_vendor/altgraph/GraphUtil.py b/src/tracecode/_vendor/altgraph/GraphUtil.py
new file mode 100644
index 0000000..fa0a2f9
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/GraphUtil.py
@@ -0,0 +1,139 @@
+"""
+altgraph.GraphUtil - Utility classes and functions
+==================================================
+"""
+
+import random
+from collections import deque
+
+from ..altgraph import Graph, GraphError
+
+
+def generate_random_graph(node_num, edge_num, self_loops=False, multi_edges=False):
+    """
+    Generates and returns a :py:class:`~altgraph.Graph.Graph` instance with
+    *node_num* nodes randomly connected by *edge_num* edges.
+    """
+    g = Graph.Graph()
+
+    if not multi_edges:
+        if self_loops:
+            max_edges = node_num * node_num
+        else:
+            max_edges = node_num * (node_num - 1)
+
+        if edge_num > max_edges:
+            raise GraphError("inconsistent arguments to 'generate_random_graph'")
+
+    nodes = range(node_num)
+
+    for node in nodes:
+        g.add_node(node)
+
+    while 1:
+        head = random.choice(nodes)
+        tail = random.choice(nodes)
+
+        # loop defense
+        if head == tail and not self_loops:
+            continue
+
+        # multiple edge defense
+        if g.edge_by_node(head, tail) is not None and not multi_edges:
+            continue
+
+        # add the edge
+        g.add_edge(head, tail)
+        if g.number_of_edges() >= edge_num:
+            break
+
+    return g
+
+
+def generate_scale_free_graph(steps, growth_num, self_loops=False, multi_edges=False):
+    """
+    Generates and returns a :py:class:`~altgraph.Graph.Graph` instance that
+    will have *steps* \\* *growth_num* nodes and a scale free (powerlaw)
+    connectivity. Starting with a fully connected graph with *growth_num*
+    nodes at every step *growth_num* nodes are added to the graph and are
+    connected to existing nodes with a probability proportional to the degree
+    of these existing nodes.
+    """
+    # The code doesn't seem to do what the documentation claims.
+    graph = Graph.Graph()
+
+    # initialize the graph
+    store = []
+    for i in range(growth_num):
+        for j in range(i + 1, growth_num):
+            store.append(i)
+            store.append(j)
+            graph.add_edge(i, j)
+
+    # generate
+    for node in range(growth_num, steps * growth_num):
+        graph.add_node(node)
+        while graph.out_degree(node) < growth_num:
+            nbr = random.choice(store)
+
+            # loop defense
+            if node == nbr and not self_loops:
+                continue
+
+            # multi edge defense
+            if graph.edge_by_node(node, nbr) and not multi_edges:
+                continue
+
+            graph.add_edge(node, nbr)
+
+        for nbr in graph.out_nbrs(node):
+            store.append(node)
+            store.append(nbr)
+
+    return graph
+
+
+def filter_stack(graph, head, filters):
+    """
+    Perform a walk in a depth-first order starting
+    at *head*.
+
+    Returns (visited, removes, orphans).
+
+    * visited: the set of visited nodes
+    * removes: the list of nodes where the node
+      data does not all *filters*
+    * orphans: tuples of (last_good, node),
+      where node is not in removes, is directly
+      reachable from a node in *removes* and
+      *last_good* is the closest upstream node that is not
+      in *removes*.
+    """
+
+    visited, removes, orphans = {head}, set(), set()
+    stack = deque([(head, head)])
+    get_data = graph.node_data
+    get_edges = graph.out_edges
+    get_tail = graph.tail
+
+    while stack:
+        last_good, node = stack.pop()
+        data = get_data(node)
+        if data is not None:
+            for filtfunc in filters:
+                if not filtfunc(data):
+                    removes.add(node)
+                    break
+            else:
+                last_good = node
+        for edge in get_edges(node):
+            tail = get_tail(edge)
+            if last_good is not node:
+                orphans.add((last_good, tail))
+            if tail not in visited:
+                visited.add(tail)
+                stack.append((last_good, tail))
+
+    orphans = [(lg, tl) for (lg, tl) in orphans if tl not in removes]
+
+    return visited, removes, orphans
diff --git a/src/tracecode/_vendor/altgraph/ObjectGraph.py b/src/tracecode/_vendor/altgraph/ObjectGraph.py
new file mode 100644
index 0000000..2ff399e
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/ObjectGraph.py
@@ -0,0 +1,212 @@
+"""
+altgraph.ObjectGraph - Graph of objects with an identifier
+==========================================================
+
+A graph of objects that have a "graphident" attribute.
+graphident is the key for the object in the graph
+"""
+
+from ..altgraph import GraphError
+from ..altgraph.Graph import Graph
+from ..altgraph.GraphUtil import filter_stack
+
+
+class ObjectGraph(object):
+    """
+    A graph of objects that have a "graphident" attribute.
+    graphident is the key for the object in the graph
+    """
+
+    def __init__(self, graph=None, debug=0):
+        if graph is None:
+            graph = Graph()
+        self.graphident = self
+        self.graph = graph
+        self.debug = debug
+        self.indent = 0
+        graph.add_node(self, None)
+
+    def __repr__(self):
+        return "<%s>" % (type(self).__name__,)
+
+    def flatten(self, condition=None, start=None):
+        """
+        Iterate over the subgraph that is entirely reachable by condition
+        starting from the given start node or the ObjectGraph root
+        """
+        if start is None:
+            start = self
+        start = self.getRawIdent(start)
+        return self.graph.iterdata(start=start, condition=condition)
+
+    def nodes(self):
+        for ident in self.graph:
+            node = self.graph.node_data(ident)
+            if node is not None:
+                yield self.graph.node_data(ident)
+
+    def get_edges(self, node):
+        if node is None:
+            node = self
+        start = self.getRawIdent(node)
+        _, _, outraw, incraw = self.graph.describe_node(start)
+
+        def iter_edges(lst, n):
+            seen = set()
+            for tpl in (self.graph.describe_edge(e) for e in lst):
+                ident = tpl[n]
+                if ident not in seen:
+                    yield self.findNode(ident)
+                    seen.add(ident)
+
+        return iter_edges(outraw, 3), iter_edges(incraw, 2)
+
+    def edgeData(self, fromNode, toNode):
+        if fromNode is None:
+            fromNode = self
+        start = self.getRawIdent(fromNode)
+        stop = self.getRawIdent(toNode)
+        edge = self.graph.edge_by_node(start, stop)
+        return self.graph.edge_data(edge)
+
+    def updateEdgeData(self, fromNode, toNode, edgeData):
+        if fromNode is None:
+            fromNode = self
+        start = self.getRawIdent(fromNode)
+        stop = self.getRawIdent(toNode)
+        edge = self.graph.edge_by_node(start, stop)
+        self.graph.update_edge_data(edge, edgeData)
+
+    def filterStack(self, filters):
+        """
+        Filter the ObjectGraph in-place by removing all edges to nodes that
+        do not match every filter in the given filter list
+
+        Returns a tuple containing the number of:
+            (nodes_visited, nodes_removed, nodes_orphaned)
+        """
+        visited, removes, orphans = filter_stack(self.graph, self, filters)
+
+        for last_good, tail in orphans:
+            self.graph.add_edge(last_good, tail, edge_data="orphan")
+
+        for node in removes:
+            self.graph.hide_node(node)
+
+        return len(visited) - 1, len(removes), len(orphans)
+
+    def removeNode(self, node):
+        """
+        Remove the given node from the graph if it exists
+        """
+        ident = self.getIdent(node)
+        if ident is not None:
+            self.graph.hide_node(ident)
+
+    def removeReference(self, fromnode, tonode):
+        """
+        Remove all edges from fromnode to tonode
+        """
+        if fromnode is None:
+            fromnode = self
+        fromident = self.getIdent(fromnode)
+        toident = self.getIdent(tonode)
+        if fromident is not None and toident is not None:
+            while True:
+                edge = self.graph.edge_by_node(fromident, toident)
+                if edge is None:
+                    break
+                self.graph.hide_edge(edge)
+
+    def getIdent(self, node):
+        """
+        Get the graph identifier for a node
+        """
+        ident = self.getRawIdent(node)
+        if ident is not None:
+            return ident
+        node = self.findNode(node)
+        if node is None:
+            return None
+        return node.graphident
+
+    def getRawIdent(self, node):
+        """
+        Get the identifier for a node object
+        """
+        if node is self:
+            return node
+        ident = getattr(node, "graphident", None)
+        return ident
+
+    def __contains__(self, node):
+        return self.findNode(node) is not None
+
+    def findNode(self, node):
+        """
+        Find the node on the graph
+        """
+        ident = self.getRawIdent(node)
+        if ident is None:
+            ident = node
+        try:
+            return self.graph.node_data(ident)
+        except KeyError:
+            return None
+
+    def addNode(self, node):
+        """
+        Add a node to the graph referenced by the root
+        """
+        self.msg(4, "addNode", node)
+
+        try:
+            self.graph.restore_node(node.graphident)
+        except GraphError:
+            self.graph.add_node(node.graphident, node)
+
+    def createReference(self, fromnode, tonode, edge_data=None):
+        """
+        Create a reference from fromnode to tonode
+        """
+        if fromnode is None:
+            fromnode = self
+        fromident, toident = self.getIdent(fromnode), self.getIdent(tonode)
+        if fromident is None or toident is None:
+            return
+        self.msg(4, "createReference", fromnode, tonode, edge_data)
+        self.graph.add_edge(fromident, toident, edge_data=edge_data)
+
+    def createNode(self, cls, name, *args, **kw):
+        """
+        Add a node of type cls to the graph if it does not already exist
+        by the given name
+        """
+        m = self.findNode(name)
+        if m is None:
+            m = cls(name, *args, **kw)
+            self.addNode(m)
+        return m
+
+    def msg(self, level, s, *args):
+        """
+        Print a debug message with the given level
+        """
+        if s and level <= self.debug:
+            print("%s%s %s" % ("  " * self.indent, s, " ".join(map(repr, args))))
+
+    def msgin(self, level, s, *args):
+        """
+        Print a debug message and indent
+        """
+        if level <= self.debug:
+            self.msg(level, s, *args)
+            self.indent = self.indent + 1
+
+    def msgout(self, level, s, *args):
+        """
+        Dedent and print a debug message
+        """
+        if level <= self.debug:
+            self.indent = self.indent - 1
+            self.msg(level, s, *args)
diff --git a/src/tracecode/_vendor/altgraph/__init__.py b/src/tracecode/_vendor/altgraph/__init__.py
new file mode 100644
index 0000000..7adc4ca
--- /dev/null
+++ b/src/tracecode/_vendor/altgraph/__init__.py
@@ -0,0 +1,149 @@
+"""
+altgraph - a python graph library
+=================================
+
+altgraph is a fork of `graphlib <http://pygraphlib.sourceforge.net>`_ tailored
+to use newer Python 2.3+ features, including additional support used by the
+py2app suite (modulegraph and macholib, specifically).
+
+altgraph is a python based graph (network) representation and manipulation
+package.  It has started out as an extension to the
+`graph_lib module
+<http://www.ece.arizona.edu/~denny/python_nest/graph_lib_1.0.1.html>`_
+written by Nathan Denny it has been significantly optimized and expanded.
+
+The :class:`altgraph.Graph.Graph` class is loosely modeled after the
+`LEDA <http://www.algorithmic-solutions.com/enleda.htm>`_
+(Library of Efficient Datatypes) representation. The library
+includes methods for constructing graphs, BFS and DFS traversals,
+topological sort, finding connected components, shortest paths as well as a
+number graph statistics functions. The library can also visualize graphs
+via `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_.
+
+The package contains the following modules:
+
+    -  the :py:mod:`altgraph.Graph` module contains the
+       :class:`~altgraph.Graph.Graph` class that stores the graph data
+
+    -  the :py:mod:`altgraph.GraphAlgo` module implements graph algorithms
+       operating on graphs (:py:class:`~altgraph.Graph.Graph`} instances)
+
+    -  the :py:mod:`altgraph.GraphStat` module contains functions for
+       computing statistical measures on graphs
+
+    -  the :py:mod:`altgraph.GraphUtil` module contains functions for
+       generating, reading and saving graphs
+
+    -  the :py:mod:`altgraph.Dot` module  contains functions for displaying
+       graphs via `graphviz <http://www.research.att.com/sw/tools/graphviz/>`_
+
+    -  the :py:mod:`altgraph.ObjectGraph` module implements a graph of
+       objects with a unique identifier
+
+Installation
+------------
+
+Download and unpack the archive then type::
+
+    python setup.py install
+
+This will install the library in the default location. For instructions on
+how to customize the install procedure read the output of::
+
+    python setup.py --help install
+
+To verify that the code works run the test suite::
+
+    python setup.py test
+
+Example usage
+-------------
+
+Lets assume that we want to analyze the graph below (links to the full picture)
+GRAPH_IMG.  Our script then might look the following way::
+
+    from altgraph import Graph, GraphAlgo, Dot
+
+    # these are the edges
+    edges = [ (1,2), (2,4), (1,3), (2,4), (3,4), (4,5), (6,5),
+        (6,14), (14,15), (6, 15),  (5,7), (7, 8), (7,13), (12,8),
+        (8,13), (11,12), (11,9), (13,11), (9,13), (13,10) ]
+
+    # creates the graph
+    graph = Graph.Graph()
+    for head, tail in edges:
+        graph.add_edge(head, tail)
+
+    # do a forward bfs from 1 at most to 20
+    print(graph.forw_bfs(1))
+
+This will print the nodes in some breadth first order::
+
+    [1, 2, 3, 4, 5, 7, 8, 13, 11, 10, 12, 9]
+
+If we wanted to get the hop-distance from node 1 to node 8
+we coud write::
+
+    print(graph.get_hops(1, 8))
+
+This will print the following::
+
+    [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]
+
+Node 1 is at 0 hops since it is the starting node, nodes 2,3 are 1 hop away ...
+node 8 is 5 hops away. To find the shortest distance between two nodes you
+can use::
+
+    print(GraphAlgo.shortest_path(graph, 1, 12))
+
+It will print the nodes on one (if there are more) the shortest paths::
+
+    [1, 2, 4, 5, 7, 13, 11, 12]
+
+To display the graph we can use the GraphViz backend::
+
+    dot = Dot.Dot(graph)
+
+    # display the graph on the monitor
+    dot.display()
+
+    # save it in an image file
+    dot.save_img(file_name='graph', file_type='gif')
+
+
+
+..
+  @author: U{Istvan Albert<http://www.personal.psu.edu/staff/i/u/iua1/>}
+
+  @license:  MIT License
+
+  Copyright (c) 2004 Istvan Albert unless otherwise noted.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to
+  deal in the Software without restriction, including without limitation the
+  rights to use, copy, modify, merge, publish, distribute, sublicense,
+  and/or sell copies of the Software, and to permit persons to whom the
+  Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+  IN THE SOFTWARE.
+  @requires: Python 2.3 or higher
+
+  @newfield contributor: Contributors:
+  @contributor: U{Reka Albert <http://www.phys.psu.edu/~ralbert/>}
+
+"""
+# import pkg_resources
+#
+# __version__ = pkg_resources.require("altgraph")[0].version
+__version__ = "0.17.3"
+
+
+class GraphError(ValueError):
+    pass
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/INSTALLER b/src/tracecode/_vendor/docopt-0.6.2.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/INSTALLER
@@ -0,0 +1,1 @@
+pip
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/LICENSE-MIT b/src/tracecode/_vendor/docopt-0.6.2.dist-info/LICENSE-MIT
new file mode 100644
index 0000000..3b2eb5c
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Vladimir Keleshev, <vladimir@keleshev.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in 
+the Software without restriction, including without limitation the rights to 
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
+of the Software, and to permit persons to whom the Software is furnished to do 
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all 
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
+SOFTWARE.
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/METADATA b/src/tracecode/_vendor/docopt-0.6.2.dist-info/METADATA
new file mode 100644
index 0000000..19c2204
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/METADATA
@@ -0,0 +1,467 @@
+Metadata-Version: 2.1
+Name: docopt
+Version: 0.6.2
+Summary: Pythonic argument parser, that will make you smile
+Home-page: http://docopt.org
+Author: Vladimir Keleshev
+Author-email: vladimir@keleshev.com
+License: MIT
+Keywords: option arguments parsing optparse argparse getopt
+Classifier: Development Status :: 3 - Alpha
+Classifier: Topic :: Utilities
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: License :: OSI Approved :: MIT License
+License-File: LICENSE-MIT
+
+``docopt`` creates *beautiful* command-line interfaces
+======================================================================
+
+Video introduction to **docopt**: `PyCon UK 2012: Create *beautiful*
+command-line interfaces with Python <http://youtu.be/pXhcPJK5cMc>`_
+
+    New in version 0.6.1:
+
+    - Fix issue `#85 <https://github.com/docopt/docopt/issues/85>`_
+      which caused improper handling of ``[options]`` shortcut
+      if it was present several times.
+
+    New in version 0.6.0:
+
+    - New argument ``options_first``, disallows interspersing options
+      and arguments.  If you supply ``options_first=True`` to
+      ``docopt``, it will interpret all arguments as positional
+      arguments after first positional argument.
+
+    - If option with argument could be repeated, its default value
+      will be interpreted as space-separated list. E.g. with
+      ``[default: ./here ./there]`` will be interpreted as
+      ``['./here', './there']``.
+
+    Breaking changes:
+
+    - Meaning of ``[options]`` shortcut slightly changed. Previously
+      it ment *"any known option"*. Now it means *"any option not in
+      usage-pattern"*.  This avoids the situation when an option is
+      allowed to be repeated unintentionaly.
+
+    - ``argv`` is ``None`` by default, not ``sys.argv[1:]``.
+      This allows ``docopt`` to always use the *latest* ``sys.argv``,
+      not ``sys.argv`` during import time.
+
+Isn't it awesome how ``optparse`` and ``argparse`` generate help
+messages based on your code?!
+
+*Hell no!*  You know what's awesome?  It's when the option parser *is*
+generated based on the beautiful help message that you write yourself!
+This way you don't need to write this stupid repeatable parser-code,
+and instead can write only the help message--*the way you want it*.
+
+**docopt** helps you create most beautiful command-line interfaces
+*easily*:
+
+.. code:: python
+
+    """Naval Fate.
+
+    Usage:
+      naval_fate.py ship new <name>...
+      naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
+      naval_fate.py ship shoot <x> <y>
+      naval_fate.py mine (set|remove) <x> <y> [--moored | --drifting]
+      naval_fate.py (-h | --help)
+      naval_fate.py --version
+
+    Options:
+      -h --help     Show this screen.
+      --version     Show version.
+      --speed=<kn>  Speed in knots [default: 10].
+      --moored      Moored (anchored) mine.
+      --drifting    Drifting mine.
+
+    """
+    from docopt import docopt
+
+
+    if __name__ == '__main__':
+        arguments = docopt(__doc__, version='Naval Fate 2.0')
+        print(arguments)
+
+Beat that! The option parser is generated based on the docstring above
+that is passed to ``docopt`` function.  ``docopt`` parses the usage
+pattern (``"Usage: ..."``) and option descriptions (lines starting
+with dash "``-``") and ensures that the program invocation matches the
+usage pattern; it parses options, arguments and commands based on
+that. The basic idea is that *a good help message has all necessary
+information in it to make a parser*.
+
+Also, `PEP 257 <http://www.python.org/dev/peps/pep-0257/>`_ recommends
+putting help message in the module docstrings.
+
+Installation
+======================================================================
+
+Use `pip <http://pip-installer.org>`_ or easy_install::
+
+    pip install docopt==0.6.2
+
+Alternatively, you can just drop ``docopt.py`` file into your
+project--it is self-contained.
+
+**docopt** is tested with Python 2.5, 2.6, 2.7, 3.2, 3.3 and PyPy.
+
+API
+======================================================================
+
+.. code:: python
+
+    from docopt import docopt
+
+.. code:: python
+
+    docopt(doc, argv=None, help=True, version=None, options_first=False)
+
+``docopt`` takes 1 required and 4 optional arguments:
+
+- ``doc`` could be a module docstring (``__doc__``) or some other
+  string that contains a **help message** that will be parsed to
+  create the option parser.  The simple rules of how to write such a
+  help message are given in next sections.  Here is a quick example of
+  such a string:
+
+.. code:: python
+
+    """Usage: my_program.py [-hso FILE] [--quiet | --verbose] [INPUT ...]
+
+    -h --help    show this
+    -s --sorted  sorted output
+    -o FILE      specify output file [default: ./test.txt]
+    --quiet      print less text
+    --verbose    print more text
+
+    """
+
+- ``argv`` is an optional argument vector; by default ``docopt`` uses
+  the argument vector passed to your program (``sys.argv[1:]``).
+  Alternatively you can supply a list of strings like ``['--verbose',
+  '-o', 'hai.txt']``.
+
+- ``help``, by default ``True``, specifies whether the parser should
+  automatically print the help message (supplied as ``doc``) and
+  terminate, in case ``-h`` or ``--help`` option is encountered
+  (options should exist in usage pattern, more on that below). If you
+  want to handle ``-h`` or ``--help`` options manually (as other
+  options), set ``help=False``.
+
+- ``version``, by default ``None``, is an optional argument that
+  specifies the version of your program. If supplied, then, (assuming
+  ``--version`` option is mentioned in usage pattern) when parser
+  encounters the ``--version`` option, it will print the supplied
+  version and terminate.  ``version`` could be any printable object,
+  but most likely a string, e.g. ``"2.1.0rc1"``.
+
+    Note, when ``docopt`` is set to automatically handle ``-h``,
+    ``--help`` and ``--version`` options, you still need to mention
+    them in usage pattern for this to work. Also, for your users to
+    know about them.
+
+- ``options_first``, by default ``False``.  If set to ``True`` will
+  disallow mixing options and positional argument.  I.e. after first
+  positional argument, all arguments will be interpreted as positional
+  even if the look like options.  This can be used for strict
+  compatibility with POSIX, or if you want to dispatch your arguments
+  to other programs.
+
+The **return** value is a simple dictionary with options, arguments
+and commands as keys, spelled exactly like in your help message.  Long
+versions of options are given priority. For example, if you invoke the
+top example as::
+
+    naval_fate.py ship Guardian move 100 150 --speed=15
+
+the return dictionary will be:
+
+.. code:: python
+
+    {'--drifting': False,    'mine': False,
+     '--help': False,        'move': True,
+     '--moored': False,      'new': False,
+     '--speed': '15',        'remove': False,
+     '--version': False,     'set': False,
+     '<name>': ['Guardian'], 'ship': True,
+     '<x>': '100',           'shoot': False,
+     '<y>': '150'}
+
+Help message format
+======================================================================
+
+Help message consists of 2 parts:
+
+- Usage pattern, e.g.::
+
+    Usage: my_program.py [-hso FILE] [--quiet | --verbose] [INPUT ...]
+
+- Option descriptions, e.g.::
+
+    -h --help    show this
+    -s --sorted  sorted output
+    -o FILE      specify output file [default: ./test.txt]
+    --quiet      print less text
+    --verbose    print more text
+
+Their format is described below; other text is ignored.
+
+Usage pattern format
+----------------------------------------------------------------------
+
+**Usage pattern** is a substring of ``doc`` that starts with
+``usage:`` (case *insensitive*) and ends with a *visibly* empty line.
+Minimum example:
+
+.. code:: python
+
+    """Usage: my_program.py
+
+    """
+
+The first word after ``usage:`` is interpreted as your program's name.
+You can specify your program's name several times to signify several
+exclusive patterns:
+
+.. code:: python
+
+    """Usage: my_program.py FILE
+              my_program.py COUNT FILE
+
+    """
+
+Each pattern can consist of the following elements:
+
+- **<arguments>**, **ARGUMENTS**. Arguments are specified as either
+  upper-case words, e.g. ``my_program.py CONTENT-PATH`` or words
+  surrounded by angular brackets: ``my_program.py <content-path>``.
+- **--options**.  Options are words started with dash (``-``), e.g.
+  ``--output``, ``-o``.  You can "stack" several of one-letter
+  options, e.g. ``-oiv`` which will be the same as ``-o -i -v``. The
+  options can have arguments, e.g.  ``--input=FILE`` or ``-i FILE`` or
+  even ``-iFILE``. However it is important that you specify option
+  descriptions if you want for option to have an argument, a default
+  value, or specify synonymous short/long versions of option (see next
+  section on option descriptions).
+- **commands** are words that do *not* follow the described above
+  conventions of ``--options`` or ``<arguments>`` or ``ARGUMENTS``,
+  plus two special commands: dash "``-``" and double dash "``--``"
+  (see below).
+
+Use the following constructs to specify patterns:
+
+- **[ ]** (brackets) **optional** elements.  e.g.: ``my_program.py
+  [-hvqo FILE]``
+- **( )** (parens) **required** elements.  All elements that are *not*
+  put in **[ ]** are also required, e.g.: ``my_program.py
+  --path=<path> <file>...`` is the same as ``my_program.py
+  (--path=<path> <file>...)``.  (Note, "required options" might be not
+  a good idea for your users).
+- **|** (pipe) **mutualy exclusive** elements. Group them using **(
+  )** if one of the mutually exclusive elements is required:
+  ``my_program.py (--clockwise | --counter-clockwise) TIME``. Group
+  them using **[ ]** if none of the mutually-exclusive elements are
+  required: ``my_program.py [--left | --right]``.
+- **...** (ellipsis) **one or more** elements. To specify that
+  arbitrary number of repeating elements could be accepted, use
+  ellipsis (``...``), e.g.  ``my_program.py FILE ...`` means one or
+  more ``FILE``-s are accepted.  If you want to accept zero or more
+  elements, use brackets, e.g.: ``my_program.py [FILE ...]``. Ellipsis
+  works as a unary operator on the expression to the left.
+- **[options]** (case sensitive) shortcut for any options.  You can
+  use it if you want to specify that the usage pattern could be
+  provided with any options defined below in the option-descriptions
+  and do not want to enumerate them all in usage-pattern.  -
+  "``[--]``". Double dash "``--``" is used by convention to separate
+  positional arguments that can be mistaken for options. In order to
+  support this convention add "``[--]``" to you usage patterns.  -
+  "``[-]``". Single dash "``-``" is used by convention to signify that
+  ``stdin`` is used instead of a file. To support this add "``[-]``"
+  to you usage patterns. "``-``" act as a normal command.
+
+If your pattern allows to match argument-less option (a flag) several
+times::
+
+    Usage: my_program.py [-v | -vv | -vvv]
+
+then number of occurences of the option will be counted. I.e.
+``args['-v']`` will be ``2`` if program was invoked as ``my_program
+-vv``. Same works for commands.
+
+If your usage patterns allows to match same-named option with argument
+or positional argument several times, the matched arguments will be
+collected into a list::
+
+    Usage: my_program.py <file> <file> --path=<path>...
+
+I.e. invoked with ``my_program.py file1 file2 --path=./here
+--path=./there`` the returned dict will contain ``args['<file>'] ==
+['file1', 'file2']`` and ``args['--path'] == ['./here', './there']``.
+
+
+Option descriptions format
+----------------------------------------------------------------------
+
+**Option descriptions** consist of a list of options that you put
+below your usage patterns.
+
+It is necessary to list option descriptions in order to specify:
+
+- synonymous short and long options,
+- if an option has an argument,
+- if option's argument has a default value.
+
+The rules are as follows:
+
+- Every line in ``doc`` that starts with ``-`` or ``--`` (not counting
+  spaces) is treated as an option description, e.g.::
+
+    Options:
+      --verbose   # GOOD
+      -o FILE     # GOOD
+    Other: --bad  # BAD, line does not start with dash "-"
+
+- To specify that option has an argument, put a word describing that
+  argument after space (or equals "``=``" sign) as shown below. Follow
+  either <angular-brackets> or UPPER-CASE convention for options'
+  arguments.  You can use comma if you want to separate options. In
+  the example below, both lines are valid, however you are recommended
+  to stick to a single style.::
+
+    -o FILE --output=FILE       # without comma, with "=" sign
+    -i <file>, --input <file>   # with comma, wihtout "=" sing
+
+- Use two spaces to separate options with their informal description::
+
+    --verbose More text.   # BAD, will be treated as if verbose option had
+                           # an argument "More", so use 2 spaces instead
+    -q        Quit.        # GOOD
+    -o FILE   Output file. # GOOD
+    --stdout  Use stdout.  # GOOD, 2 spaces
+
+- If you want to set a default value for an option with an argument,
+  put it into the option-description, in form ``[default:
+  <my-default-value>]``::
+
+    --coefficient=K  The K coefficient [default: 2.95]
+    --output=FILE    Output file [default: test.txt]
+    --directory=DIR  Some directory [default: ./]
+
+- If the option is not repeatable, the value inside ``[default: ...]``
+  will be interpeted as string.  If it *is* repeatable, it will be
+  splited into a list on whitespace::
+
+    Usage: my_program.py [--repeatable=<arg> --repeatable=<arg>]
+                         [--another-repeatable=<arg>]...
+                         [--not-repeatable=<arg>]
+
+    # will be ['./here', './there']
+    --repeatable=<arg>          [default: ./here ./there]
+
+    # will be ['./here']
+    --another-repeatable=<arg>  [default: ./here]
+
+    # will be './here ./there', because it is not repeatable
+    --not-repeatable=<arg>      [default: ./here ./there]
+
+Examples
+----------------------------------------------------------------------
+
+We have an extensive list of `examples
+<https://github.com/docopt/docopt/tree/master/examples>`_ which cover
+every aspect of functionality of **docopt**.  Try them out, read the
+source if in doubt.
+
+Subparsers, multi-level help and *huge* applications (like git)
+----------------------------------------------------------------------
+
+If you want to split your usage-pattern into several, implement
+multi-level help (whith separate help-screen for each subcommand),
+want to interface with existing scripts that don't use **docopt**, or
+you're building the next "git", you will need the new ``options_first``
+parameter (described in API section above). To get you started quickly
+we implemented a subset of git command-line interface as an example:
+`examples/git
+<https://github.com/docopt/docopt/tree/master/examples/git>`_
+
+
+Data validation
+----------------------------------------------------------------------
+
+**docopt** does one thing and does it well: it implements your
+command-line interface.  However it does not validate the input data.
+On the other hand there are libraries like `python schema
+<https://github.com/halst/schema>`_ which make validating data a
+breeze.  Take a look at `validation_example.py
+<https://github.com/docopt/docopt/tree/master/examples/validation_example.py>`_
+which uses **schema** to validate data and report an error to the
+user.
+
+Development
+======================================================================
+
+We would *love* to hear what you think about **docopt** on our `issues
+page <http://github.com/docopt/docopt/issues>`_
+
+Make pull requrests, report bugs, suggest ideas and discuss
+**docopt**. You can also drop a line directly to
+<vladimir@keleshev.com>.
+
+Porting ``docopt`` to other languages
+======================================================================
+
+We think **docopt** is so good, we want to share it beyond the Python
+community!
+
+The follosing ports are available:
+
+- `Ruby port <http://github.com/docopt/docopt.rb>`_
+- `CoffeeScript port <http://github.com/docopt/docopt.coffee>`_
+- `Lua port <http://github.com/docopt/docopt.lua>`_
+- `PHP port <http://github.com/docopt/docopt.php>`_
+
+But you can always create a port for your favorite language!  You are
+encouraged to use the Python version as a reference implementation.  A
+Language-agnostic test suite is bundled with `Python implementation
+<http://github.com/docopt/docopt>`_.
+
+Porting discussion is on `issues page
+<http://github.com/docopt/docopt/issues>`_.
+
+Changelog
+======================================================================
+
+**docopt** follows `semantic versioning <http://semver.org>`_.  The
+first release with stable API will be 1.0.0 (soon).  Until then, you
+are encouraged to specify explicitly the version in your dependency
+tools, e.g.::
+
+    pip install docopt==0.6.2
+
+- 0.6.2 `Wheel <http://pythonwheels.com/>`_ support.
+- 0.6.1 Bugfix release.
+- 0.6.0 ``options_first`` parameter.
+  **Breaking changes**: Corrected ``[options]`` meaning.
+  ``argv`` defaults to ``None``.
+- 0.5.0 Repeated options/commands are counted or accumulated into a
+  list.
+- 0.4.2 Bugfix release.
+- 0.4.0 Option descriptions become optional,
+  support for "``--``" and "``-``" commands.
+- 0.3.0 Support for (sub)commands like `git remote add`.
+  Introduce ``[options]`` shortcut for any options.
+  **Breaking changes**: ``docopt`` returns dictionary.
+- 0.2.0 Usage pattern matching. Positional arguments parsing based on
+  usage patterns.
+  **Breaking changes**: ``docopt`` returns namespace (for arguments),
+  not list. Usage pattern is formalized.
+- 0.1.0 Initial release. Options-parsing only (based on options
+  description).
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/RECORD b/src/tracecode/_vendor/docopt-0.6.2.dist-info/RECORD
new file mode 100644
index 0000000..2418101
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/RECORD
@@ -0,0 +1,9 @@
+__pycache__/docopt.cpython-39.pyc,,
+docopt-0.6.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+docopt-0.6.2.dist-info/LICENSE-MIT,sha256=PV33j1kv8kM8PGzkmECRt_SXBZ3bjGsIoGG6SON7Z_I,1097
+docopt-0.6.2.dist-info/METADATA,sha256=J79CoOZYNH-1U2_xMhC2uwA3jVNxRNFzjucxglelo2A,17936
+docopt-0.6.2.dist-info/RECORD,,
+docopt-0.6.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+docopt-0.6.2.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
+docopt-0.6.2.dist-info/top_level.txt,sha256=xAvL2ywTOdLde8wxTVye1299j65YdK3cM5963wNy5SU,7
+docopt.py,sha256=RMZQ69gz2FLIcx-j8MV1lQYwliIwDkwZVKVA14VyzFQ,19946
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/REQUESTED b/src/tracecode/_vendor/docopt-0.6.2.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/WHEEL b/src/tracecode/_vendor/docopt-0.6.2.dist-info/WHEEL
new file mode 100644
index 0000000..0b18a28
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.1)
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
+
diff --git a/src/tracecode/_vendor/docopt-0.6.2.dist-info/top_level.txt b/src/tracecode/_vendor/docopt-0.6.2.dist-info/top_level.txt
new file mode 100644
index 0000000..e5ed2a0
--- /dev/null
+++ b/src/tracecode/_vendor/docopt-0.6.2.dist-info/top_level.txt
@@ -0,0 +1,1 @@
+docopt
diff --git a/src/tracecode/_vendor/docopt.py b/src/tracecode/_vendor/docopt.py
new file mode 100644
index 0000000..bb75c18
--- /dev/null
+++ b/src/tracecode/_vendor/docopt.py
@@ -0,0 +1,579 @@
+"""Pythonic command-line interface parser that will make you smile.
+
+ * http://docopt.org
+ * Repository and issue-tracker: https://github.com/docopt/docopt
+ * Licensed under terms of MIT license (see LICENSE-MIT)
+ * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com
+
+"""
+import sys
+import re
+
+
+__all__ = ['docopt']
+__version__ = '0.6.2'
+
+
+class DocoptLanguageError(Exception):
+
+    """Error in construction of usage-message by developer."""
+
+
+class DocoptExit(SystemExit):
+
+    """Exit in case user invoked program with incorrect arguments."""
+
+    usage = ''
+
+    def __init__(self, message=''):
+        SystemExit.__init__(self, (message + '\n' + self.usage).strip())
+
+
+class Pattern(object):
+
+    def __eq__(self, other):
+        return repr(self) == repr(other)
+
+    def __hash__(self):
+        return hash(repr(self))
+
+    def fix(self):
+        self.fix_identities()
+        self.fix_repeating_arguments()
+        return self
+
+    def fix_identities(self, uniq=None):
+        """Make pattern-tree tips point to same object if they are equal."""
+        if not hasattr(self, 'children'):
+            return self
+        uniq = list(set(self.flat())) if uniq is None else uniq
+        for i, c in enumerate(self.children):
+            if not hasattr(c, 'children'):
+                assert c in uniq
+                self.children[i] = uniq[uniq.index(c)]
+            else:
+                c.fix_identities(uniq)
+
+    def fix_repeating_arguments(self):
+        """Fix elements that should accumulate/increment values."""
+        either = [list(c.children) for c in self.either.children]
+        for case in either:
+            for e in [c for c in case if case.count(c) > 1]:
+                if type(e) is Argument or type(e) is Option and e.argcount:
+                    if e.value is None:
+                        e.value = []
+                    elif type(e.value) is not list:
+                        e.value = e.value.split()
+                if type(e) is Command or type(e) is Option and e.argcount == 0:
+                    e.value = 0
+        return self
+
+    @property
+    def either(self):
+        """Transform pattern into an equivalent, with only top-level Either."""
+        # Currently the pattern will not be equivalent, but more "narrow",
+        # although good enough to reason about list arguments.
+        ret = []
+        groups = [[self]]
+        while groups:
+            children = groups.pop(0)
+            types = [type(c) for c in children]
+            if Either in types:
+                either = [c for c in children if type(c) is Either][0]
+                children.pop(children.index(either))
+                for c in either.children:
+                    groups.append([c] + children)
+            elif Required in types:
+                required = [c for c in children if type(c) is Required][0]
+                children.pop(children.index(required))
+                groups.append(list(required.children) + children)
+            elif Optional in types:
+                optional = [c for c in children if type(c) is Optional][0]
+                children.pop(children.index(optional))
+                groups.append(list(optional.children) + children)
+            elif AnyOptions in types:
+                optional = [c for c in children if type(c) is AnyOptions][0]
+                children.pop(children.index(optional))
+                groups.append(list(optional.children) + children)
+            elif OneOrMore in types:
+                oneormore = [c for c in children if type(c) is OneOrMore][0]
+                children.pop(children.index(oneormore))
+                groups.append(list(oneormore.children) * 2 + children)
+            else:
+                ret.append(children)
+        return Either(*[Required(*e) for e in ret])
+
+
+class ChildPattern(Pattern):
+
+    def __init__(self, name, value=None):
+        self.name = name
+        self.value = value
+
+    def __repr__(self):
+        return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
+
+    def flat(self, *types):
+        return [self] if not types or type(self) in types else []
+
+    def match(self, left, collected=None):
+        collected = [] if collected is None else collected
+        pos, match = self.single_match(left)
+        if match is None:
+            return False, left, collected
+        left_ = left[:pos] + left[pos + 1:]
+        same_name = [a for a in collected if a.name == self.name]
+        if type(self.value) in (int, list):
+            if type(self.value) is int:
+                increment = 1
+            else:
+                increment = ([match.value] if type(match.value) is str
+                             else match.value)
+            if not same_name:
+                match.value = increment
+                return True, left_, collected + [match]
+            same_name[0].value += increment
+            return True, left_, collected
+        return True, left_, collected + [match]
+
+
+class ParentPattern(Pattern):
+
+    def __init__(self, *children):
+        self.children = list(children)
+
+    def __repr__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                           ', '.join(repr(a) for a in self.children))
+
+    def flat(self, *types):
+        if type(self) in types:
+            return [self]
+        return sum([c.flat(*types) for c in self.children], [])
+
+
+class Argument(ChildPattern):
+
+    def single_match(self, left):
+        for n, p in enumerate(left):
+            if type(p) is Argument:
+                return n, Argument(self.name, p.value)
+        return None, None
+
+    @classmethod
+    def parse(class_, source):
+        name = re.findall(r'(<\S*?>)', source)[0]
+        value = re.findall(r'\[default: (.*)\]', source, flags=re.I)
+        return class_(name, value[0] if value else None)
+
+
+class Command(Argument):
+
+    def __init__(self, name, value=False):
+        self.name = name
+        self.value = value
+
+    def single_match(self, left):
+        for n, p in enumerate(left):
+            if type(p) is Argument:
+                if p.value == self.name:
+                    return n, Command(self.name, True)
+                else:
+                    break
+        return None, None
+
+
+class Option(ChildPattern):
+
+    def __init__(self, short=None, long=None, argcount=0, value=False):
+        assert argcount in (0, 1)
+        self.short, self.long = short, long
+        self.argcount, self.value = argcount, value
+        self.value = None if value is False and argcount else value
+
+    @classmethod
+    def parse(class_, option_description):
+        short, long, argcount, value = None, None, 0, False
+        options, _, description = option_description.strip().partition('  ')
+        options = options.replace(',', ' ').replace('=', ' ')
+        for s in options.split():
+            if s.startswith('--'):
+                long = s
+            elif s.startswith('-'):
+                short = s
+            else:
+                argcount = 1
+        if argcount:
+            matched = re.findall(r'\[default: (.*)\]', description, flags=re.I)
+            value = matched[0] if matched else None
+        return class_(short, long, argcount, value)
+
+    def single_match(self, left):
+        for n, p in enumerate(left):
+            if self.name == p.name:
+                return n, p
+        return None, None
+
+    @property
+    def name(self):
+        return self.long or self.short
+
+    def __repr__(self):
+        return 'Option(%r, %r, %r, %r)' % (self.short, self.long,
+                                           self.argcount, self.value)
+
+
+class Required(ParentPattern):
+
+    def match(self, left, collected=None):
+        collected = [] if collected is None else collected
+        l = left
+        c = collected
+        for p in self.children:
+            matched, l, c = p.match(l, c)
+            if not matched:
+                return False, left, collected
+        return True, l, c
+
+
+class Optional(ParentPattern):
+
+    def match(self, left, collected=None):
+        collected = [] if collected is None else collected
+        for p in self.children:
+            m, left, collected = p.match(left, collected)
+        return True, left, collected
+
+
+class AnyOptions(Optional):
+
+    """Marker/placeholder for [options] shortcut."""
+
+
+class OneOrMore(ParentPattern):
+
+    def match(self, left, collected=None):
+        assert len(self.children) == 1
+        collected = [] if collected is None else collected
+        l = left
+        c = collected
+        l_ = None
+        matched = True
+        times = 0
+        while matched:
+            # could it be that something didn't match but changed l or c?
+            matched, l, c = self.children[0].match(l, c)
+            times += 1 if matched else 0
+            if l_ == l:
+                break
+            l_ = l
+        if times >= 1:
+            return True, l, c
+        return False, left, collected
+
+
+class Either(ParentPattern):
+
+    def match(self, left, collected=None):
+        collected = [] if collected is None else collected
+        outcomes = []
+        for p in self.children:
+            matched, _, _ = outcome = p.match(left, collected)
+            if matched:
+                outcomes.append(outcome)
+        if outcomes:
+            return min(outcomes, key=lambda outcome: len(outcome[1]))
+        return False, left, collected
+
+
+class TokenStream(list):
+
+    def __init__(self, source, error):
+        self += source.split() if hasattr(source, 'split') else source
+        self.error = error
+
+    def move(self):
+        return self.pop(0) if len(self) else None
+
+    def current(self):
+        return self[0] if len(self) else None
+
+
+def parse_long(tokens, options):
+    """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;"""
+    long, eq, value = tokens.move().partition('=')
+    assert long.startswith('--')
+    value = None if eq == value == '' else value
+    similar = [o for o in options if o.long == long]
+    if tokens.error is DocoptExit and similar == []:  # if no exact match
+        similar = [o for o in options if o.long and o.long.startswith(long)]
+    if len(similar) > 1:  # might be simply specified ambiguously 2+ times?
+        raise tokens.error('%s is not a unique prefix: %s?' %
+                           (long, ', '.join(o.long for o in similar)))
+    elif len(similar) < 1:
+        argcount = 1 if eq == '=' else 0
+        o = Option(None, long, argcount)
+        options.append(o)
+        if tokens.error is DocoptExit:
+            o = Option(None, long, argcount, value if argcount else True)
+    else:
+        o = Option(similar[0].short, similar[0].long,
+                   similar[0].argcount, similar[0].value)
+        if o.argcount == 0:
+            if value is not None:
+                raise tokens.error('%s must not have an argument' % o.long)
+        else:
+            if value is None:
+                if tokens.current() is None:
+                    raise tokens.error('%s requires argument' % o.long)
+                value = tokens.move()
+        if tokens.error is DocoptExit:
+            o.value = value if value is not None else True
+    return [o]
+
+
+def parse_shorts(tokens, options):
+    """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;"""
+    token = tokens.move()
+    assert token.startswith('-') and not token.startswith('--')
+    left = token.lstrip('-')
+    parsed = []
+    while left != '':
+        short, left = '-' + left[0], left[1:]
+        similar = [o for o in options if o.short == short]
+        if len(similar) > 1:
+            raise tokens.error('%s is specified ambiguously %d times' %
+                               (short, len(similar)))
+        elif len(similar) < 1:
+            o = Option(short, None, 0)
+            options.append(o)
+            if tokens.error is DocoptExit:
+                o = Option(short, None, 0, True)
+        else:  # why copying is necessary here?
+            o = Option(short, similar[0].long,
+                       similar[0].argcount, similar[0].value)
+            value = None
+            if o.argcount != 0:
+                if left == '':
+                    if tokens.current() is None:
+                        raise tokens.error('%s requires argument' % short)
+                    value = tokens.move()
+                else:
+                    value = left
+                    left = ''
+            if tokens.error is DocoptExit:
+                o.value = value if value is not None else True
+        parsed.append(o)
+    return parsed
+
+
+def parse_pattern(source, options):
+    tokens = TokenStream(re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source),
+                         DocoptLanguageError)
+    result = parse_expr(tokens, options)
+    if tokens.current() is not None:
+        raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
+    return Required(*result)
+
+
+def parse_expr(tokens, options):
+    """expr ::= seq ( '|' seq )* ;"""
+    seq = parse_seq(tokens, options)
+    if tokens.current() != '|':
+        return seq
+    result = [Required(*seq)] if len(seq) > 1 else seq
+    while tokens.current() == '|':
+        tokens.move()
+        seq = parse_seq(tokens, options)
+        result += [Required(*seq)] if len(seq) > 1 else seq
+    return [Either(*result)] if len(result) > 1 else result
+
+
+def parse_seq(tokens, options):
+    """seq ::= ( atom [ '...' ] )* ;"""
+    result = []
+    while tokens.current() not in [None, ']', ')', '|']:
+        atom = parse_atom(tokens, options)
+        if tokens.current() == '...':
+            atom = [OneOrMore(*atom)]
+            tokens.move()
+        result += atom
+    return result
+
+
+def parse_atom(tokens, options):
+    """atom ::= '(' expr ')' | '[' expr ']' | 'options'
+             | long | shorts | argument | command ;
+    """
+    token = tokens.current()
+    result = []
+    if token in '([':
+        tokens.move()
+        matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token]
+        result = pattern(*parse_expr(tokens, options))
+        if tokens.move() != matching:
+            raise tokens.error("unmatched '%s'" % token)
+        return [result]
+    elif token == 'options':
+        tokens.move()
+        return [AnyOptions()]
+    elif token.startswith('--') and token != '--':
+        return parse_long(tokens, options)
+    elif token.startswith('-') and token not in ('-', '--'):
+        return parse_shorts(tokens, options)
+    elif token.startswith('<') and token.endswith('>') or token.isupper():
+        return [Argument(tokens.move())]
+    else:
+        return [Command(tokens.move())]
+
+
+def parse_argv(tokens, options, options_first=False):
+    """Parse command-line argument vector.
+
+    If options_first:
+        argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
+    else:
+        argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
+
+    """
+    parsed = []
+    while tokens.current() is not None:
+        if tokens.current() == '--':
+            return parsed + [Argument(None, v) for v in tokens]
+        elif tokens.current().startswith('--'):
+            parsed += parse_long(tokens, options)
+        elif tokens.current().startswith('-') and tokens.current() != '-':
+            parsed += parse_shorts(tokens, options)
+        elif options_first:
+            return parsed + [Argument(None, v) for v in tokens]
+        else:
+            parsed.append(Argument(None, tokens.move()))
+    return parsed
+
+
+def parse_defaults(doc):
+    # in python < 2.7 you can't pass flags=re.MULTILINE
+    split = re.split(r'\n *(<\S+?>|-\S+?)', doc)[1:]
+    split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
+    options = [Option.parse(s) for s in split if s.startswith('-')]
+    #arguments = [Argument.parse(s) for s in split if s.startswith('<')]
+    #return options, arguments
+    return options
+
+
+def printable_usage(doc):
+    # in python < 2.7 you can't pass flags=re.IGNORECASE
+    usage_split = re.split(r'([Uu][Ss][Aa][Gg][Ee]:)', doc)
+    if len(usage_split) < 3:
+        raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
+    if len(usage_split) > 3:
+        raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
+    return re.split(r'\n\s*\n', ''.join(usage_split[1:]))[0].strip()
+
+
+def formal_usage(printable_usage):
+    pu = printable_usage.split()[1:]  # split and drop "usage:"
+    return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
+
+
+def extras(help, version, options, doc):
+    if help and any((o.name in ('-h', '--help')) and o.value for o in options):
+        print(doc.strip("\n"))
+        sys.exit()
+    if version and any(o.name == '--version' and o.value for o in options):
+        print(version)
+        sys.exit()
+
+
+class Dict(dict):
+    def __repr__(self):
+        return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items()))
+
+
+def docopt(doc, argv=None, help=True, version=None, options_first=False):
+    """Parse `argv` based on command-line interface described in `doc`.
+
+    `docopt` creates your command-line interface based on its
+    description that you pass as `doc`. Such description can contain
+    --options, <positional-argument>, commands, which could be
+    [optional], (required), (mutually | exclusive) or repeated...
+
+    Parameters
+    ----------
+    doc : str
+        Description of your command-line interface.
+    argv : list of str, optional
+        Argument vector to be parsed. sys.argv[1:] is used if not
+        provided.
+    help : bool (default: True)
+        Set to False to disable automatic help on -h or --help
+        options.
+    version : any object
+        If passed, the object will be printed if --version is in
+        `argv`.
+    options_first : bool (default: False)
+        Set to True to require options preceed positional arguments,
+        i.e. to forbid options and positional arguments intermix.
+
+    Returns
+    -------
+    args : dict
+        A dictionary, where keys are names of command-line elements
+        such as e.g. "--verbose" and "<path>", and values are the
+        parsed values of those elements.
+
+    Example
+    -------
+    > from tracecode._vendor.docopt import docopt
+    > doc = '''
+    Usage:
+        my_program tcp <host> <port> [--timeout=<seconds>]
+        my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
+        my_program (-h | --help | --version)
+
+    Options:
+        -h, --help  Show this screen and exit.
+        --baud=<n>  Baudrate [default: 9600]
+    '''
+    > argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
+    > docopt(doc, argv)
+    {'--baud': '9600',
+     '--help': False,
+     '--timeout': '30',
+     '--version': False,
+     '<host>': '127.0.0.1',
+     '<port>': '80',
+     'serial': False,
+     'tcp': True}
+
+    See also
+    --------
+    * For video introduction see http://docopt.org
+    * Full documentation is available in README.rst as well as online
+      at https://github.com/docopt/docopt#readme
+
+    """
+    if argv is None:
+        argv = sys.argv[1:]
+    DocoptExit.usage = printable_usage(doc)
+    options = parse_defaults(doc)
+    pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
+    # [default] syntax for argument is disabled
+    #for a in pattern.flat(Argument):
+    #    same_name = [d for d in arguments if d.name == a.name]
+    #    if same_name:
+    #        a.value = same_name[0].value
+    argv = parse_argv(TokenStream(argv, DocoptExit), list(options),
+                      options_first)
+    pattern_options = set(pattern.flat(Option))
+    for ao in pattern.flat(AnyOptions):
+        doc_options = parse_defaults(doc)
+        ao.children = list(set(doc_options) - pattern_options)
+        #if any_options:
+        #    ao.children += [Option(o.short, o.long, o.argcount)
+        #                    for o in argv if type(o) is Option]
+    extras(help, version, argv, doc)
+    matched, left, collected = pattern.fix().match(argv)
+    if matched and left == []:  # better error message if left?
+        return Dict((a.name, a.value) for a in (pattern.flat() + collected))
+    raise DocoptExit()
diff --git a/src/tracecode/conf.py b/src/tracecode/conf.py
index 2963ac1..af0f859 100644
--- a/src/tracecode/conf.py
+++ b/src/tracecode/conf.py
@@ -54,24 +54,24 @@
     # --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=3908887, si_uid=1001} ---
     # --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0x2aaaaaacf000} ---
     # and other similar signals
-    ".*--- SIG.*",
+    r".*--- SIG.*",
     # +++ killed by SIGPIPE +++
-    ".*\+\+\+ killed by .* \+\+\+$",
+    r".*\+\+\+ killed by .* \+\+\+$",
     # rare but this can happen
-    ".*core dumped.*",
+    r".*core dumped.*",
     # +++ exited with 141 +++
     # NOTE: exit code should be tracked if the process failed?? to return 0??
-    ".*\+\+\+ exited with .* \+\+\+$",
+    r".*\+\+\+ exited with .* \+\+\+$",
     # futex(0x2aaaac4bd9d0, FUTEX_WAIT, 3963568, NULL) = ? <unavailable>
-    ".* <unavailable>$",
+    r".* <unavailable>$",
     # exit_group(141) = ?
-    ".* exit_group.* = .*$",
+    r".* exit_group.* = .*$",
     # _exit(0) = ?
-    ".* _exit\(.* = .*$",
+    r".* _exit\(.* = .*$",
     # futex(0x2aaaac4bd9d0, FUTEX_WAIT, 3663942, NULL <unfinished ...>
     # note that since we follow processes and get per-proc straces
     # futexes are typically the only unfinished business
-    ".* <unfinished \.\.\.>$",
+    r".* <unfinished \.\.\.>$",
     # The following are found when a line ends with ")"
     # which happens for failed syscalls when there is an errno returned that
     # strace was able to decode
@@ -97,7 +97,7 @@
     # (Inappropriate ioctl for device)
     # (Operation not permitted)
     # and many more
-    "^.*\)$",
+    r"^.*\)$",
 ]
 
 ###############################################################################
diff --git a/src/tracecode/tracecode.py b/src/tracecode/tracecode.py
index 0af0877..113f56e 100644
--- a/src/tracecode/tracecode.py
+++ b/src/tracecode/tracecode.py
@@ -1771,7 +1771,7 @@ def as_graph(processes, settings):
     Return a graph from a list of process objects.
     """
     logger.info("Building graph ...")
-    from altgraph.Graph import Graph
+    from tracecode._vendor.altgraph.Graph import Graph
 
     graph = Graph()
     ##############################################################################
@@ -1816,7 +1816,7 @@ def as_file_graph(processes, settings):
     connects files with an edge representing a read-from, write-to operation.
     """
     logger.info("Building graph ..")
-    from altgraph.Graph import Graph
+    from tracecode._vendor.altgraph.Graph import Graph
 
     graph = Graph()
     for proc in processes:
@@ -1977,7 +1977,7 @@ def save_graphic(graph, file_name, file_type="pdf", mode="dot"):
         logger.error("Please install graphviz from http://graphviz.org/")
         return
 
-    from altgraph import Dot
+    from tracecode._vendor.altgraph import Dot
 
     # TODO: add a style for commands and nodes
     def node_visitor(node):
@@ -2294,7 +2294,7 @@ def save_file_lists_with_counts(parsed_dir, inv_file):
     import csv
 
     logger.info("Saving file lists ...")
-    with open(inv_file) as out_file:
+    with open(inv_file, "w") as out_file:
         wrtr = csv.writer(out_file)
         for item in file_rw_counts(procs):
             wrtr.writerow(item)
@@ -2314,7 +2314,7 @@ def save_sorted(file_out, seq):
     """
     Write sorted seq to file_out file, one line per element.
     """
-    with open(file_out) as fo:
+    with open(file_out, "w") as fo:
         fo.write("\n".join(sorted(seq)))
 
 
@@ -2355,7 +2355,7 @@ def analyze_deployment_graph_from_dir(dir_path, settings):
 def analyze_deployment_graph_from_dir_to_file(dir_path, out_file, settings):
     import csv
 
-    with open(out_file) as csvfile:
+    with open(out_file, "w") as csvfile:
         wrtr = csv.writer(csvfile)
         for src_tgt in analyze_deployment_graph_from_dir(dir_path, settings):
             wrtr.writerow(src_tgt)
@@ -2378,7 +2378,7 @@ def debug_print(dir_path, pid):
 ##############################################################################
 # Command line processing
 
-from docopt import docopt
+from tracecode._vendor.docopt import docopt
 
 NOTICE = (
     """TraceCode version %s
@@ -2458,10 +2458,10 @@ def debug_print(dir_path, pid):
  analyze
   Determine sources to targets files transformations. Read parsed traces in
   PARSED_DIR and save output to ANALYSIS_FILE as lines of comma-separated
-  <source file path>,<target file path> using the sources(_from) and
-  targets(_from) options if specified. Otherwise sources and targets are
-  guessed using the guess command. File transformations are analyzed from
-  targets to sources: there are typically more sources than targets.
+  <source file path>,<target file path>. Use the --sources PATH or
+  --sources_from FILE options and the --targets PATH or --targets_from FILE
+  options as sources and targets. File transformations are analyzed from targets
+  to sources: there are typically more sources than targets.
 
  graphic
   Create file transformations graphic. Read parsed traces in PARSED_DIR and
diff --git a/vendorize.toml b/vendorize.toml
new file mode 100644
index 0000000..cf3c006
--- /dev/null
+++ b/vendorize.toml
@@ -0,0 +1,5 @@
+target = "src/tracecode/_vendor"
+packages = [
+    "docopt",
+    "altgraph",
+]


Test patch:
diff --git a/tests/test_tracecode.py b/tests/test_tracecode.py
index c7a069e..c393126 100644
--- a/tests/test_tracecode.py
+++ b/tests/test_tracecode.py
@@ -17,14 +17,18 @@
 import shutil
 import stat
 import subprocess
+import sys
 import tarfile
 import tempfile
 import unittest
 from os.path import dirname
 from os.path import join
+from pathlib import Path
 from queue import Empty as Queue_Empty
 from queue import Queue
 
+import pytest
+
 from tracecode import conf
 from tracecode import pathutils
 from tracecode import tracecode
@@ -61,6 +65,10 @@
 be deleted after the test run automatically.
 """
 
+sys_platform = str(sys.platform).lower()
+on_linux = sys_platform.startswith('linux')
+on_windows = 'win32' in sys_platform
+
 
 def to_os_native_path(s):
     """Normalize a path to use the native OS path separator."""
@@ -68,6 +76,7 @@ def to_os_native_path(s):
 
 
 class BaseTestCase(unittest.TestCase):
+
     def setUp(self):
         if not hasattr(self, "to_delete"):
             self.to_delete = []
@@ -195,8 +204,17 @@ def check_pformats(
                 expected = fin.read().splitlines()
                 assert expected == proc.pformat().splitlines()
 
+    def run_tracecode_command(self, args, cwd=ROOT_DIR):
+        args = args or []
+        args = ["venv/bin/tracecode"] + args
+        try:
+            return subprocess.check_output(args, cwd=cwd)
+        except subprocess.CalledProcessError as e:
+            raise Exception(" ".join(args), e.output) from e
+
 
 class BasicTest(BaseTestCase):
+
     def test_validate_traces(self):
         trace_dir = self.extract_trace("validate")
         rootpid, traces = tracecode.validate_traces(trace_dir)
@@ -640,17 +658,13 @@ def test_decode_descriptor(self):
         tests = [
             ("4</lib/x86_64-linux-gnu/libtinfo.so.5.9>", "/lib/x86_64-linux-gnu/libtinfo.so.5.9"),
             ("4</tmp/ccGJ3nlX.s>", "/tmp/ccGJ3nlX.s"),
-            ("4<../tmp/ccG\>J3nlX.s>", "../tmp/ccG\>J3nlX.s"),
+            ("4<../tmp/ccG\\>J3nlX.s>", "../tmp/ccG\\>J3nlX.s"),
             ("AT_FDCWD", "AT_FDCWD"),
         ]
         for s, expected in tests:
             dec = tracecode.decode_descriptor(s)
             assert expected == dec
 
-    def test_norm_path(self):
-        cwd = "/abc"
-        assert "/abc/def", tracecode.norm_path("./def" == cwd)
-
     def test_resolve_paths_with_execve(self):
         cwd = "/TEST/"
         entry = """1389098234.445561 execve("prebuilts/tools/gcc-sdk/../../gcc/bin/i686-linux-gcc", ["prebuilts/tools/gcc-sdk/../../gcc/linux-x86/host/i686-linux-glibc2.7-4.6/bin/i686-linux-gcc", "-I", "external/libsepol/include/", "-I", "external/libsepol/src/", "-I", "external/libsepol", "-I", "out/host/linux-x86/obj/STATIC_LIBRARIES/libsepol_intermediates", "-I", "libnativehelper/include/nativehelper", "-isystem", "system/core/include", "-isystem", "hardware/libhardware/include", "-isystem", "hardware/libhardware_legacy/include", "-isystem", "hardware/ril/include", "-isystem", "libnativehelper/include", "-isystem", "frameworks/native/include", "-isystem", "frameworks/native/opengl/include", "-isystem", "frameworks/av/include", "-isystem", "frameworks/base/include", "-isystem", "external/skia/include", "-isystem", "tools/include", "-isystem", "out/host/linux-x86/obj/include", "-c", "-fno-exceptions", "-Wno-multichar", "-mstackrealign", "-msse3", "-mfpmath=sse", "-m32", "-fPIC", "-include", "build/core/combo/include/arch/linux-x86/AndroidConfig.h", "-U_FORTIFY_SOURCE", "-D_FORTIFY_SOURCE=0", "-DANDROID", "-fmessage-length=0", "-W", "-Wall", "-Wno-unused", "-Winit-self", "-Wpointer-arith", "-O2", "-g", "-fno-strict-aliasing", "-DNDEBUG", "-UDEBUG", "-Wall", "-W", "-Wundef", "-Wshadow", "-Wmissing-noreturn", "-Wmissing-format-attribute", "-MD", "-MF", "out/host/linux-x86/obj/STATIC_LIBRARIES/libsepol_intermediates/src/port_record.d", "-o", "out/host/linux-x86/obj/STATIC_LIBRARIES/libsepol_intermediates/src/port_record.o", "external/libsepol/src/port_record.c"], [/* 65 vars */]) = 0"""
@@ -1045,16 +1059,12 @@ def test_Process_object_can_be_pickled(self):
         pl = pickle.loads(pd)
         assert proc.children == pl.children
 
-    def run_tracecode_command(self, args):
-        return subprocess.call(["tracecode"] + args, cwd=ROOT_DIR)
-
     def test_command_line_parse_simple(self):
         input_dir = self.extract_trace("patchelf_command")
         cwd = "/home/nexb/tools/strace/strace-4.8"
         output_dir = self.get_temp_dir()
-        args = ["-q", "--cwd=%s" % cwd, "parse", input_dir, output_dir]
-        ret = self.run_tracecode_command(args)
-        assert ret == 0, ret
+        args = ["-q", f"--cwd={cwd}", "parse", input_dir, output_dir]
+        self.run_tracecode_command(args)
         files = sorted(os.listdir(output_dir))
         assert 18 == len(files)
 
@@ -1064,14 +1074,13 @@ def test_command_line_parse_with_ignored_reads(self):
         output_dir = self.get_temp_dir()
         args = [
             "-q",
-            "--cwd=%s" % cwd,
+            f"--cwd={cwd}",
             '--ignored_reads="/blabbla/*"',
             "parse",
             input_dir,
             output_dir,
         ]
-        ret = self.run_tracecode_command(args)
-        assert ret == 0, ret
+        self.run_tracecode_command(args)
         procs = tracecode.load_from_dir(output_dir)
         ar = self.get_tst_path(join("patchelf_command2", "expected_pformats.tar.bz2"))
         expected_pf = join(self.extract_archive(ar), "expected_pformats")
@@ -1097,15 +1106,14 @@ def test_command_line_parse_with_ignored_reads_from_file(self):
         irf = self.get_tst_path("patchelf_command2/ignored_reads.lst")
         args = [
             "-q",
-            "--cwd=%s" % cwd,
+            f"--cwd={cwd}",
             "--ignored_reads_from=%s" % irf,
             '--ignored_reads="/bar/*"',
             "parse",
             input_dir,
             output_dir,
         ]
-        ret = self.run_tracecode_command(args)
-        assert ret == 0, ret
+        self.run_tracecode_command(args)
         procs = tracecode.load_from_dir(output_dir)
         ar = self.get_tst_path(join("patchelf_command2", "expected_pformats.tar.bz2"))
         extracted = self.extract_archive(ar)
@@ -1199,7 +1207,7 @@ def test_demux_single_readwrite(self):
         proc.demux(muxers)
         assert {} != proc.reads
         assert {} != proc.writes
-        assert [] == proc.readwrites
+        assert proc.readwrites == []
 
     def test_demux_cp(self):
         trace_file = self.get_tst_path("interleaving/trace/t.15480")
@@ -1237,7 +1245,7 @@ def test_demux_cp(self):
   ('/home/nexb/build/tools/systemsupport/systemsupport', '/home/nexb/bin/systemsupport')
  Children:""".splitlines()
         after = proc.pformat().splitlines()
-        assert af == after
+        assert after == af
 
     def test_demux_cp_with_file_with_same_name(self):
         trace_file = self.get_tst_path("interleaving/trace/same_name.15480")
@@ -1260,7 +1268,7 @@ def test_demux_cp_with_file_with_same_name(self):
   ('/home/nexb/build/tools/systemsupport2/hardware.sh', '/home/nexb/bin/systemsupport2/hardware.sh')
  Children:""".splitlines()
         after = proc.pformat().splitlines()
-        assert expected == after
+        assert after == expected
 
     def test_filter_ignored(self):
         proc = tracecode.Process(
@@ -1283,8 +1291,8 @@ def test_filter_ignored(self):
             ),
             ignored_execs=[],
         )
-        assert ["/read/read"] == sorted(proc.reads_paths())
-        assert ["/write/path2", "/write/read"] == sorted(proc.writes_paths())
+        assert sorted(proc.reads_paths()) == ["/read/read"]
+        assert sorted(proc.writes_paths()) == ["/write/path2", "/write/read"]
 
     def test_filter_ignored_pipes_and_sockets(self):
         proc = tracecode.Process(
@@ -1311,8 +1319,8 @@ def test_filter_ignored_pipes_and_sockets(self):
             ),
             ignored_execs=[],
         )
-        assert set([]) == proc.reads_paths()
-        assert set([]) == proc.writes_paths()
+        assert proc.reads_paths() == set([])
+        assert proc.writes_paths() == set([])
 
     def test_filter_ignored_execs(self):
         proc = tracecode.Process(
@@ -1324,11 +1332,11 @@ def test_filter_ignored_execs(self):
         proc.add_readwrite("/read/read", "/write/read", "0", "0", [], [])
         proc.add_readwrite("/my/path", "/your/path", "0", "0", [], [])
         proc.filter(ignored_reads=[], ignored_writes=[], ignored_execs=["/bin/bash"])
-        assert {} == proc.reads
-        assert {} == proc.writes
-        assert [] == proc.readwrites
+        assert proc.reads == {}
+        assert proc.writes == {}
+        assert proc.readwrites == []
         expected = tracecode.Exec("/bin/bash", "", 0)
-        assert [expected] == proc.execs
+        assert proc.execs == [expected]
 
     def test_as_ops(self):
         cwd = "/home/nexb"
@@ -1365,7 +1373,7 @@ def test_as_ops(self):
         proc = done[0]
         ao = [o.to_dict() for o in proc.as_ops()]
         ao[0]["sources"].sort()
-        assert expected == ao
+        assert ao == expected
 
     def test_as_ops_handles_multiplexing_correctly(self):
         input_dir = self.extract_trace("as_ops_multiplexed")
@@ -1386,7 +1394,7 @@ def test_as_ops_handles_multiplexing_correctly(self):
                 of.write("\n".join(ao))
 
         expected = sorted([f for f in open(ef).read().splitlines() if f])
-        assert expected == ao
+        assert ao == expected
 
     def test_stats_no_stat(self):
         input_dir = self.extract_trace("stats_patchelf")
@@ -1408,7 +1416,7 @@ def test_stats_no_stat(self):
 Unhandled syscalls
 ------------------"""
         result = tracecode.parsing_statistics(procs)
-        assert expected == result
+        assert result == expected
 
     def test_stats_stat(self):
         input_dir = self.extract_trace("stats_patchelf")
@@ -1445,9 +1453,10 @@ def test_stats_stat(self):
 arch_prctl
 """
         result = tracecode.parsing_statistics(procs)
-        assert result.splitlines() == expected.splitlines()
+        assert expected.splitlines() == result.splitlines()
 
     def test_incomplete_trace_parsing_does_not_hangs_but_timesout(self):
+
         def timeoutfunc(x):
             # short timeout
             return 1
@@ -1462,6 +1471,7 @@ def timeoutfunc(x):
             pass
 
     def test_incomplete_trace_parsing_does_not_hangs_but_timesout_todir(self):
+
         def timeoutfunc(x):
             # short timeout
             return 1
@@ -1492,7 +1502,7 @@ def test_main_parse_with_filter_does_apply_defaults(self):
         args = {"parse": True, "TRACE_DIR": raw_dir, "PARSED_DIR": parsed_dir}
         tracecode.main(args)
         proc = tracecode.Process.load(parsed_dir, 70676)
-        assert len([p for p in proc.reads if p.startswith("/lib/i386-linux-gnu/")]) == 3
+        assert 3 == len([p for p in proc.reads if p.startswith("/lib/i386-linux-gnu/")])
 
         parsed_dir2 = self.get_temp_dir()
         args = {
@@ -1503,19 +1513,20 @@ def test_main_parse_with_filter_does_apply_defaults(self):
         }
         tracecode.main(args)
         proc2 = tracecode.Process.load(parsed_dir2, 70676)
-        assert len([p for p in proc2.reads if p.startswith("/lib/i386-linux-gnu/")]) == 0
+        assert 0 == len([p for p in proc2.reads if p.startswith("/lib/i386-linux-gnu/")])
 
         parsed_dir = self.get_temp_dir()
         args = {"parse": True, "TRACE_DIR": raw_dir, "PARSED_DIR": parsed_dir}
         tracecode.main(args)
         proc3 = tracecode.Process.load(parsed_dir, 70676)
-        assert len([p for p in proc3.reads if p.startswith("/lib/i386-linux-gnu/")]) == 3
+        assert 3 == len([p for p in proc3.reads if p.startswith("/lib/i386-linux-gnu/")])
 
 
 class PathMatchingTest(BaseTestCase):
+
     def check_match(self, paths1, paths2, expected):
         matches = list(tracecode.match_paths(paths1, paths2))
-        assert sorted(matches) == sorted(expected)
+        assert sorted(expected) == sorted(matches)
 
     def test_match_paths(self):
         paths1 = ["a/b/c"]
@@ -1563,6 +1574,7 @@ def test_match_paths_empty_args3(self):
         self.check_match(paths1, paths2, expected)
 
     def test_match_paths_long(self):
+
         def get_list(path):
             f = self.get_tst_path("match_path_long/" + path)
             return sorted(open(f).read().splitlines())
@@ -1581,6 +1593,7 @@ def get_list(path):
 
 
 class FilesetTest(BaseTestCase):
+
     def test_in_fileset_basic(self):
         assert tracecode.in_fileset("/nexb/src/", tuple())
         assert tracecode.in_fileset("/nexb/src/", None)
@@ -1670,10 +1683,95 @@ def test_trace_cleaning_strace_full_in_dir(self):
             test_name, cwd, with_output_dir=True, clean=True, parallel=True, regen=False
         )
 
+    @pytest.mark.skipif(not (on_linux and tracecode.has_dot()), reason="Run on Linux, with GCC and Graphviz to run this test")
+    def test_trace_from_scratch_end_to_end(self):
+        # test a live strace run and each read/write commands
+        test_dir = self.get_temp_dir(delete=True)
+        test_file = Path(test_dir) / "hello_world.c"
+        with open(test_file, "w")  as o:
+            o.write("""
+#include <stdio.h>
+
+int main(void)
+{
+    printf("Hello World!\\n");
+}
+""")
+
+        trace_dir = self.get_temp_dir(delete=True)
+
+        target_file = f"{test_dir}/hello_world"
+        args = [
+            "strace", "-ff", "-y", "-ttt", "-a1",
+            "-o", f"{trace_dir}/trace",
+            "gcc", f"{test_file}", "-o", target_file
+        ]
+
+        try:
+            subprocess.check_output(args,
+                cwd=test_dir,
+                shell=False,
+            )
+        except subprocess.CalledProcessError as e:
+            raise Exception(' '.join(args), e.output) from e
+
+        assert os.path.exists(target_file)
+        cwd = "/home/nexb/tools/strace/strace-4.8"
+        output_dir = self.get_temp_dir()
+
+        # tracecode [options] validate  TRACE_DIR
+        self.run_tracecode_command([f"--cwd={cwd}", "validate", trace_dir])
+
+        # tracecode [options] parse     TRACE_DIR   PARSED_DIR
+        # tracecode parse /tmp/outputs /tmp/parsed_outputs
+        parsed_dir = self.get_temp_dir()
+        self.run_tracecode_command([f"--cwd={cwd}", "parse", trace_dir, parsed_dir])
+        assert os.listdir(parsed_dir)
+
+        # tracecode [options] filter    PARSED_DIR  [NEW_PARSED_DIR]
+        parsed_dir2 = self.get_temp_dir()
+        self.run_tracecode_command([f"--cwd={cwd}", "--defaults", "filter", parsed_dir, parsed_dir2])
+        assert os.listdir(parsed_dir2)
+
+        # tracecode list /tmp/parsed_outputs /tmp/a/reads /tmp/b/writes
+        reads_file = os.path.join(output_dir, "reads.txt")
+        writes_file = os.path.join(output_dir, "writes.txt")
+        self.run_tracecode_command([f"--cwd={cwd}", "list", parsed_dir, reads_file, writes_file])
+        assert os.path.exists(reads_file)
+        assert os.path.exists(writes_file)
+
+        # tracecode [options] guess     PARSED_DIR  SOURCES_FILE  TARGETS_FILE
+        sources_file = os.path.join(output_dir, "sources.txt")
+        targets_file = os.path.join(output_dir, "targets.txt")
+        self.run_tracecode_command([f"--cwd={cwd}", "guess", parsed_dir, sources_file, targets_file])
+        assert os.path.exists(sources_file)
+        assert os.path.exists(targets_file)
+
+        # tracecode [options] analyze PARSED_DIR  ANALYSIS_FILE
+        analysis_file = os.path.join(output_dir, "analysis.csv")
+        self.run_tracecode_command([
+            f"--cwd={cwd}", "analyze",
+            f"--sources_from={sources_file}",
+            f"--targets={target_file}",
+            parsed_dir, analysis_file
+        ])
+        assert os.path.exists(analysis_file)
+
+        # tracecode [options] inventory PARSED_DIR  INV_FILE
+        inventory_file = os.path.join(output_dir, "inventory.csv")
+        self.run_tracecode_command([f"--cwd={cwd}", "inventory", parsed_dir, inventory_file])
+        assert os.path.exists(inventory_file)
+
+        # tracecode [options] graphic   PARSED_DIR  GRAPH_FILE
+        graph_file = os.path.join(output_dir, "graph.txt")
+        self.run_tracecode_command([f"--cwd={cwd}", "graphic", parsed_dir, graph_file])
+        assert os.path.exists(f"{graph_file}.pdf")
+
 
 class AltGraphTest(BaseTestCase):
+
     def test_altgraph_subgraph_bug(self):
-        from altgraph.Graph import Graph
+        from tracecode._vendor.altgraph.Graph import Graph
 
         graph = Graph()
         graph.add_node("A")
@@ -1685,14 +1783,14 @@ def test_altgraph_subgraph_bug(self):
         whole_graph = graph.forw_topo_sort()
         subgraph_backward = graph.back_bfs_subgraph("C")
         subgraph_backward = subgraph_backward.forw_topo_sort()
-        assert subgraph_backward == whole_graph
+        assert whole_graph == subgraph_backward
 
         subgraph_forward = graph.forw_bfs_subgraph("A")
         subgraph_forward = subgraph_forward.forw_topo_sort()
-        assert subgraph_forward == whole_graph
+        assert whole_graph == subgraph_forward
 
     def test_altgraph_nodes_connectivity(self):
-        from altgraph.Graph import Graph
+        from tracecode._vendor.altgraph.Graph import Graph
 
         graph = Graph()
         graph.add_edge("A", "B")
@@ -1700,10 +1798,10 @@ def test_altgraph_nodes_connectivity(self):
         graph.add_edge("C", "b")
         graph.add_edge("X", "Z")
         nl = graph.node_list()
-        assert sorted(nl) == ["A", "B", "C", "X", "Z", "b"]
+        assert ["A", "B", "C", "X", "Z", "b"] == sorted(nl)
 
         A = set(graph.forw_bfs_subgraph("A").node_list())
-        assert len(A) == 4
+        assert 4 == len(A)
         assert "Z" not in A
         assert "b" in A
 
@@ -1724,7 +1822,7 @@ def check_render_graph_dot(self, test_name, procs, settings, regen=False):
         if regen or _GLOBAL_REGEN:
             shutil.copy(fn, expected)
         logger.debug("%(test_name)r graph dot saved to %(fn)s" % locals())
-        assert open(out).read().splitlines() == open(expected).read().splitlines()
+        assert open(expected).read().splitlines() == open(out).read().splitlines()
 
     @unittest.skipUnless(tracecode.has_dot(), "Install Graphviz to run tests")
     def test_render_graph_bare(self):
@@ -1774,7 +1872,7 @@ def check_render_graph_with_procs(self, test_name, cwd, settings, regen=False):
         if regen or _GLOBAL_REGEN:
             shutil.copy(out, expected)
         logger.debug("%(test_name)r graph dot saved to %(out)s" % locals())
-        assert open(expected).read().splitlines() == open(out).read().splitlines()
+        assert open(out).read().splitlines() == open(expected).read().splitlines()
 
     @unittest.skipUnless(tracecode.has_dot(), "Install Graphviz to run tests")
     def test_procs_to_graphics_from_dir_patchelf(self):
@@ -1803,6 +1901,7 @@ def test_procs_to_graphics_from_dir_bash(self):
 
 
 class ListTest(BaseTestCase):
+
     def test_reads_writes_paths(self):
         cwd = "/home/nexb"
         input_dir = self.get_tst_path("lists/trace")
@@ -1831,10 +1930,10 @@ def test_reads_writes_paths(self):
             cwd, input_dir, output_dir, parallel=False, settings=stgs, clean=False
         )
         proc = done[0]
-        assert reads == sorted(proc.reads_paths())
+        assert sorted(proc.reads_paths()) == reads
 
         writes = sorted(["/home/license", "/dev/pts/11"])
-        assert writes == sorted(proc.writes_paths())
+        assert sorted(proc.writes_paths()) == writes
 
     def test_file_lists(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
@@ -1850,7 +1949,7 @@ def test_file_lists(self):
         reads, writes = tracecode.file_lists(procs)
 
         expected_reads = open(self.get_tst_path("lists_patchelf/expected")).read().splitlines()
-        assert sorted(list(reads)) == sorted(expected_reads)
+        assert sorted(expected_reads) == sorted(list(reads))
 
         expected_writes = """/dev/pts/11
             /dev/tty
@@ -1863,7 +1962,7 @@ def test_file_lists(self):
             /tmp/ccSJnJSm.s
             /tmp/ccj50xXq.ld
             /tmp/ccxlsgGu.le""".split()
-        assert sorted(list(writes)) == sorted(expected_writes)
+        assert sorted(expected_writes) == sorted(list(writes))
 
     def test_guess_sources_and_targets_patchelf_with_defaults_settings(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
@@ -1888,7 +1987,7 @@ def test_guess_sources_and_targets_patchelf_with_defaults_settings(self):
         /home/nexb/tools/patchelf/patchelf-0.5/src/elf.h
         /home/nexb/tools/patchelf/patchelf-0.5/src/patchelf.cc""".split()
 
-        assert sorted(expected_srcs) == sorted(list(sources))
+        assert sorted(list(sources)) == sorted(expected_srcs)
 
         expected_tgts = [
             "/home/nexb/tools/patchelf/patchelf-0.5/src/patchelf",
@@ -1897,7 +1996,7 @@ def test_guess_sources_and_targets_patchelf_with_defaults_settings(self):
             "/tmp/ccj50xXq.ld",
             "/tmp/ccxlsgGu.le",
         ]
-        assert sorted(list(targets)) == expected_tgts
+        assert expected_tgts == sorted(list(targets))
 
     def test_guess_sources_and_targets_for_patchelf_with_default_settings(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
@@ -1918,14 +2017,14 @@ def test_guess_sources_and_targets_for_patchelf_with_default_settings(self):
         /home/nexb/tools/patchelf/patchelf-0.5/tests/Makefile
         /home/nexb/tools/patchelf/patchelf-0.5/src/elf.h
         /home/nexb/tools/patchelf/patchelf-0.5/src/patchelf.cc""".split()
-        assert sorted(sources) == sorted(expected_srcs)
+        assert sorted(expected_srcs) == sorted(sources)
 
         expected_tgts = [
             "/home/nexb/tools/patchelf/patchelf-0.5/src/patchelf",
             "/tmp/ccDO5gyj.c",
             "/tmp/ccHpGGfn.o",
         ]
-        assert sorted(targets) == sorted(expected_tgts)
+        assert sorted(expected_tgts) == sorted(targets)
 
     def test_guess_sources_and_targets_from_dir(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
@@ -1939,8 +2038,8 @@ def test_guess_sources_and_targets_from_dir(self):
                 "/usr/lib/gcc/*",
                 "*/Makefile",
             )
-            + conf.GCC_DEPEND_FILES
-            + conf.GCC_INCLUDES
+            +conf.GCC_DEPEND_FILES
+            +conf.GCC_INCLUDES
         )
         stgs = conf.DefaultSettings()
         stgs.ignored_reads += extra_read_ignored
@@ -1949,14 +2048,14 @@ def test_guess_sources_and_targets_from_dir(self):
         sources, targets = tracecode.guess_sources_and_targets_from_dir(dir_path=output_dir)
         expected_srcs = """/home/nexb/tools/patchelf/patchelf-0.5/src/elf.h
         /home/nexb/tools/patchelf/patchelf-0.5/src/patchelf.cc""".split()
-        assert sorted(expected_srcs) == sorted(sources)
+        assert sorted(sources) == sorted(expected_srcs)
 
         expected_tgt = sorted(
             """/home/nexb/tools/patchelf/patchelf-0.5/src/patchelf
                      /tmp/ccDO5gyj.c
                      /tmp/ccHpGGfn.o""".split()
         )
-        assert expected_tgt == sorted(list(targets))
+        assert sorted(list(targets)) == expected_tgt
 
     def test_inventory(self):
         input_dir = self.extract_trace("inventory")
@@ -1985,10 +2084,11 @@ def test_inventory(self):
                     csvfile.write(",".join(item) + "\n")
         expected = open(expect_pth).read().splitlines()
         expected = sorted(tuple(i.split(",")) for i in expected)
-        assert expected == results
+        assert results == expected
 
 
 class TraceFullGraphAnalysisTest(BaseTestCase):
+
     def test_file_sets_sources_and_targets_in_graph(self):
         input_dir = self.extract_trace("file_graph_analysis_patchelf")
         output_dir = None
@@ -2016,21 +2116,21 @@ def test_file_sets_sources_and_targets_in_graph(self):
             .read()
             .splitlines()
         )
-        assert sorted(expected_srcs) == sorted(list(srcs))
+        assert sorted(list(srcs)) == sorted(expected_srcs)
 
         expected_tgts = (
             open(self.get_tst_path("file_graph_analysis_patchelf/expected_tgts"))
             .read()
             .splitlines()
         )
-        assert sorted(expected_tgts) == sorted(list(tgts))
+        assert sorted(list(tgts)) == sorted(expected_tgts)
 
         expected_ints = (
             open(self.get_tst_path("file_graph_analysis_patchelf/expected_ints"))
             .read()
             .splitlines()
         )
-        assert sorted(expected_ints) == sorted(list(ints))
+        assert sorted(list(ints)) == sorted(expected_ints)
 
     def test_node_sets_sources_and_targets_in_graph(self):
         input_dir = self.extract_trace("file_graph_analysis_patchelf")
@@ -2066,21 +2166,21 @@ def paths(nset):
             .read()
             .splitlines()
         )
-        assert sorted(expected_srcs) == sorted(list(srcs))
+        assert sorted(list(srcs)) == sorted(expected_srcs)
 
         expected_tgts = (
             open(self.get_tst_path("file_graph_analysis_patchelf/expected_tgts"))
             .read()
             .splitlines()
         )
-        assert sorted(expected_tgts) == sorted(list(tgts))
+        assert sorted(list(tgts)) == sorted(expected_tgts)
 
         expected_ints = (
             open(self.get_tst_path("file_graph_analysis_patchelf/expected_ints"))
             .read()
             .splitlines()
         )
-        assert sorted(expected_ints) == sorted(list(ints))
+        assert sorted(list(ints)) == sorted(expected_ints)
 
     def test_analyze_file_graph_patchelf_d2d_with_default_settings(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
@@ -2095,8 +2195,8 @@ def test_analyze_file_graph_patchelf_d2d_with_default_settings(self):
                 "/usr/lib/gcc/*",
                 "*/Makefile",
             )
-            + conf.GCC_DEPEND_FILES
-            + conf.GCC_INCLUDES
+            +conf.GCC_DEPEND_FILES
+            +conf.GCC_INCLUDES
         )
         stgs = conf.DefaultSettings()
 
@@ -2127,7 +2227,7 @@ def test_analyze_file_graph_patchelf_d2d_with_default_settings(self):
                 ),
             ]
         )
-        assert expected == d2d
+        assert d2d == expected
 
     def analyze_d2d(self, test_name, cwd, settings, expected=None, regen=False):
 
@@ -2153,12 +2253,12 @@ def analyze_d2d(self, test_name, cwd, settings, expected=None, regen=False):
                 )
                 for x, y in expected
             ]
-        assert expected == d2d
+        assert d2d == expected
 
         # test also the walks inverted
         d2d = tracecode.analyze_full_graph(procs, settings, _invert=True)
         d2d = sorted(set(d2d))
-        assert expected == d2d
+        assert d2d == expected
 
     def _get_file_set(self, test_name, name):
         fs = self.get_tst_path(join(test_name, name))
@@ -2175,8 +2275,8 @@ def test_analyze_full_graph_patchelf_d2d_with_default_settings(self):
                 "/usr/lib/gcc/*",
                 "*/Makefile",
             )
-            + conf.GCC_DEPEND_FILES
-            + conf.GCC_INCLUDES
+            +conf.GCC_DEPEND_FILES
+            +conf.GCC_INCLUDES
         )
         stgs.ignored_reads += extra_read_ignored
         stgs.ignored_writes += conf.GCC_DEPEND_FILES
@@ -2211,8 +2311,8 @@ def test_analyze_strace_d2d_with_default_settings(self):
                 "/usr/lib/gcc/*",
                 "*/Makefile",
             )
-            + conf.GCC_DEPEND_FILES
-            + conf.GCC_INCLUDES
+            +conf.GCC_DEPEND_FILES
+            +conf.GCC_INCLUDES
         )
         stgs.ignored_reads += extra_read_ignored
         stgs.ignored_writes += conf.GCC_DEPEND_FILES
@@ -2232,8 +2332,8 @@ def test_analyze_cups_d2d_with_default_settings(self):
                 "*/cups/Dependencies",
                 "*/Makedefs",
             )
-            + conf.GCC_DEPEND_FILES
-            + conf.GCC_INCLUDES
+            +conf.GCC_DEPEND_FILES
+            +conf.GCC_INCLUDES
         )
         stgs.ignored_reads += extra_read_ignored
         stgs.ignored_writes += conf.GCC_DEPEND_FILES
@@ -2253,6 +2353,7 @@ def test_analyze_cups_d2d_with_default_settings(self):
 
         self.analyze_d2d(test_name, cwd, settings=stgs, regen=False)
 
+    @pytest.mark.skipif(on_windows, reason="This fails on Windows for unknown reasons")
     def test_analyze_multiplexed_copy_d2d(self):
         test_name = "d2d_multiplexed_cp"
         stgs = conf.BaseSettings()
@@ -2263,6 +2364,7 @@ def test_analyze_multiplexed_copy_d2d(self):
 
 
 class DumpTest(BaseTestCase):
+
     def test_dump(self):
         cwd = "/home/nexb/tools/patchelf/patchelf-0.5"
         test_name = "dump_patchelf"
@@ -2280,17 +2382,18 @@ def test_dump(self):
         if regen:
             shutil.copy(resloc, exploc)
         expected = open(exploc).read()
-        assert sorted(expected.splitlines()) == sorted(results.splitlines())
+        assert sorted(l for l in results.splitlines() if l) == sorted(expected.splitlines())
 
 
 class ConfTest(BaseTestCase):
+
     def test_Settings_object_can_be_pickled(self):
         stgs = conf.DefaultSettings()
         import pickle
 
         sd = pickle.dumps(stgs)
         sl = pickle.loads(sd)
-        assert stgs.dict() == sl.dict()
+        assert sl.dict() == stgs.dict()
 
     def test_formatted(self):
         expected = """format=pdf
@@ -2314,156 +2417,157 @@ def test_formatted(self):
             ),
         }
         stgs = conf.BaseSettings(**args)
-        assert expected == stgs.formatted()
+        assert stgs.formatted() == expected
 
     def test_args_subset(self):
-        assert {} == conf.args_subset({})
+        assert conf.args_subset({}) == {}
 
     def test_combined_settings_are_never_empty(self):
         stgs = conf.settings(args={})
         assert None != stgs
         expected = conf.BaseSettings()
-        assert expected.dict() == stgs.dict()
+        assert stgs.dict() == expected.dict()
 
 
 class TestPathUtils(unittest.TestCase):
+
     def test_common_path_prefix1(self):
         test = pathutils.common_path_prefix("/a/b/c", "/a/b/c")
-        assert ("a/b/c", 3) == test
+        assert test == ("a/b/c", 3)
 
     def test_common_path_prefix2(self):
         test = pathutils.common_path_prefix("/a/b/c", "/a/b")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_prefix3(self):
         test = pathutils.common_path_prefix("/a/b", "/a/b/c")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_prefix4(self):
         test = pathutils.common_path_prefix("/a", "/a")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix_path_root(self):
         test = pathutils.common_path_prefix("/a/b/c", "/")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_prefix_root_path(self):
         test = pathutils.common_path_prefix("/", "/a/b/c")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_prefix_root_root(self):
         test = pathutils.common_path_prefix("/", "/")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_prefix_path_elements_are_similar(self):
         test = pathutils.common_path_prefix("/a/b/c", "/a/b/d")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_prefix_no_match(self):
         test = pathutils.common_path_prefix("/abc/d", "/abe/f")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_prefix_ignore_training_slashes(self):
         test = pathutils.common_path_prefix("/a/b/c/", "/a/b/c/")
-        assert ("a/b/c", 3) == test
+        assert test == ("a/b/c", 3)
 
     def test_common_path_prefix8(self):
         test = pathutils.common_path_prefix("/a/b/c/", "/a/b")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_prefix10(self):
         test = pathutils.common_path_prefix("/a/b/c.txt", "/a/b/b.txt")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_prefix11(self):
         test = pathutils.common_path_prefix("/a/b/c.txt", "/a/b.txt")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix12(self):
         test = pathutils.common_path_prefix("/a/c/e/x.txt", "/a/d/a.txt")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix13(self):
         test = pathutils.common_path_prefix("/a/c/e/x.txt", "/a/d/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix14(self):
         test = pathutils.common_path_prefix("/a/c/e/", "/a/d/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix15(self):
         test = pathutils.common_path_prefix("/a/c/e/", "/a/c/a.txt")
-        assert ("a/c", 2) == test
+        assert test == ("a/c", 2)
 
     def test_common_path_prefix16(self):
         test = pathutils.common_path_prefix("/a/c/e/", "/a/c/f/")
-        assert ("a/c", 2) == test
+        assert test == ("a/c", 2)
 
     def test_common_path_prefix17(self):
         test = pathutils.common_path_prefix("/a/a.txt", "/a/b.txt/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix18(self):
         test = pathutils.common_path_prefix("/a/c/", "/a/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix19(self):
         test = pathutils.common_path_prefix("/a/c.txt", "/a/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_prefix20(self):
         test = pathutils.common_path_prefix("/a/c/", "/a/d/")
-        assert ("a", 1) == test
+        assert test == ("a", 1)
 
     def test_common_path_suffix(self):
         test = pathutils.common_path_suffix("/a/b/c", "/a/b/c")
-        assert ("a/b/c", 3) == test
+        assert test == ("a/b/c", 3)
 
     def test_common_path_suffix_absolute_relative(self):
         test = pathutils.common_path_suffix("a/b/c", "/a/b/c")
-        assert ("a/b/c", 3) == test
+        assert test == ("a/b/c", 3)
 
     def test_common_path_suffix_find_subpath(self):
         test = pathutils.common_path_suffix("/z/b/c", "/a/b/c")
-        assert ("b/c", 2) == test
+        assert test == ("b/c", 2)
 
     def test_common_path_suffix_handles_relative_path(self):
         test = pathutils.common_path_suffix("a/b", "a/b")
-        assert ("a/b", 2) == test
+        assert test == ("a/b", 2)
 
     def test_common_path_suffix_handles_relative_subpath(self):
         test = pathutils.common_path_suffix("zsds/adsds/a/b/b/c", "a//a/d//b/c")
-        assert ("b/c", 2) == test
+        assert test == ("b/c", 2)
 
     def test_common_path_suffix_ignore_and_strip_trailing_slash(self):
         test = pathutils.common_path_suffix("zsds/adsds/a/b/b/c/", "a//a/d//b/c/")
-        assert ("b/c", 2) == test
+        assert test == ("b/c", 2)
 
     def test_common_path_suffix_return_None_if_no_common_suffix(self):
         test = pathutils.common_path_suffix("/a/b/c", "/")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_suffix_return_None_if_no_common_suffix2(self):
         test = pathutils.common_path_suffix("/", "/a/b/c")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_suffix_match_only_whole_segments(self):
         # only segments are honored, commonality within segment is ignored
         test = pathutils.common_path_suffix("this/is/aaaa/great/path", "this/is/aaaaa/great/path")
-        assert ("great/path", 2) == test
+        assert test == ("great/path", 2)
 
     def test_common_path_suffix_two_root(self):
         test = pathutils.common_path_suffix("/", "/")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_suffix_empty_root(self):
         test = pathutils.common_path_suffix("", "/")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_suffix_root_empty(self):
         test = pathutils.common_path_suffix("/", "")
-        assert (None, 0) == test
+        assert test == (None, 0)
 
     def test_common_path_suffix_empty_empty(self):
         test = pathutils.common_path_suffix("", "")
-        assert (None, 0) == test
+        assert test == (None, 0)