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.
Files changed (116) hide show
  1. ewokscore/__init__.py +3 -0
  2. ewokscore/bindings.py +98 -0
  3. ewokscore/dynamictask.py +25 -0
  4. ewokscore/engine.py +92 -0
  5. ewokscore/engine_interface.py +79 -0
  6. ewokscore/entry_points.py +24 -0
  7. ewokscore/events/__init__.py +12 -0
  8. ewokscore/events/contexts.py +97 -0
  9. ewokscore/events/global_state.py +181 -0
  10. ewokscore/events/handlers/__init__.py +7 -0
  11. ewokscore/events/handlers/base.py +20 -0
  12. ewokscore/events/handlers/sqlite3.py +8 -0
  13. ewokscore/events/initialize_events.py +72 -0
  14. ewokscore/events/send_events.py +251 -0
  15. ewokscore/graph/__init__.py +2 -0
  16. ewokscore/graph/analysis.py +380 -0
  17. ewokscore/graph/compare.py +51 -0
  18. ewokscore/graph/error_handlers.py +55 -0
  19. ewokscore/graph/execute/__init__.py +0 -0
  20. ewokscore/graph/execute/sequential.py +151 -0
  21. ewokscore/graph/graph_io.py +209 -0
  22. ewokscore/graph/inputs.py +476 -0
  23. ewokscore/graph/models.py +7 -0
  24. ewokscore/graph/multigraph.py +24 -0
  25. ewokscore/graph/schema/__init__.py +109 -0
  26. ewokscore/graph/schema/metadata.py +27 -0
  27. ewokscore/graph/schema/update.py +11 -0
  28. ewokscore/graph/serialize.py +329 -0
  29. ewokscore/graph/subgraph.py +300 -0
  30. ewokscore/graph/taskgraph.py +251 -0
  31. ewokscore/graph/validate.py +52 -0
  32. ewokscore/hashing.py +293 -0
  33. ewokscore/inittask.py +272 -0
  34. ewokscore/methodtask.py +26 -0
  35. ewokscore/missing_data.py +21 -0
  36. ewokscore/model.py +9 -0
  37. ewokscore/node.py +67 -0
  38. ewokscore/notebooktask.py +96 -0
  39. ewokscore/persistence/__init__.py +5 -0
  40. ewokscore/persistence/atomic.py +71 -0
  41. ewokscore/persistence/file.py +171 -0
  42. ewokscore/persistence/json.py +31 -0
  43. ewokscore/persistence/nexus.py +78 -0
  44. ewokscore/persistence/proxy.py +214 -0
  45. ewokscore/ppftasks.py +57 -0
  46. ewokscore/progress.py +131 -0
  47. ewokscore/registration.py +67 -0
  48. ewokscore/scripttask.py +185 -0
  49. ewokscore/task.py +777 -0
  50. ewokscore/task_discovery.py +318 -0
  51. ewokscore/taskwithprogress.py +32 -0
  52. ewokscore/tests/__init__.py +0 -0
  53. ewokscore/tests/conftest.py +240 -0
  54. ewokscore/tests/discover/__init__.py +0 -0
  55. ewokscore/tests/discover/module1.py +49 -0
  56. ewokscore/tests/discover/module2.py +46 -0
  57. ewokscore/tests/examples/__init__.py +0 -0
  58. ewokscore/tests/examples/graphs/__init__.py +43 -0
  59. ewokscore/tests/examples/graphs/acyclic1.py +149 -0
  60. ewokscore/tests/examples/graphs/acyclic2.py +90 -0
  61. ewokscore/tests/examples/graphs/acyclic3.py +109 -0
  62. ewokscore/tests/examples/graphs/cyclic1.py +105 -0
  63. ewokscore/tests/examples/graphs/demo.py +135 -0
  64. ewokscore/tests/examples/graphs/empty.py +7 -0
  65. ewokscore/tests/examples/graphs/self_trigger.py +54 -0
  66. ewokscore/tests/examples/graphs/triangle1.py +54 -0
  67. ewokscore/tests/examples/loadtest/__init__.py +0 -0
  68. ewokscore/tests/examples/loadtest/graph.json +1 -0
  69. ewokscore/tests/examples/loadtest/subgraph.json +1 -0
  70. ewokscore/tests/examples/tasks/__init__.py +0 -0
  71. ewokscore/tests/examples/tasks/addfunc.py +3 -0
  72. ewokscore/tests/examples/tasks/condsumtask.py +9 -0
  73. ewokscore/tests/examples/tasks/errorsumtask.py +15 -0
  74. ewokscore/tests/examples/tasks/nooutputtask.py +14 -0
  75. ewokscore/tests/examples/tasks/notebooktask.ipynb +147 -0
  76. ewokscore/tests/examples/tasks/simplemethods.py +8 -0
  77. ewokscore/tests/examples/tasks/sumlist.py +28 -0
  78. ewokscore/tests/examples/tasks/sumtask.py +26 -0
  79. ewokscore/tests/test_default_error_handlers.py +264 -0
  80. ewokscore/tests/test_dynamic_tasks.py +30 -0
  81. ewokscore/tests/test_events.py +112 -0
  82. ewokscore/tests/test_examples.py +161 -0
  83. ewokscore/tests/test_execute_graph.py +223 -0
  84. ewokscore/tests/test_graph.py +90 -0
  85. ewokscore/tests/test_graph_inputs.py +308 -0
  86. ewokscore/tests/test_graph_io.py +191 -0
  87. ewokscore/tests/test_graph_schema.py +62 -0
  88. ewokscore/tests/test_graph_serialize.py +115 -0
  89. ewokscore/tests/test_graph_start_end_nodes.py +59 -0
  90. ewokscore/tests/test_hashing.py +162 -0
  91. ewokscore/tests/test_link_is_required.py +146 -0
  92. ewokscore/tests/test_method_task.py +68 -0
  93. ewokscore/tests/test_notebook_task.py +105 -0
  94. ewokscore/tests/test_persistence.py +112 -0
  95. ewokscore/tests/test_script_task.py +168 -0
  96. ewokscore/tests/test_sub_graph.py +261 -0
  97. ewokscore/tests/test_sub_graph_serialize.py +312 -0
  98. ewokscore/tests/test_task.py +184 -0
  99. ewokscore/tests/test_task_discovery.py +155 -0
  100. ewokscore/tests/test_task_input_model.py +309 -0
  101. ewokscore/tests/test_task_output_model.py +192 -0
  102. ewokscore/tests/test_task_progress.py +65 -0
  103. ewokscore/tests/test_variable.py +438 -0
  104. ewokscore/tests/test_variable_namespaces.py +69 -0
  105. ewokscore/tests/test_workflow_events.py +379 -0
  106. ewokscore/tests/utils/__init__.py +0 -0
  107. ewokscore/tests/utils/results.py +150 -0
  108. ewokscore/tests/utils/show.py +17 -0
  109. ewokscore/utils.py +35 -0
  110. ewokscore/variable.py +661 -0
  111. ewokscore-4.0.0.dist-info/METADATA +154 -0
  112. ewokscore-4.0.0.dist-info/RECORD +116 -0
  113. ewokscore-4.0.0.dist-info/WHEEL +5 -0
  114. ewokscore-4.0.0.dist-info/entry_points.txt +16 -0
  115. ewokscore-4.0.0.dist-info/licenses/LICENSE.md +20 -0
  116. ewokscore-4.0.0.dist-info/top_level.txt +1 -0
ewokscore/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .bindings import * # noqa: F403,F401
2
+ from .task import Task # noqa: F401
3
+ from .taskwithprogress import TaskWithProgress # noqa: F401
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
@@ -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,7 @@
1
+ # fmt: off
2
+ from ewoksutils.import_utils import instantiate_class as instantiate_handler # noqa F401
3
+
4
+ from .base import * # noqa F401
5
+ from .sqlite3 import Sqlite3EwoksEventHandler # noqa F401
6
+
7
+ # fmt: on
@@ -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)