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.
Files changed (141) hide show
  1. torc_client-0.5.0/PKG-INFO +60 -0
  2. torc_client-0.5.0/README.md +1 -0
  3. torc_client-0.5.0/pyproject.toml +158 -0
  4. torc_client-0.5.0/setup.cfg +4 -0
  5. torc_client-0.5.0/src/torc/__init__.py +32 -0
  6. torc_client-0.5.0/src/torc/api.py +334 -0
  7. torc_client-0.5.0/src/torc/apps/__init__.py +0 -0
  8. torc_client-0.5.0/src/torc/apps/management_console.py +1021 -0
  9. torc_client-0.5.0/src/torc/async_cli_command.py +220 -0
  10. torc_client-0.5.0/src/torc/cli/__init__.py +0 -0
  11. torc_client-0.5.0/src/torc/cli/collections.py +354 -0
  12. torc_client-0.5.0/src/torc/cli/common.py +333 -0
  13. torc_client-0.5.0/src/torc/cli/compute_nodes.py +79 -0
  14. torc_client-0.5.0/src/torc/cli/config.py +101 -0
  15. torc_client-0.5.0/src/torc/cli/events.py +172 -0
  16. torc_client-0.5.0/src/torc/cli/export.py +230 -0
  17. torc_client-0.5.0/src/torc/cli/files.py +139 -0
  18. torc_client-0.5.0/src/torc/cli/graphs.py +133 -0
  19. torc_client-0.5.0/src/torc/cli/hpc.py +13 -0
  20. torc_client-0.5.0/src/torc/cli/jobs.py +546 -0
  21. torc_client-0.5.0/src/torc/cli/reports.py +168 -0
  22. torc_client-0.5.0/src/torc/cli/resource_requirements.py +275 -0
  23. torc_client-0.5.0/src/torc/cli/results.py +130 -0
  24. torc_client-0.5.0/src/torc/cli/run_function.py +75 -0
  25. torc_client-0.5.0/src/torc/cli/run_postprocess.py +79 -0
  26. torc_client-0.5.0/src/torc/cli/slurm.py +778 -0
  27. torc_client-0.5.0/src/torc/cli/stats.py +40 -0
  28. torc_client-0.5.0/src/torc/cli/torc.py +150 -0
  29. torc_client-0.5.0/src/torc/cli/tui.py +12 -0
  30. torc_client-0.5.0/src/torc/cli/user_data.py +251 -0
  31. torc_client-0.5.0/src/torc/cli/workflows.py +1029 -0
  32. torc_client-0.5.0/src/torc/common.py +85 -0
  33. torc_client-0.5.0/src/torc/config.py +22 -0
  34. torc_client-0.5.0/src/torc/exceptions.py +25 -0
  35. torc_client-0.5.0/src/torc/hpc/__init__.py +0 -0
  36. torc_client-0.5.0/src/torc/hpc/common.py +44 -0
  37. torc_client-0.5.0/src/torc/hpc/hpc_interface.py +143 -0
  38. torc_client-0.5.0/src/torc/hpc/hpc_manager.py +137 -0
  39. torc_client-0.5.0/src/torc/hpc/slurm_interface.py +311 -0
  40. torc_client-0.5.0/src/torc/job_runner.py +864 -0
  41. torc_client-0.5.0/src/torc/loggers.py +73 -0
  42. torc_client-0.5.0/src/torc/openapi_client/__init__.py +94 -0
  43. torc_client-0.5.0/src/torc/openapi_client/api/__init__.py +4 -0
  44. torc_client-0.5.0/src/torc/openapi_client/api/default_api.py +41583 -0
  45. torc_client-0.5.0/src/torc/openapi_client/api_client.py +797 -0
  46. torc_client-0.5.0/src/torc/openapi_client/api_response.py +21 -0
  47. torc_client-0.5.0/src/torc/openapi_client/configuration.py +572 -0
  48. torc_client-0.5.0/src/torc/openapi_client/exceptions.py +216 -0
  49. torc_client-0.5.0/src/torc/openapi_client/models/__init__.py +77 -0
  50. torc_client-0.5.0/src/torc/openapi_client/models/add_jobs_response.py +93 -0
  51. torc_client-0.5.0/src/torc/openapi_client/models/auto_tune_status.py +87 -0
  52. torc_client-0.5.0/src/torc/openapi_client/models/aws_scheduler_model.py +91 -0
  53. torc_client-0.5.0/src/torc/openapi_client/models/compute_node_model.py +107 -0
  54. torc_client-0.5.0/src/torc/openapi_client/models/compute_node_resource_stats_model.py +103 -0
  55. torc_client-0.5.0/src/torc/openapi_client/models/compute_node_schedule_params.py +91 -0
  56. torc_client-0.5.0/src/torc/openapi_client/models/compute_node_stats.py +95 -0
  57. torc_client-0.5.0/src/torc/openapi_client/models/compute_node_stats_model.py +103 -0
  58. torc_client-0.5.0/src/torc/openapi_client/models/compute_nodes_resources.py +95 -0
  59. torc_client-0.5.0/src/torc/openapi_client/models/default_error_response.py +91 -0
  60. torc_client-0.5.0/src/torc/openapi_client/models/edge_model.py +95 -0
  61. torc_client-0.5.0/src/torc/openapi_client/models/file_model.py +95 -0
  62. torc_client-0.5.0/src/torc/openapi_client/models/get_dot_graph_response.py +85 -0
  63. torc_client-0.5.0/src/torc/openapi_client/models/get_ready_job_requirements_response.py +95 -0
  64. torc_client-0.5.0/src/torc/openapi_client/models/is_complete_response.py +87 -0
  65. torc_client-0.5.0/src/torc/openapi_client/models/job_model.py +127 -0
  66. torc_client-0.5.0/src/torc/openapi_client/models/job_process_stats_model.py +105 -0
  67. torc_client-0.5.0/src/torc/openapi_client/models/job_specification_model.py +115 -0
  68. torc_client-0.5.0/src/torc/openapi_client/models/jobs_internal.py +97 -0
  69. torc_client-0.5.0/src/torc/openapi_client/models/jobs_model.py +93 -0
  70. torc_client-0.5.0/src/torc/openapi_client/models/join_by_inbound_edge_collection_edge_response.py +95 -0
  71. torc_client-0.5.0/src/torc/openapi_client/models/join_by_outbound_edge_collection_edge_response.py +95 -0
  72. torc_client-0.5.0/src/torc/openapi_client/models/list_aws_schedulers_response.py +103 -0
  73. torc_client-0.5.0/src/torc/openapi_client/models/list_collection_names_response.py +85 -0
  74. torc_client-0.5.0/src/torc/openapi_client/models/list_compute_node_stats_response.py +103 -0
  75. torc_client-0.5.0/src/torc/openapi_client/models/list_compute_nodes_response.py +103 -0
  76. torc_client-0.5.0/src/torc/openapi_client/models/list_edges_response.py +103 -0
  77. torc_client-0.5.0/src/torc/openapi_client/models/list_events_response.py +95 -0
  78. torc_client-0.5.0/src/torc/openapi_client/models/list_files_produced_by_job.py +103 -0
  79. torc_client-0.5.0/src/torc/openapi_client/models/list_files_response.py +103 -0
  80. torc_client-0.5.0/src/torc/openapi_client/models/list_job_process_stats_response.py +103 -0
  81. torc_client-0.5.0/src/torc/openapi_client/models/list_job_specifications_response.py +103 -0
  82. torc_client-0.5.0/src/torc/openapi_client/models/list_job_user_data_consumes_response.py +103 -0
  83. torc_client-0.5.0/src/torc/openapi_client/models/list_job_user_data_stores_response.py +103 -0
  84. torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_by_needs_file_response.py +103 -0
  85. torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_by_status_response.py +103 -0
  86. torc_client-0.5.0/src/torc/openapi_client/models/list_jobs_response.py +103 -0
  87. torc_client-0.5.0/src/torc/openapi_client/models/list_local_schedulers_response.py +103 -0
  88. torc_client-0.5.0/src/torc/openapi_client/models/list_missing_user_data_response.py +85 -0
  89. torc_client-0.5.0/src/torc/openapi_client/models/list_required_existing_files_response.py +85 -0
  90. torc_client-0.5.0/src/torc/openapi_client/models/list_resource_requirements_response.py +103 -0
  91. torc_client-0.5.0/src/torc/openapi_client/models/list_results_response.py +103 -0
  92. torc_client-0.5.0/src/torc/openapi_client/models/list_scheduled_compute_nodes_response.py +103 -0
  93. torc_client-0.5.0/src/torc/openapi_client/models/list_slurm_schedulers_response.py +103 -0
  94. torc_client-0.5.0/src/torc/openapi_client/models/list_user_data_response.py +103 -0
  95. torc_client-0.5.0/src/torc/openapi_client/models/list_workflows_response.py +103 -0
  96. torc_client-0.5.0/src/torc/openapi_client/models/local_scheduler_model.py +95 -0
  97. torc_client-0.5.0/src/torc/openapi_client/models/prepare_jobs_for_scheduling_response.py +93 -0
  98. torc_client-0.5.0/src/torc/openapi_client/models/prepare_jobs_for_submission_response.py +95 -0
  99. torc_client-0.5.0/src/torc/openapi_client/models/prepare_next_jobs_for_submission_response.py +93 -0
  100. torc_client-0.5.0/src/torc/openapi_client/models/process_changed_job_inputs_response.py +85 -0
  101. torc_client-0.5.0/src/torc/openapi_client/models/resource_requirements_model.py +101 -0
  102. torc_client-0.5.0/src/torc/openapi_client/models/result_model.py +101 -0
  103. torc_client-0.5.0/src/torc/openapi_client/models/scheduled_compute_nodes_model.py +95 -0
  104. torc_client-0.5.0/src/torc/openapi_client/models/slurm_scheduler_model.py +111 -0
  105. torc_client-0.5.0/src/torc/openapi_client/models/user_data_model.py +95 -0
  106. torc_client-0.5.0/src/torc/openapi_client/models/workflow_config_model.py +119 -0
  107. torc_client-0.5.0/src/torc/openapi_client/models/workflow_model.py +99 -0
  108. torc_client-0.5.0/src/torc/openapi_client/models/workflow_specification_model.py +143 -0
  109. torc_client-0.5.0/src/torc/openapi_client/models/workflow_specifications_schedulers.py +113 -0
  110. torc_client-0.5.0/src/torc/openapi_client/models/workflow_status_model.py +99 -0
  111. torc_client-0.5.0/src/torc/openapi_client/py.typed +0 -0
  112. torc_client-0.5.0/src/torc/openapi_client/rest.py +258 -0
  113. torc_client-0.5.0/src/torc/py.typed +0 -0
  114. torc_client-0.5.0/src/torc/resource_monitor_reports.py +144 -0
  115. torc_client-0.5.0/src/torc/tests/__init__.py +0 -0
  116. torc_client-0.5.0/src/torc/tests/database_interface.py +67 -0
  117. torc_client-0.5.0/src/torc/utils/__init__.py +0 -0
  118. torc_client-0.5.0/src/torc/utils/cpu_affinity_mask_tracker.py +51 -0
  119. torc_client-0.5.0/src/torc/utils/files.py +124 -0
  120. torc_client-0.5.0/src/torc/utils/filesystem_factory.py +78 -0
  121. torc_client-0.5.0/src/torc/utils/run_command.py +142 -0
  122. torc_client-0.5.0/src/torc/utils/sql.py +119 -0
  123. torc_client-0.5.0/src/torc/workflow_builder.py +320 -0
  124. torc_client-0.5.0/src/torc/workflow_manager.py +323 -0
  125. torc_client-0.5.0/src/torc_client.egg-info/PKG-INFO +60 -0
  126. torc_client-0.5.0/src/torc_client.egg-info/SOURCES.txt +139 -0
  127. torc_client-0.5.0/src/torc_client.egg-info/dependency_links.txt +1 -0
  128. torc_client-0.5.0/src/torc_client.egg-info/entry_points.txt +2 -0
  129. torc_client-0.5.0/src/torc_client.egg-info/requires.txt +48 -0
  130. torc_client-0.5.0/src/torc_client.egg-info/top_level.txt +1 -0
  131. torc_client-0.5.0/tests/test_api.py +142 -0
  132. torc_client-0.5.0/tests/test_auto_tune_workflow.py +184 -0
  133. torc_client-0.5.0/tests/test_cpu_affinity_mask_tracker.py +30 -0
  134. torc_client-0.5.0/tests/test_examples.py +42 -0
  135. torc_client-0.5.0/tests/test_fake_slurm_workflow.py +195 -0
  136. torc_client-0.5.0/tests/test_jobs.py +15 -0
  137. torc_client-0.5.0/tests/test_prepare_jobs_for_submission.py +163 -0
  138. torc_client-0.5.0/tests/test_run_command.py +27 -0
  139. torc_client-0.5.0/tests/test_slurm_workflows.py +319 -0
  140. torc_client-0.5.0/tests/test_terminated_jobs.py +50 -0
  141. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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