cdp-client 2.0.2__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cdp-client-2.0.3/PKG-INFO +457 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/README.rst +6 -2
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/cdp.py +20 -12
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/fake_data.py +5 -3
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_connection.py +4 -2
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_node.py +55 -4
- cdp-client-2.0.3/cdp_client.egg-info/PKG-INFO +457 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/setup.py +1 -1
- cdp-client-2.0.2/PKG-INFO +0 -453
- cdp-client-2.0.2/cdp_client.egg-info/PKG-INFO +0 -453
- {cdp-client-2.0.2 → cdp-client-2.0.3}/LICENSE.txt +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/MANIFEST.in +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/__init__.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/cdp_pb2.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/__init__.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_client.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_main.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_nodetree.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client/tests/test_requests.py +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client.egg-info/SOURCES.txt +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client.egg-info/dependency_links.txt +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client.egg-info/requires.txt +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/cdp_client.egg-info/top_level.txt +0 -0
- {cdp-client-2.0.2 → cdp-client-2.0.3}/setup.cfg +0 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
Metadata-Version: 1.1
|
|
2
|
+
Name: cdp-client
|
|
3
|
+
Version: 2.0.3
|
|
4
|
+
Summary: Provides an API that allows to interact with CDP applications
|
|
5
|
+
Home-page: https://github.com/CDPTechnologies/PythonCDPClient
|
|
6
|
+
Author: CDP Technologies AS
|
|
7
|
+
Author-email: info@cdptech.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Description: CDP-Client
|
|
10
|
+
==========
|
|
11
|
+
|
|
12
|
+
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/
|
|
13
|
+
|
|
14
|
+
The API makes heavy use of promise library for asynchronous operations. For more information see https://pypi.python.org/pypi/promise
|
|
15
|
+
|
|
16
|
+
Installation
|
|
17
|
+
------------
|
|
18
|
+
|
|
19
|
+
::
|
|
20
|
+
|
|
21
|
+
$ pip install cdp-client
|
|
22
|
+
|
|
23
|
+
Usage
|
|
24
|
+
-----
|
|
25
|
+
|
|
26
|
+
The example below shows how you subscribe to CDP signal value changes.
|
|
27
|
+
|
|
28
|
+
.. code:: python
|
|
29
|
+
|
|
30
|
+
from cdp_client import cdp
|
|
31
|
+
|
|
32
|
+
def on_value_changed(value, timestamp):
|
|
33
|
+
print(value)
|
|
34
|
+
|
|
35
|
+
def subscribe_to_value_changes(node):
|
|
36
|
+
node.subscribe_to_value_changes(on_value_changed)
|
|
37
|
+
|
|
38
|
+
client = cdp.Client(host='127.0.0.1')
|
|
39
|
+
client.find_node('AppName.ComponentName.SignalName').then(subscribe_to_value_changes)
|
|
40
|
+
client.run_event_loop()
|
|
41
|
+
|
|
42
|
+
API
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
Before all examples, you need:
|
|
46
|
+
|
|
47
|
+
.. code:: python
|
|
48
|
+
|
|
49
|
+
from cdp_client import cdp
|
|
50
|
+
|
|
51
|
+
Global API
|
|
52
|
+
~~~~~~~~~~
|
|
53
|
+
|
|
54
|
+
Client(host, port, auto_reconnect, notification_listener, encryption_parameters)
|
|
55
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
56
|
+
|
|
57
|
+
- Arguments
|
|
58
|
+
|
|
59
|
+
host - String for hosts ip address
|
|
60
|
+
|
|
61
|
+
port - Optional port number to connect to. If not specified default port 7689 is used.
|
|
62
|
+
|
|
63
|
+
auto_reconnect - Optional argument to enable/disable automatic reconnect when connection is lost. Defaults to True if not specified.
|
|
64
|
+
|
|
65
|
+
notification_listener - NotificationListener object whose methods are called on different connection events (e.g. when server requires credentials)
|
|
66
|
+
|
|
67
|
+
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
|
|
68
|
+
|
|
69
|
+
- Returns
|
|
70
|
+
|
|
71
|
+
The connected client object.
|
|
72
|
+
|
|
73
|
+
- Usage example
|
|
74
|
+
|
|
75
|
+
.. code:: python
|
|
76
|
+
|
|
77
|
+
client = cdp.Client(host='127.0.0.1')
|
|
78
|
+
|
|
79
|
+
- Usage example with password authentication
|
|
80
|
+
|
|
81
|
+
.. code:: python
|
|
82
|
+
|
|
83
|
+
class MyListener(cdp.NotificationListener):
|
|
84
|
+
def credentials_requested(self, request):
|
|
85
|
+
if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
|
|
86
|
+
# Do something to gather username and password variables (either sync or async way) and then call:
|
|
87
|
+
request.accept({'Username': 'test', 'Password': '12345678'});
|
|
88
|
+
|
|
89
|
+
client = cdp.Client(host='127.0.0.1', notification_listener=MyListener())
|
|
90
|
+
|
|
91
|
+
- Usage example with password authentication and encryption in use, without server certificate verification
|
|
92
|
+
|
|
93
|
+
.. code:: python
|
|
94
|
+
|
|
95
|
+
import ssl
|
|
96
|
+
|
|
97
|
+
class MyListener(cdp.NotificationListener):
|
|
98
|
+
def credentials_requested(self, request):
|
|
99
|
+
if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
|
|
100
|
+
# Do something to gather username and password variables (either sync or async way) and then call:
|
|
101
|
+
request.accept({'Username': 'test', 'Password': '12345678'});
|
|
102
|
+
|
|
103
|
+
client = cdp.Client(host='127.0.0.1', notification_listener=MyListener(),
|
|
104
|
+
encryption_parameters={'use_encryption': True, 'cert_reqs': ssl.CERT_NONE})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
- Usage example with password authentication and encryption in use, with server certificate verification
|
|
108
|
+
|
|
109
|
+
.. code:: python
|
|
110
|
+
|
|
111
|
+
import ssl
|
|
112
|
+
|
|
113
|
+
class MyListener(cdp.NotificationListener):
|
|
114
|
+
def credentials_requested(self, request):
|
|
115
|
+
if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
|
|
116
|
+
# Do something to gather username and password variables (either sync or async way) and then call:
|
|
117
|
+
request.accept({'Username': 'test', 'Password': '12345678'});
|
|
118
|
+
|
|
119
|
+
client = cdp.Client(host='127.0.0.1', notification_listener=MyListener(),
|
|
120
|
+
encryption_parameters={'use_encryption': True,
|
|
121
|
+
'cert_reqs': ssl.CERT_REQUIRED,
|
|
122
|
+
'ca_certs': 'StudioAPI.crt',
|
|
123
|
+
'check_hostname': False},
|
|
124
|
+
|
|
125
|
+
Instance Methods / Client
|
|
126
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
127
|
+
|
|
128
|
+
client.root_node()
|
|
129
|
+
^^^^^^^^^^^^^^^^^^
|
|
130
|
+
|
|
131
|
+
Gets the application Node object of the connected application.
|
|
132
|
+
|
|
133
|
+
- Returns
|
|
134
|
+
|
|
135
|
+
Promise containing root Node object when fulfilled.
|
|
136
|
+
|
|
137
|
+
- Usage
|
|
138
|
+
|
|
139
|
+
.. code:: python
|
|
140
|
+
|
|
141
|
+
client.root_node().then(on_success).catch(on_error)
|
|
142
|
+
|
|
143
|
+
client.find_node(path)
|
|
144
|
+
^^^^^^^^^^^^^^^^^^^^^^
|
|
145
|
+
|
|
146
|
+
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.**
|
|
147
|
+
|
|
148
|
+
- Arguments
|
|
149
|
+
|
|
150
|
+
path - Dot separated string to target node
|
|
151
|
+
|
|
152
|
+
- Returns
|
|
153
|
+
|
|
154
|
+
Promise containing requested Node object when fulfilled. Otherwise NotFoundError when rejected.
|
|
155
|
+
|
|
156
|
+
- Usage
|
|
157
|
+
|
|
158
|
+
.. code:: python
|
|
159
|
+
|
|
160
|
+
client.find_node('AppName.ComponentName.SignalName').then(on_success).catch(on_error)
|
|
161
|
+
|
|
162
|
+
client.run_event_loop()
|
|
163
|
+
^^^^^^^^^^^^^^^^^^^^^^^
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
client.disconnect()
|
|
168
|
+
^^^^^^^^^^^^^^^^^^^
|
|
169
|
+
|
|
170
|
+
Stops the event loop and closes the connection to connected application. This method also releases the blocking run_event_loop call.
|
|
171
|
+
|
|
172
|
+
Instance Methods / Node
|
|
173
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
174
|
+
|
|
175
|
+
node.name()
|
|
176
|
+
^^^^^^^^^^^
|
|
177
|
+
|
|
178
|
+
- Returns
|
|
179
|
+
|
|
180
|
+
The name of the Node object. Names in a parent node are all unique.
|
|
181
|
+
|
|
182
|
+
node.path()
|
|
183
|
+
^^^^^^^^^^^
|
|
184
|
+
|
|
185
|
+
- Returns
|
|
186
|
+
|
|
187
|
+
A dot separated path of the Node object starting with application name.
|
|
188
|
+
|
|
189
|
+
node.parent()
|
|
190
|
+
^^^^^^^^^^^^^
|
|
191
|
+
|
|
192
|
+
- Returns
|
|
193
|
+
|
|
194
|
+
The parent Node object.
|
|
195
|
+
|
|
196
|
+
node.type()
|
|
197
|
+
^^^^^^^^^^^
|
|
198
|
+
|
|
199
|
+
- Returns
|
|
200
|
+
|
|
201
|
+
The type of the Node object returned as one of the cdp.NodeType values.
|
|
202
|
+
|
|
203
|
+
node.last_value()
|
|
204
|
+
^^^^^^^^^^^^^^^^^
|
|
205
|
+
|
|
206
|
+
- Returns
|
|
207
|
+
|
|
208
|
+
The last known value received by the Node object.
|
|
209
|
+
|
|
210
|
+
node.set_value(value, timestamp)
|
|
211
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
212
|
+
|
|
213
|
+
Sets a new value for the Node object. Timestamp will be ignored in current implementation.
|
|
214
|
+
|
|
215
|
+
- Arguments
|
|
216
|
+
|
|
217
|
+
value - New value
|
|
218
|
+
|
|
219
|
+
timestamp - UTC time in nanoseconds since Epoch
|
|
220
|
+
|
|
221
|
+
node.is_read_only()
|
|
222
|
+
^^^^^^^^^^^^^^^^^^^
|
|
223
|
+
|
|
224
|
+
- Returns
|
|
225
|
+
|
|
226
|
+
False if nodes value cannot be set, otherwise True.
|
|
227
|
+
|
|
228
|
+
node.is_leaf()
|
|
229
|
+
^^^^^^^^^^^^^^
|
|
230
|
+
|
|
231
|
+
- Returns
|
|
232
|
+
|
|
233
|
+
True if node doesn't have any children, otherwise False.
|
|
234
|
+
|
|
235
|
+
node.child(name)
|
|
236
|
+
^^^^^^^^^^^^^^^^
|
|
237
|
+
|
|
238
|
+
- Arguments
|
|
239
|
+
|
|
240
|
+
name - Child nodes name to search for
|
|
241
|
+
|
|
242
|
+
- Returns
|
|
243
|
+
|
|
244
|
+
Promise containing requested Node object when fulfilled.
|
|
245
|
+
|
|
246
|
+
- Usage
|
|
247
|
+
|
|
248
|
+
.. code:: python
|
|
249
|
+
|
|
250
|
+
node.child('NodeName').then(on_success).catch(on_error)
|
|
251
|
+
|
|
252
|
+
node.children()
|
|
253
|
+
^^^^^^^^^^^^^^^
|
|
254
|
+
|
|
255
|
+
- Returns
|
|
256
|
+
|
|
257
|
+
Promise containing all children of this Node object when fulfilled.
|
|
258
|
+
|
|
259
|
+
- Usage
|
|
260
|
+
|
|
261
|
+
.. code:: python
|
|
262
|
+
|
|
263
|
+
node.children().then(on_success).catch(on_error)
|
|
264
|
+
|
|
265
|
+
node.for_each_child(callback)
|
|
266
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
267
|
+
|
|
268
|
+
Loops through all children and calls callback function for each of them
|
|
269
|
+
|
|
270
|
+
- Arguments
|
|
271
|
+
|
|
272
|
+
callback - Function(node)
|
|
273
|
+
|
|
274
|
+
- Returns
|
|
275
|
+
|
|
276
|
+
Promise containing all children of this Node object when fulfilled.
|
|
277
|
+
|
|
278
|
+
- Usage
|
|
279
|
+
|
|
280
|
+
.. code:: python
|
|
281
|
+
|
|
282
|
+
def on_callback(child):
|
|
283
|
+
do something
|
|
284
|
+
|
|
285
|
+
node.for_each_child(on_callback)
|
|
286
|
+
|
|
287
|
+
node.subscribe_to_structure_changes(callback)
|
|
288
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
289
|
+
|
|
290
|
+
Starts listening structure changes and passes the changes to provided callback funtion
|
|
291
|
+
|
|
292
|
+
- Arguments
|
|
293
|
+
|
|
294
|
+
callback - Function(added_nodes, removed_nodes) where added_nodes and removed_nodes is a list
|
|
295
|
+
|
|
296
|
+
- Usage
|
|
297
|
+
|
|
298
|
+
.. code:: python
|
|
299
|
+
|
|
300
|
+
def on_change(added_nodes, removed_nodes):
|
|
301
|
+
do something
|
|
302
|
+
|
|
303
|
+
node.subscribe_to_structure_changes(on_change)
|
|
304
|
+
|
|
305
|
+
node.subscribe_to_value_changes(callback, fs=5, sample_rate=0)
|
|
306
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
307
|
+
|
|
308
|
+
Starts listening value changes and passes the changes to provided callback function
|
|
309
|
+
|
|
310
|
+
- Arguments
|
|
311
|
+
|
|
312
|
+
callback - Function(value, timestamp)
|
|
313
|
+
|
|
314
|
+
fs - Maximum frequency that value updates are expected (controls how many changes are sent in a single packet). Defaults to 5 hz.
|
|
315
|
+
|
|
316
|
+
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.
|
|
317
|
+
|
|
318
|
+
- Usage
|
|
319
|
+
|
|
320
|
+
.. code:: python
|
|
321
|
+
|
|
322
|
+
def on_change(value, timestamp):
|
|
323
|
+
do something
|
|
324
|
+
|
|
325
|
+
node.subscribe_to_value_changes(on_change)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
node.unsubscribe_from_structure_changes(callback)
|
|
329
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
330
|
+
|
|
331
|
+
Stops listening previously subscribed structure changes
|
|
332
|
+
|
|
333
|
+
- Arguments
|
|
334
|
+
|
|
335
|
+
callback - Function(added_nodes, removed_nodes) where added_nodes and removed_nodes is a list
|
|
336
|
+
|
|
337
|
+
- Usage
|
|
338
|
+
|
|
339
|
+
.. code:: python
|
|
340
|
+
|
|
341
|
+
def on_change(added_nodes, removed_nodes):
|
|
342
|
+
do something
|
|
343
|
+
|
|
344
|
+
node.unsubscribe_from_structure_changes(on_change)
|
|
345
|
+
|
|
346
|
+
node.unsubscribe_from_value_changes(callback)
|
|
347
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
348
|
+
|
|
349
|
+
Stops listening previously subscribed value changes
|
|
350
|
+
|
|
351
|
+
- Arguments
|
|
352
|
+
|
|
353
|
+
callback - Function(value, timestamp)
|
|
354
|
+
|
|
355
|
+
- Usage
|
|
356
|
+
|
|
357
|
+
.. code:: python
|
|
358
|
+
|
|
359
|
+
def on_change(value, timestamp):
|
|
360
|
+
do something
|
|
361
|
+
|
|
362
|
+
node.unsubscribe_from_value_changes(on_change)
|
|
363
|
+
|
|
364
|
+
Notification Listener
|
|
365
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
366
|
+
|
|
367
|
+
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.
|
|
368
|
+
The notification_listener parameter must be a object of type class cdp.NotificationListener.
|
|
369
|
+
|
|
370
|
+
class NotificationListener
|
|
371
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
372
|
+
|
|
373
|
+
.. code:: python
|
|
374
|
+
|
|
375
|
+
class NotificationListener:
|
|
376
|
+
def application_acceptance_requested(self, request=AuthRequest()):
|
|
377
|
+
request.accept()
|
|
378
|
+
|
|
379
|
+
def credentials_requested(self, request=AuthRequest()):
|
|
380
|
+
raise NotImplementedError("NotificationListener credentials_requested() not implemented!")
|
|
381
|
+
|
|
382
|
+
NotificationListener.application_acceptance_requested(self, request=AuthRequest())
|
|
383
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
384
|
+
|
|
385
|
+
Called by Client when new application TLS or plain TCP connection is established.
|
|
386
|
+
Can be used to prompt the user a System Use Notification (a message that can be configured in CDP Studio Security settings).
|
|
387
|
+
|
|
388
|
+
- Arguments
|
|
389
|
+
|
|
390
|
+
request - a object that has method accept() that should be called to accept the connection and a reject() to reject the connection.
|
|
391
|
+
|
|
392
|
+
- Usage
|
|
393
|
+
|
|
394
|
+
.. code:: python
|
|
395
|
+
|
|
396
|
+
class MyListener(cdp.NotificationListener):
|
|
397
|
+
def application_acceptance_requested(self, request):
|
|
398
|
+
if request.system_use_notification():
|
|
399
|
+
# Pop up a System Use Notification message and ask for confirmation to continue,
|
|
400
|
+
# then based on the user answer call either request.accept() or request.reject()
|
|
401
|
+
else:
|
|
402
|
+
request.accept()
|
|
403
|
+
|
|
404
|
+
client = cdp.Client(host='127.0.0.1', port=7689, notification_listener=MyListener())
|
|
405
|
+
|
|
406
|
+
NotificationListener.credentials_requested(self, request=AuthRequest())
|
|
407
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
408
|
+
Called by Client when server is requesting credentials (authentication or idle lockout re-authentication).
|
|
409
|
+
|
|
410
|
+
- Arguments
|
|
411
|
+
|
|
412
|
+
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.
|
|
413
|
+
|
|
414
|
+
- Usage
|
|
415
|
+
|
|
416
|
+
.. code:: python
|
|
417
|
+
|
|
418
|
+
class MyListener(cdp.NotificationListener):
|
|
419
|
+
def credentials_requested(self, request):
|
|
420
|
+
if request.user_auth_result().code() == cdp.AuthResultCode.CREDENTIALS_REQUIRED:
|
|
421
|
+
# Do something to gather username and password variables (either sync or async way) and then call:
|
|
422
|
+
request.accept({'Username': 'test', 'Password': '12345678'});
|
|
423
|
+
if request.user_auth_result().code() == cdp.AuthResultCode.REAUTHENTICATION_REQUIRED:
|
|
424
|
+
# Pop user a message that idle lockout was happened and server requires new authentication to continue:
|
|
425
|
+
request.accept({'Username': 'test', 'Password': '12345678'});
|
|
426
|
+
|
|
427
|
+
client = cdp.Client(host='127.0.0.1', port=7689, notification_listener=MyListener())
|
|
428
|
+
|
|
429
|
+
Tests
|
|
430
|
+
-----
|
|
431
|
+
|
|
432
|
+
To run the test suite execute the following command in package root folder:
|
|
433
|
+
|
|
434
|
+
.. code:: sh
|
|
435
|
+
|
|
436
|
+
$ python setup.py test
|
|
437
|
+
|
|
438
|
+
License
|
|
439
|
+
-------
|
|
440
|
+
|
|
441
|
+
`MIT
|
|
442
|
+
License <https://github.com/CDPTechnologies/PythonCDPClient/blob/master/LICENSE.txt>`__
|
|
443
|
+
|
|
444
|
+
Keywords: cdp cdpstudio studio client cdp-client cdp_client
|
|
445
|
+
Platform: UNKNOWN
|
|
446
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
447
|
+
Classifier: Environment :: Console
|
|
448
|
+
Classifier: Intended Audience :: Developers
|
|
449
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
450
|
+
Classifier: Operating System :: OS Independent
|
|
451
|
+
Classifier: Topic :: Software Development
|
|
452
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
453
|
+
Classifier: Programming Language :: Python :: 3
|
|
454
|
+
Classifier: Programming Language :: Python :: 3.3
|
|
455
|
+
Classifier: Programming Language :: Python :: 3.4
|
|
456
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
457
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
@@ -294,8 +294,8 @@ Starts listening structure changes and passes the changes to provided callback f
|
|
|
294
294
|
|
|
295
295
|
node.subscribe_to_structure_changes(on_change)
|
|
296
296
|
|
|
297
|
-
node.subscribe_to_value_changes(callback)
|
|
298
|
-
|
|
297
|
+
node.subscribe_to_value_changes(callback, fs=5, sample_rate=0)
|
|
298
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
299
299
|
|
|
300
300
|
Starts listening value changes and passes the changes to provided callback function
|
|
301
301
|
|
|
@@ -303,6 +303,10 @@ Starts listening value changes and passes the changes to provided callback funct
|
|
|
303
303
|
|
|
304
304
|
callback - Function(value, timestamp)
|
|
305
305
|
|
|
306
|
+
fs - Maximum frequency that value updates are expected (controls how many changes are sent in a single packet). Defaults to 5 hz.
|
|
307
|
+
|
|
308
|
+
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.
|
|
309
|
+
|
|
306
310
|
- Usage
|
|
307
311
|
|
|
308
312
|
.. code:: python
|
|
@@ -218,16 +218,18 @@ class Node:
|
|
|
218
218
|
def subscribe_to_structure_changes(self, callback):
|
|
219
219
|
self._structure_subscriptions.append(callback)
|
|
220
220
|
|
|
221
|
-
def subscribe_to_value_changes(self, callback):
|
|
222
|
-
self._value_subscriptions.append(callback)
|
|
223
|
-
self.
|
|
221
|
+
def subscribe_to_value_changes(self, callback, fs=5, sample_rate=0):
|
|
222
|
+
self._value_subscriptions.append((callback, fs, sample_rate))
|
|
223
|
+
self._send_value_request()
|
|
224
224
|
|
|
225
225
|
def unsubscribe_from_structure_changes(self, callback):
|
|
226
226
|
self._structure_subscriptions.remove(callback)
|
|
227
227
|
|
|
228
228
|
def unsubscribe_from_value_changes(self, callback):
|
|
229
|
-
self._value_subscriptions.
|
|
230
|
-
if
|
|
229
|
+
self._value_subscriptions = [i for i in self._value_subscriptions if i[0] != callback]
|
|
230
|
+
if self._value_subscriptions:
|
|
231
|
+
self._send_value_request()
|
|
232
|
+
else:
|
|
231
233
|
self._connection.send_value_unrequest(self._id())
|
|
232
234
|
|
|
233
235
|
def _id(self):
|
|
@@ -243,11 +245,16 @@ class Node:
|
|
|
243
245
|
|
|
244
246
|
def fetch_value(node):
|
|
245
247
|
if self._value_subscriptions:
|
|
246
|
-
self.
|
|
248
|
+
self._send_value_request()
|
|
247
249
|
return Promise(lambda resolve, reject: resolve(node))
|
|
248
250
|
|
|
249
251
|
return fetch_structure().then(update_structure).then(fetch_value)
|
|
250
252
|
|
|
253
|
+
def _send_value_request(self):
|
|
254
|
+
max_fs = max(self._value_subscriptions, key=lambda e: e[1])[1]
|
|
255
|
+
max_sample_rate = max(self._value_subscriptions, key=lambda e: e[2])[2]
|
|
256
|
+
self._connection.send_value_request(self._id(), max_fs, max_sample_rate)
|
|
257
|
+
|
|
251
258
|
def _update_structure(self, structure):
|
|
252
259
|
self._structure = structure
|
|
253
260
|
new_children = list(self._structure.node)
|
|
@@ -287,7 +294,7 @@ class Node:
|
|
|
287
294
|
|
|
288
295
|
def _update_value(self, variant):
|
|
289
296
|
self._value = self._value_from_variant(self._structure.info.value_type, variant)
|
|
290
|
-
for callback in self._value_subscriptions:
|
|
297
|
+
for callback, fs, sample_rate in self._value_subscriptions:
|
|
291
298
|
callback(self._value, variant.timestamp + self._connection.server_time_difference() * nanoseconds_in_second)
|
|
292
299
|
|
|
293
300
|
@staticmethod
|
|
@@ -416,13 +423,13 @@ class Connection:
|
|
|
416
423
|
self._compose_and_send_structure_request(node_id)
|
|
417
424
|
return p
|
|
418
425
|
|
|
419
|
-
def send_value_request(self, node_id):
|
|
426
|
+
def send_value_request(self, node_id, fs, sample_rate):
|
|
420
427
|
self._update_time_difference()
|
|
421
|
-
self._compose_and_send_value_request(node_id)
|
|
428
|
+
self._compose_and_send_value_request(node_id, fs, sample_rate)
|
|
422
429
|
|
|
423
430
|
def send_value_unrequest(self, node_id):
|
|
424
431
|
self._update_time_difference()
|
|
425
|
-
self._compose_and_send_value_request(node_id, True)
|
|
432
|
+
self._compose_and_send_value_request(node_id, 1, 0, True)
|
|
426
433
|
|
|
427
434
|
def send_value(self, variant):
|
|
428
435
|
self._update_time_difference()
|
|
@@ -677,12 +684,13 @@ class Connection:
|
|
|
677
684
|
data.structure_request.append(node_id)
|
|
678
685
|
self._ws.send(data.SerializeToString())
|
|
679
686
|
|
|
680
|
-
def _compose_and_send_value_request(self, node_id, stop=False):
|
|
687
|
+
def _compose_and_send_value_request(self, node_id, fs, sample_rate, stop=False):
|
|
681
688
|
data = proto.Container()
|
|
682
689
|
data.message_type = proto.Container.eGetterRequest
|
|
683
690
|
value = proto.ValueRequest()
|
|
684
691
|
value.node_id = node_id
|
|
685
|
-
value.fs =
|
|
692
|
+
value.fs = fs
|
|
693
|
+
value.sample_rate = sample_rate
|
|
686
694
|
if stop:
|
|
687
695
|
value.stop = stop
|
|
688
696
|
data.getter_request.extend([value])
|
|
@@ -101,12 +101,13 @@ def create_error_response():
|
|
|
101
101
|
return response
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def create_value_request(node_id):
|
|
104
|
+
def create_value_request(node_id, fs, sample_rate):
|
|
105
105
|
request = proto.Container()
|
|
106
106
|
request.message_type = proto.Container.eGetterRequest
|
|
107
107
|
value = proto.ValueRequest()
|
|
108
108
|
value.node_id = node_id
|
|
109
|
-
value.fs =
|
|
109
|
+
value.fs = fs
|
|
110
|
+
value.sample_rate = sample_rate
|
|
110
111
|
request.getter_request.extend([value])
|
|
111
112
|
return request
|
|
112
113
|
|
|
@@ -116,7 +117,8 @@ def create_value_unrequest(node_id):
|
|
|
116
117
|
request.message_type = proto.Container.eGetterRequest
|
|
117
118
|
value = proto.ValueRequest()
|
|
118
119
|
value.node_id = node_id
|
|
119
|
-
value.fs =
|
|
120
|
+
value.fs = 1
|
|
121
|
+
value.sample_rate = 0
|
|
120
122
|
value.stop = True
|
|
121
123
|
request.getter_request.extend([value])
|
|
122
124
|
return request
|
|
@@ -67,8 +67,10 @@ class ConnectionTester(unittest.TestCase):
|
|
|
67
67
|
@mock.patch.object(cdp.websocket.WebSocketApp, 'send')
|
|
68
68
|
def test_sending_value_request(self, mock_send):
|
|
69
69
|
node_id = 1
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
fs = 1
|
|
71
|
+
sample_rate = 1
|
|
72
|
+
self._connection.send_value_request(node_id, fs, sample_rate)
|
|
73
|
+
mock_send.assert_any_call(fake_data.create_value_request(node_id, fs, sample_rate).SerializeToString())
|
|
72
74
|
|
|
73
75
|
@mock.patch.object(cdp.websocket.WebSocketApp, 'send')
|
|
74
76
|
def test_sending_value_unrequest(self, mock_send):
|
|
@@ -149,12 +149,11 @@ class NodeTester(unittest.TestCase):
|
|
|
149
149
|
def on_change(value, timestamp):
|
|
150
150
|
actual_values.append(value)
|
|
151
151
|
timestamps.append(timestamp)
|
|
152
|
-
|
|
153
152
|
actual_values = []
|
|
154
153
|
timestamps = []
|
|
155
154
|
node = cdp.Node(None, self._connection, data.value1_node)
|
|
156
|
-
node.subscribe_to_value_changes(on_change)
|
|
157
|
-
mock_send_value_request.assert_called_once_with(node._id())
|
|
155
|
+
node.subscribe_to_value_changes(on_change, 10, 5)
|
|
156
|
+
mock_send_value_request.assert_called_once_with(node._id(), 10, 5)
|
|
158
157
|
|
|
159
158
|
expected_values = [data.value1, data.value2]
|
|
160
159
|
for value in expected_values:
|
|
@@ -165,6 +164,42 @@ class NodeTester(unittest.TestCase):
|
|
|
165
164
|
for index, value in enumerate(expected_values):
|
|
166
165
|
self.assertEquals(actual_values[index], value.d_value)
|
|
167
166
|
self.assertEquals(timestamps[index], value.timestamp)
|
|
167
|
+
|
|
168
|
+
@mock.patch.object(cdp.Connection, 'send_value_request')
|
|
169
|
+
def test_value_subscription_max_fs(self, mock_send_value_request):
|
|
170
|
+
def on_change1():
|
|
171
|
+
pass
|
|
172
|
+
def on_change2():
|
|
173
|
+
pass
|
|
174
|
+
def on_change3():
|
|
175
|
+
pass
|
|
176
|
+
node = cdp.Node(None, self._connection, data.value1_node)
|
|
177
|
+
node.subscribe_to_value_changes(on_change1, 3, 1)
|
|
178
|
+
mock_send_value_request.assert_called_once_with(node._id(), 3, 1)
|
|
179
|
+
mock_send_value_request.reset_mock()
|
|
180
|
+
node.subscribe_to_value_changes(on_change2, 10, 6)
|
|
181
|
+
mock_send_value_request.assert_called_once_with(node._id(), 10, 6)
|
|
182
|
+
mock_send_value_request.reset_mock()
|
|
183
|
+
node.subscribe_to_value_changes(on_change3, 1, 3)
|
|
184
|
+
mock_send_value_request.assert_called_once_with(node._id(), 10, 6)
|
|
185
|
+
|
|
186
|
+
@mock.patch.object(cdp.Connection, 'send_structure_request')
|
|
187
|
+
@mock.patch.object(cdp.Connection, 'send_value_request')
|
|
188
|
+
def test_value_subscription_max_fs_on_update(self, mock_send_value_request, mock_send_structure_request):
|
|
189
|
+
def on_change1():
|
|
190
|
+
pass
|
|
191
|
+
def on_change2():
|
|
192
|
+
pass
|
|
193
|
+
def on_change3():
|
|
194
|
+
pass
|
|
195
|
+
node = cdp.Node(None, self._connection, data.value1_node)
|
|
196
|
+
node.subscribe_to_value_changes(on_change1, 3, 1)
|
|
197
|
+
node.subscribe_to_value_changes(on_change2, 10, 6)
|
|
198
|
+
node.subscribe_to_value_changes(on_change3, 1, 6)
|
|
199
|
+
mock_send_value_request.reset_mock()
|
|
200
|
+
mock_send_structure_request.return_value = Promise(lambda resolve, reject: resolve(self._root_node))
|
|
201
|
+
node._update()
|
|
202
|
+
mock_send_value_request.assert_called_once_with(node._id(), 10, 6)
|
|
168
203
|
|
|
169
204
|
@mock.patch.object(cdp.Connection, 'send_value_request')
|
|
170
205
|
@mock.patch.object(cdp.Connection, 'send_value_unrequest')
|
|
@@ -175,8 +210,24 @@ class NodeTester(unittest.TestCase):
|
|
|
175
210
|
values = []
|
|
176
211
|
node = cdp.Node(None, self._connection, data.value1_node)
|
|
177
212
|
node.subscribe_to_value_changes(on_change)
|
|
178
|
-
mock_send_value_request.assert_called_once_with(node._id())
|
|
213
|
+
mock_send_value_request.assert_called_once_with(node._id(), 5, 0)
|
|
179
214
|
node.unsubscribe_from_value_changes(on_change)
|
|
180
215
|
mock_send_value_unrequest.assert_called_once_with(node._id())
|
|
181
216
|
node._update_value(data.value1)
|
|
182
217
|
self.assertEquals(len(values), 0)
|
|
218
|
+
|
|
219
|
+
@mock.patch.object(cdp.Connection, 'send_value_request')
|
|
220
|
+
def test_value_unsubscription_max_fs(self, mock_send_value_request):
|
|
221
|
+
def on_change1():
|
|
222
|
+
pass
|
|
223
|
+
def on_change2():
|
|
224
|
+
pass
|
|
225
|
+
def on_change3():
|
|
226
|
+
pass
|
|
227
|
+
node = cdp.Node(None, self._connection, data.value1_node)
|
|
228
|
+
node.subscribe_to_value_changes(on_change1, 5, 1)
|
|
229
|
+
node.subscribe_to_value_changes(on_change2, 1, 3)
|
|
230
|
+
node.subscribe_to_value_changes(on_change3, 10, 6)
|
|
231
|
+
mock_send_value_request.reset_mock()
|
|
232
|
+
node.unsubscribe_from_value_changes(on_change3)
|
|
233
|
+
mock_send_value_request.assert_called_once_with(node._id(), 5, 3)
|