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.
- takflow/__init__.py +13 -0
- takflow/_version.py +24 -0
- takflow/backends/__init__.py +3 -0
- takflow/backends/ecflow/__init__.py +6 -0
- takflow/backends/ecflow/backend.py +140 -0
- takflow/backends/ecflow/serializer/__init__.py +54 -0
- takflow/backends/ecflow/serializer/attributes.py +314 -0
- takflow/backends/ecflow/serializer/defs.py +51 -0
- takflow/backends/ecflow/serializer/node.py +375 -0
- takflow/backends/runtime/__init__.py +158 -0
- takflow/backends/runtime/orvix.py +64 -0
- takflow/backends/runtime/slsubmit.py +125 -0
- takflow/backends/takler/__init__.py +6 -0
- takflow/backends/takler/backend.py +158 -0
- takflow/build_info.py +46 -0
- takflow/config/__init__.py +35 -0
- takflow/config/scheduling.py +61 -0
- takflow/config/types.py +69 -0
- takflow/config/workflow.py +75 -0
- takflow/config/workload.py +78 -0
- takflow/flow/__init__.py +26 -0
- takflow/flow/backend.py +180 -0
- takflow/flow/hook.py +312 -0
- takflow/flow/node.py +297 -0
- takflow/jobspec/__init__.py +22 -0
- takflow/jobspec/highlevel.py +66 -0
- takflow/jobspec/model.py +84 -0
- takflow/jobspec/render.py +75 -0
- takflow/spec/__init__.py +8 -0
- takflow/spec/jobspec/VERSION +1 -0
- takflow/spec/jobspec/__init__.py +49 -0
- takflow/spec/jobspec/conformance/__init__.py +68 -0
- takflow/spec/jobspec/conformance/golden/donau/fcst_parallel.submit +10 -0
- takflow/spec/jobspec/conformance/golden/donau/graph_mem.submit +8 -0
- takflow/spec/jobspec/conformance/golden/donau/post_serial.submit +7 -0
- takflow/spec/jobspec/conformance/golden/local/fcst_parallel.submit +2 -0
- takflow/spec/jobspec/conformance/golden/local/graph_mem.submit +2 -0
- takflow/spec/jobspec/conformance/golden/local/post_serial.submit +2 -0
- takflow/spec/jobspec/conformance/golden/slurm/fcst_parallel.submit +12 -0
- takflow/spec/jobspec/conformance/golden/slurm/graph_mem.submit +10 -0
- takflow/spec/jobspec/conformance/golden/slurm/post_serial.submit +8 -0
- takflow/spec/jobspec/conformance/regen.py +73 -0
- takflow/spec/jobspec/conformance/vectors/fcst_parallel.sh +12 -0
- takflow/spec/jobspec/conformance/vectors/graph_mem.sh +10 -0
- takflow/spec/jobspec/conformance/vectors/post_serial.sh +8 -0
- takflow/spec/jobspec/jobspec.schema.json +29 -0
- takflow/toolkit/__init__.py +45 -0
- takflow/toolkit/credential.py +144 -0
- takflow/toolkit/job.py +282 -0
- takflow/toolkit/resource.py +75 -0
- takflow/toolkit/util.py +20 -0
- takflow-0.1.0.dist-info/METADATA +393 -0
- takflow-0.1.0.dist-info/RECORD +56 -0
- takflow-0.1.0.dist-info/WHEEL +5 -0
- takflow-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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,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)})"
|