cdp-client 2.2.2
Provides an API that allows to interact with CDP applications
License: MIT
Requires Python: >=3.8
Keywords: cdp cdpstudio studio client cdp-client cdp_client

CDP-Client
==========

A simple python interface for the CDP Studio development platform that allows Python scripts to interact with CDP Applications - retrieve CDP Application structures and read-write object values. For more information about CDP Studio see https://cdpstudio.com/

The API makes heavy use of promise library for asynchronous operations. For more information see https://pypi.python.org/pypi/promise

Installation
------------

::

    $ pip install cdp-client

Usage
-----

The example below shows how you subscribe to CDP signal value changes.

.. code:: python

    from cdp_client import cdp

    def on_value_changed(value, timestamp):
        print(value)
	
    def subscribe_to_value_changes(node):
        node.subscribe_to_value_changes(on_value_changed)
	
    client = cdp.Client(host='127.0.0.1')
    client.find_node('AppName.ComponentName.SignalName').then(subscribe_to_value_changes)
    client.run_event_loop()

API
---

Before all examples, you need:

.. code:: python

    from cdp_client import cdp

Global API
~~~~~~~~~~

Client(host, port, auto_reconnect, notification_listener, encryption_parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

- Arguments

    host - String for hosts ip address

    port - Optional port number to connect to. If not specified default port 7689 is used.

    auto_reconnect - Optional argument to enable/disable automatic reconnect when connection is lost. Defaults to True if not specified.

    notification_listener - NotificationListener object whose methods are called on different connection events (e.g. when server requires credentials)

    encryption_parameters - Optional argument to set encryption and its parameters, TLS certificates verification etc. Parameter is compatible with python websocket client 'sslopt' parameter. For more information see https://pypi.org/project/websocket_client

- Returns

    The connected client object.

- Usage example

    .. code:: python

        client = cdp.Client(host='127.0.0.1')

- Usage example with password authentication

    .. code:: python

        class MyListener(cdp.NotificationListener):
            def credentials_requested(self, request):
                if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
                    # Do something to gather username and password variables (either sync or async way) and then call:
                    request.accept({'Username': 'test', 'Password': '12345678'});

        client = cdp.Client(host='127.0.0.1', notification_listener=MyListener())

- Usage example with password authentication and encryption in use, without server certificate verification

    .. code:: python

        import ssl

        class MyListener(cdp.NotificationListener):
            def credentials_requested(self, request):
                if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
                    # Do something to gather username and password variables (either sync or async way) and then call:
                    request.accept({'Username': 'test', 'Password': '12345678'});

        client = cdp.Client(host='127.0.0.1', notification_listener=MyListener(),
                            encryption_parameters={'use_encryption': True, 'cert_reqs': ssl.CERT_NONE})


- Usage example with password authentication and encryption in use, with server certificate verification

    .. code:: python

        import ssl

        class MyListener(cdp.NotificationListener):
            def credentials_requested(self, request):
                if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
                    # Do something to gather username and password variables (either sync or async way) and then call:
                    request.accept({'Username': 'test', 'Password': '12345678'});

        client = cdp.Client(host='127.0.0.1', notification_listener=MyListener(),
                            encryption_parameters={'use_encryption': True,
                                                   'cert_reqs': ssl.CERT_REQUIRED,
                                                   'ca_certs': 'StudioAPI.crt',
                                                   'check_hostname': False},

Instance Methods / Client
~~~~~~~~~~~~~~~~~~~~~~~~~

client.root_node()
^^^^^^^^^^^^^^^^^^

Gets the application Node object of the connected application.

- Returns

    Promise containing root Node object when fulfilled.

- Usage

    .. code:: python

        client.root_node().then(on_success).catch(on_error)

client.find_node(path)
^^^^^^^^^^^^^^^^^^^^^^

Searches for the node specified by full dot separated path. **The requested node must reside in the application client was connected to. Root node is not considered part of the path.**

- Arguments

    path - Dot separated string to target node

- Returns

    Promise containing requested Node object when fulfilled. Otherwise NotFoundError when rejected.

- Usage

    .. code:: python

        client.find_node('AppName.ComponentName.SignalName').then(on_success).catch(on_error)

client.run_event_loop()
^^^^^^^^^^^^^^^^^^^^^^^

Runs the event loop that serves network communication layer for incoming/outgoing data. **This is a blocking call that must be run for any communication to happen.** The method can be cancelled by calling disconnect.

client.disconnect()
^^^^^^^^^^^^^^^^^^^

Stops the event loop and closes the connection to connected application. This method also releases the blocking run_event_loop call.

Instance Methods / Node
~~~~~~~~~~~~~~~~~~~~~~~

node.name()
^^^^^^^^^^^

- Returns

    The name of the Node object. Names in a parent node are all unique.

node.path()
^^^^^^^^^^^

- Returns

    A dot separated path of the Node object starting with application name.

node.parent()
^^^^^^^^^^^^^

- Returns

    The parent Node object.

node.type()
^^^^^^^^^^^

- Returns

    The type of the Node object returned as one of the cdp.NodeType values.

node.class_name()
^^^^^^^^^^^^^^^^^

- Returns

    CDP class name as a string.

node.last_value()
^^^^^^^^^^^^^^^^^

- Returns

    The last known value received by the Node object.

node.set_value(value, timestamp)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sets a new value for the Node object. Timestamp will be ignored in current implementation.

- Arguments

    value - New value

    timestamp - UTC time in nanoseconds since Epoch

node.is_read_only()
^^^^^^^^^^^^^^^^^^^

- Returns

    False if nodes value cannot be set, otherwise True.

node.is_leaf()
^^^^^^^^^^^^^^

- Returns

    True if node doesn't have any children, otherwise False.

node.child(name)
^^^^^^^^^^^^^^^^

- Arguments

    name - Child nodes name to search for

- Returns

    Promise containing requested Node object when fulfilled.

- Usage

    .. code:: python

        node.child('NodeName').then(on_success).catch(on_error)

node.children()
^^^^^^^^^^^^^^^

- Returns

    Promise containing all children of this Node object when fulfilled.

- Usage

    .. code:: python

        node.children().then(on_success).catch(on_error)

node.for_each_child(callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Loops through all children and calls callback function for each of them

- Arguments

    callback - Function(node)

- Returns

    Promise containing all children of this Node object when fulfilled.

- Usage

    .. code:: python

        def on_callback(child):
            do something

        node.for_each_child(on_callback)

node.subscribe_to_structure_changes(callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Starts listening structure changes and passes the changes to provided callback funtion

- Arguments

    callback - Function(added_nodes, removed_nodes) where added_nodes and removed_nodes is a list

- Usage

    .. code:: python

        def on_change(added_nodes, removed_nodes):
            do something

        node.subscribe_to_structure_changes(on_change)

node.subscribe_to_value_changes(callback, fs=5, sample_rate=0)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Starts listening value changes and passes the changes to provided callback function

- Arguments

    callback - Function(value, timestamp)

    fs - Maximum frequency that value updates are expected (controls how many changes are sent in a single packet). Defaults to 5 hz.
    
    sample_rate - Maximum amount of value updates sent per second (controls the amount of data transferred). Zero means all samples must be provided. Defaults to 0.

- Usage

    .. code:: python

        def on_change(value, timestamp):
            do something

        node.subscribe_to_value_changes(on_change)


node.unsubscribe_from_structure_changes(callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Stops listening previously subscribed structure changes

- Arguments

    callback - Function(added_nodes, removed_nodes) where added_nodes and removed_nodes is a list

- Usage

    .. code:: python

        def on_change(added_nodes, removed_nodes):
            do something

        node.unsubscribe_from_structure_changes(on_change)

node.unsubscribe_from_value_changes(callback)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Stops listening previously subscribed value changes

- Arguments

    callback - Function(value, timestamp)

- Usage

    .. code:: python

        def on_change(value, timestamp):
            do something
	
        node.unsubscribe_from_value_changes(on_change)

Notification Listener
~~~~~~~~~~~~~~~~~~~~~

To handle different connection events (like prompt user to accept a system use notification message or request user to enter credentials for authentication or idle lockout re-authentication) a notification_listener parameter must be provided to the Client.
The notification_listener parameter must be a object of type class cdp.NotificationListener.

class NotificationListener
^^^^^^^^^^^^^^^^^^^^^^^^^^

    .. code:: python

        class NotificationListener:
            def application_acceptance_requested(self, request=AuthRequest()):
                request.accept()

            def credentials_requested(self, request=AuthRequest()):
                raise NotImplementedError("NotificationListener credentials_requested() not implemented!")

NotificationListener.application_acceptance_requested(self, request=AuthRequest())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Called by Client when new application TLS or plain TCP connection is established.
Can be used to prompt the user a System Use Notification (a message that can be configured in CDP Studio Security settings).

- Arguments

    request - a object that has method accept() that should be called to accept the connection and a reject() to reject the connection.

- Usage

    .. code:: python

        class MyListener(cdp.NotificationListener):
            def application_acceptance_requested(self, request):
                if request.system_use_notification():
                    # Pop up a System Use Notification message and ask for confirmation to continue,
                    # then based on the user answer call either request.accept() or request.reject()
                else:
                    request.accept()

        client = cdp.Client(host='127.0.0.1', port=7689, notification_listener=MyListener())

NotificationListener.credentials_requested(self, request=AuthRequest())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Called by Client when server is requesting credentials (authentication or idle lockout re-authentication).

- Arguments

    request - a object that has method accept(data=dict()) that should be called (with credentials) for authentication try, and also a method reject() to reject the connection.

- Usage

    .. code:: python

        class MyListener(cdp.NotificationListener):
            def credentials_requested(self, request):
                if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
                    # Do something to gather username and password variables (either sync or async way) and then call:
                    request.accept({'Username': 'test', 'Password': '12345678'});
                if request.user_auth_result().code() == cdp.AuthResultCode.REAUTHENTICATION_REQUIRED:
                    # Pop user a message that idle lockout was happened and server requires new authentication to continue:
                    request.accept({'Username': 'test', 'Password': '12345678'});

        client = cdp.Client(host='127.0.0.1', port=7689, notification_listener=MyListener())

Tests
-----

To run the test suite execute the following command in package root folder:

.. code:: sh

    $ python setup.py test

License
-------

`MIT
License <https://github.com/CDPTechnologies/PythonCDPClient/blob/master/LICENSE.txt>`__