lima2.client 2.2.0__tar.gz → 2.2.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.
- lima2_client-2.2.1/PKG-INFO +92 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/README.md +5 -6
- lima2_client-2.2.1/pyproject.toml +72 -0
- lima2_client-2.2.1/setup.cfg +4 -0
- lima2_client-2.2.1/src/finite_state_machine/__init__.py +6 -0
- lima2_client-2.2.1/src/finite_state_machine/draw_state_diagram.py +109 -0
- lima2_client-2.2.1/src/finite_state_machine/examples/async_github_pull_request.py +46 -0
- lima2_client-2.2.1/src/finite_state_machine/examples/async_turnstile.py +21 -0
- lima2_client-2.2.1/src/finite_state_machine/examples/boolean_field.py +22 -0
- lima2_client-2.2.1/src/finite_state_machine/examples/github_pull_request.py +30 -0
- lima2_client-2.2.1/src/finite_state_machine/examples/turnstile.py +17 -0
- lima2_client-2.2.1/src/finite_state_machine/exceptions.py +13 -0
- lima2_client-2.2.1/src/finite_state_machine/state_machine.py +151 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/__init__.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/client.py +7 -4
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/convert.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/detector.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/__init__.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/dense_frame.py +2 -6
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/exception.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/smx_sparse_frame.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/sparse_frame.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/devencoded/structured_array.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/pipeline.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/pipelines/__init__.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/pipelines/legacy.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/pipelines/smx.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/pipelines/xpcs.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/progress_counter.py +0 -2
- lima2_client-2.2.1/src/lima2/client/py.typed +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/shell.py +27 -14
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/state_machine.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/topology.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/client/utils.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/master_file/generate.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/server/__init__.py +2 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/server/control.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/server/receiver.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/src/lima2/server/timer.py +0 -2
- lima2_client-2.2.1/src/lima2_client.egg-info/PKG-INFO +92 -0
- {lima2_client-2.2.0/src/lima2.client.egg-info → lima2_client-2.2.1/src/lima2_client.egg-info}/SOURCES.txt +16 -7
- lima2_client-2.2.1/src/lima2_client.egg-info/entry_points.txt +2 -0
- lima2_client-2.2.1/src/lima2_client.egg-info/requires.txt +29 -0
- lima2_client-2.2.1/src/lima2_client.egg-info/top_level.txt +2 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_detector.py +0 -2
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_state_machine.py +1 -3
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_timer.py +1 -3
- lima2_client-2.2.0/PKG-INFO +0 -68
- lima2_client-2.2.0/pyproject.toml +0 -6
- lima2_client-2.2.0/setup.cfg +0 -54
- lima2_client-2.2.0/src/lima2.client.egg-info/PKG-INFO +0 -68
- lima2_client-2.2.0/src/lima2.client.egg-info/entry_points.txt +0 -2
- lima2_client-2.2.0/src/lima2.client.egg-info/requires.txt +0 -13
- lima2_client-2.2.0/src/lima2.client.egg-info/top_level.txt +0 -1
- {lima2_client-2.2.0 → lima2_client-2.2.1}/LICENSE +0 -0
- /lima2_client-2.2.0/src/lima2/client/py.typed → /lima2_client-2.2.1/src/finite_state_machine/examples/__init__.py +0 -0
- {lima2_client-2.2.0/src/lima2.client.egg-info → lima2_client-2.2.1/src/lima2_client.egg-info}/dependency_links.txt +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_client.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_convert.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_dense_frame.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_pipeline.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_pipelines.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_progress_counter.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_sparse_frame.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_topology.py +0 -0
- {lima2_client-2.2.0 → lima2_client-2.2.1}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lima2-client
|
|
3
|
+
Version: 2.2.1
|
|
4
|
+
Summary: Python client library for Lima2 detector
|
|
5
|
+
Author: BCU (ESRF)
|
|
6
|
+
License: Copyright (c) 2020-2024 Beamline Control Unit, ESRF
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
|
|
14
|
+
Project-URL: Repository, https://gitlab.esrf.fr/limagroup/lima2-client
|
|
15
|
+
Project-URL: Documentation, https://limagroup.gitlab-pages.esrf.fr/lima2-client/
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pytango
|
|
23
|
+
Requires-Dist: gevent
|
|
24
|
+
Requires-Dist: jsonschema
|
|
25
|
+
Requires-Dist: jsonschema-default
|
|
26
|
+
Requires-Dist: beartype
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: pyyaml
|
|
29
|
+
Requires-Dist: typing-extensions
|
|
30
|
+
Requires-Dist: h5py
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: black==22.3; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Requires-Dist: types-pyyaml; extra == "dev"
|
|
35
|
+
Requires-Dist: types-jsonschema; extra == "dev"
|
|
36
|
+
Requires-Dist: flake8; extra == "dev"
|
|
37
|
+
Requires-Dist: ipython; extra == "dev"
|
|
38
|
+
Requires-Dist: pytango-db; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-forked; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
45
|
+
Requires-Dist: sphinxcontrib-mermaid; extra == "docs"
|
|
46
|
+
Requires-Dist: sphinxcontrib-napoleon; extra == "docs"
|
|
47
|
+
Requires-Dist: sphinx-immaterial; extra == "docs"
|
|
48
|
+
Requires-Dist: recommonmark; extra == "docs"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# LImA 2 Client library
|
|
52
|
+
|
|
53
|
+
This project provides a Python interface to the LImA 2 distributed system.
|
|
54
|
+
|
|
55
|
+
Checkout the [documentation here](https://limagroup.gitlab-pages.esrf.fr/lima2-client/).
|
|
56
|
+
|
|
57
|
+
### Development setup
|
|
58
|
+
|
|
59
|
+
In order to install lima2-client in a new conda environment:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
conda create -n l2c -c esrf-bcu python==3.10 pytango pytango-db
|
|
63
|
+
pip install .[dev]
|
|
64
|
+
```
|
|
65
|
+
The `dev` extra dependency adds linting, formatting, static analysis and testing tools.
|
|
66
|
+
It also installs `ipython` required to run the `lima2_shell`.
|
|
67
|
+
|
|
68
|
+
### Quickstart with blissdemo
|
|
69
|
+
|
|
70
|
+
With `bliss-demo-servers` running, you can run an interactive Lima2 client shell to manipulate the
|
|
71
|
+
Lima2 simulator devices and test the client API:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
export TANGO_HOST="localhost:10000" # Assuming you are running bliss-demo-servers locally
|
|
75
|
+
lima2_shell
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `lima2_shell` utility instantiates a `Client` object using config from `l2c_config.yaml`,
|
|
79
|
+
creates a set of default control, acquisition and processing parameters, and starts an ipython session.
|
|
80
|
+
|
|
81
|
+
### Bootstrapping the documentation
|
|
82
|
+
|
|
83
|
+
The source for the documentation is in the `docs` folder. Here are the instructions to built and read it locally. The documentation is built with [Doxygen](http://www.doxygen.org/) and [Sphinx](http://www.sphinx-doc.org). The sphinx template is from [Sphinx Material](https://bashtage.github.io/sphinx-material/).
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
conda create -n doc --file docs/requirements.txt -c conda-forge
|
|
87
|
+
conda activate doc
|
|
88
|
+
cd docs
|
|
89
|
+
make html
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The html documentation is generated in `docs/.build/html`.
|
|
@@ -7,14 +7,13 @@ Checkout the [documentation here](https://limagroup.gitlab-pages.esrf.fr/lima2-c
|
|
|
7
7
|
### Development setup
|
|
8
8
|
|
|
9
9
|
In order to install lima2-client in a new conda environment:
|
|
10
|
+
|
|
10
11
|
```sh
|
|
11
|
-
conda create -n l2c -c esrf-bcu python pytango
|
|
12
|
-
pip install .
|
|
13
|
-
```
|
|
14
|
-
and, if you need the lima2-client shell:
|
|
15
|
-
```sh
|
|
16
|
-
pip install .[shell]
|
|
12
|
+
conda create -n l2c -c esrf-bcu python==3.10 pytango pytango-db
|
|
13
|
+
pip install .[dev]
|
|
17
14
|
```
|
|
15
|
+
The `dev` extra dependency adds linting, formatting, static analysis and testing tools.
|
|
16
|
+
It also installs `ipython` required to run the `lima2_shell`.
|
|
18
17
|
|
|
19
18
|
### Quickstart with blissdemo
|
|
20
19
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools_scm]
|
|
6
|
+
version_scheme = "guess-next-dev"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "lima2-client"
|
|
10
|
+
requires-python = ">= 3.9"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
authors = [{ name = "BCU (ESRF)" }]
|
|
13
|
+
description = "Python client library for Lima2 detector"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
license = { file = "LICENSE" }
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pytango",
|
|
23
|
+
"gevent",
|
|
24
|
+
"jsonschema",
|
|
25
|
+
"jsonschema-default",
|
|
26
|
+
"beartype",
|
|
27
|
+
"numpy",
|
|
28
|
+
"pyyaml",
|
|
29
|
+
"typing-extensions",
|
|
30
|
+
"h5py",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Repository = "https://gitlab.esrf.fr/limagroup/lima2-client"
|
|
35
|
+
Documentation = "https://limagroup.gitlab-pages.esrf.fr/lima2-client/"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
lima2_shell = "lima2.client.shell:main [dev]"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"black==22.3",
|
|
43
|
+
"mypy",
|
|
44
|
+
"types-pyyaml",
|
|
45
|
+
"types-jsonschema",
|
|
46
|
+
"flake8",
|
|
47
|
+
"ipython",
|
|
48
|
+
"pytango-db",
|
|
49
|
+
"pytest",
|
|
50
|
+
"pytest-forked",
|
|
51
|
+
"pytest-asyncio",
|
|
52
|
+
"pytest-cov",
|
|
53
|
+
]
|
|
54
|
+
docs = [
|
|
55
|
+
"sphinx",
|
|
56
|
+
"sphinxcontrib-mermaid",
|
|
57
|
+
"sphinxcontrib-napoleon",
|
|
58
|
+
"sphinx-immaterial",
|
|
59
|
+
"recommonmark",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
addopts = "-v"
|
|
64
|
+
testpaths = "tests"
|
|
65
|
+
required_plugins = [
|
|
66
|
+
"pytest-cov>=6.0.0",
|
|
67
|
+
"pytest-forked>=1.6.0",
|
|
68
|
+
"pytest-asyncio>=0.25.3",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.run]
|
|
72
|
+
omit = ["src/lima2/client/shell.py"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from finite_state_machine.state_machine import TransitionDetails
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def import_state_machine_class(path): # pragma: no cover
|
|
12
|
+
"""Load State Machine workflow class
|
|
13
|
+
|
|
14
|
+
Copied from https://packaging.python.org/specifications/entry-points/#data-model
|
|
15
|
+
"""
|
|
16
|
+
modname, qualname_separator, qualname = path.partition(":")
|
|
17
|
+
obj = importlib.import_module(modname)
|
|
18
|
+
if qualname_separator:
|
|
19
|
+
for attr in qualname.split("."):
|
|
20
|
+
obj = getattr(obj, attr)
|
|
21
|
+
return obj
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_state_diagram_markdown(cls, initial_state):
|
|
25
|
+
"""Create State Diagram in Mermaid Markdown
|
|
26
|
+
|
|
27
|
+
https://mermaid-js.github.io/mermaid/diagrams-and-syntax-and-examples/stateDiagram.html
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
class_fns = inspect.getmembers(cls, predicate=inspect.isfunction)
|
|
31
|
+
state_transitions: List[TransitionDetails] = [
|
|
32
|
+
func._fsm for name, func in class_fns if hasattr(func, "_fsm")
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
transition_template = " {source} --> {target} : {name}\n"
|
|
36
|
+
|
|
37
|
+
all_state_transitions = []
|
|
38
|
+
for state_transition in state_transitions:
|
|
39
|
+
if isinstance(state_transition.source, list):
|
|
40
|
+
for src in state_transition.source:
|
|
41
|
+
t = transition_template.format(
|
|
42
|
+
source=src,
|
|
43
|
+
target=state_transition.target,
|
|
44
|
+
name=state_transition.name,
|
|
45
|
+
)
|
|
46
|
+
all_state_transitions.append(t)
|
|
47
|
+
|
|
48
|
+
if state_transition.on_error:
|
|
49
|
+
t = transition_template.format(
|
|
50
|
+
source=src, target=state_transition.on_error, name="on_error"
|
|
51
|
+
)
|
|
52
|
+
all_state_transitions.append(t)
|
|
53
|
+
else:
|
|
54
|
+
t = transition_template.format(
|
|
55
|
+
source=state_transition.source,
|
|
56
|
+
target=state_transition.target,
|
|
57
|
+
name=state_transition.name,
|
|
58
|
+
)
|
|
59
|
+
all_state_transitions.append(t)
|
|
60
|
+
|
|
61
|
+
if state_transition.on_error:
|
|
62
|
+
t = transition_template.format(
|
|
63
|
+
source=state_transition.source,
|
|
64
|
+
target=state_transition.on_error,
|
|
65
|
+
name="on_error",
|
|
66
|
+
)
|
|
67
|
+
all_state_transitions.append(t)
|
|
68
|
+
|
|
69
|
+
mermaid_markdown = "stateDiagram-v2\n"
|
|
70
|
+
if initial_state:
|
|
71
|
+
mermaid_markdown += f" [*] --> {initial_state}\n"
|
|
72
|
+
for t in all_state_transitions:
|
|
73
|
+
mermaid_markdown += t
|
|
74
|
+
|
|
75
|
+
return mermaid_markdown
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_args(): # pragma: no cover
|
|
79
|
+
description = "Create State Diagram for a State Machine"
|
|
80
|
+
parser = argparse.ArgumentParser(description=description)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--class",
|
|
83
|
+
type=str,
|
|
84
|
+
help="Path to State Machine class, e.g. importable.module:class",
|
|
85
|
+
required=True,
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--initial_state",
|
|
89
|
+
type=str,
|
|
90
|
+
help="Initial state of State Machine",
|
|
91
|
+
required=False,
|
|
92
|
+
)
|
|
93
|
+
return vars(parser.parse_args())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(): # pragma: no cover
|
|
97
|
+
args = parse_args()
|
|
98
|
+
class_path = args["class"]
|
|
99
|
+
initial_state = args["initial_state"]
|
|
100
|
+
|
|
101
|
+
sys.path.append(os.getcwd())
|
|
102
|
+
class_obj = import_state_machine_class(class_path)
|
|
103
|
+
|
|
104
|
+
markdown = generate_state_diagram_markdown(class_obj, initial_state)
|
|
105
|
+
print(markdown)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from finite_state_machine import StateMachine, transition
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def async_is_approved_or_is_admin(machine, user):
|
|
5
|
+
return machine.num_approvals >= 1 or user.is_admin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sync_is_approved_or_is_admin(machine, user):
|
|
9
|
+
return machine.num_approvals >= 1 or user.is_admin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitHubPullRequest(StateMachine):
|
|
13
|
+
"""State Machine to represent a GitHub Pull Request workflow"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.state = "opened"
|
|
17
|
+
self.num_approvals = 0
|
|
18
|
+
self.changes_request = False
|
|
19
|
+
|
|
20
|
+
@transition(source="opened", target="opened")
|
|
21
|
+
async def approve(self):
|
|
22
|
+
self.num_approvals += 1
|
|
23
|
+
|
|
24
|
+
@transition(source="opened", target="opened")
|
|
25
|
+
async def request_changes(self):
|
|
26
|
+
self.changes_request = True
|
|
27
|
+
|
|
28
|
+
@transition(source="opened", target="closed")
|
|
29
|
+
async def close_pull_request(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@transition(
|
|
33
|
+
source="opened",
|
|
34
|
+
target="merged",
|
|
35
|
+
conditions=[async_is_approved_or_is_admin],
|
|
36
|
+
)
|
|
37
|
+
async def fully_async_merge_pull_request(self, user):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@transition(
|
|
41
|
+
source="opened",
|
|
42
|
+
target="merged",
|
|
43
|
+
conditions=[sync_is_approved_or_is_admin],
|
|
44
|
+
)
|
|
45
|
+
async def async_merge_pull_request_with_sync_condition(self, user):
|
|
46
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from finite_state_machine import StateMachine, transition
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Turnstile(StateMachine):
|
|
5
|
+
initial_state = "close"
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.state = self.initial_state
|
|
9
|
+
super().__init__()
|
|
10
|
+
|
|
11
|
+
@transition(source=["close", "open"], target="open", on_error="error_state")
|
|
12
|
+
async def insert_coin(self):
|
|
13
|
+
return "inserted coin"
|
|
14
|
+
|
|
15
|
+
@transition(source="open", target="close")
|
|
16
|
+
async def pass_thru(self):
|
|
17
|
+
return "passed thru"
|
|
18
|
+
|
|
19
|
+
@transition(source="close", target="close", on_error="error_state")
|
|
20
|
+
async def error_function(self):
|
|
21
|
+
raise ValueError
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from finite_state_machine import StateMachine, transition
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def account_in_good_standing(self):
|
|
5
|
+
return len(self.account.bills_outstanding) == 0
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnableFeatureStateMachine(StateMachine):
|
|
9
|
+
"""Example state machine around a feature flag field"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, account):
|
|
12
|
+
self.account = account
|
|
13
|
+
self.state = account.feature["enabled"]
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
@transition(source=False, target=True, conditions=[account_in_good_standing])
|
|
17
|
+
def enable_feature(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@transition(source=True, target=False)
|
|
21
|
+
def disable_feature(self):
|
|
22
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from finite_state_machine import StateMachine, transition
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_approved_or_is_admin(machine, user):
|
|
5
|
+
return machine.num_approvals >= 1 or user.is_admin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GitHubPullRequest(StateMachine):
|
|
9
|
+
"""State Machine to represent a GitHub Pull Request workflow"""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.state = "opened"
|
|
13
|
+
self.num_approvals = 0
|
|
14
|
+
self.changes_request = False
|
|
15
|
+
|
|
16
|
+
@transition(source="opened", target="opened")
|
|
17
|
+
def approve(self):
|
|
18
|
+
self.num_approvals += 1
|
|
19
|
+
|
|
20
|
+
@transition(source="opened", target="opened")
|
|
21
|
+
def request_changes(self):
|
|
22
|
+
self.changes_request = True
|
|
23
|
+
|
|
24
|
+
@transition(source="opened", target="closed")
|
|
25
|
+
def close_pull_request(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@transition(source="opened", target="merged", conditions=[is_approved_or_is_admin])
|
|
29
|
+
def merge_pull_request(self, user):
|
|
30
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from finite_state_machine import StateMachine, transition
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Turnstile(StateMachine):
|
|
5
|
+
initial_state = "close"
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.state = self.initial_state
|
|
9
|
+
super().__init__()
|
|
10
|
+
|
|
11
|
+
@transition(source=["close", "open"], target="open")
|
|
12
|
+
def insert_coin(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@transition(source="open", target="close")
|
|
16
|
+
def pass_thru(self):
|
|
17
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class TransitionNotAllowed(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InvalidStartState(TransitionNotAllowed):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConditionsNotMet(TransitionNotAllowed):
|
|
10
|
+
def __init__(self, conditions):
|
|
11
|
+
conditions_not_met = ", ".join(condition.__name__ for condition in conditions)
|
|
12
|
+
message = f"Following conditions did not return True: {conditions_not_met}"
|
|
13
|
+
super().__init__(message)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from enum import Enum
|
|
3
|
+
import functools
|
|
4
|
+
import types
|
|
5
|
+
from typing import NamedTuple, Union
|
|
6
|
+
|
|
7
|
+
from .exceptions import ConditionsNotMet, InvalidStartState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StateMachine:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
try:
|
|
13
|
+
self.state
|
|
14
|
+
except AttributeError:
|
|
15
|
+
raise ValueError("Need to set a state instance variable")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransitionDetails(NamedTuple):
|
|
19
|
+
name: str
|
|
20
|
+
source: Union[list, bool, int, str]
|
|
21
|
+
target: Union[bool, int, str]
|
|
22
|
+
conditions: list
|
|
23
|
+
on_error: Union[bool, int, str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class transition:
|
|
27
|
+
def __init__(self, source, target, conditions=None, on_error=None, reraise=False):
|
|
28
|
+
allowed_types = (str, bool, int, Enum)
|
|
29
|
+
|
|
30
|
+
if isinstance(source, allowed_types):
|
|
31
|
+
source = [source]
|
|
32
|
+
if not isinstance(source, list):
|
|
33
|
+
raise ValueError("Source can be a bool, int, string, Enum, or list")
|
|
34
|
+
for item in source:
|
|
35
|
+
if not isinstance(item, allowed_types):
|
|
36
|
+
raise ValueError("Source can be a bool, int, string, Enum, or list")
|
|
37
|
+
self.source = source
|
|
38
|
+
|
|
39
|
+
if not isinstance(target, allowed_types):
|
|
40
|
+
raise ValueError("Target needs to be a bool, int or string")
|
|
41
|
+
self.target = target
|
|
42
|
+
|
|
43
|
+
if not conditions:
|
|
44
|
+
conditions = []
|
|
45
|
+
if not isinstance(conditions, list):
|
|
46
|
+
raise ValueError("conditions must be a list")
|
|
47
|
+
for condition in conditions:
|
|
48
|
+
if not isinstance(condition, types.FunctionType):
|
|
49
|
+
raise ValueError("conditions list must contain functions")
|
|
50
|
+
self.conditions = conditions
|
|
51
|
+
|
|
52
|
+
if on_error:
|
|
53
|
+
if not isinstance(on_error, allowed_types):
|
|
54
|
+
raise ValueError("on_error needs to be a bool, int or string")
|
|
55
|
+
self.on_error = on_error
|
|
56
|
+
|
|
57
|
+
self.reraise = reraise
|
|
58
|
+
|
|
59
|
+
def __call__(self, func):
|
|
60
|
+
func._fsm = TransitionDetails(
|
|
61
|
+
func.__name__,
|
|
62
|
+
self.source,
|
|
63
|
+
self.target,
|
|
64
|
+
self.conditions,
|
|
65
|
+
self.on_error,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@functools.wraps(func)
|
|
69
|
+
def sync_callable(*args, **kwargs):
|
|
70
|
+
try:
|
|
71
|
+
state_machine, rest = args
|
|
72
|
+
except ValueError:
|
|
73
|
+
state_machine = args[0]
|
|
74
|
+
|
|
75
|
+
if state_machine.state not in self.source:
|
|
76
|
+
exception_message = (
|
|
77
|
+
f"Current state is {state_machine.state}. "
|
|
78
|
+
f"{func.__name__} allows transitions from {self.source}."
|
|
79
|
+
)
|
|
80
|
+
raise InvalidStartState(exception_message)
|
|
81
|
+
|
|
82
|
+
conditions_not_met = []
|
|
83
|
+
for condition in self.conditions:
|
|
84
|
+
if condition(*args, **kwargs) is not True:
|
|
85
|
+
conditions_not_met.append(condition)
|
|
86
|
+
if conditions_not_met:
|
|
87
|
+
raise ConditionsNotMet(conditions_not_met)
|
|
88
|
+
|
|
89
|
+
if not self.on_error:
|
|
90
|
+
result = func(*args, **kwargs)
|
|
91
|
+
state_machine.state = self.target
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
result = func(*args, **kwargs)
|
|
96
|
+
state_machine.state = self.target
|
|
97
|
+
return result
|
|
98
|
+
except Exception:
|
|
99
|
+
# TODO should we log this somewhere?
|
|
100
|
+
# logger.error? maybe have an optional parameter to set this up
|
|
101
|
+
# how to libraries log?
|
|
102
|
+
state_machine.state = self.on_error
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
@functools.wraps(func)
|
|
106
|
+
async def async_callable(*args, **kwargs):
|
|
107
|
+
try:
|
|
108
|
+
state_machine, rest = args
|
|
109
|
+
except ValueError:
|
|
110
|
+
state_machine = args[0]
|
|
111
|
+
|
|
112
|
+
if state_machine.state not in self.source:
|
|
113
|
+
exception_message = (
|
|
114
|
+
f"Current state is {state_machine.state}. "
|
|
115
|
+
f"{func.__name__} allows transitions from {self.source}."
|
|
116
|
+
)
|
|
117
|
+
raise InvalidStartState(exception_message)
|
|
118
|
+
|
|
119
|
+
conditions_not_met = []
|
|
120
|
+
for condition in self.conditions:
|
|
121
|
+
if asyncio.iscoroutinefunction(condition):
|
|
122
|
+
condition_result = await condition(*args, **kwargs)
|
|
123
|
+
else:
|
|
124
|
+
condition_result = condition(*args, **kwargs)
|
|
125
|
+
if condition_result is not True:
|
|
126
|
+
conditions_not_met.append(condition)
|
|
127
|
+
if conditions_not_met:
|
|
128
|
+
raise ConditionsNotMet(conditions_not_met)
|
|
129
|
+
|
|
130
|
+
if not self.on_error:
|
|
131
|
+
result = await func(*args, **kwargs)
|
|
132
|
+
state_machine.state = self.target
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
result = await func(*args, **kwargs)
|
|
137
|
+
state_machine.state = self.target
|
|
138
|
+
return result
|
|
139
|
+
except Exception as e:
|
|
140
|
+
# TODO should we log this somewhere?
|
|
141
|
+
# logger.error? maybe have an optional parameter to set this up
|
|
142
|
+
# how to libraries log?
|
|
143
|
+
state_machine.state = self.on_error
|
|
144
|
+
if self.reraise:
|
|
145
|
+
raise e
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if asyncio.iscoroutinefunction(func):
|
|
149
|
+
return async_callable
|
|
150
|
+
else:
|
|
151
|
+
return sync_callable
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
1
|
# This file is part of the Lima2 project
|
|
4
2
|
#
|
|
5
3
|
# Copyright (c) 2020-2024 Beamline Control Unit, ESRF
|
|
@@ -241,10 +239,15 @@ class Client:
|
|
|
241
239
|
# Select the pipeline according to the Tango class (Homogeneous processing for now)
|
|
242
240
|
pipeline_class = pipelines.get_class(tango_class_name=class_names[0])
|
|
243
241
|
|
|
244
|
-
# Note: sort list of pipeline instances by name such that pipelines are ordered
|
|
242
|
+
# Note: sort list of pipeline instances by id in name such that pipelines are ordered
|
|
245
243
|
# by receiver index. This is necessary e.g. in a strict round-robin topology to
|
|
246
244
|
# be able to find a frame by index.
|
|
247
|
-
|
|
245
|
+
def rcv_idx(name):
|
|
246
|
+
return int(name.split("@")[1])
|
|
247
|
+
|
|
248
|
+
proc_devs = [
|
|
249
|
+
tg.DeviceProxy(url) for url in sorted(list(instances), key=rcv_idx)
|
|
250
|
+
]
|
|
248
251
|
|
|
249
252
|
# Instantiate pipeline
|
|
250
253
|
self.__pipelines[uuid] = pipeline_class(
|