orca-auto 6.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. orca_auto/__init__.py +11 -0
  2. orca_auto/_process_evidence.py +60 -0
  3. orca_auto/_version.py +45 -0
  4. orca_auto/activity/__init__.py +115 -0
  5. orca_auto/activity/_cancel.py +140 -0
  6. orca_auto/activity/_clear.py +142 -0
  7. orca_auto/activity/_collectors.py +104 -0
  8. orca_auto/activity/_list.py +65 -0
  9. orca_auto/activity/_orca.py +305 -0
  10. orca_auto/activity/_queue_records.py +244 -0
  11. orca_auto/activity/_sources.py +129 -0
  12. orca_auto/activity_labels.py +221 -0
  13. orca_auto/activity_rendering.py +171 -0
  14. orca_auto/activity_view.py +296 -0
  15. orca_auto/cli.py +90 -0
  16. orca_auto/cli_handlers.py +355 -0
  17. orca_auto/cli_parsers.py +476 -0
  18. orca_auto/cli_queue.py +527 -0
  19. orca_auto/cli_systemd_apply.py +147 -0
  20. orca_auto/cli_systemd_freshness.py +724 -0
  21. orca_auto/cli_systemd_restart.py +256 -0
  22. orca_auto/cli_systemd_restart_guard.py +219 -0
  23. orca_auto/cli_systemd_status.py +202 -0
  24. orca_auto/cli_systemd_units.py +212 -0
  25. orca_auto/cli_worker_supervision.py +273 -0
  26. orca_auto/cli_workers.py +323 -0
  27. orca_auto/core/__init__.py +1 -0
  28. orca_auto/core/activity.py +154 -0
  29. orca_auto/core/activity_icons.py +41 -0
  30. orca_auto/core/admission/__init__.py +73 -0
  31. orca_auto/core/admission/engine_process.py +486 -0
  32. orca_auto/core/admission/persistence.py +87 -0
  33. orca_auto/core/admission/records.py +159 -0
  34. orca_auto/core/admission/store.py +838 -0
  35. orca_auto/core/app_ids.py +20 -0
  36. orca_auto/core/artifacts.py +44 -0
  37. orca_auto/core/commands/__init__.py +1 -0
  38. orca_auto/core/commands/queue.py +155 -0
  39. orca_auto/core/commands/run_dir.py +332 -0
  40. orca_auto/core/commands/worker_options.py +21 -0
  41. orca_auto/core/config/__init__.py +22 -0
  42. orca_auto/core/config/bounded_yaml.py +140 -0
  43. orca_auto/core/config/discovery.py +73 -0
  44. orca_auto/core/config/engines.py +305 -0
  45. orca_auto/core/config/files.py +386 -0
  46. orca_auto/core/config/schema.py +342 -0
  47. orca_auto/core/config/scratch.py +46 -0
  48. orca_auto/core/engine_catalog.py +179 -0
  49. orca_auto/core/engine_process.py +421 -0
  50. orca_auto/core/engine_runner.py +457 -0
  51. orca_auto/core/engine_runtime.py +57 -0
  52. orca_auto/core/engine_scratch.py +1513 -0
  53. orca_auto/core/engines/__init__.py +50 -0
  54. orca_auto/core/engines/artifacts.py +173 -0
  55. orca_auto/core/engines/command_result.py +67 -0
  56. orca_auto/core/engines/definition_builder.py +164 -0
  57. orca_auto/core/engines/definitions.py +82 -0
  58. orca_auto/core/engines/identity.py +59 -0
  59. orca_auto/core/engines/queue_worker.py +205 -0
  60. orca_auto/core/engines/registry.py +22 -0
  61. orca_auto/core/engines/worker_child.py +109 -0
  62. orca_auto/core/extensions.py +50 -0
  63. orca_auto/core/geometry_limits.py +9 -0
  64. orca_auto/core/indexing/__init__.py +25 -0
  65. orca_auto/core/indexing/engine_artifacts.py +323 -0
  66. orca_auto/core/indexing/engine_job_locations.py +129 -0
  67. orca_auto/core/indexing/engine_records.py +156 -0
  68. orca_auto/core/indexing/engines.py +16 -0
  69. orca_auto/core/indexing/location.py +17 -0
  70. orca_auto/core/indexing/roots.py +168 -0
  71. orca_auto/core/indexing/store.py +260 -0
  72. orca_auto/core/machine_observation.py +296 -0
  73. orca_auto/core/messaging/__init__.py +73 -0
  74. orca_auto/core/messaging/channel.py +33 -0
  75. orca_auto/core/messaging/config_io.py +61 -0
  76. orca_auto/core/messaging/discord_bot.py +155 -0
  77. orca_auto/core/messaging/discord_http.py +120 -0
  78. orca_auto/core/messaging/registry.py +50 -0
  79. orca_auto/core/messaging/render_discord.py +241 -0
  80. orca_auto/core/messaging/richtext.py +139 -0
  81. orca_auto/core/notifications/__init__.py +2 -0
  82. orca_auto/core/notifications/engines.py +278 -0
  83. orca_auto/core/paths/__init__.py +55 -0
  84. orca_auto/core/paths/reserved.py +149 -0
  85. orca_auto/core/paths/validation.py +251 -0
  86. orca_auto/core/paths/workflow.py +327 -0
  87. orca_auto/core/queue/__init__.py +78 -0
  88. orca_auto/core/queue/cancellable.py +269 -0
  89. orca_auto/core/queue/child/__init__.py +1 -0
  90. orca_auto/core/queue/child/execution.py +101 -0
  91. orca_auto/core/queue/child/process.py +220 -0
  92. orca_auto/core/queue/dependencies.py +71 -0
  93. orca_auto/core/queue/engine/__init__.py +1 -0
  94. orca_auto/core/queue/engine/admission.py +139 -0
  95. orca_auto/core/queue/engine/artifacts.py +670 -0
  96. orca_auto/core/queue/engine/child.py +184 -0
  97. orca_auto/core/queue/engine/execution.py +123 -0
  98. orca_auto/core/queue/engine/input_snapshot.py +762 -0
  99. orca_auto/core/queue/engine/lifecycle.py +47 -0
  100. orca_auto/core/queue/engine/runtime.py +389 -0
  101. orca_auto/core/queue/engine/snapshot_intent.py +720 -0
  102. orca_auto/core/queue/engine/worker_execution.py +348 -0
  103. orca_auto/core/queue/enqueue_publication.py +783 -0
  104. orca_auto/core/queue/execution.py +184 -0
  105. orca_auto/core/queue/generation.py +134 -0
  106. orca_auto/core/queue/lifecycle/__init__.py +75 -0
  107. orca_auto/core/queue/lifecycle/hooks.py +80 -0
  108. orca_auto/core/queue/lifecycle/reconcile.py +136 -0
  109. orca_auto/core/queue/lifecycle/shutdown.py +250 -0
  110. orca_auto/core/queue/lifecycle/terminal.py +261 -0
  111. orca_auto/core/queue/metadata.py +105 -0
  112. orca_auto/core/queue/persistence.py +178 -0
  113. orca_auto/core/queue/priority.py +25 -0
  114. orca_auto/core/queue/processes.py +327 -0
  115. orca_auto/core/queue/publication.py +150 -0
  116. orca_auto/core/queue/resource_requests.py +37 -0
  117. orca_auto/core/queue/store.py +1016 -0
  118. orca_auto/core/queue/terminal_sync.py +156 -0
  119. orca_auto/core/queue/types.py +30 -0
  120. orca_auto/core/queue/worker/__init__.py +90 -0
  121. orca_auto/core/queue/worker/admission.py +302 -0
  122. orca_auto/core/queue/worker/execution_dependencies.py +93 -0
  123. orca_auto/core/queue/worker/loop.py +222 -0
  124. orca_auto/core/queue/worker/models.py +49 -0
  125. orca_auto/core/queue/worker/process.py +509 -0
  126. orca_auto/core/state/__init__.py +1 -0
  127. orca_auto/core/state/engine.py +315 -0
  128. orca_auto/core/statuses.py +142 -0
  129. orca_auto/core/terminal.py +162 -0
  130. orca_auto/core/utils/__init__.py +51 -0
  131. orca_auto/core/utils/coercion.py +118 -0
  132. orca_auto/core/utils/lock.py +130 -0
  133. orca_auto/core/utils/persistence.py +192 -0
  134. orca_auto/core/utils/process.py +297 -0
  135. orca_auto/core/utils/process_tracking.py +80 -0
  136. orca_auto/core/workflow_identity.py +12 -0
  137. orca_auto/orca/__init__.py +1 -0
  138. orca_auto/orca/admission_env.py +11 -0
  139. orca_auto/orca/attempt/__init__.py +1 -0
  140. orca_auto/orca/attempt/engine.py +452 -0
  141. orca_auto/orca/attempt/notifications.py +59 -0
  142. orca_auto/orca/attempt/reporting.py +262 -0
  143. orca_auto/orca/attempt/resume.py +95 -0
  144. orca_auto/orca/cli_logging.py +44 -0
  145. orca_auto/orca/commands/__init__.py +0 -0
  146. orca_auto/orca/commands/init.py +369 -0
  147. orca_auto/orca/commands/queue.py +53 -0
  148. orca_auto/orca/commands/run_inp.py +92 -0
  149. orca_auto/orca/completion_rules.py +41 -0
  150. orca_auto/orca/config.py +184 -0
  151. orca_auto/orca/config_validation.py +44 -0
  152. orca_auto/orca/direct_cancel.py +211 -0
  153. orca_auto/orca/engine.py +37 -0
  154. orca_auto/orca/evidence.py +161 -0
  155. orca_auto/orca/execution.py +496 -0
  156. orca_auto/orca/execution_binding.py +1857 -0
  157. orca_auto/orca/frequencies.py +298 -0
  158. orca_auto/orca/inp_rewriter.py +120 -0
  159. orca_auto/orca/input_artifacts.py +81 -0
  160. orca_auto/orca/input_blocks.py +725 -0
  161. orca_auto/orca/input_references.py +291 -0
  162. orca_auto/orca/job_locations/__init__.py +31 -0
  163. orca_auto/orca/job_locations/_artifacts.py +126 -0
  164. orca_auto/orca/job_locations/_contract_context.py +170 -0
  165. orca_auto/orca/job_locations/_contract_payload.py +592 -0
  166. orca_auto/orca/job_locations/_generation.py +148 -0
  167. orca_auto/orca/job_locations/_models.py +73 -0
  168. orca_auto/orca/job_locations/_records.py +363 -0
  169. orca_auto/orca/job_locations/_runtime_context.py +753 -0
  170. orca_auto/orca/job_locations/_tracking.py +37 -0
  171. orca_auto/orca/job_locations/_utils.py +276 -0
  172. orca_auto/orca/job_type.py +29 -0
  173. orca_auto/orca/launch_gate.py +88 -0
  174. orca_auto/orca/molecule_key.py +156 -0
  175. orca_auto/orca/notifications.py +172 -0
  176. orca_auto/orca/orca_chemistry.py +114 -0
  177. orca_auto/orca/orca_opt_progress.py +73 -0
  178. orca_auto/orca/orca_runner.py +531 -0
  179. orca_auto/orca/out_analyzer.py +384 -0
  180. orca_auto/orca/output_status.py +82 -0
  181. orca_auto/orca/parser/__init__.py +270 -0
  182. orca_auto/orca/parser/extractors.py +173 -0
  183. orca_auto/orca/parser/io.py +33 -0
  184. orca_auto/orca/parser/patterns.py +175 -0
  185. orca_auto/orca/queue/__init__.py +1 -0
  186. orca_auto/orca/queue/adapter.py +794 -0
  187. orca_auto/orca/queue/cancellation.py +96 -0
  188. orca_auto/orca/queue/entries.py +154 -0
  189. orca_auto/orca/queue/orphans.py +285 -0
  190. orca_auto/orca/queue/publication_repair.py +204 -0
  191. orca_auto/orca/queue/replay.py +1372 -0
  192. orca_auto/orca/queue/terminal_replay.py +194 -0
  193. orca_auto/orca/queue/worker.py +311 -0
  194. orca_auto/orca/queue/worker_runtime.py +85 -0
  195. orca_auto/orca/queue/worker_tracking.py +243 -0
  196. orca_auto/orca/relaxed_scan.py +251 -0
  197. orca_auto/orca/report/__init__.py +60 -0
  198. orca_auto/orca/report/attempts.py +137 -0
  199. orca_auto/orca/report/composer.py +281 -0
  200. orca_auto/orca/report/frequencies.py +50 -0
  201. orca_auto/orca/report/interaction_energy.py +322 -0
  202. orca_auto/orca/report/irc.py +775 -0
  203. orca_auto/orca/report/neb.py +760 -0
  204. orca_auto/orca/report/opt.py +299 -0
  205. orca_auto/orca/report/render.py +307 -0
  206. orca_auto/orca/report/rmsd.py +366 -0
  207. orca_auto/orca/report/scan.py +304 -0
  208. orca_auto/orca/report/settings.py +52 -0
  209. orca_auto/orca/report/si.py +176 -0
  210. orca_auto/orca/report/sp.py +243 -0
  211. orca_auto/orca/resource_directives.py +239 -0
  212. orca_auto/orca/run_cleanup.py +275 -0
  213. orca_auto/orca/run_context.py +241 -0
  214. orca_auto/orca/run_lock.py +37 -0
  215. orca_auto/orca/run_snapshot.py +328 -0
  216. orca_auto/orca/scratch.py +41 -0
  217. orca_auto/orca/state.py +550 -0
  218. orca_auto/orca/state_machine.py +99 -0
  219. orca_auto/orca/state_reading.py +442 -0
  220. orca_auto/orca/statuses.py +25 -0
  221. orca_auto/orca/submission.py +640 -0
  222. orca_auto/orca/types.py +90 -0
  223. orca_auto/orca/worker_execution.py +806 -0
  224. orca_auto/py.typed +1 -0
  225. orca_auto/systemd_plan.py +515 -0
  226. orca_auto/terminal_table.py +206 -0
  227. orca_auto-6.0.0.dist-info/METADATA +124 -0
  228. orca_auto-6.0.0.dist-info/RECORD +232 -0
  229. orca_auto-6.0.0.dist-info/WHEEL +5 -0
  230. orca_auto-6.0.0.dist-info/entry_points.txt +2 -0
  231. orca_auto-6.0.0.dist-info/licenses/LICENSE +21 -0
  232. orca_auto-6.0.0.dist-info/top_level.txt +1 -0
orca_auto/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Shared namespace package for the chemistry automation monorepo."""
2
+
3
+ from pkgutil import extend_path
4
+
5
+ # Core owns this initializer; separately installed extensions own subpackages.
6
+ # Never discover sibling checkout sources unless they were explicitly installed.
7
+ __path__ = extend_path(__path__, __name__)
8
+
9
+ from ._version import __version__
10
+
11
+ __all__ = ["__version__"]
@@ -0,0 +1,60 @@
1
+ """Process-local provenance used by the systemd freshness check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from importlib import import_module
8
+ from pathlib import Path
9
+
10
+ PROCESS_IMPORT_SOURCE_ENV = "ORCA_AUTO_PROCESS_IMPORT_SOURCE"
11
+ PROCESS_WORKFLOW_IMPORT_SOURCE_ENV = "ORCA_AUTO_PROCESS_WORKFLOW_IMPORT_SOURCE"
12
+
13
+
14
+ def _workflow_worker_requested(argv: list[str]) -> bool:
15
+ return "--app=workflow" in argv or any(
16
+ flag == "--app" and value == "workflow" for flag, value in zip(argv, argv[1:], strict=False)
17
+ )
18
+
19
+
20
+ def exec_with_import_source_evidence() -> None:
21
+ """Re-exec the module CLI once with its actual imported source in environ.
22
+
23
+ ``/proc/<pid>/environ`` exposes only the environment installed by ``exec``;
24
+ mutating ``os.environ`` after Python starts is not reliable process evidence.
25
+ Re-exec preserves the systemd MainPID and kernel process start identity while
26
+ making the source path observable without a sidecar state file.
27
+ """
28
+
29
+ if sys.argv[1:3] != ["queue", "worker"]:
30
+ return
31
+ import_source = str(Path(__file__).resolve(strict=False))
32
+ environment = os.environ.copy()
33
+ environment[PROCESS_IMPORT_SOURCE_ENV] = import_source
34
+ if _workflow_worker_requested(sys.argv[3:]):
35
+ from orca_auto.core.extensions import require_workflows
36
+
37
+ require_workflows()
38
+ workflow_module = import_module("orca_auto.flow")
39
+ workflow_source = workflow_module.__file__
40
+ if workflow_source is None:
41
+ raise ValueError("workflow import source is unavailable")
42
+ environment[PROCESS_WORKFLOW_IMPORT_SOURCE_ENV] = str(
43
+ Path(workflow_source).resolve(strict=True)
44
+ )
45
+ else:
46
+ environment.pop(PROCESS_WORKFLOW_IMPORT_SOURCE_ENV, None)
47
+ if environment == os.environ:
48
+ return
49
+ os.execve(
50
+ sys.executable,
51
+ [sys.executable, "-m", "orca_auto.cli", *sys.argv[1:]],
52
+ environment,
53
+ )
54
+
55
+
56
+ __all__ = [
57
+ "PROCESS_IMPORT_SOURCE_ENV",
58
+ "PROCESS_WORKFLOW_IMPORT_SOURCE_ENV",
59
+ "exec_with_import_source_evidence",
60
+ ]
orca_auto/_version.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from importlib import metadata
5
+ from pathlib import Path
6
+
7
+
8
+ def package_version() -> str:
9
+ try:
10
+ return metadata.version("orca_auto")
11
+ except metadata.PackageNotFoundError:
12
+ return "0.0.0+unknown"
13
+
14
+
15
+ def _source_pyproject_version(source_root: Path) -> str | None:
16
+ try:
17
+ with open(source_root / "pyproject.toml", "rb") as fh:
18
+ project = tomllib.load(fh)["project"]
19
+ if project["name"] != "orca_auto":
20
+ return None
21
+ return str(project["version"])
22
+ except (OSError, tomllib.TOMLDecodeError, KeyError, TypeError):
23
+ return None
24
+
25
+
26
+ def installed_version_drift(source_root: Path | None = None) -> tuple[str, str] | None:
27
+ """Report `(installed, source)` when the two disagree, else `None`.
28
+
29
+ An editable install freezes its distribution metadata at install time while
30
+ the interpreter keeps importing the checkout, so a live deployment can run
31
+ one version's code and declare another until someone reruns the install.
32
+ Returns `None` when there is no source `pyproject.toml` to compare against,
33
+ which is the normal shape of a wheel install rather than a drift verdict.
34
+ """
35
+ root = source_root if source_root is not None else Path(__file__).resolve().parents[2]
36
+ source = _source_pyproject_version(root)
37
+ if source is None:
38
+ return None
39
+ installed = package_version()
40
+ return None if installed == source else (installed, source)
41
+
42
+
43
+ __version__ = package_version()
44
+
45
+ __all__ = ["__version__", "installed_version_drift", "package_version"]
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from orca_auto.core.activity import (
7
+ ActivityCancelRequest,
8
+ ActivityListRequest,
9
+ ActivityRecord,
10
+ ActivitySourceRequest,
11
+ ResolvedActivitySources,
12
+ )
13
+ from orca_auto.core.statuses import WORKFLOW_TERMINAL_STATUSES
14
+
15
+ from . import _cancel as _activity_cancel
16
+ from . import _clear as _activity_clear
17
+ from . import _collectors as _activity_collectors
18
+ from . import _list as _activity_list
19
+ from . import _sources as _activity_sources
20
+
21
+ _ACTIVITY_CLEARABLE_TERMINAL_STATUSES = WORKFLOW_TERMINAL_STATUSES
22
+
23
+
24
+ def list_activities(
25
+ *,
26
+ workflow_root: str | Path | None = None,
27
+ shared_config: str | None = None,
28
+ refresh: bool = False,
29
+ limit: int = 0,
30
+ crest_config: str | None = None,
31
+ xtb_config: str | None = None,
32
+ orca_config: str | None = None,
33
+ ) -> dict[str, Any]:
34
+ return _activity_list.list_activities(
35
+ workflow_root=workflow_root,
36
+ shared_config=shared_config,
37
+ refresh=refresh,
38
+ limit=limit,
39
+ crest_config=crest_config,
40
+ xtb_config=xtb_config,
41
+ orca_config=orca_config,
42
+ )
43
+
44
+
45
+ def clear_activities(
46
+ *,
47
+ workflow_root: str | Path | None = None,
48
+ shared_config: str | None = None,
49
+ crest_config: str | None = None,
50
+ xtb_config: str | None = None,
51
+ orca_config: str | None = None,
52
+ ) -> dict[str, Any]:
53
+ return _activity_clear.clear_activities(
54
+ workflow_root=workflow_root,
55
+ shared_config=shared_config,
56
+ crest_config=crest_config,
57
+ xtb_config=xtb_config,
58
+ orca_config=orca_config,
59
+ clearable_terminal_statuses=_ACTIVITY_CLEARABLE_TERMINAL_STATUSES,
60
+ )
61
+
62
+
63
+ def cancel_activity(
64
+ *,
65
+ target: str,
66
+ workflow_root: str | Path | None = None,
67
+ shared_config: str | None = None,
68
+ crest_config: str | None = None,
69
+ xtb_config: str | None = None,
70
+ orca_config: str | None = None,
71
+ ) -> dict[str, Any]:
72
+ request = ActivityCancelRequest(
73
+ target=target,
74
+ sources=ActivitySourceRequest(
75
+ workflow_root=workflow_root,
76
+ shared_config=shared_config,
77
+ crest_config=crest_config,
78
+ xtb_config=xtb_config,
79
+ orca_config=orca_config,
80
+ ),
81
+ )
82
+ resolved = _activity_sources.resolve_activity_source_request(request.sources)
83
+ record = _activity_cancel.match_activity_record(
84
+ _activity_collectors.collect_activity_records(
85
+ workflow_root=resolved.workflow_root,
86
+ refresh=False,
87
+ crest_config=resolved.crest_config,
88
+ xtb_config=resolved.xtb_config,
89
+ orca_config=resolved.orca_config,
90
+ ),
91
+ request.target,
92
+ )
93
+
94
+ if record.kind == "workflow":
95
+ result = _activity_cancel.cancel_workflow_activity(record, resolved, request)
96
+ return _activity_cancel.cancel_activity_payload(
97
+ record,
98
+ result,
99
+ fallback_status="cancelled",
100
+ )
101
+
102
+ result = _activity_cancel.cancel_non_workflow_activity(record, resolved, request)
103
+ return _activity_cancel.cancel_activity_payload(record, result, fallback_status="failed")
104
+
105
+
106
+ __all__ = [
107
+ "ActivityCancelRequest",
108
+ "ActivityListRequest",
109
+ "ActivityRecord",
110
+ "ActivitySourceRequest",
111
+ "ResolvedActivitySources",
112
+ "cancel_activity",
113
+ "clear_activities",
114
+ "list_activities",
115
+ ]
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from importlib import import_module
5
+ from typing import Any
6
+
7
+ from orca_auto.core.activity import ActivityCancelRequest, ActivityRecord, ResolvedActivitySources
8
+ from orca_auto.core.engine_catalog import (
9
+ EngineCatalogEntry,
10
+ find_engine_catalog_entry_by_source_id,
11
+ )
12
+ from orca_auto.core.extensions import require_workflows
13
+ from orca_auto.core.utils import normalize_text
14
+ from orca_auto.orca.direct_cancel import cancel_target as cancel_orca_target
15
+
16
+ _CANCEL_ENGINE_TARGETS: dict[str, str | Callable[..., dict[str, Any]]] = {
17
+ "crest": "orca_auto.flow.submitters.crest",
18
+ "xtb": "orca_auto.flow.submitters.xtb",
19
+ }
20
+
21
+
22
+ def match_activity_record(records: list[ActivityRecord], target: str) -> ActivityRecord:
23
+ normalized_target = normalize_text(target)
24
+ if not normalized_target:
25
+ raise ValueError("Cancel target is empty.")
26
+
27
+ exact_matches = [
28
+ record
29
+ for record in records
30
+ if normalized_target in {record.activity_id, record.cancel_target}
31
+ ]
32
+ if len(exact_matches) == 1:
33
+ return exact_matches[0]
34
+ if len(exact_matches) > 1:
35
+ raise ValueError(
36
+ f"Ambiguous activity target: {normalized_target}. Matches: "
37
+ + ", ".join(sorted(record.activity_id for record in exact_matches))
38
+ )
39
+
40
+ alias_matches = [record for record in records if normalized_target in set(record.aliases)]
41
+ if len(alias_matches) == 1:
42
+ return alias_matches[0]
43
+ if len(alias_matches) > 1:
44
+ raise ValueError(
45
+ f"Ambiguous activity target: {normalized_target}. Matches: "
46
+ + ", ".join(sorted(record.activity_id for record in alias_matches))
47
+ )
48
+ raise LookupError(f"Activity target not found: {normalized_target}")
49
+
50
+
51
+ def cancel_activity_payload(
52
+ record: ActivityRecord,
53
+ result: dict[str, Any],
54
+ *,
55
+ fallback_status: str,
56
+ ) -> dict[str, Any]:
57
+ return {
58
+ "activity_id": record.activity_id,
59
+ "kind": record.kind,
60
+ "engine": record.engine,
61
+ "source": record.source,
62
+ "label": record.label,
63
+ "status": normalize_text(result.get("status")) or fallback_status,
64
+ "cancel_target": record.cancel_target,
65
+ "result": result,
66
+ }
67
+
68
+
69
+ def cancel_workflow_activity(
70
+ record: ActivityRecord,
71
+ resolved: ResolvedActivitySources,
72
+ request: ActivityCancelRequest,
73
+ ) -> dict[str, Any]:
74
+ require_workflows()
75
+ from orca_auto.flow.orchestration import cancel_materialized_workflow
76
+
77
+ return cancel_materialized_workflow(
78
+ target=record.cancel_target,
79
+ workflow_root=resolved.workflow_root or "",
80
+ crest_config=resolved.crest_config,
81
+ orca_config=resolved.orca_config,
82
+ )
83
+
84
+
85
+ def cancel_workflow_stage_engine_activity(
86
+ entry: EngineCatalogEntry,
87
+ record: ActivityRecord,
88
+ resolved: ResolvedActivitySources,
89
+ request: ActivityCancelRequest,
90
+ ) -> dict[str, Any]:
91
+ del request
92
+ config_path = normalize_text(resolved.config_for_engine(entry.engine_id))
93
+ if not config_path:
94
+ raise ValueError(
95
+ f"{entry.engine_id}_config is required to cancel {entry.engine_id} activities."
96
+ )
97
+ cancel_target = _CANCEL_ENGINE_TARGETS.get(entry.engine_id)
98
+ if cancel_target is None:
99
+ raise ValueError(f"No cancellation handler for catalog engine: {entry.engine_id}")
100
+ require_workflows()
101
+ if isinstance(cancel_target, str):
102
+ cancel_target = import_module(cancel_target).cancel_target
103
+ return cancel_target(
104
+ target=record.cancel_target,
105
+ config_path=config_path,
106
+ )
107
+
108
+
109
+ def cancel_orca_activity(
110
+ record: ActivityRecord,
111
+ resolved: ResolvedActivitySources,
112
+ request: ActivityCancelRequest,
113
+ ) -> dict[str, Any]:
114
+ config_path = normalize_text(resolved.orca_config)
115
+ if not config_path:
116
+ raise ValueError("orca_auto_config is required to cancel orca_auto ORCA activities.")
117
+ return cancel_orca_target(
118
+ target=record.cancel_target,
119
+ config_path=config_path,
120
+ )
121
+
122
+
123
+ def cancel_non_workflow_activity(
124
+ record: ActivityRecord,
125
+ resolved: ResolvedActivitySources,
126
+ request: ActivityCancelRequest,
127
+ ) -> dict[str, Any]:
128
+ entry = find_engine_catalog_entry_by_source_id(record.source)
129
+ if entry is None:
130
+ raise ValueError(f"Unsupported activity source: {record.source}")
131
+ if entry.workflow_stage_role == "workflow-stage":
132
+ return cancel_workflow_stage_engine_activity(
133
+ entry,
134
+ record,
135
+ resolved,
136
+ request,
137
+ )
138
+ if entry.activity_role == "orca-run":
139
+ return cancel_orca_activity(record, resolved, request)
140
+ raise ValueError(f"Unsupported activity role for source {record.source}: {entry.activity_role}")
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from orca_auto.core.activity import ActivitySourceRequest, ResolvedActivitySources
7
+ from orca_auto.core.engine_catalog import (
8
+ activity_engine_entries,
9
+ get_engine_catalog_entry,
10
+ workflow_stage_engine_entries,
11
+ )
12
+ from orca_auto.core.engine_runtime import engine_runtime_paths
13
+ from orca_auto.core.engines import own_engine_accept_entry
14
+ from orca_auto.core.extensions import workflows_available
15
+ from orca_auto.core.queue import clear_terminal as clear_queue_terminal
16
+ from orca_auto.core.utils import normalize_text
17
+
18
+ from . import _sources
19
+ from ._queue_records import engine_queue_roots
20
+
21
+ _ENGINE_QUEUE_CLEAR_SOURCES = tuple(
22
+ (entry.engine_id, f"{entry.engine_id}_queue_entries")
23
+ for entry in workflow_stage_engine_entries()
24
+ )
25
+
26
+
27
+ def _clear_counts() -> dict[str, int]:
28
+ cleared = {"workflows": 0}
29
+ for entry in activity_engine_entries():
30
+ cleared[f"{entry.engine_id}_queue_entries"] = 0
31
+ cleared["orca_run_states"] = 0
32
+ return cleared
33
+
34
+
35
+ def _clear_engine_queue(
36
+ resolved: ResolvedActivitySources,
37
+ *,
38
+ engine: str,
39
+ ) -> int:
40
+ config_path = normalize_text(resolved.config_for_engine(engine))
41
+ if not config_path:
42
+ return 0
43
+ entry = get_engine_catalog_entry(engine)
44
+ if entry.workflow_stage_role == "workflow-stage" and not normalize_text(resolved.workflow_root):
45
+ return 0
46
+ return sum(
47
+ clear_queue_terminal(
48
+ root,
49
+ select_entry_fn=own_engine_accept_entry(engine),
50
+ )
51
+ for root in engine_queue_roots(config_path, engine=engine)
52
+ )
53
+
54
+
55
+ def _clear_workflow_registry(
56
+ resolved: ResolvedActivitySources,
57
+ *,
58
+ clearable_terminal_statuses: frozenset[str],
59
+ ) -> int:
60
+ if not workflows_available() or not normalize_text(resolved.workflow_root):
61
+ return 0
62
+ from orca_auto.flow.registry import clear_terminal_workflow_registry
63
+
64
+ return clear_terminal_workflow_registry(
65
+ str(resolved.workflow_root),
66
+ statuses=clearable_terminal_statuses,
67
+ )
68
+
69
+
70
+ def _clear_engine_queues(
71
+ resolved: ResolvedActivitySources,
72
+ ) -> dict[str, int]:
73
+ cleared = {cleared_key: 0 for _, cleared_key in _ENGINE_QUEUE_CLEAR_SOURCES}
74
+ for engine, cleared_key in _ENGINE_QUEUE_CLEAR_SOURCES:
75
+ cleared[cleared_key] += _clear_engine_queue(
76
+ resolved,
77
+ engine=engine,
78
+ )
79
+ return cleared
80
+
81
+
82
+ def _clear_orca_terminal_entries(
83
+ resolved: ResolvedActivitySources,
84
+ ) -> tuple[int, int]:
85
+ if not normalize_text(resolved.orca_config):
86
+ return 0, 0
87
+ from orca_auto.orca.run_cleanup import (
88
+ clear_terminal_entries as clear_orca_terminal_entries,
89
+ )
90
+
91
+ allowed_root = engine_runtime_paths(str(resolved.orca_config), engine="orca")["allowed_root"]
92
+ return clear_orca_terminal_entries(allowed_root)
93
+
94
+
95
+ def _clear_sources_payload(resolved: ResolvedActivitySources) -> dict[str, str]:
96
+ workflow_root_text = normalize_text(resolved.workflow_root)
97
+ return {
98
+ "workflow_root": str(Path(workflow_root_text).expanduser().resolve())
99
+ if workflow_root_text
100
+ else "",
101
+ **{
102
+ f"{entry.engine_id}_config": normalize_text(resolved.config_for_engine(entry.engine_id))
103
+ for entry in activity_engine_entries()
104
+ },
105
+ }
106
+
107
+
108
+ def clear_activities(
109
+ *,
110
+ workflow_root: str | Path | None = None,
111
+ shared_config: str | None = None,
112
+ crest_config: str | None = None,
113
+ xtb_config: str | None = None,
114
+ orca_config: str | None = None,
115
+ clearable_terminal_statuses: frozenset[str],
116
+ ) -> dict[str, Any]:
117
+ source_request = ActivitySourceRequest(
118
+ workflow_root=workflow_root,
119
+ shared_config=shared_config,
120
+ crest_config=crest_config,
121
+ xtb_config=xtb_config,
122
+ orca_config=orca_config,
123
+ )
124
+ resolved = _sources.resolve_activity_source_request(source_request)
125
+ _sources.require_activity_support(resolved)
126
+
127
+ cleared = _clear_counts()
128
+ cleared["workflows"] = _clear_workflow_registry(
129
+ resolved,
130
+ clearable_terminal_statuses=clearable_terminal_statuses,
131
+ )
132
+ for key, count in _clear_engine_queues(resolved).items():
133
+ cleared[key] += count
134
+ queue_count, run_count = _clear_orca_terminal_entries(resolved)
135
+ cleared["orca_queue_entries"] += queue_count
136
+ cleared["orca_run_states"] += run_count
137
+
138
+ return {
139
+ "total_cleared": sum(int(value) for value in cleared.values()),
140
+ "cleared": cleared,
141
+ "sources": _clear_sources_payload(resolved),
142
+ }
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from orca_auto.core.activity import (
9
+ ActivityListRequest,
10
+ ActivityRecord,
11
+ ActivitySourceRequest,
12
+ ResolvedActivitySources,
13
+ sort_key,
14
+ )
15
+ from orca_auto.core.engine_catalog import EngineCatalogEntry, activity_engine_entries
16
+ from orca_auto.core.extensions import workflows_available
17
+ from orca_auto.core.utils import normalize_text
18
+
19
+ from . import _sources
20
+ from ._queue_records import collect_catalog_engine_activity
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ActivityListProvider:
25
+ source: str
26
+ collect: Callable[[Any, ActivityListRequest], list[ActivityRecord]]
27
+
28
+
29
+ def collect_workflow_activity(
30
+ resolved: ResolvedActivitySources,
31
+ request: ActivityListRequest,
32
+ ) -> list[ActivityRecord]:
33
+ if not normalize_text(resolved.workflow_root):
34
+ return []
35
+ from orca_auto.flow.activity._workflow_records import workflow_records
36
+
37
+ return workflow_records(
38
+ workflow_root=str(resolved.workflow_root),
39
+ refresh=request.refresh,
40
+ )
41
+
42
+
43
+ def _catalog_activity_provider(entry: EngineCatalogEntry) -> ActivityListProvider:
44
+ def collect(
45
+ resolved: ResolvedActivitySources,
46
+ request: ActivityListRequest,
47
+ ) -> list[ActivityRecord]:
48
+ return collect_catalog_engine_activity(entry, resolved, request)
49
+
50
+ return ActivityListProvider(entry.source_id, collect)
51
+
52
+
53
+ def activity_list_providers() -> tuple[ActivityListProvider, ...]:
54
+ providers = (
55
+ [ActivityListProvider("orca_auto_flow", collect_workflow_activity)]
56
+ if workflows_available()
57
+ else []
58
+ )
59
+ for entry in activity_engine_entries():
60
+ providers.append(_catalog_activity_provider(entry))
61
+ return tuple(providers)
62
+
63
+
64
+ def collect_activity_records_from_request(
65
+ request: ActivityListRequest,
66
+ ) -> list[ActivityRecord]:
67
+ resolved = _sources.resolve_activity_source_request(request.sources)
68
+ _sources.require_activity_support(resolved)
69
+ rows: list[ActivityRecord] = []
70
+ for provider in activity_list_providers():
71
+ rows.extend(provider.collect(resolved, request))
72
+ return sorted(rows, key=sort_key, reverse=True)
73
+
74
+
75
+ def collect_activity_records(
76
+ *,
77
+ workflow_root: str | Path | None = None,
78
+ shared_config: str | None = None,
79
+ refresh: bool = False,
80
+ crest_config: str | None = None,
81
+ xtb_config: str | None = None,
82
+ orca_config: str | None = None,
83
+ ) -> list[ActivityRecord]:
84
+ return collect_activity_records_from_request(
85
+ ActivityListRequest(
86
+ sources=ActivitySourceRequest(
87
+ workflow_root=workflow_root,
88
+ shared_config=shared_config,
89
+ crest_config=crest_config,
90
+ xtb_config=xtb_config,
91
+ orca_config=orca_config,
92
+ ),
93
+ refresh=refresh,
94
+ ),
95
+ )
96
+
97
+
98
+ __all__ = [
99
+ "ActivityListProvider",
100
+ "activity_list_providers",
101
+ "collect_activity_records",
102
+ "collect_activity_records_from_request",
103
+ "collect_workflow_activity",
104
+ ]
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from orca_auto.core.activity import ActivityListRequest, ActivitySourceRequest
7
+ from orca_auto.core.engine_catalog import activity_engine_entries
8
+ from orca_auto.core.utils import normalize_text
9
+
10
+ from . import _sources
11
+ from ._collectors import collect_activity_records
12
+
13
+
14
+ def list_activities(
15
+ *,
16
+ workflow_root: str | Path | None = None,
17
+ shared_config: str | None = None,
18
+ refresh: bool = False,
19
+ limit: int = 0,
20
+ crest_config: str | None = None,
21
+ xtb_config: str | None = None,
22
+ orca_config: str | None = None,
23
+ ) -> dict[str, Any]:
24
+ request = ActivityListRequest(
25
+ sources=ActivitySourceRequest(
26
+ workflow_root=workflow_root,
27
+ shared_config=shared_config,
28
+ crest_config=crest_config,
29
+ xtb_config=xtb_config,
30
+ orca_config=orca_config,
31
+ ),
32
+ refresh=refresh,
33
+ limit=limit,
34
+ )
35
+ resolved = _sources.resolve_activity_source_request(request.sources)
36
+ records = collect_activity_records(
37
+ workflow_root=resolved.workflow_root,
38
+ refresh=request.refresh,
39
+ crest_config=resolved.crest_config,
40
+ xtb_config=resolved.xtb_config,
41
+ orca_config=resolved.orca_config,
42
+ )
43
+ if request.limit > 0:
44
+ records = records[: request.limit]
45
+ workflow_root_text = normalize_text(resolved.workflow_root)
46
+ return {
47
+ "count": len(records),
48
+ "activities": [record.to_dict() for record in records],
49
+ "sources": {
50
+ "workflow_root": str(Path(workflow_root_text).expanduser().resolve())
51
+ if workflow_root_text
52
+ else "",
53
+ **{
54
+ f"{entry.engine_id}_config": normalize_text(
55
+ resolved.config_for_engine(entry.engine_id)
56
+ )
57
+ for entry in activity_engine_entries()
58
+ },
59
+ },
60
+ }
61
+
62
+
63
+ __all__ = [
64
+ "list_activities",
65
+ ]