ewokscore 4.0.0__py3-none-any.whl
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.
- ewokscore/__init__.py +3 -0
- ewokscore/bindings.py +98 -0
- ewokscore/dynamictask.py +25 -0
- ewokscore/engine.py +92 -0
- ewokscore/engine_interface.py +79 -0
- ewokscore/entry_points.py +24 -0
- ewokscore/events/__init__.py +12 -0
- ewokscore/events/contexts.py +97 -0
- ewokscore/events/global_state.py +181 -0
- ewokscore/events/handlers/__init__.py +7 -0
- ewokscore/events/handlers/base.py +20 -0
- ewokscore/events/handlers/sqlite3.py +8 -0
- ewokscore/events/initialize_events.py +72 -0
- ewokscore/events/send_events.py +251 -0
- ewokscore/graph/__init__.py +2 -0
- ewokscore/graph/analysis.py +380 -0
- ewokscore/graph/compare.py +51 -0
- ewokscore/graph/error_handlers.py +55 -0
- ewokscore/graph/execute/__init__.py +0 -0
- ewokscore/graph/execute/sequential.py +151 -0
- ewokscore/graph/graph_io.py +209 -0
- ewokscore/graph/inputs.py +476 -0
- ewokscore/graph/models.py +7 -0
- ewokscore/graph/multigraph.py +24 -0
- ewokscore/graph/schema/__init__.py +109 -0
- ewokscore/graph/schema/metadata.py +27 -0
- ewokscore/graph/schema/update.py +11 -0
- ewokscore/graph/serialize.py +329 -0
- ewokscore/graph/subgraph.py +300 -0
- ewokscore/graph/taskgraph.py +251 -0
- ewokscore/graph/validate.py +52 -0
- ewokscore/hashing.py +293 -0
- ewokscore/inittask.py +272 -0
- ewokscore/methodtask.py +26 -0
- ewokscore/missing_data.py +21 -0
- ewokscore/model.py +9 -0
- ewokscore/node.py +67 -0
- ewokscore/notebooktask.py +96 -0
- ewokscore/persistence/__init__.py +5 -0
- ewokscore/persistence/atomic.py +71 -0
- ewokscore/persistence/file.py +171 -0
- ewokscore/persistence/json.py +31 -0
- ewokscore/persistence/nexus.py +78 -0
- ewokscore/persistence/proxy.py +214 -0
- ewokscore/ppftasks.py +57 -0
- ewokscore/progress.py +131 -0
- ewokscore/registration.py +67 -0
- ewokscore/scripttask.py +185 -0
- ewokscore/task.py +777 -0
- ewokscore/task_discovery.py +318 -0
- ewokscore/taskwithprogress.py +32 -0
- ewokscore/tests/__init__.py +0 -0
- ewokscore/tests/conftest.py +240 -0
- ewokscore/tests/discover/__init__.py +0 -0
- ewokscore/tests/discover/module1.py +49 -0
- ewokscore/tests/discover/module2.py +46 -0
- ewokscore/tests/examples/__init__.py +0 -0
- ewokscore/tests/examples/graphs/__init__.py +43 -0
- ewokscore/tests/examples/graphs/acyclic1.py +149 -0
- ewokscore/tests/examples/graphs/acyclic2.py +90 -0
- ewokscore/tests/examples/graphs/acyclic3.py +109 -0
- ewokscore/tests/examples/graphs/cyclic1.py +105 -0
- ewokscore/tests/examples/graphs/demo.py +135 -0
- ewokscore/tests/examples/graphs/empty.py +7 -0
- ewokscore/tests/examples/graphs/self_trigger.py +54 -0
- ewokscore/tests/examples/graphs/triangle1.py +54 -0
- ewokscore/tests/examples/loadtest/__init__.py +0 -0
- ewokscore/tests/examples/loadtest/graph.json +1 -0
- ewokscore/tests/examples/loadtest/subgraph.json +1 -0
- ewokscore/tests/examples/tasks/__init__.py +0 -0
- ewokscore/tests/examples/tasks/addfunc.py +3 -0
- ewokscore/tests/examples/tasks/condsumtask.py +9 -0
- ewokscore/tests/examples/tasks/errorsumtask.py +15 -0
- ewokscore/tests/examples/tasks/nooutputtask.py +14 -0
- ewokscore/tests/examples/tasks/notebooktask.ipynb +147 -0
- ewokscore/tests/examples/tasks/simplemethods.py +8 -0
- ewokscore/tests/examples/tasks/sumlist.py +28 -0
- ewokscore/tests/examples/tasks/sumtask.py +26 -0
- ewokscore/tests/test_default_error_handlers.py +264 -0
- ewokscore/tests/test_dynamic_tasks.py +30 -0
- ewokscore/tests/test_events.py +112 -0
- ewokscore/tests/test_examples.py +161 -0
- ewokscore/tests/test_execute_graph.py +223 -0
- ewokscore/tests/test_graph.py +90 -0
- ewokscore/tests/test_graph_inputs.py +308 -0
- ewokscore/tests/test_graph_io.py +191 -0
- ewokscore/tests/test_graph_schema.py +62 -0
- ewokscore/tests/test_graph_serialize.py +115 -0
- ewokscore/tests/test_graph_start_end_nodes.py +59 -0
- ewokscore/tests/test_hashing.py +162 -0
- ewokscore/tests/test_link_is_required.py +146 -0
- ewokscore/tests/test_method_task.py +68 -0
- ewokscore/tests/test_notebook_task.py +105 -0
- ewokscore/tests/test_persistence.py +112 -0
- ewokscore/tests/test_script_task.py +168 -0
- ewokscore/tests/test_sub_graph.py +261 -0
- ewokscore/tests/test_sub_graph_serialize.py +312 -0
- ewokscore/tests/test_task.py +184 -0
- ewokscore/tests/test_task_discovery.py +155 -0
- ewokscore/tests/test_task_input_model.py +309 -0
- ewokscore/tests/test_task_output_model.py +192 -0
- ewokscore/tests/test_task_progress.py +65 -0
- ewokscore/tests/test_variable.py +438 -0
- ewokscore/tests/test_variable_namespaces.py +69 -0
- ewokscore/tests/test_workflow_events.py +379 -0
- ewokscore/tests/utils/__init__.py +0 -0
- ewokscore/tests/utils/results.py +150 -0
- ewokscore/tests/utils/show.py +17 -0
- ewokscore/utils.py +35 -0
- ewokscore/variable.py +661 -0
- ewokscore-4.0.0.dist-info/METADATA +154 -0
- ewokscore-4.0.0.dist-info/RECORD +116 -0
- ewokscore-4.0.0.dist-info/WHEEL +5 -0
- ewokscore-4.0.0.dist-info/entry_points.txt +16 -0
- ewokscore-4.0.0.dist-info/licenses/LICENSE.md +20 -0
- ewokscore-4.0.0.dist-info/top_level.txt +1 -0
ewokscore/__init__.py
ADDED
ewokscore/bindings.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from .events import job_decorator as execute_graph_decorator
|
|
9
|
+
from .graph import TaskGraph
|
|
10
|
+
from .graph import load_graph as _load_graph
|
|
11
|
+
from .graph.execute import sequential
|
|
12
|
+
from .graph.graph_io import update_default_inputs
|
|
13
|
+
from .graph.serialize import GraphRepresentation
|
|
14
|
+
from .node import NodeIdType
|
|
15
|
+
from .task import Task
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"execute_graph",
|
|
19
|
+
"load_graph",
|
|
20
|
+
"save_graph",
|
|
21
|
+
"convert_graph",
|
|
22
|
+
"graph_is_supported",
|
|
23
|
+
"execute_graph_decorator",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_graph(
|
|
28
|
+
graph: Any,
|
|
29
|
+
inputs: Optional[List[dict]] = None,
|
|
30
|
+
representation: Optional[Union[GraphRepresentation, str]] = None,
|
|
31
|
+
root_dir: Optional[Union[str, Path]] = None,
|
|
32
|
+
root_module: Optional[str] = None,
|
|
33
|
+
) -> TaskGraph:
|
|
34
|
+
taskgraph = _load_graph(
|
|
35
|
+
source=graph,
|
|
36
|
+
representation=representation,
|
|
37
|
+
root_dir=root_dir,
|
|
38
|
+
root_module=root_module,
|
|
39
|
+
)
|
|
40
|
+
if inputs:
|
|
41
|
+
update_default_inputs(taskgraph.graph, inputs)
|
|
42
|
+
return taskgraph
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def save_graph(
|
|
46
|
+
graph: TaskGraph,
|
|
47
|
+
destination,
|
|
48
|
+
representation: Optional[Union[GraphRepresentation, str]] = None,
|
|
49
|
+
**save_options,
|
|
50
|
+
) -> Union[str, dict]:
|
|
51
|
+
return graph.dump(destination, representation=representation, **save_options)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def convert_graph(
|
|
55
|
+
source,
|
|
56
|
+
destination,
|
|
57
|
+
inputs: Optional[List[dict]] = None,
|
|
58
|
+
load_options: Optional[dict] = None,
|
|
59
|
+
save_options: Optional[dict] = None,
|
|
60
|
+
) -> Union[str, dict]:
|
|
61
|
+
if load_options is None:
|
|
62
|
+
load_options = dict()
|
|
63
|
+
if save_options is None:
|
|
64
|
+
save_options = dict()
|
|
65
|
+
graph = load_graph(source, inputs=inputs, **load_options)
|
|
66
|
+
return save_graph(graph, destination, **save_options)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@execute_graph_decorator(engine="core")
|
|
70
|
+
def execute_graph(
|
|
71
|
+
graph,
|
|
72
|
+
inputs: Optional[List[dict]] = None,
|
|
73
|
+
load_options: Optional[dict] = None,
|
|
74
|
+
varinfo: Optional[dict] = None,
|
|
75
|
+
execinfo: Optional[dict] = None,
|
|
76
|
+
task_options: Optional[dict] = None,
|
|
77
|
+
raise_on_error: Optional[bool] = True,
|
|
78
|
+
outputs: Optional[List[dict]] = None,
|
|
79
|
+
merge_outputs: Optional[bool] = True,
|
|
80
|
+
output_tasks: Optional[bool] = False,
|
|
81
|
+
) -> Union[Dict[NodeIdType, Task], Dict[str, Any]]:
|
|
82
|
+
if load_options is None:
|
|
83
|
+
load_options = dict()
|
|
84
|
+
taskgraph = load_graph(graph, inputs=inputs, **load_options)
|
|
85
|
+
return sequential.execute_graph(
|
|
86
|
+
taskgraph.graph,
|
|
87
|
+
varinfo=varinfo,
|
|
88
|
+
execinfo=execinfo,
|
|
89
|
+
task_options=task_options,
|
|
90
|
+
raise_on_error=raise_on_error,
|
|
91
|
+
outputs=outputs,
|
|
92
|
+
merge_outputs=merge_outputs,
|
|
93
|
+
output_tasks=output_tasks,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def graph_is_supported(graph: TaskGraph) -> bool:
|
|
98
|
+
return not graph.is_cyclic and not graph.has_conditional_links
|
ewokscore/dynamictask.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ewoksutils.import_utils import import_method
|
|
5
|
+
|
|
6
|
+
from .task import Task
|
|
7
|
+
|
|
8
|
+
_GENERATORS = dict()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_task_class_generator(generator_qualname: str) -> Callable[[str], Task]:
|
|
12
|
+
if not generator_qualname:
|
|
13
|
+
return Task.get_subclass
|
|
14
|
+
task_class_generator = _GENERATORS.get(generator_qualname, None)
|
|
15
|
+
if task_class_generator is not None:
|
|
16
|
+
return task_class_generator
|
|
17
|
+
task_class_generator = import_method(generator_qualname)
|
|
18
|
+
_GENERATORS[generator_qualname] = task_class_generator
|
|
19
|
+
return task_class_generator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_dynamically_task_class(
|
|
23
|
+
generator_qualname: Optional[str], registry_name: str
|
|
24
|
+
) -> Task:
|
|
25
|
+
return get_task_class_generator(generator_qualname)(registry_name)
|
ewokscore/engine.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from . import bindings
|
|
9
|
+
from .engine_interface import Path
|
|
10
|
+
from .engine_interface import RawExecInfoType
|
|
11
|
+
from .engine_interface import TaskGraph
|
|
12
|
+
from .engine_interface import WorkflowEngineWithSerialization
|
|
13
|
+
from .node import NodeIdType
|
|
14
|
+
from .task import Task
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoreWorkflowEngine(WorkflowEngineWithSerialization):
|
|
18
|
+
|
|
19
|
+
def execute_graph(
|
|
20
|
+
self,
|
|
21
|
+
graph: Any,
|
|
22
|
+
*,
|
|
23
|
+
inputs: Optional[List[dict]] = None,
|
|
24
|
+
load_options: Optional[dict] = None,
|
|
25
|
+
varinfo: Optional[dict] = None,
|
|
26
|
+
execinfo: RawExecInfoType = None,
|
|
27
|
+
task_options: Optional[dict] = None,
|
|
28
|
+
outputs: Optional[List[dict]] = None,
|
|
29
|
+
merge_outputs: Optional[bool] = True,
|
|
30
|
+
# Engine specific:
|
|
31
|
+
output_tasks: Optional[bool] = False,
|
|
32
|
+
raise_on_error: Optional[bool] = True,
|
|
33
|
+
) -> Union[Dict[NodeIdType, Task], Dict[str, Any]]:
|
|
34
|
+
return bindings.execute_graph(
|
|
35
|
+
graph,
|
|
36
|
+
inputs=inputs,
|
|
37
|
+
load_options=load_options,
|
|
38
|
+
varinfo=varinfo,
|
|
39
|
+
execinfo=execinfo,
|
|
40
|
+
task_options=task_options,
|
|
41
|
+
raise_on_error=raise_on_error,
|
|
42
|
+
outputs=outputs,
|
|
43
|
+
merge_outputs=merge_outputs,
|
|
44
|
+
output_tasks=output_tasks,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def deserialize_graph(
|
|
48
|
+
self,
|
|
49
|
+
graph: Any,
|
|
50
|
+
*,
|
|
51
|
+
inputs: Optional[List[dict]] = None,
|
|
52
|
+
representation: Optional[str] = None,
|
|
53
|
+
root_dir: Optional[Union[str, Path]] = None,
|
|
54
|
+
root_module: Optional[str] = None,
|
|
55
|
+
# Serializer specific:
|
|
56
|
+
**load_options,
|
|
57
|
+
) -> TaskGraph:
|
|
58
|
+
return bindings.load_graph(
|
|
59
|
+
graph,
|
|
60
|
+
inputs=inputs,
|
|
61
|
+
representation=representation,
|
|
62
|
+
root_dir=root_dir,
|
|
63
|
+
root_module=root_module,
|
|
64
|
+
**load_options,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def serialize_graph(
|
|
68
|
+
self,
|
|
69
|
+
graph: TaskGraph,
|
|
70
|
+
destination,
|
|
71
|
+
*,
|
|
72
|
+
representation: Optional[str] = None,
|
|
73
|
+
# Serializer specific:
|
|
74
|
+
**save_options,
|
|
75
|
+
) -> Union[str, dict]:
|
|
76
|
+
return bindings.save_graph(
|
|
77
|
+
graph, destination, representation=representation, **save_options
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get_graph_representation(self, graph: Any) -> Optional[str]:
|
|
81
|
+
if isinstance(graph, Mapping):
|
|
82
|
+
return "json_dict"
|
|
83
|
+
if isinstance(graph, (str, Path)):
|
|
84
|
+
if graph == "test_core":
|
|
85
|
+
return "test_core"
|
|
86
|
+
if isinstance(graph, str) and "{" in graph and "}" in graph:
|
|
87
|
+
return "json_string"
|
|
88
|
+
filename = str(graph).lower()
|
|
89
|
+
if filename.endswith(".json"):
|
|
90
|
+
return "json"
|
|
91
|
+
elif filename.endswith((".yml", ".yaml")):
|
|
92
|
+
return "yaml"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from .events.contexts import RawExecInfoType
|
|
10
|
+
from .graph import TaskGraph
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkflowEngine(ABC):
|
|
14
|
+
"""Python projects that provide Ewoks engines for deserializing, serializing and executing
|
|
15
|
+
computational Ewoks graphs can implement this interface.
|
|
16
|
+
|
|
17
|
+
To make it discoverable it can be added as an entry-point the the project. For
|
|
18
|
+
example in a `pyproject.toml` file:
|
|
19
|
+
|
|
20
|
+
.. code-block: toml
|
|
21
|
+
|
|
22
|
+
[project.entry-points."ewoks.engines"]
|
|
23
|
+
"<engine-name>" = "<project-name>.engine:MyWorkflowEngine"
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def execute_graph(
|
|
28
|
+
self,
|
|
29
|
+
graph: TaskGraph,
|
|
30
|
+
*,
|
|
31
|
+
inputs: Optional[List[dict]] = None,
|
|
32
|
+
load_options: Optional[dict] = None,
|
|
33
|
+
varinfo: Optional[dict] = None,
|
|
34
|
+
execinfo: Optional[RawExecInfoType] = None,
|
|
35
|
+
task_options: Optional[dict] = None,
|
|
36
|
+
outputs: Optional[List[dict]] = None,
|
|
37
|
+
merge_outputs: Optional[bool] = True,
|
|
38
|
+
# Engine specific:
|
|
39
|
+
**execute_options,
|
|
40
|
+
) -> Optional[dict]:
|
|
41
|
+
"""Execute a computional Ewoks graph."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WorkflowEngineWithSerialization(WorkflowEngine):
|
|
46
|
+
"""Ewoks engines with graph serialization capabilities."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def deserialize_graph(
|
|
50
|
+
self,
|
|
51
|
+
graph: Any,
|
|
52
|
+
*,
|
|
53
|
+
inputs: Optional[List[dict]] = None,
|
|
54
|
+
representation: Optional[str] = None,
|
|
55
|
+
root_dir: Optional[Union[str, Path]] = None,
|
|
56
|
+
root_module: Optional[str] = None,
|
|
57
|
+
# Serializer specific:
|
|
58
|
+
**deserialize_options,
|
|
59
|
+
) -> TaskGraph:
|
|
60
|
+
"""Convert a computational graph representation to the canonical in-memory representation `TaskGraph`."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def serialize_graph(
|
|
65
|
+
self,
|
|
66
|
+
graph: TaskGraph,
|
|
67
|
+
destination: Any,
|
|
68
|
+
*,
|
|
69
|
+
representation: Optional[str] = None,
|
|
70
|
+
# Serializer specific:
|
|
71
|
+
**serialize_options,
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Convert the canonical computational graph representation `TaskGraph` to another representation."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_graph_representation(self, graph: Any) -> Optional[str]:
|
|
78
|
+
"""Return the representation if the engine recognizes the graph object."""
|
|
79
|
+
pass
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 9):
|
|
5
|
+
from importlib.metadata import EntryPoint
|
|
6
|
+
|
|
7
|
+
from importlib_metadata import entry_points as _entry_points
|
|
8
|
+
|
|
9
|
+
def entry_points(group: str) -> Tuple[EntryPoint]:
|
|
10
|
+
return tuple(_entry_points(group=group))
|
|
11
|
+
|
|
12
|
+
elif sys.version_info < (3, 10):
|
|
13
|
+
from importlib.metadata import EntryPoint
|
|
14
|
+
from importlib.metadata import entry_points as _entry_points
|
|
15
|
+
|
|
16
|
+
def entry_points(group: str) -> Tuple[EntryPoint]:
|
|
17
|
+
return _entry_points().get(group, ())
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
from importlib.metadata import EntryPoint
|
|
21
|
+
from importlib.metadata import entry_points as _entry_points
|
|
22
|
+
|
|
23
|
+
def entry_points(group: str) -> Tuple[EntryPoint]:
|
|
24
|
+
return tuple(_entry_points(group=group))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Ewoks sends job, workflow and node events during workflow execution."""
|
|
2
|
+
|
|
3
|
+
from .contexts import job_context # noqa F401
|
|
4
|
+
from .contexts import job_decorator # noqa F401
|
|
5
|
+
from .contexts import node_context # noqa F401
|
|
6
|
+
from .contexts import workflow_context # noqa F401
|
|
7
|
+
from .global_state import add_handler # noqa F401
|
|
8
|
+
from .global_state import cleanup # noqa F401
|
|
9
|
+
from .global_state import remove_handler # noqa F401
|
|
10
|
+
from .send_events import send_job_event # noqa F401
|
|
11
|
+
from .send_events import send_task_event # noqa F401
|
|
12
|
+
from .send_events import send_workflow_event # noqa F401
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Contexts for job, workflow or node events. Allows for initializing
|
|
2
|
+
event fields and sending start/end events.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from . import global_state
|
|
11
|
+
from .initialize_events import init_job
|
|
12
|
+
from .initialize_events import init_node
|
|
13
|
+
from .initialize_events import init_workflow
|
|
14
|
+
from .send_events import ExecInfoType
|
|
15
|
+
from .send_events import send_job_event
|
|
16
|
+
from .send_events import send_workflow_event
|
|
17
|
+
|
|
18
|
+
RawExecInfoType = Union[Mapping, bool, str, None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def job_decorator(**static_job_info):
|
|
22
|
+
def _job_decorator(method):
|
|
23
|
+
@wraps(method)
|
|
24
|
+
def wrapper(*args, execinfo: RawExecInfoType = None, **kw):
|
|
25
|
+
with job_context(execinfo, **static_job_info) as execinfo:
|
|
26
|
+
return method(*args, execinfo=execinfo, **kw)
|
|
27
|
+
|
|
28
|
+
return wrapper
|
|
29
|
+
|
|
30
|
+
return _job_decorator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def job_context(execinfo: RawExecInfoType, **static_job_info) -> ExecInfoType:
|
|
35
|
+
if execinfo is None:
|
|
36
|
+
execinfo = global_state.ENABLE_EWOKS_EVENTS_BY_DEFAULT
|
|
37
|
+
|
|
38
|
+
if isinstance(execinfo, str):
|
|
39
|
+
execinfo = {"job_id": execinfo}
|
|
40
|
+
elif isinstance(execinfo, bool):
|
|
41
|
+
if execinfo:
|
|
42
|
+
execinfo = dict()
|
|
43
|
+
else:
|
|
44
|
+
execinfo = None
|
|
45
|
+
elif execinfo is None:
|
|
46
|
+
pass
|
|
47
|
+
elif not isinstance(execinfo, Mapping):
|
|
48
|
+
raise TypeError
|
|
49
|
+
|
|
50
|
+
execinfo = init_job(execinfo, **static_job_info)
|
|
51
|
+
if execinfo is None:
|
|
52
|
+
yield None
|
|
53
|
+
else:
|
|
54
|
+
with _context(execinfo, "job", send_job_event, execinfo["job_id"]) as execinfo:
|
|
55
|
+
yield execinfo
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@contextmanager
|
|
59
|
+
def workflow_context(execinfo: ExecInfoType, **kw) -> ExecInfoType:
|
|
60
|
+
execinfo = init_workflow(execinfo, **kw)
|
|
61
|
+
if execinfo is None:
|
|
62
|
+
yield None
|
|
63
|
+
else:
|
|
64
|
+
with _context(
|
|
65
|
+
execinfo, "workflow", send_workflow_event, execinfo["workflow_id"]
|
|
66
|
+
) as execinfo:
|
|
67
|
+
yield execinfo
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@contextmanager
|
|
71
|
+
def node_context(execinfo: ExecInfoType, **kw) -> ExecInfoType:
|
|
72
|
+
yield init_node(execinfo, **kw)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextmanager
|
|
76
|
+
def _context(
|
|
77
|
+
execinfo: ExecInfoType, context, send_context_event, obj_id
|
|
78
|
+
) -> ExecInfoType:
|
|
79
|
+
contexts = execinfo.get("contexts")
|
|
80
|
+
if contexts is None:
|
|
81
|
+
contexts = {"job": list(), "workflow": list()}
|
|
82
|
+
execinfo["contexts"] = contexts
|
|
83
|
+
obj_ids = contexts[context]
|
|
84
|
+
first_context = obj_id not in obj_ids
|
|
85
|
+
if first_context:
|
|
86
|
+
send_context_event(execinfo=execinfo, event="start")
|
|
87
|
+
obj_ids.append(obj_id)
|
|
88
|
+
try:
|
|
89
|
+
yield execinfo
|
|
90
|
+
except BaseException as e:
|
|
91
|
+
if not execinfo.get("exception"):
|
|
92
|
+
execinfo["exception"] = e
|
|
93
|
+
raise
|
|
94
|
+
finally:
|
|
95
|
+
if first_context:
|
|
96
|
+
obj_ids.remove(obj_id)
|
|
97
|
+
send_context_event(execinfo=execinfo, event="end")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Manage the EWOKS event logger which is a global object"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import Iterator
|
|
9
|
+
from typing import List
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
from ewoksutils.logging_utils.asyncwrapper import AsyncHandlerWrapper
|
|
14
|
+
from ewoksutils.logging_utils.cleanup import cleanup_handler
|
|
15
|
+
from ewoksutils.logging_utils.cleanup import cleanup_logger
|
|
16
|
+
from ewoksutils.logging_utils.cleanup import protect_logging_state
|
|
17
|
+
|
|
18
|
+
from .handlers import instantiate_handler
|
|
19
|
+
from .handlers import is_ewoks_event_handler
|
|
20
|
+
|
|
21
|
+
_app_logger = logging.getLogger(__name__)
|
|
22
|
+
EWOKS_EVENT_LOGGER_NAME = f"__{__name__}__"
|
|
23
|
+
ENABLE_EWOKS_EVENTS_BY_DEFAULT = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def send(
|
|
27
|
+
*args,
|
|
28
|
+
handlers: Optional[List[Dict[str, Any]]] = None,
|
|
29
|
+
asynchronous: Optional[bool] = None,
|
|
30
|
+
**kw,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Log an EWOKS event with the EWOKS event handlers and the application handlers."""
|
|
33
|
+
with _ewoks_event_logger(
|
|
34
|
+
handlers=handlers, asynchronous=asynchronous, cleanup_on_different_handlers=True
|
|
35
|
+
) as logger:
|
|
36
|
+
# Send to the EWOKS event handlers
|
|
37
|
+
logger.info(*args, **kw)
|
|
38
|
+
# Send to the application log handlers
|
|
39
|
+
_app_logger.info(*args, **kw)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def add_handler(
|
|
43
|
+
handler: logging.Handler,
|
|
44
|
+
asynchronous: Optional[bool] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Add a handler to the global EWOKS event logger."""
|
|
47
|
+
with _ewoks_event_logger() as logger:
|
|
48
|
+
if _has_handler_instance(logger, handler):
|
|
49
|
+
return
|
|
50
|
+
if asynchronous is None and is_ewoks_event_handler(handler):
|
|
51
|
+
asynchronous = handler.BLOCKING
|
|
52
|
+
if asynchronous:
|
|
53
|
+
handler = AsyncHandlerWrapper(handler)
|
|
54
|
+
logger.addHandler(handler)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def remove_handler(handler: logging.Handler) -> None:
|
|
58
|
+
"""Remove a handler from all loggers that receive EWOKS events."""
|
|
59
|
+
with _ewoks_event_logger() as logger:
|
|
60
|
+
for linstance, hinstance in _iter_handler_owners(logger, handler):
|
|
61
|
+
linstance.removeHandler(hinstance)
|
|
62
|
+
cleanup_handler(hinstance)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cleanup():
|
|
66
|
+
"""Pending events will be dropped"""
|
|
67
|
+
with protect_logging_state():
|
|
68
|
+
_cleanup_ewoks_event_logger()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _after_fork_in_child():
|
|
72
|
+
cleanup()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if hasattr(os, "register_at_fork"):
|
|
76
|
+
os.register_at_fork(after_in_child=_after_fork_in_child)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def _ewoks_event_logger(
|
|
81
|
+
handlers: Optional[List[Dict[str, Any]]] = None,
|
|
82
|
+
asynchronous: Optional[bool] = None,
|
|
83
|
+
cleanup_on_different_handlers: bool = False,
|
|
84
|
+
) -> Iterator[logging.Logger]:
|
|
85
|
+
"""Initialize and yield the EWOKS event logger"""
|
|
86
|
+
# Issue with logging and forking:
|
|
87
|
+
# https://pythonspeed.com/articles/python-multiprocessing/
|
|
88
|
+
|
|
89
|
+
with protect_logging_state():
|
|
90
|
+
if _ewoks_event_logger_requires_cleanup(
|
|
91
|
+
handlers=handlers,
|
|
92
|
+
cleanup_on_different_handlers=cleanup_on_different_handlers,
|
|
93
|
+
):
|
|
94
|
+
_cleanup_ewoks_event_logger()
|
|
95
|
+
if _ewoks_event_logger_requires_init():
|
|
96
|
+
_init_ewoks_event_logger(handlers, asynchronous)
|
|
97
|
+
yield logging.getLogger(EWOKS_EVENT_LOGGER_NAME)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _cleanup_ewoks_event_logger():
|
|
101
|
+
"""Cleanup and delete the global EWOKS event logger"""
|
|
102
|
+
cleanup_logger(EWOKS_EVENT_LOGGER_NAME)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _ewoks_event_logger_requires_init() -> bool:
|
|
106
|
+
logger = logging.getLogger(EWOKS_EVENT_LOGGER_NAME)
|
|
107
|
+
return not hasattr(logger, "ewoks_pid")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _ewoks_event_logger_requires_cleanup(
|
|
111
|
+
handlers: Optional[List[Dict[str, Any]]] = None,
|
|
112
|
+
cleanup_on_different_handlers: bool = False,
|
|
113
|
+
) -> bool:
|
|
114
|
+
logger = logging.getLogger(EWOKS_EVENT_LOGGER_NAME)
|
|
115
|
+
|
|
116
|
+
ewoks_pid = getattr(logger, "ewoks_pid", None)
|
|
117
|
+
if ewoks_pid is not None and ewoks_pid != os.getpid():
|
|
118
|
+
# Process forked
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
if cleanup_on_different_handlers:
|
|
122
|
+
ewoks_handlers = getattr(logger, "ewoks_handlers", None)
|
|
123
|
+
if ewoks_handlers != handlers:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _init_ewoks_event_logger(
|
|
130
|
+
handlers: Optional[List[Dict[str, Any]]], asynchronous: Optional[bool]
|
|
131
|
+
):
|
|
132
|
+
logger = logging.getLogger(EWOKS_EVENT_LOGGER_NAME)
|
|
133
|
+
logger.setLevel(logging.DEBUG)
|
|
134
|
+
logger.ewoks_pid = os.getpid()
|
|
135
|
+
logger.ewoks_handlers = handlers
|
|
136
|
+
logger.propagate = False
|
|
137
|
+
if not handlers:
|
|
138
|
+
return
|
|
139
|
+
for desc in handlers:
|
|
140
|
+
try:
|
|
141
|
+
kwargs = {
|
|
142
|
+
arg["name"]: arg["value"] for arg in desc.get("arguments", list())
|
|
143
|
+
}
|
|
144
|
+
handler = instantiate_handler(desc["class"], **kwargs)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"cannot create an EWOKS event handler from the description"
|
|
148
|
+
) from e
|
|
149
|
+
asynchronous_handler = desc.get("asynchronous", asynchronous)
|
|
150
|
+
add_handler(handler, asynchronous=asynchronous_handler)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _iter_loggers(logger: logging.Logger) -> Iterator[logging.Logger]:
|
|
154
|
+
"""Yield all loggers which will receive EWOKS events."""
|
|
155
|
+
_logger = logger
|
|
156
|
+
while _logger is not None:
|
|
157
|
+
yield _logger
|
|
158
|
+
if not _logger.propagate:
|
|
159
|
+
return
|
|
160
|
+
_logger = _logger.parent
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _iter_handler_owners(
|
|
164
|
+
logger: logging.Logger, instance: logging.Handler
|
|
165
|
+
) -> Iterator[Tuple[logging.Logger, logging.Handler]]:
|
|
166
|
+
"""Yield all loggers which have a specific handler (or a handler that wraps the specific event handler)."""
|
|
167
|
+
for _logger in _iter_loggers(logger):
|
|
168
|
+
for handler in _logger.handlers:
|
|
169
|
+
if handler is instance:
|
|
170
|
+
yield _logger, instance
|
|
171
|
+
elif isinstance(handler, AsyncHandlerWrapper):
|
|
172
|
+
instance2 = handler.wrapped_handler
|
|
173
|
+
if instance2 is instance:
|
|
174
|
+
yield _logger, instance2
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _has_handler_instance(logger: logging.Logger, instance: logging.Handler) -> bool:
|
|
178
|
+
"""Is this handler registered with a logger or it's parents?"""
|
|
179
|
+
for _ in _iter_handler_owners(logger, instance):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from ewoksutils.event_utils import FIELD_TYPES
|
|
4
|
+
|
|
5
|
+
__all__ = ["is_ewoks_event_handler", "EwoksEventHandlerMixIn", "EwoksEventHandler"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_ewoks_event_handler(handler):
|
|
9
|
+
return isinstance(handler, EwoksEventHandlerMixIn)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EwoksEventHandlerMixIn:
|
|
13
|
+
BLOCKING = False
|
|
14
|
+
FIELD_TYPES = FIELD_TYPES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EwoksEventHandler(EwoksEventHandlerMixIn, logging.Handler):
|
|
18
|
+
"""Base class for handling ewoks events on the publishing side (implement the `emit` method)."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ewoksutils.logging_utils.sqlite3 import Sqlite3Handler
|
|
2
|
+
|
|
3
|
+
from .base import EwoksEventHandlerMixIn
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Sqlite3EwoksEventHandler(EwoksEventHandlerMixIn, Sqlite3Handler):
|
|
7
|
+
def __init__(self, uri: str):
|
|
8
|
+
super().__init__(uri=uri, table="ewoks_events", field_types=self.FIELD_TYPES)
|