torc-client 0.5.0__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.
- torc_client-0.5.0/PKG-INFO +60 -0
- torc_client-0.5.0/README.md +1 -0
- torc_client-0.5.0/pyproject.toml +158 -0
- torc_client-0.5.0/setup.cfg +4 -0
- torc_client-0.5.0/src/torc/__init__.py +32 -0
- torc_client-0.5.0/src/torc/api.py +334 -0
- torc_client-0.5.0/src/torc/apps/__init__.py +0 -0
- torc_client-0.5.0/src/torc/apps/management_console.py +1021 -0
- torc_client-0.5.0/src/torc/async_cli_command.py +220 -0
- torc_client-0.5.0/src/torc/cli/__init__.py +0 -0
- torc_client-0.5.0/src/torc/cli/collections.py +354 -0
- torc_client-0.5.0/src/torc/cli/common.py +333 -0
- torc_client-0.5.0/src/torc/cli/compute_nodes.py +79 -0
- torc_client-0.5.0/src/torc/cli/config.py +101 -0
- torc_client-0.5.0/src/torc/cli/events.py +172 -0
- torc_client-0.5.0/src/torc/cli/export.py +230 -0
- torc_client-0.5.0/src/torc/cli/files.py +139 -0
- torc_client-0.5.0/src/torc/cli/graphs.py +133 -0
- torc_client-0.5.0/src/torc/cli/hpc.py +13 -0
- torc_client-0.5.0/src/torc/cli/jobs.py +546 -0
- torc_client-0.5.0/src/torc/cli/reports.py +168 -0
- torc_client-0.5.0/src/torc/cli/resource_requirements.py +275 -0
- torc_client-0.5.0/src/torc/cli/results.py +130 -0
- torc_client-0.5.0/src/torc/cli/run_function.py +75 -0
- torc_client-0.5.0/src/torc/cli/run_postprocess.py +79 -0
- torc_client-0.5.0/src/torc/cli/slurm.py +778 -0
- torc_client-0.5.0/src/torc/cli/stats.py +40 -0
- torc_client-0.5.0/src/torc/cli/torc.py +150 -0
- torc_client-0.5.0/src/torc/cli/tui.py +12 -0
- torc_client-0.5.0/src/torc/cli/user_data.py +251 -0
- torc_client-0.5.0/src/torc/cli/workflows.py +1029 -0
- torc_client-0.5.0/src/torc/common.py +85 -0
- torc_client-0.5.0/src/torc/config.py +22 -0
- torc_client-0.5.0/src/torc/exceptions.py +25 -0
- torc_client-0.5.0/src/torc/hpc/__init__.py +0 -0
- torc_client-0.5.0/src/torc/hpc/common.py +44 -0
- torc_client-0.5.0/src/torc/hpc/hpc_interface.py +143 -0
- torc_client-0.5.0/src/torc/hpc/hpc_manager.py +137 -0
- torc_client-0.5.0/src/torc/hpc/slurm_interface.py +311 -0
- torc_client-0.5.0/src/torc/job_runner.py +864 -0
- torc_client-0.5.0/src/torc/loggers.py +73 -0
- torc_client-0.5.0/src/torc/openapi_client/__init__.py +94 -0
- torc_client-0.5.0/src/torc/openapi_client/api/__init__.py +4 -0
- torc_client-0.5.0/src/torc/openapi_client/api/default_api.py +41583 -0
- torc_client-0.5.0/src/torc/openapi_client/api_client.py +797 -0
- torc_client-0.5.0/src/torc/openapi_client/api_response.py +21 -0
- torc_client-0.5.0/src/torc/openapi_client/configuration.py +572 -0
- torc_client-0.5.0/src/torc/openapi_client/exceptions.py +216 -0
- torc_client-0.5.0/src/torc/openapi_client/models/__init__.py +77 -0
- torc_client-0.5.0/src/torc/openapi_client/models/add_jobs_response.py +93 -0
- torc_client-0.5.0/src/torc/openapi_client/models/auto_tune_status.py +87 -0
- torc_client-0.5.0/src/torc/openapi_client/models/aws_scheduler_model.py +91 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_node_model.py +107 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_node_resource_stats_model.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_node_schedule_params.py +91 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_node_stats.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_node_stats_model.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/compute_nodes_resources.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/default_error_response.py +91 -0
- torc_client-0.5.0/src/torc/openapi_client/models/edge_model.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/file_model.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/get_dot_graph_response.py +85 -0
- torc_client-0.5.0/src/torc/openapi_client/models/get_ready_job_requirements_response.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/is_complete_response.py +87 -0
- torc_client-0.5.0/src/torc/openapi_client/models/job_model.py +127 -0
- torc_client-0.5.0/src/torc/openapi_client/models/job_process_stats_model.py +105 -0
- torc_client-0.5.0/src/torc/openapi_client/models/job_specification_model.py +115 -0
- torc_client-0.5.0/src/torc/openapi_client/models/jobs_internal.py +97 -0
- torc_client-0.5.0/src/torc/openapi_client/models/jobs_model.py +93 -0
- torc_client-0.5.0/src/torc/openapi_client/models/join_by_inbound_edge_collection_edge_response.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/join_by_outbound_edge_collection_edge_response.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_aws_schedulers_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_collection_names_response.py +85 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_compute_node_stats_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_compute_nodes_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_edges_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_events_response.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_files_produced_by_job.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_files_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_job_process_stats_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_job_specifications_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_job_user_data_consumes_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_job_user_data_stores_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_by_needs_file_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_by_status_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_local_schedulers_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_missing_user_data_response.py +85 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_required_existing_files_response.py +85 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_resource_requirements_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_results_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_scheduled_compute_nodes_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_slurm_schedulers_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_user_data_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/list_workflows_response.py +103 -0
- torc_client-0.5.0/src/torc/openapi_client/models/local_scheduler_model.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/prepare_jobs_for_scheduling_response.py +93 -0
- torc_client-0.5.0/src/torc/openapi_client/models/prepare_jobs_for_submission_response.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/prepare_next_jobs_for_submission_response.py +93 -0
- torc_client-0.5.0/src/torc/openapi_client/models/process_changed_job_inputs_response.py +85 -0
- torc_client-0.5.0/src/torc/openapi_client/models/resource_requirements_model.py +101 -0
- torc_client-0.5.0/src/torc/openapi_client/models/result_model.py +101 -0
- torc_client-0.5.0/src/torc/openapi_client/models/scheduled_compute_nodes_model.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/slurm_scheduler_model.py +111 -0
- torc_client-0.5.0/src/torc/openapi_client/models/user_data_model.py +95 -0
- torc_client-0.5.0/src/torc/openapi_client/models/workflow_config_model.py +119 -0
- torc_client-0.5.0/src/torc/openapi_client/models/workflow_model.py +99 -0
- torc_client-0.5.0/src/torc/openapi_client/models/workflow_specification_model.py +143 -0
- torc_client-0.5.0/src/torc/openapi_client/models/workflow_specifications_schedulers.py +113 -0
- torc_client-0.5.0/src/torc/openapi_client/models/workflow_status_model.py +99 -0
- torc_client-0.5.0/src/torc/openapi_client/py.typed +0 -0
- torc_client-0.5.0/src/torc/openapi_client/rest.py +258 -0
- torc_client-0.5.0/src/torc/py.typed +0 -0
- torc_client-0.5.0/src/torc/resource_monitor_reports.py +144 -0
- torc_client-0.5.0/src/torc/tests/__init__.py +0 -0
- torc_client-0.5.0/src/torc/tests/database_interface.py +67 -0
- torc_client-0.5.0/src/torc/utils/__init__.py +0 -0
- torc_client-0.5.0/src/torc/utils/cpu_affinity_mask_tracker.py +51 -0
- torc_client-0.5.0/src/torc/utils/files.py +124 -0
- torc_client-0.5.0/src/torc/utils/filesystem_factory.py +78 -0
- torc_client-0.5.0/src/torc/utils/run_command.py +142 -0
- torc_client-0.5.0/src/torc/utils/sql.py +119 -0
- torc_client-0.5.0/src/torc/workflow_builder.py +320 -0
- torc_client-0.5.0/src/torc/workflow_manager.py +323 -0
- torc_client-0.5.0/src/torc_client.egg-info/PKG-INFO +60 -0
- torc_client-0.5.0/src/torc_client.egg-info/SOURCES.txt +139 -0
- torc_client-0.5.0/src/torc_client.egg-info/dependency_links.txt +1 -0
- torc_client-0.5.0/src/torc_client.egg-info/entry_points.txt +2 -0
- torc_client-0.5.0/src/torc_client.egg-info/requires.txt +48 -0
- torc_client-0.5.0/src/torc_client.egg-info/top_level.txt +1 -0
- torc_client-0.5.0/tests/test_api.py +142 -0
- torc_client-0.5.0/tests/test_auto_tune_workflow.py +184 -0
- torc_client-0.5.0/tests/test_cpu_affinity_mask_tracker.py +30 -0
- torc_client-0.5.0/tests/test_examples.py +42 -0
- torc_client-0.5.0/tests/test_fake_slurm_workflow.py +195 -0
- torc_client-0.5.0/tests/test_jobs.py +15 -0
- torc_client-0.5.0/tests/test_prepare_jobs_for_submission.py +163 -0
- torc_client-0.5.0/tests/test_run_command.py +27 -0
- torc_client-0.5.0/tests/test_slurm_workflows.py +319 -0
- torc_client-0.5.0/tests/test_terminated_jobs.py +50 -0
- torc_client-0.5.0/tests/test_workflow.py +816 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torc-client
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Workflow management system
|
|
5
|
+
Author-email: Daniel Thom <daniel.thom@nrel.gov>, Joseph McKinsey <joseph.mckinsey@nrel.gov>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Keywords: hpc,workflow,pipeline
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Natural Language :: English
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: <3.14,>=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: click<9,>=8.2
|
|
14
|
+
Requires-Dist: dynaconf
|
|
15
|
+
Requires-Dist: graphviz
|
|
16
|
+
Requires-Dist: json5
|
|
17
|
+
Requires-Dist: isodate
|
|
18
|
+
Requires-Dist: loguru
|
|
19
|
+
Requires-Dist: plotly<6,>=5.19
|
|
20
|
+
Requires-Dist: psutil<6,>=5.9
|
|
21
|
+
Requires-Dist: prettytable<4,>=3.10
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
23
|
+
Requires-Dist: rmon
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: rich_click
|
|
26
|
+
Requires-Dist: textual<4,>=3.2
|
|
27
|
+
Requires-Dist: toml
|
|
28
|
+
Requires-Dist: urllib3<3.0.0,>=2.1.0
|
|
29
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
30
|
+
Requires-Dist: typing-extensions>=4.7.1
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: black; extra == "dev"
|
|
33
|
+
Requires-Dist: bump-my-version; extra == "dev"
|
|
34
|
+
Requires-Dist: filelock; extra == "dev"
|
|
35
|
+
Requires-Dist: flake8; extra == "dev"
|
|
36
|
+
Requires-Dist: furo; extra == "dev"
|
|
37
|
+
Requires-Dist: ghp-import; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy; extra == "dev"
|
|
39
|
+
Requires-Dist: myst_parser; extra == "dev"
|
|
40
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff; extra == "dev"
|
|
44
|
+
Requires-Dist: sphinx; extra == "dev"
|
|
45
|
+
Requires-Dist: sphinx-click; extra == "dev"
|
|
46
|
+
Requires-Dist: sphinxcontrib-openapi; extra == "dev"
|
|
47
|
+
Requires-Dist: autodoc_pydantic~=2.0; extra == "dev"
|
|
48
|
+
Requires-Dist: sphinx-copybutton; extra == "dev"
|
|
49
|
+
Requires-Dist: sphinx-tabs~=3.4; extra == "dev"
|
|
50
|
+
Requires-Dist: textual-dev; extra == "dev"
|
|
51
|
+
Requires-Dist: types-networkx; extra == "dev"
|
|
52
|
+
Requires-Dist: types-python-dateutil; extra == "dev"
|
|
53
|
+
Requires-Dist: types-psutil; extra == "dev"
|
|
54
|
+
Requires-Dist: types-toml; extra == "dev"
|
|
55
|
+
Provides-Extra: plots
|
|
56
|
+
Requires-Dist: networkx; extra == "plots"
|
|
57
|
+
Requires-Dist: networkxgmml; extra == "plots"
|
|
58
|
+
Requires-Dist: pygraphviz; extra == "plots"
|
|
59
|
+
|
|
60
|
+
# Workflow Management System
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Workflow Management System
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "torc-client"
|
|
7
|
+
# Note: Do not update manually. Use bump-my-version, such as
|
|
8
|
+
# $ bump-my-version bump minor
|
|
9
|
+
version = "0.5.0"
|
|
10
|
+
description = "Workflow management system"
|
|
11
|
+
requires-python = ">=3.11,<3.14"
|
|
12
|
+
license = "BSD-3-Clause"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Daniel Thom", email = "daniel.thom@nrel.gov" },
|
|
16
|
+
{ name = "Joseph McKinsey", email = "joseph.mckinsey@nrel.gov" },
|
|
17
|
+
]
|
|
18
|
+
keywords = ["hpc", "workflow", "pipeline"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Natural Language :: English",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"click >= 8.2, < 9",
|
|
26
|
+
"dynaconf",
|
|
27
|
+
"graphviz",
|
|
28
|
+
"json5",
|
|
29
|
+
"isodate",
|
|
30
|
+
"loguru",
|
|
31
|
+
"plotly >= 5.19, < 6",
|
|
32
|
+
"psutil >= 5.9, < 6",
|
|
33
|
+
"prettytable >= 3.10, < 4",
|
|
34
|
+
"pydantic >= 2.10, < 3",
|
|
35
|
+
"rmon",
|
|
36
|
+
"rich",
|
|
37
|
+
"rich_click",
|
|
38
|
+
"textual >= 3.2, < 4",
|
|
39
|
+
"toml",
|
|
40
|
+
# These are required by the openapi_client. Keep in sync with its setup.py.
|
|
41
|
+
"urllib3 >= 2.1.0, < 3.0.0",
|
|
42
|
+
"python-dateutil >= 2.8.2",
|
|
43
|
+
"typing-extensions >= 4.7.1",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = [
|
|
51
|
+
"black",
|
|
52
|
+
"bump-my-version",
|
|
53
|
+
"filelock",
|
|
54
|
+
"flake8",
|
|
55
|
+
"furo",
|
|
56
|
+
"ghp-import",
|
|
57
|
+
"mypy",
|
|
58
|
+
"myst_parser",
|
|
59
|
+
"pre-commit",
|
|
60
|
+
"pytest",
|
|
61
|
+
"pytest-cov",
|
|
62
|
+
"ruff",
|
|
63
|
+
"sphinx",
|
|
64
|
+
"sphinx-click",
|
|
65
|
+
"sphinxcontrib-openapi",
|
|
66
|
+
"autodoc_pydantic~=2.0",
|
|
67
|
+
"sphinx-copybutton",
|
|
68
|
+
"sphinx-tabs~=3.4",
|
|
69
|
+
"textual-dev",
|
|
70
|
+
"types-networkx",
|
|
71
|
+
"types-python-dateutil",
|
|
72
|
+
"types-psutil",
|
|
73
|
+
"types-toml",
|
|
74
|
+
]
|
|
75
|
+
plots = ["networkx", "networkxgmml", "pygraphviz"]
|
|
76
|
+
|
|
77
|
+
[project.scripts]
|
|
78
|
+
torc = "torc.cli.torc:cli"
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
pythonpath = "src"
|
|
82
|
+
minversion = "6.0"
|
|
83
|
+
addopts = "-ra"
|
|
84
|
+
testpaths = ["tests"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
# Exclude a variety of commonly ignored directories.
|
|
88
|
+
exclude = [
|
|
89
|
+
".git",
|
|
90
|
+
".ruff_cache",
|
|
91
|
+
".venv",
|
|
92
|
+
"_build",
|
|
93
|
+
"build",
|
|
94
|
+
"dist",
|
|
95
|
+
"env",
|
|
96
|
+
"venv",
|
|
97
|
+
"src/torc/openapi_client/*",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
line-length = 99
|
|
101
|
+
indent-width = 4
|
|
102
|
+
|
|
103
|
+
target-version = "py312"
|
|
104
|
+
|
|
105
|
+
[tool.mypy]
|
|
106
|
+
check_untyped_defs = true
|
|
107
|
+
files = [
|
|
108
|
+
"src",
|
|
109
|
+
"tests",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[[tool.mypy.overrides]]
|
|
113
|
+
ignore_missing_imports = true
|
|
114
|
+
module = "graphviz.*"
|
|
115
|
+
|
|
116
|
+
[[tool.mypy.overrides]]
|
|
117
|
+
ignore_missing_imports = true
|
|
118
|
+
module = "networkxgmml.*"
|
|
119
|
+
|
|
120
|
+
[[tool.mypy.overrides]]
|
|
121
|
+
ignore_missing_imports = true
|
|
122
|
+
module = "isodate.*"
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint]
|
|
125
|
+
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
|
126
|
+
select = [
|
|
127
|
+
"C901", # McCabe complexity
|
|
128
|
+
"E4", # Subset of pycodestyle (E)
|
|
129
|
+
"E7",
|
|
130
|
+
"E9",
|
|
131
|
+
"EM", # string formatting in an exception message
|
|
132
|
+
"F", # Pyflakes
|
|
133
|
+
"W", # pycodestyle warnings
|
|
134
|
+
]
|
|
135
|
+
ignore = []
|
|
136
|
+
|
|
137
|
+
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
138
|
+
fixable = ["ALL"]
|
|
139
|
+
unfixable = []
|
|
140
|
+
|
|
141
|
+
[tool.ruff.format]
|
|
142
|
+
quote-style = "double"
|
|
143
|
+
indent-style = "space"
|
|
144
|
+
skip-magic-trailing-comma = false
|
|
145
|
+
line-ending = "auto"
|
|
146
|
+
docstring-code-format = true
|
|
147
|
+
docstring-code-line-length = "dynamic"
|
|
148
|
+
|
|
149
|
+
[tool.ruff.lint.per-file-ignores]
|
|
150
|
+
"__init__.py" = ["E402"]
|
|
151
|
+
"**/{tests,docs,tools}/*" = ["E402"]
|
|
152
|
+
|
|
153
|
+
[tool.coverage.run]
|
|
154
|
+
# List directories or file patterns to omit from coverage
|
|
155
|
+
omit = [
|
|
156
|
+
"tests/*",
|
|
157
|
+
"docs/*",
|
|
158
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""torc package"""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
|
|
6
|
+
from torc.api import (
|
|
7
|
+
add_jobs,
|
|
8
|
+
iter_documents,
|
|
9
|
+
make_api,
|
|
10
|
+
make_job_label,
|
|
11
|
+
map_function_to_jobs,
|
|
12
|
+
send_api_command,
|
|
13
|
+
)
|
|
14
|
+
from torc.config import torc_settings
|
|
15
|
+
from torc.loggers import setup_logging
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__version__ = metadata.metadata("torc-client")["Version"]
|
|
19
|
+
|
|
20
|
+
warnings.filterwarnings("once", category=DeprecationWarning)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
"add_jobs",
|
|
25
|
+
"iter_documents",
|
|
26
|
+
"make_api",
|
|
27
|
+
"make_job_label",
|
|
28
|
+
"map_function_to_jobs",
|
|
29
|
+
"send_api_command",
|
|
30
|
+
"setup_logging",
|
|
31
|
+
"torc_settings",
|
|
32
|
+
)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Functions to access the Torc Database API"""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable, Generator, Optional
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from rmon.timing.timer_stats import Timer
|
|
9
|
+
|
|
10
|
+
from torc.openapi_client import ApiClient, DefaultApi
|
|
11
|
+
from torc.openapi_client.configuration import Configuration
|
|
12
|
+
from torc.openapi_client.rest import ApiException
|
|
13
|
+
from torc.openapi_client.models.job_model import JobModel
|
|
14
|
+
from torc.openapi_client.models.jobs_model import JobsModel
|
|
15
|
+
from torc.openapi_client.models.user_data_model import UserDataModel
|
|
16
|
+
from torc.common import timer_stats_collector, check_function
|
|
17
|
+
from torc.exceptions import DatabaseOffline
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_api(database_url) -> DefaultApi:
|
|
21
|
+
"""Instantiate an OpenAPI client object from a database URL."""
|
|
22
|
+
configuration = Configuration()
|
|
23
|
+
configuration.host = database_url
|
|
24
|
+
return DefaultApi(ApiClient(configuration))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def wait_for_healthy_database(
|
|
28
|
+
api: DefaultApi, timeout_minutes: float = 20, poll_seconds: float = 60
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Ping the database until it's responding or timeout_minutes is exceeded.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
api : DefaultApi
|
|
35
|
+
timeout_minutes : float
|
|
36
|
+
Number of minutes to wait for the database to become healthy.
|
|
37
|
+
poll_seconds : float
|
|
38
|
+
Number of seconds to wait in between each poll.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
DatabaseOffline
|
|
43
|
+
Raised if the timeout is exceeded.
|
|
44
|
+
"""
|
|
45
|
+
logger.info(
|
|
46
|
+
"Wait for the database to become healthy: timeout_minutes={}, poll_seconds={}",
|
|
47
|
+
timeout_minutes,
|
|
48
|
+
poll_seconds,
|
|
49
|
+
)
|
|
50
|
+
end = time.time() + timeout_minutes * 60
|
|
51
|
+
while time.time() < end:
|
|
52
|
+
try:
|
|
53
|
+
send_api_command(api.ping)
|
|
54
|
+
logger.info("The database is healthy again.")
|
|
55
|
+
return
|
|
56
|
+
except DatabaseOffline:
|
|
57
|
+
logger.exception("Database is still offline")
|
|
58
|
+
time.sleep(poll_seconds)
|
|
59
|
+
|
|
60
|
+
msg = "Timed out waiting for database to become healthy"
|
|
61
|
+
raise DatabaseOffline(msg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def iter_documents(func: Callable, *args, skip=0, **kwargs) -> Generator[Any, None, None]:
|
|
65
|
+
"""Return a generator of documents where the API service employs batching.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
func
|
|
70
|
+
API function
|
|
71
|
+
|
|
72
|
+
Yields
|
|
73
|
+
------
|
|
74
|
+
OpenAPI [pydantic] model or dict, depending on what the API function returns
|
|
75
|
+
"""
|
|
76
|
+
if "limit" in kwargs and kwargs["limit"] is None:
|
|
77
|
+
kwargs.pop("limit")
|
|
78
|
+
limit = kwargs.get("limit")
|
|
79
|
+
|
|
80
|
+
has_more = True
|
|
81
|
+
docs_received = 0
|
|
82
|
+
while has_more and (limit is None or docs_received < limit):
|
|
83
|
+
result = func(*args, skip=skip, **kwargs)
|
|
84
|
+
yield from result.items
|
|
85
|
+
skip += result.count
|
|
86
|
+
docs_received += result.count
|
|
87
|
+
has_more = result.has_more
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def make_job_label(job: JobModel, include_status: bool = False) -> str:
|
|
91
|
+
"""Return a user-friendly label for the job for log statements."""
|
|
92
|
+
base = f"Job name={job.name} key={job.key}"
|
|
93
|
+
if include_status:
|
|
94
|
+
return f"{base} status={job.status}"
|
|
95
|
+
return base
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def map_job_keys_to_names(api: DefaultApi, workflow_key, filters=None) -> dict[str, str]:
|
|
99
|
+
"""Return a mapping of job key to name."""
|
|
100
|
+
filters = filters or {}
|
|
101
|
+
return {x.key: x.name for x in iter_documents(api.list_jobs, workflow_key, **filters)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_DATABASE_KEYS = {"_id", "_key", "_rev", "_oldRev", "id", "key", "rev"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def remove_db_keys(data: dict) -> dict[str, Any]:
|
|
108
|
+
"""Remove internal database keys from data."""
|
|
109
|
+
return {x: data[x] for x in set(data) - _DATABASE_KEYS}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def send_api_command(func, *args, raise_on_error=True, timeout=120, **kwargs) -> Any:
|
|
113
|
+
"""Send an API command while tracking time, if timer_stats_collector is enabled.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
func : function
|
|
118
|
+
API function
|
|
119
|
+
args : arguments to forward to func
|
|
120
|
+
raise_on_error : bool
|
|
121
|
+
Raise an exception if there is an error, defaults to True.
|
|
122
|
+
timeout : float
|
|
123
|
+
Timeout in seconds
|
|
124
|
+
kwargs : keyword arguments to forward to func
|
|
125
|
+
|
|
126
|
+
Raises
|
|
127
|
+
------
|
|
128
|
+
ApiException
|
|
129
|
+
Raised for errors detected by the server.
|
|
130
|
+
DatabaseOffline
|
|
131
|
+
Raised for all connection errors.
|
|
132
|
+
"""
|
|
133
|
+
with Timer(timer_stats_collector, func.__name__):
|
|
134
|
+
try:
|
|
135
|
+
logger.trace("Send API command {}", func.__name__)
|
|
136
|
+
return func(*args, _request_timeout=timeout, **kwargs)
|
|
137
|
+
except ApiException:
|
|
138
|
+
# This covers all errors reported by the server.
|
|
139
|
+
logger.exception("Failed to send API command {}", func.__name__)
|
|
140
|
+
if raise_on_error:
|
|
141
|
+
raise
|
|
142
|
+
logger.info("Exception is ignored.")
|
|
143
|
+
return None
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
# This covers all connection errors. It is likely too risky to try to catch
|
|
146
|
+
# all possible errors from the underlying libraries (OS, urllib3, etc).
|
|
147
|
+
logger.exception("Failed to send API command {}", func.__name__)
|
|
148
|
+
if raise_on_error:
|
|
149
|
+
msg = f"Received exception from API client: {exc=}"
|
|
150
|
+
raise DatabaseOffline(msg) from exc
|
|
151
|
+
logger.info("Exception is ignored.")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def add_jobs(api: DefaultApi, workflow_key: str, jobs, max_transfer_size=10_000) -> list[JobModel]:
|
|
156
|
+
"""Add an iterable of jobs to the workflow.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
api : DefaultApi
|
|
161
|
+
workflow_key : str
|
|
162
|
+
jobs : list
|
|
163
|
+
Any iterable of JobModel
|
|
164
|
+
max_transfer_size : int
|
|
165
|
+
Maximum number of jobs to add per API call. 10,000 is recommended.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
list
|
|
170
|
+
List of keys of created jobs. Provided in same order as jobs.
|
|
171
|
+
"""
|
|
172
|
+
added_jobs = []
|
|
173
|
+
batch = []
|
|
174
|
+
for job in jobs:
|
|
175
|
+
batch.append(job)
|
|
176
|
+
if len(batch) > max_transfer_size:
|
|
177
|
+
res = send_api_command(api.add_jobs, workflow_key, JobsModel(jobs=batch))
|
|
178
|
+
added_jobs += res.items
|
|
179
|
+
batch.clear()
|
|
180
|
+
|
|
181
|
+
if batch:
|
|
182
|
+
res = send_api_command(api.add_jobs, workflow_key, JobsModel(jobs=batch))
|
|
183
|
+
added_jobs += res.items
|
|
184
|
+
|
|
185
|
+
return added_jobs
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def map_function_to_jobs(
|
|
189
|
+
api: DefaultApi,
|
|
190
|
+
workflow_key,
|
|
191
|
+
module: str,
|
|
192
|
+
func: str,
|
|
193
|
+
params: list[dict],
|
|
194
|
+
postprocess_func: str | None = None,
|
|
195
|
+
module_directory=None,
|
|
196
|
+
resource_requirements=None,
|
|
197
|
+
scheduler=None,
|
|
198
|
+
start_index=1,
|
|
199
|
+
name_prefix="",
|
|
200
|
+
blocked_by: Optional[list[str]] = None,
|
|
201
|
+
) -> list[JobModel]:
|
|
202
|
+
"""Add a job that will call func for each item in params.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
api : DefaultApi
|
|
207
|
+
workflow_key : str
|
|
208
|
+
module : str
|
|
209
|
+
Name of module that contains func. If it is not available in the Python path, specify
|
|
210
|
+
the parent directory in module_directory.
|
|
211
|
+
func : str
|
|
212
|
+
Name of the function in module to be called.
|
|
213
|
+
params : list[dict]
|
|
214
|
+
Each item in this list will be passed to func. The contents must be serializable to
|
|
215
|
+
JSON.
|
|
216
|
+
postprocess_func : str
|
|
217
|
+
Optional name of the function in module to be called to postprocess all results.
|
|
218
|
+
module_directory : str | None
|
|
219
|
+
Required if module is not importable.
|
|
220
|
+
resource_requirements : str | None
|
|
221
|
+
Optional id of resource_requirements that should be used by each job.
|
|
222
|
+
scheduler : str | None
|
|
223
|
+
Optional id of scheduler that should be used by each job.
|
|
224
|
+
start_index : int
|
|
225
|
+
Starting index to use for job names.
|
|
226
|
+
name_prefix : str
|
|
227
|
+
Prepend job names with this prefix; defaults to an empty string. Names will be the
|
|
228
|
+
index converted to a string.
|
|
229
|
+
blocked_by : None | list[str]
|
|
230
|
+
Job IDs that should block all jobs created by this function.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
list[JobModel]
|
|
235
|
+
"""
|
|
236
|
+
jobs = []
|
|
237
|
+
output_data_ids = []
|
|
238
|
+
for i, job_params in enumerate(params, start=start_index):
|
|
239
|
+
check_function(module, func, module_directory)
|
|
240
|
+
data = {
|
|
241
|
+
"module": module,
|
|
242
|
+
"func": func,
|
|
243
|
+
"params": job_params,
|
|
244
|
+
}
|
|
245
|
+
if module_directory is not None:
|
|
246
|
+
data["module_directory"] = module_directory
|
|
247
|
+
job_name = f"{name_prefix}{i}"
|
|
248
|
+
input_ud = api.add_user_data(
|
|
249
|
+
workflow_key, UserDataModel(name=f"input_{job_name}", data=data)
|
|
250
|
+
)
|
|
251
|
+
output_ud = api.add_user_data(
|
|
252
|
+
workflow_key, UserDataModel(name=f"output_{job_name}", data={})
|
|
253
|
+
)
|
|
254
|
+
assert input_ud.id is not None
|
|
255
|
+
assert output_ud.id is not None
|
|
256
|
+
output_data_ids.append(output_ud.id)
|
|
257
|
+
job = JobModel(
|
|
258
|
+
name=job_name,
|
|
259
|
+
command="torc jobs run-function",
|
|
260
|
+
input_user_data=[input_ud.id],
|
|
261
|
+
output_user_data=[output_ud.id],
|
|
262
|
+
resource_requirements=resource_requirements,
|
|
263
|
+
scheduler=scheduler,
|
|
264
|
+
blocked_by=blocked_by,
|
|
265
|
+
)
|
|
266
|
+
jobs.append(job)
|
|
267
|
+
|
|
268
|
+
if postprocess_func is not None:
|
|
269
|
+
check_function(module, postprocess_func, module_directory)
|
|
270
|
+
data = {
|
|
271
|
+
"module": module,
|
|
272
|
+
"func": postprocess_func,
|
|
273
|
+
}
|
|
274
|
+
if module_directory is not None:
|
|
275
|
+
data["module_directory"] = module_directory
|
|
276
|
+
input_ud = api.add_user_data(
|
|
277
|
+
workflow_key, UserDataModel(name="input_postprocess", data=data)
|
|
278
|
+
)
|
|
279
|
+
output_ud = api.add_user_data(
|
|
280
|
+
workflow_key, UserDataModel(name="postprocess_result", data=data)
|
|
281
|
+
)
|
|
282
|
+
assert input_ud.id is not None
|
|
283
|
+
assert output_ud.id is not None
|
|
284
|
+
jobs.append(
|
|
285
|
+
JobModel(
|
|
286
|
+
name="postprocess",
|
|
287
|
+
command="torc jobs run-postprocess",
|
|
288
|
+
input_user_data=[input_ud.id] + output_data_ids,
|
|
289
|
+
output_user_data=[output_ud.id],
|
|
290
|
+
resource_requirements=resource_requirements,
|
|
291
|
+
scheduler=scheduler,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return add_jobs(api, workflow_key, jobs)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def sanitize_workflow(data: dict[str, Any]) -> dict[str, Any]:
|
|
299
|
+
"""Sanitize a WorkflowSpecificationModel dictionary in place so that it can be loaded into
|
|
300
|
+
the database.
|
|
301
|
+
"""
|
|
302
|
+
for item in itertools.chain(
|
|
303
|
+
[data.get("config")],
|
|
304
|
+
data.get("files", []),
|
|
305
|
+
data.get("resource_requirements", []),
|
|
306
|
+
):
|
|
307
|
+
if item is not None:
|
|
308
|
+
for key in _DATABASE_KEYS:
|
|
309
|
+
item.pop(key, None)
|
|
310
|
+
_sanitize_collections(data)
|
|
311
|
+
_sanitize_schedulers(data)
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _sanitize_collections(data: dict[str, Any]) -> None:
|
|
316
|
+
for collection in ("jobs", "resource_requirements", "files", "schedulers"):
|
|
317
|
+
if collection in data and not data[collection]:
|
|
318
|
+
data.pop(collection)
|
|
319
|
+
for collection in ("jobs", "resource_requirements", "files"):
|
|
320
|
+
for item in data.get(collection, []):
|
|
321
|
+
for field in [k for k, v in item.items() if v is None]:
|
|
322
|
+
item.pop(field)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _sanitize_schedulers(data: dict[str, Any]) -> None:
|
|
326
|
+
for field in ("aws_schedulers", "local_schedulers", "slurm_schedulers"):
|
|
327
|
+
schedulers = data.get("schedulers", {})
|
|
328
|
+
if schedulers and field in schedulers and not schedulers[field]:
|
|
329
|
+
data["schedulers"].pop(field)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def list_model_fields(cls) -> list[str]:
|
|
333
|
+
"""Return a list of the model's fields."""
|
|
334
|
+
return list(cls.model_json_schema()["properties"].keys())
|
|
File without changes
|