This library interacts with DiscoveryDV, a data visualization suite,
over a ZMQ-based API. In order to use all features of this software,
DiscoveryDV must be installed and run.
# License
Please see LICENSE.md for information about use, redistribution, and
modification of this library.
The DiscoveryDV application (not included with this package) and the
DiscoveryDV name are copyright DecisionVis LLC.
# Requirements
* PyZMQ (pyzmq>=17.0.0)
* MessagePack (msgpack-python>=0.4.8)
* Python >= 3.6
# Usage
The DiscoveryDV Python module has 3 main components:
1. `Connection`
* This class provides an interface with DiscoveryDV.
* It is a "Context Manager" and ideally should be used within Python's `with` statement.
* If used outside of a `with` statement, the connection object must be connected
first (`connection_obj.connect()`), and must be closed when finished (`connection_obj.close()`).
* The connection can be re-connected with `connection_obj.connect()`; optionally a different
TCP/IP address, and/or different ports can be passed as arguments.
* The current application state for DiscoveryDV can be retrieved with `connection_obj.state`;
it can be resynchronized by calling `connection_obj.reload()` and accessing `connection_obj.state` again.
2. `State`
* This class represents a DiscoveryDV "state" object.
* It is analogous to Python's `namedtuple`.
* It contains `_fields`, which is a tuple of strings containing field names
* Fields may be accessed using dot-notation (e.g. `state.name`) or by subscripting (e.g. `state['name']`).
* Values may be `State` objects, `Collection` objects, or Python values (`int`, `float`, `bool`, `str`)
* Fields may be set using `=` (e.g. `state.name = "New Name"`, `state['name'] = "New Name"`).
* Fields may be reset to defaults by deletion (e.g. `del state.name`, `del state['name']`).
* Printing this object will provide an API path and a list of valid fields.
* This object supplies a `repr` that "pretty prints" the `State` recursively
3. `Collection`
* This class represents a DiscoveryDV "collection" object.
* It is analogous to Python's `tuple`, but can also be queried by an child's
name (similar to Python's `dict`).
* Internally in DiscoveryDV these are referred to as `OrderedLookup` or `NameLookup`;
the main difference is that a `NameLookup` requires children names to be unique.
* Children are always `State` objects that contain a `name` field
* Children may be accessed by subscripting, either using a numerical index
(e.g. `0`, `-1`) or a string name (e.g. `'Page 001`).
* Children may be added by calling `collection_obj.insert(index)` or `collection_obj.add()`;
an optional name argument may be supplied to create a child with a specified name.
* Children may be deleted by calling `collection_obj.delete(index/name)` or `del collection_obj[index/name]`.
* Printing this object will provide an API path and a list of children's names.
* This object supplies a `repr` that "pretty prints" the `Collection` recursively
# Example
## Making a Connection
You will first need to make a connection to DiscoveryDV. It is best to use the
`Connection` class in a `with` statement:
```
with Connection('127.0.0.1', 33929, 33930) as ddv:
# Commands applied to the "ddv" connection object
...
```
Python's `with` clause will create the connection, connect it to DiscoveryDV,
and properly close the connection outside the scope of the `with` clause, and
in the event of an exception.
Alternately, an instance of a `Connection` can be created, but must be connected
before performing actions, and closed afterward:
```
ddv = Connection('127.0.0.1', 33929, 33930)
ddv.connect()
# Commands applied to the "ddv" connection object
...
ddv.close()
```
You may wish to include the `ddv.close()` command in a `finally` clause to ensure
that the connection is properly closed in the event of an exception.
## Working with the DiscoveryDV application state
Once connected, the application state can be retrieved as a `State` object.
This object is an abstraction of the API commands that can be performed in
DiscoveryDV.
For example, the following DVS script creates a page, adds a file,
creates a PCPlot, and sets up some axes:
```
add /page/ "LTM"
add /page/LTM/file/ "LTM"
set /page/LTM/file/LTM/path/ "ltm.xlsx"
set /page/LTM/file/LTM/type/ excel
add /page/LTM/pcplot/ "PCPlot"
add /page/LTM/pcplot/PCPlot/parallel/ "Cost" "Error" "Risk" "Mass"
set /page/LTM/pcplot/PCPlot/axis/c/name/ "Cost"
```
We can replicate this script with the DiscoveryDV Python module:
```
with Connection('127.0.0.1', 33929, 33930) as ddv:
ddv.state.page.add("LTM")
ddv.state.page["LTM"].file.add("LTM")
ddv.state.page["LTM"].file["LTM"].path = "ltm.xlsx"
ddv.state.page["LTM"].file["LTM"].type = "excel"
ddv.state.page["LTM"].pcplot.add("PCPlot")
ddv.state.page["LTM"].pcplot["PCPlot"].parallel.add("Cost")
ddv.state.page["LTM"].pcplot["PCPlot"].parallel.add("Error")
ddv.state.page["LTM"].pcplot["PCPlot"].parallel.add("Risk")
ddv.state.page["LTM"].pcplot["PCPlot"].parallel.add("Mass")
ddv.state.page["LTM"].pcplot["PCPlot"].axis.c.name = "Cost"
```
We could write this is a more Pythonic approach:
```
with Connection('127.0.0.1', 33929, 33930) as ddv:
state = ddv.state
page = state.page.add("LTM")
file = page.file.add("LTM")
file.path = "ltm.xlsx"
file.type = "excel"
pcplot = page.pcplot.add("PCPlot")
for name in ("Cost", "Error", "Risk", "Mass"):
_ = pcplot.parallel.add(name)
pcplot.axis.c.name = "Cost"
```
## Performing "actions"
The DiscoveryDV API has several commands that do not modify the state. These
"actions" can also be performed using this module.
You can see a list of commands you can run by querying the connection object's
`commands`: `print(connection_obj.commands)`.
Try running `print(connection_obj.help("verb")` to see information from DiscoveryDV
on a particular command. Actions for the `Connection` object take the same arguments
as API commands in DiscoveryDV.
You can also perform state modification commands directly using the connection
object. For these commands, you will need to provide a full path. Paths in
this module are specified using Python tuples rather than "/"-separated as they
are in the DiscoveryDV API. For example, `/page/0/name/` would be `('page', 0, 'name')`.
# Troubleshooting
It is often useful to `print` the `Connection` object and any relevant `State`/`Collection`
objects, or for more detailed information, `print` their `repr` (e.g. `print(repr(connection_obj.state))`).
* `ValueError` with no response from DiscoveryDV:
* Ensure that DiscoveryDV is running
* Ensure that the module's `Connection` object has been connected if not using a with statement
1. `connection_instance.connect()`
* Check that the `Connection` object's address and ports match DiscoveryDV
1. `print(connection_instance)`
2. Compare address and ports to the log message when starting DiscoveryDV
3. Reconnect and specify address and ports `connection_instance.connect('127.0.0.1', 33929, 33930)`
* `KeyError` or `IndexError` when performing state changes
* Confirm that the name or index are correct
* `print(collection_obj)` and compare names/positions
* `print(state_obj)` and compare field names
* Ensure that the module's `State` object is synchronized with DiscoveryDV
1. `connection_instance.reload()`
2. `state_obj = connection_instance.state`
* `ValueError` when trying to set a `State` field
* DiscoveryDV only allows certain types/ranges of values based on a path
* It may be useful to `print` the output from `obj.help()` (for `State` or `Collection`),
or call `Connection`'s `help` method using a full path (e.g. `connection_obj.help(path)`).
# Release Notes
## Version 0.1 - (August 9, 2019):
- Initial release.