hpcflow 0.1.9__py3-none-any.whl → 0.2.0a271__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.
- hpcflow/__init__.py +2 -11
- hpcflow/__pyinstaller/__init__.py +5 -0
- hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
- hpcflow/_version.py +1 -1
- hpcflow/app.py +43 -0
- hpcflow/cli.py +2 -462
- hpcflow/data/demo_data_manifest/__init__.py +3 -0
- hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
- hpcflow/data/jinja_templates/test/test_template.txt +8 -0
- hpcflow/data/programs/hello_world/README.md +1 -0
- hpcflow/data/programs/hello_world/hello_world.c +87 -0
- hpcflow/data/programs/hello_world/linux/hello_world +0 -0
- hpcflow/data/programs/hello_world/macos/hello_world +0 -0
- hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
- hpcflow/data/scripts/__init__.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
- hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/generate_t1_file_01.py +7 -0
- hpcflow/data/scripts/import_future_script.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
- hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
- hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/parse_t1_file_01.py +4 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/__init__.py +1 -0
- hpcflow/data/template_components/command_files.yaml +26 -0
- hpcflow/data/template_components/environments.yaml +13 -0
- hpcflow/data/template_components/parameters.yaml +14 -0
- hpcflow/data/template_components/task_schemas.yaml +139 -0
- hpcflow/data/workflows/workflow_1.yaml +5 -0
- hpcflow/examples.ipynb +1037 -0
- hpcflow/sdk/__init__.py +149 -0
- hpcflow/sdk/app.py +4266 -0
- hpcflow/sdk/cli.py +1479 -0
- hpcflow/sdk/cli_common.py +385 -0
- hpcflow/sdk/config/__init__.py +5 -0
- hpcflow/sdk/config/callbacks.py +246 -0
- hpcflow/sdk/config/cli.py +388 -0
- hpcflow/sdk/config/config.py +1410 -0
- hpcflow/sdk/config/config_file.py +501 -0
- hpcflow/sdk/config/errors.py +272 -0
- hpcflow/sdk/config/types.py +150 -0
- hpcflow/sdk/core/__init__.py +38 -0
- hpcflow/sdk/core/actions.py +3857 -0
- hpcflow/sdk/core/app_aware.py +25 -0
- hpcflow/sdk/core/cache.py +224 -0
- hpcflow/sdk/core/command_files.py +814 -0
- hpcflow/sdk/core/commands.py +424 -0
- hpcflow/sdk/core/element.py +2071 -0
- hpcflow/sdk/core/enums.py +221 -0
- hpcflow/sdk/core/environment.py +256 -0
- hpcflow/sdk/core/errors.py +1043 -0
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +809 -0
- hpcflow/sdk/core/loop.py +1320 -0
- hpcflow/sdk/core/loop_cache.py +282 -0
- hpcflow/sdk/core/object_list.py +933 -0
- hpcflow/sdk/core/parameters.py +3371 -0
- hpcflow/sdk/core/rule.py +196 -0
- hpcflow/sdk/core/run_dir_files.py +57 -0
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +3792 -0
- hpcflow/sdk/core/task_schema.py +993 -0
- hpcflow/sdk/core/test_utils.py +538 -0
- hpcflow/sdk/core/types.py +447 -0
- hpcflow/sdk/core/utils.py +1207 -0
- hpcflow/sdk/core/validation.py +87 -0
- hpcflow/sdk/core/values.py +477 -0
- hpcflow/sdk/core/workflow.py +4820 -0
- hpcflow/sdk/core/zarr_io.py +206 -0
- hpcflow/sdk/data/__init__.py +13 -0
- hpcflow/sdk/data/config_file_schema.yaml +34 -0
- hpcflow/sdk/data/config_schema.yaml +260 -0
- hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
- hpcflow/sdk/data/files_spec_schema.yaml +5 -0
- hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
- hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
- hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
- hpcflow/sdk/demo/__init__.py +3 -0
- hpcflow/sdk/demo/cli.py +242 -0
- hpcflow/sdk/helper/__init__.py +3 -0
- hpcflow/sdk/helper/cli.py +137 -0
- hpcflow/sdk/helper/helper.py +300 -0
- hpcflow/sdk/helper/watcher.py +192 -0
- hpcflow/sdk/log.py +288 -0
- hpcflow/sdk/persistence/__init__.py +18 -0
- hpcflow/sdk/persistence/base.py +2817 -0
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +39 -0
- hpcflow/sdk/persistence/json.py +954 -0
- hpcflow/sdk/persistence/pending.py +948 -0
- hpcflow/sdk/persistence/store_resource.py +203 -0
- hpcflow/sdk/persistence/types.py +309 -0
- hpcflow/sdk/persistence/utils.py +73 -0
- hpcflow/sdk/persistence/zarr.py +2388 -0
- hpcflow/sdk/runtime.py +320 -0
- hpcflow/sdk/submission/__init__.py +3 -0
- hpcflow/sdk/submission/enums.py +70 -0
- hpcflow/sdk/submission/jobscript.py +2379 -0
- hpcflow/sdk/submission/schedulers/__init__.py +281 -0
- hpcflow/sdk/submission/schedulers/direct.py +233 -0
- hpcflow/sdk/submission/schedulers/sge.py +376 -0
- hpcflow/sdk/submission/schedulers/slurm.py +598 -0
- hpcflow/sdk/submission/schedulers/utils.py +25 -0
- hpcflow/sdk/submission/shells/__init__.py +52 -0
- hpcflow/sdk/submission/shells/base.py +229 -0
- hpcflow/sdk/submission/shells/bash.py +504 -0
- hpcflow/sdk/submission/shells/os_version.py +115 -0
- hpcflow/sdk/submission/shells/powershell.py +352 -0
- hpcflow/sdk/submission/submission.py +1402 -0
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +194 -0
- hpcflow/sdk/utils/arrays.py +69 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +31 -0
- hpcflow/sdk/utils/strings.py +69 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +123 -0
- hpcflow/tests/data/__init__.py +0 -0
- hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_1.json +10 -0
- hpcflow/tests/data/workflow_1.yaml +5 -0
- hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
- hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
- hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
- hpcflow/tests/programs/test_programs.py +180 -0
- hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +1361 -0
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
- hpcflow/tests/unit/test_action.py +1066 -0
- hpcflow/tests/unit/test_action_rule.py +24 -0
- hpcflow/tests/unit/test_app.py +132 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +172 -0
- hpcflow/tests/unit/test_command.py +377 -0
- hpcflow/tests/unit/test_config.py +195 -0
- hpcflow/tests/unit/test_config_file.py +162 -0
- hpcflow/tests/unit/test_element.py +666 -0
- hpcflow/tests/unit/test_element_iteration.py +88 -0
- hpcflow/tests/unit/test_element_set.py +158 -0
- hpcflow/tests/unit/test_group.py +115 -0
- hpcflow/tests/unit/test_input_source.py +1479 -0
- hpcflow/tests/unit/test_input_value.py +398 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +1247 -0
- hpcflow/tests/unit/test_loop.py +2674 -0
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
- hpcflow/tests/unit/test_object_list.py +116 -0
- hpcflow/tests/unit/test_parameter.py +243 -0
- hpcflow/tests/unit/test_persistence.py +664 -0
- hpcflow/tests/unit/test_resources.py +243 -0
- hpcflow/tests/unit/test_run.py +286 -0
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +9 -0
- hpcflow/tests/unit/test_schema_input.py +372 -0
- hpcflow/tests/unit/test_shell.py +129 -0
- hpcflow/tests/unit/test_slurm.py +39 -0
- hpcflow/tests/unit/test_submission.py +502 -0
- hpcflow/tests/unit/test_task.py +2560 -0
- hpcflow/tests/unit/test_task_schema.py +182 -0
- hpcflow/tests/unit/test_utils.py +616 -0
- hpcflow/tests/unit/test_value_sequence.py +549 -0
- hpcflow/tests/unit/test_values.py +91 -0
- hpcflow/tests/unit/test_workflow.py +827 -0
- hpcflow/tests/unit/test_workflow_template.py +186 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/unit/utils/test_strings.py +97 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +355 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +564 -0
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6794 -0
- hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
- hpcflow-0.2.0a271.dist-info/METADATA +65 -0
- hpcflow-0.2.0a271.dist-info/RECORD +237 -0
- {hpcflow-0.1.9.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
- hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
- hpcflow/api.py +0 -458
- hpcflow/archive/archive.py +0 -308
- hpcflow/archive/cloud/cloud.py +0 -47
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -432
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -232
- hpcflow/copytree.py +0 -66
- hpcflow/data/examples/_config.yml +0 -14
- hpcflow/data/examples/damask/demo/1.run.yml +0 -4
- hpcflow/data/examples/damask/demo/2.process.yml +0 -29
- hpcflow/data/examples/damask/demo/geom.geom +0 -2052
- hpcflow/data/examples/damask/demo/load.load +0 -1
- hpcflow/data/examples/damask/demo/material.config +0 -185
- hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
- hpcflow/data/examples/damask/inputs/load.load +0 -1
- hpcflow/data/examples/damask/inputs/material.config +0 -185
- hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
- hpcflow/data/examples/damask/profiles/damask.yml +0 -4
- hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
- hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
- hpcflow/data/examples/damask/profiles/default.yml +0 -6
- hpcflow/data/examples/thinking.yml +0 -177
- hpcflow/errors.py +0 -2
- hpcflow/init_db.py +0 -37
- hpcflow/models.py +0 -2549
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -323
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -167
- hpcflow/variables.py +0 -544
- hpcflow-0.1.9.dist-info/METADATA +0 -168
- hpcflow-0.1.9.dist-info/RECORD +0 -45
- hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.9.dist-info/top_level.txt +0 -1
- /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
- /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
- /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File-system watcher classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from logging import Logger
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
11
|
+
from watchdog.observers.polling import PollingObserver
|
|
12
|
+
from watchdog.events import (
|
|
13
|
+
FileSystemEvent,
|
|
14
|
+
FileSystemEventHandler,
|
|
15
|
+
PatternMatchingEventHandler,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _PMEHDelegate(PatternMatchingEventHandler):
|
|
20
|
+
def __init__(self, pattern: str, on_modified: Callable[[FileSystemEvent], None]):
|
|
21
|
+
super().__init__(patterns=[pattern])
|
|
22
|
+
self.__on_modified = on_modified
|
|
23
|
+
|
|
24
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
25
|
+
self.__on_modified(event)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _FSEHDelegate(FileSystemEventHandler):
|
|
29
|
+
def __init__(self, on_modified: Callable[[FileSystemEvent], None]) -> None:
|
|
30
|
+
self.__on_modified = on_modified
|
|
31
|
+
|
|
32
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
33
|
+
self.__on_modified(event)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MonitorController:
|
|
37
|
+
"""
|
|
38
|
+
Controller for tracking watch files.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
workflow_dirs_file_path: str | Path,
|
|
44
|
+
watch_interval: float | timedelta,
|
|
45
|
+
logger: Logger,
|
|
46
|
+
):
|
|
47
|
+
if isinstance(watch_interval, timedelta):
|
|
48
|
+
self.watch_interval = int(watch_interval.total_seconds())
|
|
49
|
+
else:
|
|
50
|
+
self.watch_interval = int(watch_interval)
|
|
51
|
+
self.workflow_dirs_file_path = Path(workflow_dirs_file_path).absolute()
|
|
52
|
+
self.logger = logger
|
|
53
|
+
|
|
54
|
+
if not self.workflow_dirs_file_path.exists():
|
|
55
|
+
self.logger.info(
|
|
56
|
+
f"Watch file does not exist; creating {str(self.workflow_dirs_file_path)}."
|
|
57
|
+
)
|
|
58
|
+
with self.workflow_dirs_file_path.open("wt") as fp:
|
|
59
|
+
fp.write("\n")
|
|
60
|
+
|
|
61
|
+
self.logger.info(f"Watching file: {str(self.workflow_dirs_file_path)}")
|
|
62
|
+
|
|
63
|
+
self.event_handler = _PMEHDelegate("watch_workflows.txt", self.on_modified)
|
|
64
|
+
|
|
65
|
+
self.observer = PollingObserver(timeout=self.watch_interval)
|
|
66
|
+
self.observer.schedule(
|
|
67
|
+
self.event_handler,
|
|
68
|
+
path=cast("str", self.workflow_dirs_file_path.parent),
|
|
69
|
+
recursive=False,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.observer.start()
|
|
73
|
+
|
|
74
|
+
workflow_paths = self.parse_watch_workflows_file(
|
|
75
|
+
self.workflow_dirs_file_path, logger=self.logger
|
|
76
|
+
)
|
|
77
|
+
self.workflow_monitor = WorkflowMonitor(
|
|
78
|
+
workflow_paths,
|
|
79
|
+
watch_interval=self.watch_interval,
|
|
80
|
+
logger=self.logger,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def parse_watch_workflows_file(
|
|
85
|
+
path: str | Path, logger: Logger
|
|
86
|
+
) -> list[dict[str, Path]]:
|
|
87
|
+
"""
|
|
88
|
+
Parse the file describing what workflows to watch.
|
|
89
|
+
"""
|
|
90
|
+
# TODO: and parse element IDs as well; and record which are set/unset.
|
|
91
|
+
with Path(path).open("rt") as fp:
|
|
92
|
+
lns = fp.readlines()
|
|
93
|
+
|
|
94
|
+
wks: list[dict[str, Path]] = []
|
|
95
|
+
for ln in lns:
|
|
96
|
+
ln_s = ln.strip()
|
|
97
|
+
if not ln_s:
|
|
98
|
+
continue
|
|
99
|
+
wk_path = Path(ln_s).absolute()
|
|
100
|
+
if not wk_path.is_dir():
|
|
101
|
+
logger.warning(f"{str(wk_path)} is not a workflow")
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
wks.append(
|
|
105
|
+
{
|
|
106
|
+
"path": wk_path,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return wks
|
|
111
|
+
|
|
112
|
+
def on_modified(self, event: FileSystemEvent):
|
|
113
|
+
"""
|
|
114
|
+
Callback when files are modified.
|
|
115
|
+
"""
|
|
116
|
+
self.logger.info(f"Watch file modified: {event.src_path!r}")
|
|
117
|
+
wks = self.parse_watch_workflows_file(
|
|
118
|
+
cast("str", event.src_path), logger=self.logger
|
|
119
|
+
)
|
|
120
|
+
self.workflow_monitor.update_workflow_paths(wks)
|
|
121
|
+
|
|
122
|
+
def join(self) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Join the worker thread.
|
|
125
|
+
"""
|
|
126
|
+
self.observer.join()
|
|
127
|
+
|
|
128
|
+
def stop(self) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Stop this monitor.
|
|
131
|
+
"""
|
|
132
|
+
self.observer.stop()
|
|
133
|
+
self.observer.join() # wait for it to stop!
|
|
134
|
+
self.workflow_monitor.stop()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class WorkflowMonitor:
|
|
138
|
+
"""
|
|
139
|
+
Workflow monitor.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
workflow_paths: list[dict[str, Path]],
|
|
145
|
+
watch_interval: float | timedelta,
|
|
146
|
+
logger: Logger,
|
|
147
|
+
):
|
|
148
|
+
if isinstance(watch_interval, timedelta):
|
|
149
|
+
self.watch_interval = int(watch_interval.total_seconds())
|
|
150
|
+
else:
|
|
151
|
+
self.watch_interval = int(watch_interval)
|
|
152
|
+
|
|
153
|
+
self.event_handler = _FSEHDelegate(self.on_modified)
|
|
154
|
+
self.workflow_paths = workflow_paths
|
|
155
|
+
self.logger = logger
|
|
156
|
+
|
|
157
|
+
self._monitor_workflow_paths()
|
|
158
|
+
|
|
159
|
+
def _monitor_workflow_paths(self) -> None:
|
|
160
|
+
observer = PollingObserver(timeout=self.watch_interval)
|
|
161
|
+
self.observer: PollingObserver | None = observer
|
|
162
|
+
for i in self.workflow_paths:
|
|
163
|
+
observer.schedule(
|
|
164
|
+
self.event_handler, path=cast("str", i["path"]), recursive=False
|
|
165
|
+
)
|
|
166
|
+
self.logger.info(f"Watching workflow: {i['path'].name}")
|
|
167
|
+
|
|
168
|
+
observer.start()
|
|
169
|
+
|
|
170
|
+
def on_modified(self, event: FileSystemEvent):
|
|
171
|
+
"""
|
|
172
|
+
Triggered on a workflow being modified.
|
|
173
|
+
"""
|
|
174
|
+
self.logger.info(f"Workflow modified: {event.src_path!r}")
|
|
175
|
+
|
|
176
|
+
def update_workflow_paths(self, new_paths: list[dict[str, Path]]):
|
|
177
|
+
"""
|
|
178
|
+
Change the set of paths to monitored workflows.
|
|
179
|
+
"""
|
|
180
|
+
self.logger.info("Updating watched workflows.")
|
|
181
|
+
self.stop()
|
|
182
|
+
self.workflow_paths = new_paths
|
|
183
|
+
self._monitor_workflow_paths()
|
|
184
|
+
|
|
185
|
+
def stop(self) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Stop this monitor.
|
|
188
|
+
"""
|
|
189
|
+
if self.observer:
|
|
190
|
+
self.observer.stop()
|
|
191
|
+
self.observer.join() # wait for it to stop!
|
|
192
|
+
self.observer = None
|
hpcflow/sdk/log.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface to the standard logger, and performance logging utility.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from functools import wraps
|
|
7
|
+
import logging
|
|
8
|
+
import logging.handlers
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import time
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
import statistics
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import ClassVar, TypeVar, TYPE_CHECKING
|
|
16
|
+
from typing_extensions import ParamSpec
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .app import BaseApp
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
P = ParamSpec("P")
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class _Summary:
|
|
28
|
+
"""
|
|
29
|
+
Summary of a particular node's execution time.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
number: int
|
|
33
|
+
mean: float
|
|
34
|
+
stddev: float
|
|
35
|
+
min: float
|
|
36
|
+
max: float
|
|
37
|
+
sum: float
|
|
38
|
+
children: dict[tuple[str, ...], _Summary]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TimeIt:
|
|
42
|
+
"""
|
|
43
|
+
Method execution time instrumentation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
#: Whether the instrumentation is active.
|
|
47
|
+
active: ClassVar = False
|
|
48
|
+
#: Where to log to.
|
|
49
|
+
file_path: ClassVar[str | None] = None
|
|
50
|
+
#: The details be tracked.
|
|
51
|
+
timers: ClassVar[dict[tuple[str, ...], list[float]]] = defaultdict(list)
|
|
52
|
+
#: Traces of the stack.
|
|
53
|
+
trace: ClassVar[list[str]] = []
|
|
54
|
+
#: Trace indices.
|
|
55
|
+
trace_idx: ClassVar[list[int]] = []
|
|
56
|
+
#: Preceding traces.
|
|
57
|
+
trace_prev: ClassVar[list[str]] = []
|
|
58
|
+
#: Preceding trace indices.
|
|
59
|
+
trace_idx_prev: ClassVar[list[int]] = []
|
|
60
|
+
|
|
61
|
+
def __enter__(self):
|
|
62
|
+
self.__class__.active = True
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
66
|
+
try:
|
|
67
|
+
self.__class__.summarise_string()
|
|
68
|
+
finally:
|
|
69
|
+
self.__class__.reset()
|
|
70
|
+
self.__class__.active = False
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def decorator(cls, func: Callable[P, T]) -> Callable[P, T]:
|
|
74
|
+
"""
|
|
75
|
+
Decorator for a method that is to have its execution time monitored.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@wraps(func)
|
|
79
|
+
def wrapper(*args, **kwargs) -> T:
|
|
80
|
+
if not cls.active:
|
|
81
|
+
return func(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
cls.trace.append(func.__qualname__)
|
|
84
|
+
|
|
85
|
+
if cls.trace_prev == cls.trace:
|
|
86
|
+
new_trace_idx = cls.trace_idx_prev[-1] + 1
|
|
87
|
+
else:
|
|
88
|
+
new_trace_idx = 0
|
|
89
|
+
cls.trace_idx.append(new_trace_idx)
|
|
90
|
+
|
|
91
|
+
tic = time.perf_counter()
|
|
92
|
+
out = func(*args, **kwargs)
|
|
93
|
+
toc = time.perf_counter()
|
|
94
|
+
elapsed = toc - tic
|
|
95
|
+
|
|
96
|
+
cls.timers[tuple(cls.trace)].append(elapsed)
|
|
97
|
+
|
|
98
|
+
cls.trace_prev = list(cls.trace)
|
|
99
|
+
cls.trace_idx_prev = list(cls.trace_idx)
|
|
100
|
+
|
|
101
|
+
cls.trace.pop()
|
|
102
|
+
cls.trace_idx.pop()
|
|
103
|
+
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
return wrapper
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def _summarise(cls) -> dict[tuple[str, ...], _Summary]:
|
|
110
|
+
"""
|
|
111
|
+
Produce a machine-readable summary of method execution time statistics.
|
|
112
|
+
"""
|
|
113
|
+
stats = {
|
|
114
|
+
k: _Summary(
|
|
115
|
+
len(v),
|
|
116
|
+
statistics.mean(v),
|
|
117
|
+
statistics.pstdev(v),
|
|
118
|
+
min(v),
|
|
119
|
+
max(v),
|
|
120
|
+
sum(v),
|
|
121
|
+
{},
|
|
122
|
+
)
|
|
123
|
+
for k, v in cls.timers.items()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# make a graph
|
|
127
|
+
for key in sorted(stats, key=lambda x: len(x), reverse=True):
|
|
128
|
+
if len(key) == 1:
|
|
129
|
+
continue
|
|
130
|
+
value = stats.pop(key)
|
|
131
|
+
parent_key = key[:-1]
|
|
132
|
+
if parent_key in stats:
|
|
133
|
+
stats[parent_key].children[key] = value
|
|
134
|
+
|
|
135
|
+
return stats
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def summarise_string(cls) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Produce a human-readable summary of method execution time statistics.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def _format_nodes(
|
|
144
|
+
node: dict[tuple[str, ...], _Summary],
|
|
145
|
+
depth: int = 0,
|
|
146
|
+
depth_final: Sequence[bool] = (),
|
|
147
|
+
):
|
|
148
|
+
for idx, (k, v) in enumerate(node.items()):
|
|
149
|
+
is_final_child = idx == len(node) - 1
|
|
150
|
+
angle = "└ " if is_final_child else "├ "
|
|
151
|
+
bars = ""
|
|
152
|
+
if depth > 0:
|
|
153
|
+
bars = "".join(f"{'│ ' if not i else ' '}" for i in depth_final)
|
|
154
|
+
k_str = bars + (angle if depth > 0 else "") + f"{k[depth]}"
|
|
155
|
+
min_str = f"{v.min:10.6f}" if v.number > 1 else f"{f'-':^12s}"
|
|
156
|
+
max_str = f"{v.max:10.6f}" if v.number > 1 else f"{f'-':^12s}"
|
|
157
|
+
stddev_str = f"({v.stddev:8.6f})" if v.number > 1 else f"{f' ':^10s}"
|
|
158
|
+
out.append(
|
|
159
|
+
f"{k_str:.<80s} {v.sum:12.6f} "
|
|
160
|
+
f"{v.mean:10.6f} {stddev_str} {v.number:8d} "
|
|
161
|
+
f"{min_str} {max_str} "
|
|
162
|
+
)
|
|
163
|
+
depth_final_next = list(depth_final)
|
|
164
|
+
if depth > 0:
|
|
165
|
+
depth_final_next.append(is_final_child)
|
|
166
|
+
_format_nodes(v.children, depth + 1, depth_final_next)
|
|
167
|
+
|
|
168
|
+
summary = cls._summarise()
|
|
169
|
+
|
|
170
|
+
out = [
|
|
171
|
+
f"{'function':^80s} {'sum /s':^12s} {'mean (stddev) /s':^20s} {'N':^8s} "
|
|
172
|
+
f"{'min /s':^12s} {'max /s':^12s}"
|
|
173
|
+
]
|
|
174
|
+
_format_nodes(summary)
|
|
175
|
+
out_str = "\n".join(out)
|
|
176
|
+
if cls.file_path:
|
|
177
|
+
Path(cls.file_path).write_text(out_str, encoding="utf-8")
|
|
178
|
+
else:
|
|
179
|
+
print(out_str)
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def reset(cls):
|
|
183
|
+
cls.timers = defaultdict(list)
|
|
184
|
+
cls.trace = []
|
|
185
|
+
cls.trace_idx = []
|
|
186
|
+
cls.trace_prev = []
|
|
187
|
+
cls.trace_idx_prev = []
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AppLog:
|
|
191
|
+
"""
|
|
192
|
+
Application log control.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
#: Default logging level for the console.
|
|
196
|
+
DEFAULT_LOG_CONSOLE_LEVEL: ClassVar = "WARNING"
|
|
197
|
+
#: Default logging level for log files.
|
|
198
|
+
DEFAULT_LOG_FILE_LEVEL: ClassVar = "WARNING"
|
|
199
|
+
|
|
200
|
+
def __init__(self, app: BaseApp, log_console_level: str | None = None) -> None:
|
|
201
|
+
#: The application context.
|
|
202
|
+
self._app = app
|
|
203
|
+
#: The base logger for the application.
|
|
204
|
+
self.logger = logging.getLogger(app.package_name)
|
|
205
|
+
self.logger.setLevel(logging.WARNING)
|
|
206
|
+
#: The handler for directing logging messages to the console.
|
|
207
|
+
self.console_handler = self.__add_console_logger(
|
|
208
|
+
level=log_console_level or AppLog.DEFAULT_LOG_CONSOLE_LEVEL
|
|
209
|
+
)
|
|
210
|
+
self.file_handler: logging.FileHandler | None = None
|
|
211
|
+
|
|
212
|
+
def _ensure_logger_level(self):
|
|
213
|
+
"""Ensure the logger's level is set to a level that triggers the handlers.
|
|
214
|
+
|
|
215
|
+
Notes
|
|
216
|
+
-----
|
|
217
|
+
Previously, we fixed the logger to DEBUG, but we found other Python packages
|
|
218
|
+
could then trigger debug logs in hpcflow even though the handlers were set to e.g.
|
|
219
|
+
ERROR.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
min_level = min((handler.level for handler in self.logger.handlers), default=0)
|
|
223
|
+
if self.logger.level != min_level:
|
|
224
|
+
self.logger.setLevel(min_level)
|
|
225
|
+
|
|
226
|
+
def __add_console_logger(self, level: str, fmt: str | None = None) -> logging.Handler:
|
|
227
|
+
fmt = fmt or "%(levelname)s %(name)s: %(message)s"
|
|
228
|
+
handler = logging.StreamHandler()
|
|
229
|
+
handler.setFormatter(logging.Formatter(fmt))
|
|
230
|
+
handler.setLevel(level)
|
|
231
|
+
self.logger.addHandler(handler)
|
|
232
|
+
self._ensure_logger_level()
|
|
233
|
+
return handler
|
|
234
|
+
|
|
235
|
+
def update_console_level(self, new_level: str | None = None) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Set the logging level for console messages.
|
|
238
|
+
"""
|
|
239
|
+
new_level = new_level or AppLog.DEFAULT_LOG_CONSOLE_LEVEL
|
|
240
|
+
self.console_handler.setLevel(new_level.upper())
|
|
241
|
+
self._ensure_logger_level()
|
|
242
|
+
|
|
243
|
+
def update_file_level(self, new_level: str | None = None) -> None:
|
|
244
|
+
if self.file_handler:
|
|
245
|
+
new_level = new_level or AppLog.DEFAULT_LOG_FILE_LEVEL
|
|
246
|
+
self.file_handler.setLevel(new_level.upper())
|
|
247
|
+
self._ensure_logger_level()
|
|
248
|
+
|
|
249
|
+
def add_file_logger(
|
|
250
|
+
self,
|
|
251
|
+
path: str | Path,
|
|
252
|
+
level: str | None = None,
|
|
253
|
+
fmt: str | None = None,
|
|
254
|
+
max_bytes: int | None = None,
|
|
255
|
+
backup_count: int = 4,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""
|
|
258
|
+
Add a log file.
|
|
259
|
+
"""
|
|
260
|
+
path = Path(path)
|
|
261
|
+
fmt = fmt or "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
|
262
|
+
level = level or AppLog.DEFAULT_LOG_FILE_LEVEL
|
|
263
|
+
max_bytes = max_bytes or int(50e6)
|
|
264
|
+
|
|
265
|
+
if not path.parent.is_dir():
|
|
266
|
+
self.logger.info(f"Generating log file parent directory: {path.parent!r}")
|
|
267
|
+
path.parent.mkdir(exist_ok=True, parents=True)
|
|
268
|
+
|
|
269
|
+
handler = logging.handlers.RotatingFileHandler(
|
|
270
|
+
filename=path,
|
|
271
|
+
maxBytes=max_bytes,
|
|
272
|
+
backupCount=backup_count,
|
|
273
|
+
)
|
|
274
|
+
handler.setFormatter(logging.Formatter(fmt))
|
|
275
|
+
handler.setLevel(level.upper())
|
|
276
|
+
self.logger.addHandler(handler)
|
|
277
|
+
self.file_handler = handler
|
|
278
|
+
self._ensure_logger_level()
|
|
279
|
+
|
|
280
|
+
def remove_file_handler(self) -> None:
|
|
281
|
+
"""Remove the file handler."""
|
|
282
|
+
if self.file_handler:
|
|
283
|
+
self.logger.debug(
|
|
284
|
+
f"Removing file handler from the AppLog: {self.file_handler!r}."
|
|
285
|
+
)
|
|
286
|
+
self.logger.removeHandler(self.file_handler)
|
|
287
|
+
self.file_handler = None
|
|
288
|
+
self._ensure_logger_level()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow persistence subsystem.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .base import PersistentStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def store_cls_from_str(store_format: str) -> type[PersistentStore]:
|
|
13
|
+
"""
|
|
14
|
+
Get the class that implements the persistence store from its name.
|
|
15
|
+
"""
|
|
16
|
+
from .discovery import store_cls_from_str as impl
|
|
17
|
+
|
|
18
|
+
return impl(store_format)
|