ztl 0.0.1__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.
- ztl-0.0.1/LICENSE +25 -0
- ztl-0.0.1/PKG-INFO +65 -0
- ztl-0.0.1/README.md +19 -0
- ztl-0.0.1/pyproject.toml +30 -0
- ztl-0.0.1/setup.cfg +4 -0
- ztl-0.0.1/setup.py +17 -0
- ztl-0.0.1/src/ztl/__init__.py +0 -0
- ztl-0.0.1/src/ztl/core/__init__.py +0 -0
- ztl-0.0.1/src/ztl/core/client.py +109 -0
- ztl-0.0.1/src/ztl/core/protocol.py +69 -0
- ztl-0.0.1/src/ztl/core/server.py +102 -0
- ztl-0.0.1/src/ztl/core/task.py +95 -0
- ztl-0.0.1/src/ztl/example/__init__.py +0 -0
- ztl-0.0.1/src/ztl/example/simple_client.py +39 -0
- ztl-0.0.1/src/ztl/example/simple_server.py +68 -0
- ztl-0.0.1/src/ztl/script/__init__.py +0 -0
- ztl-0.0.1/src/ztl/script/run_script.py +214 -0
- ztl-0.0.1/src/ztl.egg-info/PKG-INFO +65 -0
- ztl-0.0.1/src/ztl.egg-info/SOURCES.txt +24 -0
- ztl-0.0.1/src/ztl.egg-info/dependency_links.txt +1 -0
- ztl-0.0.1/src/ztl.egg-info/entry_points.txt +4 -0
- ztl-0.0.1/src/ztl.egg-info/requires.txt +4 -0
- ztl-0.0.1/src/ztl.egg-info/top_level.txt +1 -0
- ztl-0.0.1/test/test_installation.py +23 -0
- ztl-0.0.1/test/test_instances.py +15 -0
- ztl-0.0.1/test/test_lifecycle.py +75 -0
ztl-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, UH Robot House / User Projects
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
ztl-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ztl
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A thin library relying on zmq to dispatch tasks
|
|
5
|
+
Home-page: https://gitlab.com/robothouse/rh-user/ztl/
|
|
6
|
+
Author: Patrick Holthaus
|
|
7
|
+
Author-email: Patrick Holthaus <patrick.holthaus@googlemail.com>
|
|
8
|
+
License: BSD 2-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2023, UH Robot House / User Projects
|
|
11
|
+
All rights reserved.
|
|
12
|
+
|
|
13
|
+
Redistribution and use in source and binary forms, with or without
|
|
14
|
+
modification, are permitted provided that the following conditions are met:
|
|
15
|
+
|
|
16
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
17
|
+
list of conditions and the following disclaimer.
|
|
18
|
+
|
|
19
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
20
|
+
this list of conditions and the following disclaimer in the documentation
|
|
21
|
+
and/or other materials provided with the distribution.
|
|
22
|
+
|
|
23
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
24
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
25
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
26
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
27
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
28
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
29
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
30
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
31
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
32
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, https://gitlab.com/robothouse/rh-user/ztl/
|
|
35
|
+
Project-URL: Bug Tracker, https://gitlab.com/robothouse/rh-user/ztl/issues/
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Requires-Python: >=3.7
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Requires-Dist: oyaml
|
|
43
|
+
Requires-Dist: pyzmq
|
|
44
|
+
Requires-Dist: pytest
|
|
45
|
+
Requires-Dist: pytest-xprocess
|
|
46
|
+
|
|
47
|
+
This project contains the `ZTL` library enabling light-weight and widely compatible **remote task execution** using [ZeroMQ](https://zeromq.org/).
|
|
48
|
+
|
|
49
|
+
The basic communication principle is as follows:
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
Thereby, each task has the following lifecycle:
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
An example communication could look like this:
|
|
58
|
+
|
|
59
|
+
Request:
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
Reply:
|
|
64
|
+
|
|
65
|
+

|
ztl-0.0.1/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
This project contains the `ZTL` library enabling light-weight and widely compatible **remote task execution** using [ZeroMQ](https://zeromq.org/).
|
|
2
|
+
|
|
3
|
+
The basic communication principle is as follows:
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Thereby, each task has the following lifecycle:
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
An example communication could look like this:
|
|
12
|
+
|
|
13
|
+
Request:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
Reply:
|
|
18
|
+
|
|
19
|
+

|
ztl-0.0.1/pyproject.toml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ztl"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Patrick Holthaus", email="patrick.holthaus@googlemail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "A thin library relying on zmq to dispatch tasks"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = {file = "LICENSE"}
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: BSD License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"oyaml",
|
|
18
|
+
"pyzmq",
|
|
19
|
+
"pytest",
|
|
20
|
+
"pytest-xprocess"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
ztl_simple_client = "ztl.example.simple_client:main_cli"
|
|
25
|
+
ztl_simple_server = "ztl.example.simple_server:main_cli"
|
|
26
|
+
ztl_run_script = "ztl.script.run_script:main_cli"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
"Homepage" = "https://gitlab.com/robothouse/rh-user/ztl/"
|
|
30
|
+
"Bug Tracker" = "https://gitlab.com/robothouse/rh-user/ztl/issues/"
|
ztl-0.0.1/setup.cfg
ADDED
ztl-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from distutils.core import setup
|
|
4
|
+
|
|
5
|
+
setup(name='ztl',
|
|
6
|
+
version='0.1',
|
|
7
|
+
description='A thin library relying on zmq to dispatch tasks',
|
|
8
|
+
author='Patrick Holthaus',
|
|
9
|
+
author_email='patrick.holthaus@googlemail.com',
|
|
10
|
+
url='https://gitlab.com/robothouse/rh-user/ztl/',
|
|
11
|
+
package_dir={'':'src'},
|
|
12
|
+
packages=['ztl', 'ztl.core', 'ztl.example', 'ztl.script'],
|
|
13
|
+
scripts=['src/ztl/example/simple_client.py',
|
|
14
|
+
'src/ztl/example/simple_server.py',
|
|
15
|
+
'src/ztl/script/run_script.py'
|
|
16
|
+
]
|
|
17
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import zmq
|
|
2
|
+
import time
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
logging.basicConfig(level=logging.INFO)
|
|
6
|
+
|
|
7
|
+
from ztl.core.protocol import Message, Request, State
|
|
8
|
+
|
|
9
|
+
class RemoteTask(object):
|
|
10
|
+
|
|
11
|
+
def __init__(self, host, port, scope, timeout=2000):
|
|
12
|
+
|
|
13
|
+
self.logger = logging.getLogger('remote-task')
|
|
14
|
+
self.context = zmq.Context()
|
|
15
|
+
self.socket = self.context.socket(zmq.REQ)
|
|
16
|
+
self.socket.setsockopt(zmq.RCVTIMEO, timeout)
|
|
17
|
+
address = "tcp://" + str(host) + ":" + str(port)
|
|
18
|
+
self.socket.connect(address)
|
|
19
|
+
self.scope = scope
|
|
20
|
+
|
|
21
|
+
self.logger.info("Remote task interface initialised at '%s'.", address)
|
|
22
|
+
|
|
23
|
+
def trigger(self, payload):
|
|
24
|
+
"""
|
|
25
|
+
Trigger a task at the remote interface.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
payload: The payload containing the task description.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
id: An ID for the task assigned by the server if accepted, -1 if rejected or server not reachable or communication error.
|
|
34
|
+
"""
|
|
35
|
+
msg = Message.encode(self.scope, Request.INIT, -1, payload)
|
|
36
|
+
try:
|
|
37
|
+
self.socket.send(msg)
|
|
38
|
+
reply = Message.decode(self.socket.recv())
|
|
39
|
+
return int(reply["id"])
|
|
40
|
+
except Exception as e:
|
|
41
|
+
self.logger.error(e)
|
|
42
|
+
return -1
|
|
43
|
+
|
|
44
|
+
def abort(self, mid, payload="abort command"):
|
|
45
|
+
"""
|
|
46
|
+
Aborts a task at the remote interface.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
mid: The ID of the task to abort.
|
|
51
|
+
payload: An optional payload containing an updated task description. Default: "abort command"
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
status: The updated status after attempting to abort. Task might not be aborted. -1 if server not reachable or communication error.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
msg = Message.encode(self.scope, Request.ABORT, mid, payload)
|
|
59
|
+
self.socket.send(msg)
|
|
60
|
+
reply = Message.decode(self.socket.recv())
|
|
61
|
+
return int(reply["state"])
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.error(e)
|
|
64
|
+
return -1
|
|
65
|
+
|
|
66
|
+
def status(self, mid, payload="status update"):
|
|
67
|
+
"""
|
|
68
|
+
Query the status of a given task at the remote interface.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
mid: The ID of the task to query about.
|
|
73
|
+
payload: An optional payload containing an updated task description. Default: "status update"
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
id: The current status of the task at the server. -1 if server not reachable or communication error.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
msg = Message.encode(self.scope, Request.STATUS, mid, payload)
|
|
81
|
+
self.socket.send(msg)
|
|
82
|
+
reply = Message.decode(self.socket.recv())
|
|
83
|
+
return int(reply["state"])
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(e)
|
|
86
|
+
return -1
|
|
87
|
+
|
|
88
|
+
def wait(self, mid, payload = "waiting poll", timeout = 5.0):
|
|
89
|
+
"""
|
|
90
|
+
Wait for a task to complete at the remote interface.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
mid: The ID of the task to wait for.
|
|
95
|
+
payload: An optional payload containing an updated task description. Default: "waiting poll"
|
|
96
|
+
timeout: Maximum time to wait. Default: 5.0 secs.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
id: The last task status after waiting complete. -1 if server not reachable or communication error.
|
|
101
|
+
"""
|
|
102
|
+
start = time.time()
|
|
103
|
+
state = -1
|
|
104
|
+
while (time.time() - start) < timeout and mid > 0:
|
|
105
|
+
state = self.status(mid, payload)
|
|
106
|
+
if state > State.ACCEPTED:
|
|
107
|
+
return state
|
|
108
|
+
time.sleep(.1)
|
|
109
|
+
return state
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
class Request:
|
|
4
|
+
|
|
5
|
+
INIT = 1
|
|
6
|
+
STATUS = 2
|
|
7
|
+
ABORT = 3
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def name(code):
|
|
11
|
+
if code == Request.INIT: return "INIT"
|
|
12
|
+
if code == Request.STATUS: return "STATUS"
|
|
13
|
+
if code == Request.ABORT: return "ABORT"
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
class State:
|
|
17
|
+
|
|
18
|
+
INITIATED = 0
|
|
19
|
+
ACCEPTED = 1
|
|
20
|
+
REJECTED = 2
|
|
21
|
+
FAILED = 3
|
|
22
|
+
ABORTED = 4
|
|
23
|
+
COMPLETED = 5
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def name(code):
|
|
27
|
+
if code == State.INITIATED: return "INITIATED"
|
|
28
|
+
if code == State.ACCEPTED: return "ACCEPTED"
|
|
29
|
+
if code == State.REJECTED: return "REJECTED"
|
|
30
|
+
if code == State.FAILED: return "FAILED"
|
|
31
|
+
if code == State.ABORTED: return "ABORTED"
|
|
32
|
+
if code == State.COMPLETED: return "COMPLETED"
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
class Message:
|
|
36
|
+
|
|
37
|
+
SEPARATOR = ";"
|
|
38
|
+
FIELDS = ["scope", "state", "id", "payload"]
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def encode(scope, state, mid, payload):
|
|
42
|
+
msg = {"scope": str(scope),
|
|
43
|
+
"state": str(state),
|
|
44
|
+
"id": str(mid),
|
|
45
|
+
"payload": str(payload)}
|
|
46
|
+
return (msg["scope"] + Message.SEPARATOR + msg["state"] + Message.SEPARATOR + msg["id"] + Message.SEPARATOR + msg["payload"]).encode('utf-8')
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def decode(message):
|
|
50
|
+
split = message.decode("utf-8").split(Message.SEPARATOR)
|
|
51
|
+
unfolded = dict(zip(Message.FIELDS, split))
|
|
52
|
+
return unfolded
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Task:
|
|
56
|
+
|
|
57
|
+
SEPARATOR = "^"
|
|
58
|
+
FIELDS = ["handler", "component", "goal"]
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def decode(message):
|
|
62
|
+
cmd = base64.b64decode(bytes(str(message).encode("utf-8"))).split(Task.SEPARATOR)
|
|
63
|
+
return dict(zip(Task.FIELDS, cmd))
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def encode(handler, component, goal):
|
|
67
|
+
joined = str(handler) + Task.SEPARATOR + str(component) + Task.SEPARATOR + str(goal)
|
|
68
|
+
code = base64.b64encode(joined.encode('utf-8'))
|
|
69
|
+
return code.decode("utf-8")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import zmq
|
|
2
|
+
import time
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
logging.basicConfig(level=logging.INFO)
|
|
6
|
+
|
|
7
|
+
from ztl.core.protocol import Message, Request, State
|
|
8
|
+
|
|
9
|
+
class TaskServer(object):
|
|
10
|
+
|
|
11
|
+
def __init__(self, port):
|
|
12
|
+
self.logger = logging.getLogger('remote-task')
|
|
13
|
+
context = zmq.Context()
|
|
14
|
+
self.socket = context.socket(zmq.REP)
|
|
15
|
+
address = "tcp://*:" + str(port)
|
|
16
|
+
self.socket.bind(address)
|
|
17
|
+
self.controllers = {}
|
|
18
|
+
self.logger.info("Task Server listening at '%s'" % address)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def send_message(self, scope, mid, state, payload):
|
|
22
|
+
self.socket.send(Message.encode(scope, mid, state, payload))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register(self, scope, controller):
|
|
26
|
+
"""
|
|
27
|
+
Register a new controller for requests on a specific scope.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
scope: The scope for which the server should dispatch tasks to the controller. Will replace any existing controller for this scope.
|
|
32
|
+
controller: A controller object that will be called with requests (init, status, abort)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
self.logger.info("Registering controller for scope '%s'." % scope)
|
|
36
|
+
self.controllers[scope] = controller
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def unregister(self, scope):
|
|
40
|
+
"""
|
|
41
|
+
Unregister any controller on a given scope.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
scope: The scope for which the server should not dispatch tasks any longer.
|
|
46
|
+
"""
|
|
47
|
+
self.controllers[scope] = None
|
|
48
|
+
self.logger.info("Controller for scope '%s' removed." % scope)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def listen(self):
|
|
52
|
+
"""
|
|
53
|
+
Begin listening to requests and dispatching them to any controllers if available. This method blocks until interrupted.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
while True:
|
|
57
|
+
try:
|
|
58
|
+
message = self.socket.recv()
|
|
59
|
+
request = Message.decode(message)
|
|
60
|
+
|
|
61
|
+
if all(field in request for field in Message.FIELDS):
|
|
62
|
+
|
|
63
|
+
scope = request["scope"]
|
|
64
|
+
if scope in self.controllers:
|
|
65
|
+
controller = self.controllers[scope]
|
|
66
|
+
|
|
67
|
+
state = int(request["state"])
|
|
68
|
+
mid = int(request["id"])
|
|
69
|
+
payload = request["payload"]
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if state == Request.INIT:
|
|
73
|
+
ticket, response = controller.init(payload)
|
|
74
|
+
if ticket > 0:
|
|
75
|
+
self.send_message(scope, State.ACCEPTED, ticket, response)
|
|
76
|
+
else:
|
|
77
|
+
self.send_message(scope, State.REJECTED, ticket, response)
|
|
78
|
+
elif state == Request.STATUS:
|
|
79
|
+
status, response = controller.status(mid, payload)
|
|
80
|
+
self.send_message(scope, status, mid, response)
|
|
81
|
+
elif state == Request.ABORT:
|
|
82
|
+
status, response = controller.abort(mid, payload)
|
|
83
|
+
self.send_message(scope, status, mid, response)
|
|
84
|
+
else:
|
|
85
|
+
self.send_message(scope, State.REJECTED, mid, "Invalid request")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logging.error(e)
|
|
88
|
+
self.send_message(scope, State.FAILED, mid, "Controller threw exception: " + str(e))
|
|
89
|
+
else:
|
|
90
|
+
self.send_message(scope, State.REJECTED, -1, "No controller for scope: " + scope)
|
|
91
|
+
self.logger.warning("No controller for scope '%s', ignoring." % scope)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
self.send_message(scope, State.REJECTED, -1, "Unknown protocol")
|
|
95
|
+
self.logger.warning("Unknown command received '%s', ignoring." % message)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logging.error(e)
|
|
99
|
+
time.sleep(1)
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
logging.error("Interrupted, exiting.")
|
|
102
|
+
sys.exit(1)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
logging.basicConfig(level=logging.INFO)
|
|
3
|
+
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from ztl.core.protocol import State
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecutableTask(object):
|
|
9
|
+
|
|
10
|
+
def initialise(self):
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(self):
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def abort(self):
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskController(object):
|
|
23
|
+
|
|
24
|
+
def init(self, payload):
|
|
25
|
+
return -1, "Not implemented"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def status(self, mid, payload):
|
|
29
|
+
return State.REJECTED, "Not implemented"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def abort(self, mid, payload):
|
|
33
|
+
return State.REJECTED, "Not implemented"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskExecutor(Thread):
|
|
37
|
+
|
|
38
|
+
def __init__(self, cls, *parameters):
|
|
39
|
+
Thread.__init__(self)
|
|
40
|
+
self.logger = logging.getLogger(type(cls).__name__)
|
|
41
|
+
self.cls = cls
|
|
42
|
+
self.parameters = parameters
|
|
43
|
+
self.task = None
|
|
44
|
+
self._state = State.INITIATED
|
|
45
|
+
self._prevent = False
|
|
46
|
+
self.start()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(self):
|
|
50
|
+
self.logger.debug("Initiating task with parameters '%s'...", self.parameters)
|
|
51
|
+
self.task = self.cls(*self.parameters)
|
|
52
|
+
success = self.task.initialise()
|
|
53
|
+
if success:
|
|
54
|
+
if not self._prevent:
|
|
55
|
+
self.logger.debug("Accepting and executing task...")
|
|
56
|
+
self._state = State.ACCEPTED
|
|
57
|
+
success = self.task.execute()
|
|
58
|
+
if success:
|
|
59
|
+
self.logger.debug("Task execution completed successfully.")
|
|
60
|
+
self._state = State.COMPLETED
|
|
61
|
+
else:
|
|
62
|
+
self.logger.debug("Task execution failed.")
|
|
63
|
+
self._state = State.FAILED
|
|
64
|
+
else:
|
|
65
|
+
self._state = State.FAILED
|
|
66
|
+
logging.warn("Task execution prevented during initialising.")
|
|
67
|
+
else:
|
|
68
|
+
self.logger.debug("Task initialising failed, rejecting task.")
|
|
69
|
+
self._state = State.REJECTED
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def stop(self):
|
|
73
|
+
if self.task is None:
|
|
74
|
+
self.logger.debug("Preventing task from executing...")
|
|
75
|
+
self._prevent = True
|
|
76
|
+
self._state = State.ABORTED
|
|
77
|
+
else:
|
|
78
|
+
self.logger.debug("Aborting task execution...")
|
|
79
|
+
success = self.task.abort()
|
|
80
|
+
if success:
|
|
81
|
+
print("Task aborted successfully.")
|
|
82
|
+
self._state = State.ABORTED
|
|
83
|
+
else:
|
|
84
|
+
self.logger.debug("Task could not be aborted.")
|
|
85
|
+
return self._state
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def abort(self):
|
|
89
|
+
print("Aborting immediately as requested...")
|
|
90
|
+
self._state = State.ABORTED
|
|
91
|
+
super().abort()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def state(self):
|
|
95
|
+
return self._state
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ztl.core.client import RemoteTask
|
|
6
|
+
from ztl.core.protocol import State
|
|
7
|
+
|
|
8
|
+
def main_cli():
|
|
9
|
+
host = sys.argv[1]
|
|
10
|
+
scope = sys.argv[2]
|
|
11
|
+
payload = sys.argv[3]
|
|
12
|
+
|
|
13
|
+
print("Connecting to host '%s' at scope '%s'..." % (host, scope))
|
|
14
|
+
task = RemoteTask(str(host), 5555, scope)
|
|
15
|
+
print("Triggering task with payload '%s'..." % payload)
|
|
16
|
+
mid = task.trigger(payload)
|
|
17
|
+
|
|
18
|
+
if mid > 0:
|
|
19
|
+
print("Waiting 5s for task with ID '%s'..." % mid)
|
|
20
|
+
state = task.wait(mid, 5)
|
|
21
|
+
if state < 0:
|
|
22
|
+
print("Could not wait for task with ID '%s'." % mid)
|
|
23
|
+
elif state <= State.ACCEPTED:
|
|
24
|
+
print("Aborting task with ID '%s'..." % mid)
|
|
25
|
+
state = task.abort(mid)
|
|
26
|
+
if state == State.ABORTED:
|
|
27
|
+
print("Task with ID '%s' aborted." % mid)
|
|
28
|
+
elif state <= State.ACCEPTED:
|
|
29
|
+
print("Could not abort Task with ID '%s', waiting for completion." % mid)
|
|
30
|
+
task.wait(mid)
|
|
31
|
+
print("Task with ID '%s' finished after unsuccessful abort signal." % mid)
|
|
32
|
+
else:
|
|
33
|
+
print("Task with ID '%s' finished while waiting." % mid)
|
|
34
|
+
else:
|
|
35
|
+
print("Task '%s' could not be triggered.")
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
|
|
39
|
+
main_cli()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from ztl.core.server import TaskServer
|
|
7
|
+
from ztl.core.protocol import State
|
|
8
|
+
from ztl.core.task import ExecutableTask, TaskExecutor, TaskController
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DummyTask(ExecutableTask):
|
|
12
|
+
|
|
13
|
+
def __init__(self, duration):
|
|
14
|
+
self.active = True
|
|
15
|
+
self.duration = duration
|
|
16
|
+
|
|
17
|
+
def initialise(self):
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def execute(self):
|
|
21
|
+
start = time.time()
|
|
22
|
+
while self.active and time.time() - start < self.duration:
|
|
23
|
+
time.sleep(.1)
|
|
24
|
+
return self.active
|
|
25
|
+
|
|
26
|
+
def abort(self):
|
|
27
|
+
self.active = False
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SimpleTaskController(TaskController):
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.current_id = 0
|
|
35
|
+
self.running = {}
|
|
36
|
+
|
|
37
|
+
def init(self, payload):
|
|
38
|
+
self.current_id += 1
|
|
39
|
+
print("Initialising Task ID '%s' (%s)..." % (self.current_id, payload))
|
|
40
|
+
self.running[self.current_id] = TaskExecutor(DummyTask, int(payload))
|
|
41
|
+
return self.current_id, ""
|
|
42
|
+
|
|
43
|
+
def status(self, mid, payload):
|
|
44
|
+
if mid in self.running:
|
|
45
|
+
print("Status Task ID '%s' (%s)..." % (mid, payload))
|
|
46
|
+
return self.running[mid].state(), mid
|
|
47
|
+
else:
|
|
48
|
+
return State.REJECTED, "Invalid ID"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def abort(self, mid, payload):
|
|
52
|
+
if mid in self.running:
|
|
53
|
+
print("Aborting Task ID '%s' (%s)..." % (mid, payload))
|
|
54
|
+
return self.running[mid].stop(), mid
|
|
55
|
+
else:
|
|
56
|
+
return State.REJECTED, "Invalid ID"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main_cli():
|
|
60
|
+
scope = sys.argv[1]
|
|
61
|
+
server = TaskServer(5555)
|
|
62
|
+
server.register(scope, SimpleTaskController())
|
|
63
|
+
server.listen()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
|
|
68
|
+
main_cli()
|
|
File without changes
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import oyaml as yaml
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO)
|
|
10
|
+
|
|
11
|
+
from sys import stdin, exit
|
|
12
|
+
from ztl.core.protocol import State, Task
|
|
13
|
+
from ztl.core.client import RemoteTask
|
|
14
|
+
|
|
15
|
+
class _Getch:
|
|
16
|
+
"""Gets a single character from standard input. Does not echo to the
|
|
17
|
+
screen."""
|
|
18
|
+
def __init__(self):
|
|
19
|
+
try:
|
|
20
|
+
self.impl = _GetchWindows()
|
|
21
|
+
except ImportError:
|
|
22
|
+
self.impl = _GetchUnix()
|
|
23
|
+
|
|
24
|
+
def __call__(self): return self.impl()
|
|
25
|
+
|
|
26
|
+
class _GetchUnix:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
import tty, sys
|
|
29
|
+
|
|
30
|
+
def __call__(self):
|
|
31
|
+
import sys, tty, termios
|
|
32
|
+
fd = sys.stdin.fileno()
|
|
33
|
+
old_settings = termios.tcgetattr(fd)
|
|
34
|
+
try:
|
|
35
|
+
tty.setraw(sys.stdin.fileno())
|
|
36
|
+
ch = sys.stdin.read(1)
|
|
37
|
+
finally:
|
|
38
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
39
|
+
return ch
|
|
40
|
+
|
|
41
|
+
class _GetchWindows:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
import msvcrt
|
|
44
|
+
|
|
45
|
+
def __call__(self):
|
|
46
|
+
import msvcrt
|
|
47
|
+
return msvcrt.getch()
|
|
48
|
+
|
|
49
|
+
class ScriptExecutor(object):
|
|
50
|
+
|
|
51
|
+
tasks = {}
|
|
52
|
+
|
|
53
|
+
def __init__(self, configfile, scriptfile):
|
|
54
|
+
self.logger = logging.getLogger('script-exec')
|
|
55
|
+
|
|
56
|
+
self.getch = _Getch()
|
|
57
|
+
|
|
58
|
+
self.lastScene = None
|
|
59
|
+
|
|
60
|
+
with open(configfile) as f:
|
|
61
|
+
self.config = yaml.safe_load(f)
|
|
62
|
+
|
|
63
|
+
rs = self.config["remotes"]
|
|
64
|
+
for r in rs.keys():
|
|
65
|
+
self.logger.info("Initialising remote task interface '%s'..." % r)
|
|
66
|
+
self.tasks[r] = RemoteTask(rs[r]["host"], rs[r]["port"], rs[r]["scope"])
|
|
67
|
+
|
|
68
|
+
with open(scriptfile) as f:
|
|
69
|
+
self.script = yaml.safe_load(f)
|
|
70
|
+
|
|
71
|
+
def parse_stage(self, stage):
|
|
72
|
+
name = str(stage)
|
|
73
|
+
delay = 0
|
|
74
|
+
wait = True
|
|
75
|
+
|
|
76
|
+
o = name.find("(")
|
|
77
|
+
c = name.find(")")
|
|
78
|
+
if o > 0 and c > 0:
|
|
79
|
+
params = name[o+1:c]
|
|
80
|
+
name = name[0:o]
|
|
81
|
+
for param in params.split(","):
|
|
82
|
+
pp = param.split("=")
|
|
83
|
+
if len(pp) is 2:
|
|
84
|
+
key = pp[0].strip()
|
|
85
|
+
value = pp[1].strip()
|
|
86
|
+
if key == "delay":
|
|
87
|
+
delay = int(value)
|
|
88
|
+
if key == "wait":
|
|
89
|
+
wait = value.lower() in ['true', '1', 't', 'y', 'yes', 'on']
|
|
90
|
+
return name, delay, wait
|
|
91
|
+
|
|
92
|
+
def execute_scene(self, scene):
|
|
93
|
+
|
|
94
|
+
scene_name, scene_delay, scene_wait = self.parse_stage(scene)
|
|
95
|
+
steps = list(self.script[scene].keys())
|
|
96
|
+
|
|
97
|
+
print(("EXECUTING SCENE '%s'" % scene_name) + (" WITH DELAY %ss" % scene_delay if scene_delay > 0 else "") + "...")
|
|
98
|
+
|
|
99
|
+
if scene_delay > 0:
|
|
100
|
+
time.sleep(scene_delay)
|
|
101
|
+
|
|
102
|
+
for step in steps:
|
|
103
|
+
step_name, step_delay, step_wait = self.parse_stage(step)
|
|
104
|
+
print(("STARTING STEP '%s'" % step_name) + (" IN BACKGROUND" if not step_wait else "") + (" WITH DELAY %ss" % step_delay if step_delay > 0 else "") + "...")
|
|
105
|
+
|
|
106
|
+
if step_delay > 0:
|
|
107
|
+
time.sleep(step_delay)
|
|
108
|
+
|
|
109
|
+
task_ids = []
|
|
110
|
+
handlers = self.script[scene][step].keys()
|
|
111
|
+
for handler in handlers:
|
|
112
|
+
if handler in self.tasks:
|
|
113
|
+
components = self.script[scene][step][handler].keys()
|
|
114
|
+
for component in components:
|
|
115
|
+
component_name, component_delay, component_wait = self.parse_stage(component)
|
|
116
|
+
goal = self.script[scene][step][handler][component]
|
|
117
|
+
|
|
118
|
+
if component_delay > 0:
|
|
119
|
+
time.sleep(component_delay)
|
|
120
|
+
|
|
121
|
+
remote_id = self.tasks[handler].trigger(Task.encode(handler, component_name, goal))
|
|
122
|
+
if remote_id > 0:
|
|
123
|
+
if step_wait and component_wait:
|
|
124
|
+
task_ids.append(str(remote_id) + ":" + str(handler) + ":" + str(component_name) + ":" + str(goal))
|
|
125
|
+
else:
|
|
126
|
+
# MOVE TO DEBUG AFTER FINISHING THIS FEATURE
|
|
127
|
+
self.logger.info("Component '%s' on handler '%s' for step '%s' TRIGGERED TO RUN IN BACKGROUND." % (component_name, handler, step_name))
|
|
128
|
+
else:
|
|
129
|
+
self.logger.error("Component '%s' on handler '%s' for step '%s' COULD NOT BE TRIGGERED." % (component_name, handler, step_name))
|
|
130
|
+
else:
|
|
131
|
+
self.logger.error("No remote for handler '%s'. Step '%s' COULD NOT BE TRIGGERED." % (handler, step_name))
|
|
132
|
+
|
|
133
|
+
running = True
|
|
134
|
+
while running:
|
|
135
|
+
running = False
|
|
136
|
+
for task_id in task_ids:
|
|
137
|
+
components = task_id.split(":")
|
|
138
|
+
remote_id = int(components[0])
|
|
139
|
+
rid = components[1]
|
|
140
|
+
status = self.tasks[rid].wait(remote_id, task_id, timeout=100)
|
|
141
|
+
running = running or status <= State.ACCEPTED
|
|
142
|
+
time.sleep(.1)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def confirm_scene(self, scene):
|
|
146
|
+
print("\n----------------------------")
|
|
147
|
+
print("ABOUT TO EXECUTE SCENE '%s'" % scene)
|
|
148
|
+
|
|
149
|
+
steps = list(self.script[scene].keys())
|
|
150
|
+
|
|
151
|
+
for step in steps:
|
|
152
|
+
print("STEP: %s" % step)
|
|
153
|
+
handlers = self.script[scene][step].keys()
|
|
154
|
+
for handler in handlers:
|
|
155
|
+
components = self.script[scene][step][handler].keys()
|
|
156
|
+
for component in components:
|
|
157
|
+
goal = self.script[scene][step][handler][component]
|
|
158
|
+
print("\t%s [%s]: -> %s" % (handler, component, goal))
|
|
159
|
+
|
|
160
|
+
if self.lastScene == None:
|
|
161
|
+
print("PRESS <ENTER> TO CONFIRM or ANY OTHER KEY TO SKIP")
|
|
162
|
+
return self.get_key()
|
|
163
|
+
else:
|
|
164
|
+
print("PRESS <ENTER> TO CONFIRM, PRESS <R> TO REPLAY LAST SCENE OR ANY OTHER KEY TO SKIP")
|
|
165
|
+
return self.get_key()
|
|
166
|
+
|
|
167
|
+
def get_key(self):
|
|
168
|
+
first_char = self.getch()
|
|
169
|
+
# The idea would be to allow further decomposition of the getch e.g. if arrows keys are pressed
|
|
170
|
+
return first_char
|
|
171
|
+
|
|
172
|
+
def execute(self):
|
|
173
|
+
try:
|
|
174
|
+
for scene in self.script.keys():
|
|
175
|
+
repeat = True
|
|
176
|
+
while repeat:
|
|
177
|
+
keyPressed = self.confirm_scene(scene)
|
|
178
|
+
if keyPressed == "\r" or keyPressed == b"\r":
|
|
179
|
+
self.execute_scene(scene)
|
|
180
|
+
self.lastScene = scene
|
|
181
|
+
repeat = False
|
|
182
|
+
elif self.lastScene != None and (keyPressed == "r" or keyPressed == "R" or keyPressed == b"r" or keyPressed == b"R"):
|
|
183
|
+
print("\n Repeating Scene '%s" % (self.lastScene))
|
|
184
|
+
self.execute_scene(self.lastScene)
|
|
185
|
+
else:
|
|
186
|
+
print("\nSKIPPING SCENE '%s'" % (scene))
|
|
187
|
+
repeat = False
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.logger.error(e)
|
|
191
|
+
exit(1)
|
|
192
|
+
except KeyboardInterrupt:
|
|
193
|
+
self.logger.error("Interrupted, exiting.")
|
|
194
|
+
exit(1)
|
|
195
|
+
|
|
196
|
+
def main_cli():
|
|
197
|
+
|
|
198
|
+
cfg_file = os.environ.get('XDG_CONFIG_HOME', os.environ.get('HOME', '/home/demo') + '/.config') + '/zmq-remotes.yaml'
|
|
199
|
+
|
|
200
|
+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
201
|
+
parser.add_argument("-c", "--config", type=str,
|
|
202
|
+
help="Configuration file location.", default=cfg_file)
|
|
203
|
+
parser.add_argument("-s", "--script", type=str,
|
|
204
|
+
help="Script file to execute.", required=True)
|
|
205
|
+
|
|
206
|
+
args, unknown = parser.parse_known_args()
|
|
207
|
+
run = ScriptExecutor(args.config, args.script)
|
|
208
|
+
run.execute()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
|
|
213
|
+
main_cli()
|
|
214
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ztl
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A thin library relying on zmq to dispatch tasks
|
|
5
|
+
Home-page: https://gitlab.com/robothouse/rh-user/ztl/
|
|
6
|
+
Author: Patrick Holthaus
|
|
7
|
+
Author-email: Patrick Holthaus <patrick.holthaus@googlemail.com>
|
|
8
|
+
License: BSD 2-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2023, UH Robot House / User Projects
|
|
11
|
+
All rights reserved.
|
|
12
|
+
|
|
13
|
+
Redistribution and use in source and binary forms, with or without
|
|
14
|
+
modification, are permitted provided that the following conditions are met:
|
|
15
|
+
|
|
16
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
17
|
+
list of conditions and the following disclaimer.
|
|
18
|
+
|
|
19
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
20
|
+
this list of conditions and the following disclaimer in the documentation
|
|
21
|
+
and/or other materials provided with the distribution.
|
|
22
|
+
|
|
23
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
24
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
25
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
26
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
27
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
28
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
29
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
30
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
31
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
32
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, https://gitlab.com/robothouse/rh-user/ztl/
|
|
35
|
+
Project-URL: Bug Tracker, https://gitlab.com/robothouse/rh-user/ztl/issues/
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Requires-Python: >=3.7
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Requires-Dist: oyaml
|
|
43
|
+
Requires-Dist: pyzmq
|
|
44
|
+
Requires-Dist: pytest
|
|
45
|
+
Requires-Dist: pytest-xprocess
|
|
46
|
+
|
|
47
|
+
This project contains the `ZTL` library enabling light-weight and widely compatible **remote task execution** using [ZeroMQ](https://zeromq.org/).
|
|
48
|
+
|
|
49
|
+
The basic communication principle is as follows:
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
Thereby, each task has the following lifecycle:
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
An example communication could look like this:
|
|
58
|
+
|
|
59
|
+
Request:
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
Reply:
|
|
64
|
+
|
|
65
|
+

|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/ztl/__init__.py
|
|
6
|
+
src/ztl.egg-info/PKG-INFO
|
|
7
|
+
src/ztl.egg-info/SOURCES.txt
|
|
8
|
+
src/ztl.egg-info/dependency_links.txt
|
|
9
|
+
src/ztl.egg-info/entry_points.txt
|
|
10
|
+
src/ztl.egg-info/requires.txt
|
|
11
|
+
src/ztl.egg-info/top_level.txt
|
|
12
|
+
src/ztl/core/__init__.py
|
|
13
|
+
src/ztl/core/client.py
|
|
14
|
+
src/ztl/core/protocol.py
|
|
15
|
+
src/ztl/core/server.py
|
|
16
|
+
src/ztl/core/task.py
|
|
17
|
+
src/ztl/example/__init__.py
|
|
18
|
+
src/ztl/example/simple_client.py
|
|
19
|
+
src/ztl/example/simple_server.py
|
|
20
|
+
src/ztl/script/__init__.py
|
|
21
|
+
src/ztl/script/run_script.py
|
|
22
|
+
test/test_installation.py
|
|
23
|
+
test/test_instances.py
|
|
24
|
+
test/test_lifecycle.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ztl
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
class TestUnitTests(TestCase):
|
|
4
|
+
|
|
5
|
+
def test_unit_testing(self):
|
|
6
|
+
self.assertTrue(True)
|
|
7
|
+
|
|
8
|
+
class TestImports(TestCase):
|
|
9
|
+
|
|
10
|
+
def test_zmq_import(self):
|
|
11
|
+
import zmq
|
|
12
|
+
|
|
13
|
+
def test_ztl_protocol_imports(self):
|
|
14
|
+
from ztl.core.protocol import Message, Request, State, Task
|
|
15
|
+
|
|
16
|
+
def test_ztl_server_imports(self):
|
|
17
|
+
from ztl.core.server import TaskServer
|
|
18
|
+
|
|
19
|
+
def test_ztl_client_imports(self):
|
|
20
|
+
from ztl.core.client import RemoteTask
|
|
21
|
+
|
|
22
|
+
def test_ztl_task_imports(self):
|
|
23
|
+
from ztl.core.task import ExecutableTask, TaskController, TaskExecutor
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from ztl.core.client import RemoteTask
|
|
6
|
+
from ztl.core.server import TaskServer
|
|
7
|
+
|
|
8
|
+
class TestConstruction(TestCase):
|
|
9
|
+
|
|
10
|
+
def test_server_construction(self):
|
|
11
|
+
server = TaskServer(6666)
|
|
12
|
+
server.register("/dummy", None)
|
|
13
|
+
|
|
14
|
+
def test_client_construction(self):
|
|
15
|
+
RemoteTask("localhost", 6666, "/dummy")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
from ztl.core.client import RemoteTask
|
|
6
|
+
from ztl.core.server import TaskServer
|
|
7
|
+
from ztl.core.protocol import State
|
|
8
|
+
|
|
9
|
+
@pytest.mark.usefixtures("ztl_server")
|
|
10
|
+
class TestLocalLife(TestCase):
|
|
11
|
+
|
|
12
|
+
def test_no_controller(self):
|
|
13
|
+
host = "localhost"
|
|
14
|
+
scope = "/no"
|
|
15
|
+
payload = "does not matter"
|
|
16
|
+
|
|
17
|
+
task = RemoteTask("localhost", 7777, scope)
|
|
18
|
+
mid = task.trigger(payload)
|
|
19
|
+
|
|
20
|
+
assert mid < 0
|
|
21
|
+
|
|
22
|
+
def test_none_controller(self):
|
|
23
|
+
host = "localhost"
|
|
24
|
+
scope = "/none"
|
|
25
|
+
payload = "does not matter"
|
|
26
|
+
|
|
27
|
+
task = RemoteTask("localhost", 7777, scope)
|
|
28
|
+
mid = task.trigger(payload)
|
|
29
|
+
|
|
30
|
+
assert mid < 0
|
|
31
|
+
|
|
32
|
+
@pytest.mark.usefixtures("ztl_simple_server")
|
|
33
|
+
class TestLifeCycle(TestCase):
|
|
34
|
+
|
|
35
|
+
def test_reject(self):
|
|
36
|
+
host = "localhost"
|
|
37
|
+
scope = "/test"
|
|
38
|
+
payload = "illegal payload"
|
|
39
|
+
|
|
40
|
+
task = RemoteTask("localhost", 5555, scope)
|
|
41
|
+
mid = task.trigger(payload)
|
|
42
|
+
|
|
43
|
+
assert mid < 0
|
|
44
|
+
|
|
45
|
+
def test_abort(self):
|
|
46
|
+
host = "localhost"
|
|
47
|
+
scope = "/test"
|
|
48
|
+
payload = 5
|
|
49
|
+
|
|
50
|
+
task = RemoteTask("localhost", 5555, scope)
|
|
51
|
+
mid = task.trigger(payload)
|
|
52
|
+
|
|
53
|
+
assert mid > 0
|
|
54
|
+
|
|
55
|
+
state = task.wait(mid, .1)
|
|
56
|
+
assert state == State.ACCEPTED
|
|
57
|
+
|
|
58
|
+
state = task.abort(mid)
|
|
59
|
+
assert state == State.ABORTED
|
|
60
|
+
|
|
61
|
+
def test_completion(self):
|
|
62
|
+
host = "localhost"
|
|
63
|
+
scope = "/test"
|
|
64
|
+
payload = 5
|
|
65
|
+
|
|
66
|
+
task = RemoteTask("localhost", 5555, scope)
|
|
67
|
+
mid = task.trigger(payload)
|
|
68
|
+
|
|
69
|
+
assert mid > 0
|
|
70
|
+
|
|
71
|
+
state = task.wait(mid, .1)
|
|
72
|
+
assert state == State.ACCEPTED
|
|
73
|
+
|
|
74
|
+
state = task.wait(mid, 3.1)
|
|
75
|
+
assert state == State.COMPLETED
|