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.
