takflow 0.1.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 (56) hide show
  1. takflow/__init__.py +13 -0
  2. takflow/_version.py +24 -0
  3. takflow/backends/__init__.py +3 -0
  4. takflow/backends/ecflow/__init__.py +6 -0
  5. takflow/backends/ecflow/backend.py +140 -0
  6. takflow/backends/ecflow/serializer/__init__.py +54 -0
  7. takflow/backends/ecflow/serializer/attributes.py +314 -0
  8. takflow/backends/ecflow/serializer/defs.py +51 -0
  9. takflow/backends/ecflow/serializer/node.py +375 -0
  10. takflow/backends/runtime/__init__.py +158 -0
  11. takflow/backends/runtime/orvix.py +64 -0
  12. takflow/backends/runtime/slsubmit.py +125 -0
  13. takflow/backends/takler/__init__.py +6 -0
  14. takflow/backends/takler/backend.py +158 -0
  15. takflow/build_info.py +46 -0
  16. takflow/config/__init__.py +35 -0
  17. takflow/config/scheduling.py +61 -0
  18. takflow/config/types.py +69 -0
  19. takflow/config/workflow.py +75 -0
  20. takflow/config/workload.py +78 -0
  21. takflow/flow/__init__.py +26 -0
  22. takflow/flow/backend.py +180 -0
  23. takflow/flow/hook.py +312 -0
  24. takflow/flow/node.py +297 -0
  25. takflow/jobspec/__init__.py +22 -0
  26. takflow/jobspec/highlevel.py +66 -0
  27. takflow/jobspec/model.py +84 -0
  28. takflow/jobspec/render.py +75 -0
  29. takflow/spec/__init__.py +8 -0
  30. takflow/spec/jobspec/VERSION +1 -0
  31. takflow/spec/jobspec/__init__.py +49 -0
  32. takflow/spec/jobspec/conformance/__init__.py +68 -0
  33. takflow/spec/jobspec/conformance/golden/donau/fcst_parallel.submit +10 -0
  34. takflow/spec/jobspec/conformance/golden/donau/graph_mem.submit +8 -0
  35. takflow/spec/jobspec/conformance/golden/donau/post_serial.submit +7 -0
  36. takflow/spec/jobspec/conformance/golden/local/fcst_parallel.submit +2 -0
  37. takflow/spec/jobspec/conformance/golden/local/graph_mem.submit +2 -0
  38. takflow/spec/jobspec/conformance/golden/local/post_serial.submit +2 -0
  39. takflow/spec/jobspec/conformance/golden/slurm/fcst_parallel.submit +12 -0
  40. takflow/spec/jobspec/conformance/golden/slurm/graph_mem.submit +10 -0
  41. takflow/spec/jobspec/conformance/golden/slurm/post_serial.submit +8 -0
  42. takflow/spec/jobspec/conformance/regen.py +73 -0
  43. takflow/spec/jobspec/conformance/vectors/fcst_parallel.sh +12 -0
  44. takflow/spec/jobspec/conformance/vectors/graph_mem.sh +10 -0
  45. takflow/spec/jobspec/conformance/vectors/post_serial.sh +8 -0
  46. takflow/spec/jobspec/jobspec.schema.json +29 -0
  47. takflow/toolkit/__init__.py +45 -0
  48. takflow/toolkit/credential.py +144 -0
  49. takflow/toolkit/job.py +282 -0
  50. takflow/toolkit/resource.py +75 -0
  51. takflow/toolkit/util.py +20 -0
  52. takflow-0.1.0.dist-info/METADATA +393 -0
  53. takflow-0.1.0.dist-info/RECORD +56 -0
  54. takflow-0.1.0.dist-info/WHEEL +5 -0
  55. takflow-0.1.0.dist-info/licenses/LICENSE +202 -0
  56. takflow-0.1.0.dist-info/top_level.txt +1 -0
takflow/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """takflow — unified workflow-generation framework.
2
+
3
+ Phase 0 exposes the ``jobspec`` contract layer. Later phases add the engine
4
+ abstraction, hooks, config base, job/resource/credential/util, and the CLI
5
+ skeleton (see ``../TAKFLOW_DESIGN.md``).
6
+ """
7
+
8
+ try:
9
+ from ._version import version as __version__
10
+ except ImportError: # pragma: no cover
11
+ __version__ = "unknown"
12
+
13
+ __all__ = ["__version__"]
takflow/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,3 @@
1
+ """
2
+ takflow 后端转换层。
3
+ """
@@ -0,0 +1,6 @@
1
+ """
2
+ ecflow 后端:将 takflow 的抽象流程定义转换为 ecFlow ``.def`` 文件。
3
+ """
4
+ from takflow.backends.ecflow.backend import EcflowBackend
5
+
6
+ __all__ = ["EcflowBackend"]
@@ -0,0 +1,140 @@
1
+ """
2
+ ecflow 后端实现。
3
+
4
+ 将统一后端接口映射到 ``takflow.backends.ecflow.serializer`` API(一个纯 Python 的
5
+ ecflow ``.def`` 生成库,由 ``light_ecflow`` 折叠而来)。
6
+ """
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Union
9
+
10
+ from takflow.flow import Node, NodeType, WorkflowBackend
11
+ from takflow.backends.ecflow.serializer import (
12
+ Clock,
13
+ DState,
14
+ Defs,
15
+ Family,
16
+ RepeatDate,
17
+ RepeatDay,
18
+ Suite,
19
+ Task,
20
+ )
21
+
22
+
23
+ class EcflowBackend(WorkflowBackend):
24
+ """
25
+ ecflow 工作流引擎后端。
26
+ """
27
+
28
+ @property
29
+ def backend_type(self) -> str:
30
+ return "ecflow"
31
+
32
+ def create_suite(self, name: str) -> Any:
33
+ return Suite(name)
34
+
35
+ def create_family(self, name: str) -> Any:
36
+ return Family(name)
37
+
38
+ def create_task(self, name: str) -> Any:
39
+ return Task(name)
40
+
41
+ def add_family(self, parent: Node, family: Node) -> None:
42
+ parent.raw.add_family(family.raw)
43
+
44
+ def add_task(self, parent: Node, task: Node) -> None:
45
+ parent.raw.add_task(task.raw)
46
+
47
+ def add_variable(
48
+ self,
49
+ node: Node,
50
+ name: str,
51
+ value: Union[str, int, float, bool],
52
+ ) -> None:
53
+ node.raw.add_variable(name, str(value))
54
+
55
+ def add_variables(
56
+ self,
57
+ node: Node,
58
+ variables: Dict[str, Union[str, int, float, bool]],
59
+ ) -> None:
60
+ node.raw.add_variable({k: str(v) for k, v in variables.items()})
61
+
62
+ def add_trigger(self, node: Node, expression: str) -> None:
63
+ node.raw.add_trigger(expression)
64
+
65
+ def add_complete_trigger(self, node: Node, expression: str) -> None:
66
+ node.raw.add_complete(expression)
67
+
68
+ def add_event(self, node: Node, name: str) -> None:
69
+ node.raw.add_event(name)
70
+
71
+ def add_meter(
72
+ self,
73
+ node: Node,
74
+ name: str,
75
+ min_value: int,
76
+ max_value: int,
77
+ ) -> None:
78
+ node.raw.add_meter(name, min_value, max_value)
79
+
80
+ def add_limit(self, node: Node, name: str, limit: int) -> None:
81
+ node.raw.add_limit(name, limit)
82
+
83
+ def add_inlimit(self, node: Node, limit_name: str, tokens: int = 1) -> None:
84
+ node.raw.add_inlimit(limit_name, tokens)
85
+
86
+ def add_late(
87
+ self,
88
+ node: Node,
89
+ submitted: tuple[int, int] | None = None,
90
+ active: tuple[int, int] | None = None,
91
+ complete: tuple[int, int] | None = None,
92
+ complete_relative: bool = False,
93
+ ) -> None:
94
+ from takflow.backends.ecflow.serializer import Late
95
+
96
+ late = Late()
97
+ if submitted is not None:
98
+ late.submitted(*submitted)
99
+ if active is not None:
100
+ late.active(*active)
101
+ if complete is not None:
102
+ late.complete(*complete, relative=complete_relative)
103
+ node.raw.add_late(late)
104
+
105
+ def add_repeat_date(
106
+ self,
107
+ node: Node,
108
+ name: str,
109
+ start_date: int,
110
+ end_date: int,
111
+ ) -> None:
112
+ node.raw.add_repeat(RepeatDate(name, start_date, end_date))
113
+
114
+ def add_repeat_day(self, node: Node, step: int = 1, *, clock_kind: str = "hybrid") -> None:
115
+ node.raw.add_clock(Clock(clock_kind))
116
+ node.raw.add_repeat(RepeatDay(step))
117
+
118
+ def add_time(self, node: Node, time_str: str) -> None:
119
+ node.raw.add_time(time_str)
120
+
121
+ def set_defstatus_complete(self, node: Node) -> None:
122
+ node.raw.add_defstatus(DState.complete)
123
+
124
+ def set_defstatus_queued(self, node: Node) -> None:
125
+ node.raw.add_defstatus(DState.queued)
126
+
127
+ def save_suite(self, suite: Node, output_path: Path) -> None:
128
+ defs = Defs()
129
+ defs.add_suite(suite.raw)
130
+ output_path.parent.mkdir(parents=True, exist_ok=True)
131
+ defs.save_as_defs(str(output_path))
132
+
133
+ def node_path(self, node: Node) -> str:
134
+ return node.raw.get_abs_node_path()
135
+
136
+ def list_children(self, node: Node) -> List[Any]:
137
+ return list(node.raw.nodes)
138
+
139
+ def get_node_name(self, raw_node: Any) -> str:
140
+ return raw_node.name()
@@ -0,0 +1,54 @@
1
+ """takflow.serialize.ecflow: pure-Python ecFlow ``.def`` file generator.
2
+
3
+ Originally ``light_ecflow``; folded into takflow as one serialization backend.
4
+ Exposes a small subset of the ecFlow Python API without the C++ dependency.
5
+ """
6
+
7
+ try:
8
+ from takflow._version import version as __version__
9
+ except ImportError: # pragma: no cover
10
+ __version__ = "unknown"
11
+
12
+ from .attributes import (
13
+ Clock,
14
+ Complete,
15
+ DState,
16
+ DefStatus,
17
+ Edit,
18
+ Event,
19
+ InLimit,
20
+ Late,
21
+ Limit,
22
+ Meter,
23
+ PartTrigger,
24
+ RepeatDate,
25
+ RepeatDay,
26
+ Time,
27
+ Trigger,
28
+ )
29
+ from .defs import Defs
30
+ from .node import Family, Node, Suite, Task
31
+
32
+ __all__ = [
33
+ "__version__",
34
+ "Clock",
35
+ "Complete",
36
+ "DState",
37
+ "DefStatus",
38
+ "Defs",
39
+ "Edit",
40
+ "Event",
41
+ "Family",
42
+ "InLimit",
43
+ "Late",
44
+ "Limit",
45
+ "Meter",
46
+ "Node",
47
+ "PartTrigger",
48
+ "RepeatDate",
49
+ "RepeatDay",
50
+ "Suite",
51
+ "Task",
52
+ "Time",
53
+ "Trigger",
54
+ ]
@@ -0,0 +1,314 @@
1
+ """
2
+ ecFlow-style attribute objects for the light-ecflow stub.
3
+
4
+ These classes represent the constructs that can be attached to a node, either
5
+ via the method API (``add_trigger(...)``) or via the constructor-style API
6
+ (``Task("t1", Trigger("..."))``).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+ from typing import Any, Dict, Optional, Union
12
+
13
+
14
+ class DState(str, Enum):
15
+ """Default node states supported by ``defstatus``."""
16
+
17
+ complete = "complete"
18
+ queued = "queued"
19
+ suspended = "suspended"
20
+ aborted = "aborted"
21
+ active = "active"
22
+ unknown = "unknown"
23
+
24
+
25
+ class Edit:
26
+ """Represents one or more variables attached to a node."""
27
+
28
+ def __init__(self, **variables: Any):
29
+ self.variables: Dict[str, Any] = variables
30
+
31
+ def __repr__(self) -> str:
32
+ return f"Edit({self.variables!r})"
33
+
34
+
35
+ class Trigger:
36
+ """Represents a trigger expression."""
37
+
38
+ def __init__(self, expression: str):
39
+ self.expression = expression
40
+
41
+ def to_def(self) -> str:
42
+ return f"trigger {self.expression}"
43
+
44
+ def __repr__(self) -> str:
45
+ return f"Trigger({self.expression!r})"
46
+
47
+
48
+ class PartTrigger:
49
+ """Represents a partial trigger expression used with ``add_part_trigger``."""
50
+
51
+ def __init__(self, expression: str, and_flag: bool = True):
52
+ self.expression = expression
53
+ self.and_flag = and_flag
54
+
55
+ def __repr__(self) -> str:
56
+ return f"PartTrigger({self.expression!r}, and_flag={self.and_flag})"
57
+
58
+
59
+ class Complete:
60
+ """Represents a complete expression."""
61
+
62
+ def __init__(self, expression: str):
63
+ self.expression = expression
64
+
65
+ def to_def(self) -> str:
66
+ return f"complete {self.expression}"
67
+
68
+ def __repr__(self) -> str:
69
+ return f"Complete({self.expression!r})"
70
+
71
+
72
+ class DefStatus:
73
+ """Represents a default node status."""
74
+
75
+ def __init__(self, state: Union[DState, str]):
76
+ if isinstance(state, str):
77
+ state = DState(state)
78
+ self.state: DState = state
79
+
80
+ def to_def(self) -> str:
81
+ return f"defstatus {self.state.value}"
82
+
83
+ def __repr__(self) -> str:
84
+ return f"DefStatus({self.state.value!r})"
85
+
86
+
87
+ class Event:
88
+ """Represents an event attached to a node."""
89
+
90
+ def __init__(self, name: str):
91
+ self.name = name
92
+
93
+ def to_def(self) -> str:
94
+ return f"event {self.name}"
95
+
96
+ def __repr__(self) -> str:
97
+ return f"Event({self.name!r})"
98
+
99
+
100
+ class Meter:
101
+ """Represents a meter attached to a node."""
102
+
103
+ def __init__(self, name: str, min_value: int, max_value: int):
104
+ self.name = name
105
+ self.min_value = min_value
106
+ self.max_value = max_value
107
+
108
+ def to_def(self) -> str:
109
+ return f"meter {self.name} {self.min_value} {self.max_value}"
110
+
111
+ def __repr__(self) -> str:
112
+ return f"Meter({self.name!r}, {self.min_value}, {self.max_value})"
113
+
114
+
115
+ class Limit:
116
+ """Represents a limit attached to a node."""
117
+
118
+ def __init__(self, name: str, value: int):
119
+ self.name = name
120
+ self.value = value
121
+
122
+ def to_def(self) -> str:
123
+ return f"limit {self.name} {self.value}"
124
+
125
+ def __repr__(self) -> str:
126
+ return f"Limit({self.name!r}, {self.value})"
127
+
128
+
129
+ class InLimit:
130
+ """Represents an in-limit attached to a node."""
131
+
132
+ def __init__(self, name: str, tokens: int = 1):
133
+ self.name = name
134
+ self.tokens = tokens
135
+
136
+ def to_def(self) -> str:
137
+ if self.tokens == 1:
138
+ return f"inlimit {self.name}"
139
+ return f"inlimit {self.name} {self.tokens}"
140
+
141
+ def __repr__(self) -> str:
142
+ return f"InLimit({self.name!r}, tokens={self.tokens})"
143
+
144
+
145
+ class Time:
146
+ """Represents a time dependency attached to a node."""
147
+
148
+ def __init__(self, time_str: str):
149
+ self.time_str = time_str
150
+
151
+ def to_def(self) -> str:
152
+ return f"time {self.time_str}"
153
+
154
+ def __repr__(self) -> str:
155
+ return f"Time({self.time_str!r})"
156
+
157
+
158
+ class Late:
159
+ """Represents a "late" monitoring attribute attached to a node.
160
+
161
+ Mirrors the native ecFlow ``Late`` object used by GFS/MESO post
162
+ graph builders:
163
+
164
+ - ``late.submitted(h, m)`` -> ``late -s HH:MM``
165
+ - ``late.complete(h, m, relative=False)`` -> ``late -c HH:MM``;
166
+ with ``relative=True`` -> ``late -c +HH:MM``.
167
+ - ``late.active(h, m, relative=False)`` -> ``late -a HH:MM`` / ``late -a +HH:MM``.
168
+
169
+ Attributes are serialized in the order submitted/active/complete, matching
170
+ native ecFlow output.
171
+ """
172
+
173
+ def __init__(self):
174
+ self.submitted_time: Optional[tuple[int, int]] = None
175
+ self.active_time: Optional[tuple[int, int]] = None
176
+ self.complete_time: Optional[tuple[int, int]] = None
177
+ self.complete_relative: bool = False
178
+ self.active_relative: bool = False
179
+
180
+ def submitted(self, hours: int, minutes: int) -> "Late":
181
+ self.submitted_time = (hours, minutes)
182
+ return self
183
+
184
+ def active(self, hours: int, minutes: int, relative: bool = False) -> "Late":
185
+ self.active_time = (hours, minutes)
186
+ self.active_relative = relative
187
+ return self
188
+
189
+ def complete(self, hours: int, minutes: int, relative: bool = False) -> "Late":
190
+ self.complete_time = (hours, minutes)
191
+ self.complete_relative = relative
192
+ return self
193
+
194
+ @staticmethod
195
+ def _fmt(value: tuple[int, int]) -> str:
196
+ h, m = value
197
+ return f"{h:02d}:{m:02d}"
198
+
199
+ def to_def(self) -> str:
200
+ parts = ["late"]
201
+ if self.submitted_time is not None:
202
+ parts.append(f"-s {self._fmt(self.submitted_time)}")
203
+ if self.active_time is not None:
204
+ prefix = "+" if self.active_relative else ""
205
+ parts.append(f"-a {prefix}{self._fmt(self.active_time)}")
206
+ if self.complete_time is not None:
207
+ prefix = "+" if self.complete_relative else ""
208
+ parts.append(f"-c {prefix}{self._fmt(self.complete_time)}")
209
+ return " ".join(parts)
210
+
211
+ def __repr__(self) -> str:
212
+ return f"Late({self.to_def()!r})"
213
+
214
+
215
+ class Repeat:
216
+ """Base class for repeat attributes."""
217
+
218
+ def to_def(self) -> str:
219
+ raise NotImplementedError
220
+
221
+
222
+ class RepeatDate(Repeat):
223
+ """Represents a date-based repeat attribute."""
224
+
225
+ def __init__(self, name: str, start: int, end: int, step: int = 1):
226
+ self.name = name
227
+ self.start = int(start)
228
+ self.end = int(end)
229
+ self.step = int(step)
230
+
231
+ def to_def(self) -> str:
232
+ return f"repeat date {self.name} {self.start} {self.end} {self.step}"
233
+
234
+ def __repr__(self) -> str:
235
+ return (
236
+ f"RepeatDate({self.name!r}, {self.start}, {self.end}, step={self.step})"
237
+ )
238
+
239
+
240
+ class RepeatDay(Repeat):
241
+ """Represents a day-based repeat attribute (suite level, tied to clock)."""
242
+
243
+ def __init__(self, step: int = 1):
244
+ self.step = int(step)
245
+
246
+ def to_def(self) -> str:
247
+ return f"repeat day {self.step}"
248
+
249
+ def __repr__(self) -> str:
250
+ return f"RepeatDay({self.step})"
251
+
252
+
253
+ class Clock:
254
+ """
255
+ Represents a suite clock.
256
+
257
+ Supports several calling conventions:
258
+
259
+ - ``Clock(True)`` -> hybrid clock, no initial date/time.
260
+ - ``Clock(False)`` -> real clock, no initial date/time.
261
+ - ``Clock("hybrid", date_str, time_str)``
262
+ - ``Clock("real", time_str)``
263
+ """
264
+
265
+ def __init__(self, *args: Any):
266
+ self.kind: str = "hybrid"
267
+ self.date: Optional[str] = None
268
+ self.time: Optional[str] = None
269
+
270
+ if not args:
271
+ return
272
+
273
+ first = args[0]
274
+ if isinstance(first, bool):
275
+ self.kind = "hybrid" if first else "real"
276
+ remaining = args[1:]
277
+ elif isinstance(first, str):
278
+ self.kind = first.lower()
279
+ remaining = args[1:]
280
+ else:
281
+ raise TypeError(f"Unexpected Clock argument type: {type(first)}")
282
+
283
+ if len(remaining) == 1:
284
+ self.time = str(remaining[0])
285
+ elif len(remaining) == 2:
286
+ self.date = str(remaining[0])
287
+ self.time = str(remaining[1])
288
+ elif len(remaining) > 2:
289
+ raise TypeError(f"Clock accepts at most 3 arguments, got {len(args)}")
290
+
291
+ def to_def(self) -> str:
292
+ parts = ["clock", self.kind]
293
+ if self.date:
294
+ parts.append(self.date)
295
+ if self.time:
296
+ parts.append(self.time)
297
+ return " ".join(parts)
298
+
299
+ def __repr__(self) -> str:
300
+ return f"Clock({self.to_def()!r})"
301
+
302
+
303
+ def _quote(value: str) -> str:
304
+ """
305
+ Quote a variable value in single quotes, matching the native ecflow output.
306
+
307
+ Values containing single quotes are wrapped in double quotes instead.
308
+ """
309
+ if "'" not in value:
310
+ return f"'{value}'"
311
+ if '"' not in value:
312
+ return f'"{value}"'
313
+ escaped = value.replace("'", "\\'")
314
+ return f"'{escaped}'"
@@ -0,0 +1,51 @@
1
+ """
2
+ Top-level ``Defs`` container that mimics ``ecflow.Defs``.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+ from typing import Union
8
+
9
+ from .node import Suite
10
+
11
+
12
+ class Defs:
13
+ """
14
+ Container for one or more suites.
15
+
16
+ Example:
17
+
18
+ defs = Defs()
19
+ defs.add_suite(Suite("my_suite"))
20
+ defs.save_as_defs("my_suite.def")
21
+ """
22
+
23
+ def __init__(self, *suites: Suite):
24
+ self._suites: list[Suite] = list(suites)
25
+
26
+ def add_suite(self, suite: Suite) -> Suite:
27
+ """Add a suite to the definition."""
28
+ self._suites.append(suite)
29
+ return suite
30
+
31
+ def to_def(self) -> str:
32
+ """Return the full ``.def`` text."""
33
+ lines: list[str] = []
34
+ for suite in self._suites:
35
+ lines.extend(suite.to_def(0))
36
+ text = "\n".join(lines)
37
+ if text and not text.endswith("\n"):
38
+ text += "\n"
39
+ return text
40
+
41
+ def save_as_defs(self, path: Union[str, Path]) -> None:
42
+ """Write the definition tree to an ecFlow ``.def`` file."""
43
+ path = Path(path)
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ path.write_text(self.to_def(), encoding="utf-8")
46
+
47
+ def __str__(self) -> str:
48
+ return self.to_def()
49
+
50
+ def __repr__(self) -> str:
51
+ return f"Defs(suites={len(self._suites)})"