From 3ba1d671ba243977aaafefcc0058d2e439f4f171 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Wed, 9 Jul 2025 15:59:40 +0000 Subject: [PATCH 1/8] adding print tool --- tracksuite/ecflow_client.py | 10 +++++- tracksuite/print.py | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tracksuite/print.py diff --git a/tracksuite/ecflow_client.py b/tracksuite/ecflow_client.py index cde24a6..db347b8 100644 --- a/tracksuite/ecflow_client.py +++ b/tracksuite/ecflow_client.py @@ -51,12 +51,20 @@ def get_defs(self): def get_suite(self, name: str) -> ecflow.Suite: """ - Get the suite definition from the server. + Get the suite object from the server for a given suite name. """ defs = self.get_defs() suite = defs.find_suite(name) return suite + def get_node(self, name: str) -> ecflow.Node: + """ + Get a node object from the server for a given path. + """ + defs = self.get_defs() + node = defs.find_abs_node(name) + return node + def set_state(self, node_path: str, state: str): """ Set the state of the node on the server. diff --git a/tracksuite/print.py b/tracksuite/print.py new file mode 100644 index 0000000..5795548 --- /dev/null +++ b/tracksuite/print.py @@ -0,0 +1,63 @@ + +import os +import argparse +import logging as log +from tracksuite.ecflow_client import EcflowClient + +import ecflow + + +def print_state(node): + if node.get_state() == ecflow.State.complete: + return "βœ…" + elif node.get_state() == ecflow.State.active: + return "▢️" + elif node.get_state() == ecflow.State.queued: + return "πŸ”„" + elif node.get_state() == ecflow.State.submitted: + return "πŸ”„" + elif node.get_state() == ecflow.State.aborted: + return "❌" + else: + raise ValueError(f"Unknown state: {node.get_state()}") + + +def print_node_status(node, indent=""): + print(f"| {indent}{node.name()} | {print_state(node)} |") + + for child in node.nodes: + print_node_status(child, indent + "--") + + +def get_parser(): + description = "Replace suite on server and keep some attributes from the old one" + parser = argparse.ArgumentParser(description=description) + parser.add_argument("node", help="Ecflow node on server to print") + parser.add_argument("--host", default=os.getenv("HOSTNAME"), help="Target host") + parser.add_argument("--port", default=3141, help="Ecflow port") + return parser + + +def main(args=None): + parser = get_parser() + args = parser.parse_args() + + log.info("Revert options:") + log.info(f" - node: {args.node}") + log.info(f" - host: {args.host}") + log.info(f" - port: {args.port}") + + client = EcflowClient(args.host, args.port) + + # stage the suite running on the server + defs = client.get_defs() + node = defs.find_abs_node(args.node) + # node = client.get_node(args.node) + + print("| Node | State |") + print("| --- | --- |") + print_node_status(node) + + +if __name__ == "__main__": + main() From 1eb31bc9218781ea28e0d240785cc181e7b4b2f7 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Thu, 10 Jul 2025 15:26:33 +0000 Subject: [PATCH 2/8] cleanup tool --- pyproject.toml | 1 + tracksuite/print.py | 257 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 238 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bde5787..fc3e45a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ exclude = ["tests"] tracksuite-revert = "tracksuite.revert:main" tracksuite-update-defs = "tracksuite.definition:main" tracksuite-replace = "tracksuite.replace:main" + tracksuite-print = "tracksuite.print:main" [tool.setuptools_scm] write_to = "tracksuite/_version.py" diff --git a/tracksuite/print.py b/tracksuite/print.py index 5795548..c332c58 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -1,19 +1,52 @@ +""" +tracksuite.print +================ + +Provides utilities for printing ecFlow node trees with state icons and formatting options. +Supports raw, Markdown, and HTML output formats. Displays job output logs for tasks if available. + +Classes: + SuiteDisplay: Extracts and formats ecFlow node trees and associated logs. + Formatter: Abstract base class for output formatters. + RawFormatter: Formatter for plain text output. + MarkdownFormatter: Formatter for Markdown output. + HTMLFormatter: Formatter for HTML output. + +Functions: + get_state_icon(node): Returns a unicode icon for the ecFlow node state. + get_formatter(format_type): Returns a formatter instance for the given format type. + get_parser(): Returns an argument parser for the CLI. + main(args=None): CLI entry point. + +Usage: + tracksuite-print [--host HOST] [--port PORT] [-f FORMAT] +""" import os import argparse -import logging as log from tracksuite.ecflow_client import EcflowClient - import ecflow -def print_state(node): +def get_state_icon(node): + """ + Return a unicode icon representing the state of the given ecFlow node. + + Args: + node (ecflow.Node): The ecFlow node. + + Returns: + str: Unicode icon for the node state. + + Raises: + ValueError: If the node state is unknown. + """ if node.get_state() == ecflow.State.complete: return "βœ…" elif node.get_state() == ecflow.State.active: return "▢️" elif node.get_state() == ecflow.State.queued: - return "πŸ”„" + return "⏸️" elif node.get_state() == ecflow.State.submitted: return "πŸ”„" elif node.get_state() == ecflow.State.aborted: @@ -22,41 +55,225 @@ def print_state(node): raise ValueError(f"Unknown state: {node.get_state()}") -def print_node_status(node, indent=""): - print(f"| {indent}{node.name()} | {print_state(node)} |") +class SuiteDisplay: + """ + Extracts and formats an ecFlow node tree and associated job output logs. + + Args: + format (str): Output format ('raw', 'md', or 'html'). + """ + + def __init__(self, format="raw"): + self.tree = "" + self.content = {} + self.formatter = get_formatter(format) + + def process_task(self, task): + """ + Process a task node, extracting its job output if available. + + Args: + task (ecflow.Task): The task node. + + Returns: + str: Path label for the task. + """ + var = task.find_gen_variable("ECF_JOBOUT") + jobout = getattr(var, "value")() + if jobout and os.path.exists(jobout): + with open(jobout, "r") as f: + jobout_content = f.read() + else: + jobout_content = "Could not find jobout file." + path = f"{task.get_abs_node_path()} + {get_state_icon(task)}" + self.content[path] = jobout_content + return path + + def extract_node_tree(self, node, prefix=""): + """ + Recursively extract the node tree structure and build formatted output. + + Args: + node (ecflow.Node): The root node. + prefix (str): Prefix for tree formatting. + """ + if self.tree == "": + self.tree = f"{node.name()}/\n" + + children = node.nodes + n_child = 0 + for child in children: + n_child += 1 + children = node.nodes + for i, child in enumerate(children): + connector = "└── " if i == n_child - 1 else "β”œβ”€β”€ " + state_icon = get_state_icon(child) + if isinstance(child, ecflow.Task): + anchor = self.process_task(child) + label = self.formatter.label(anchor, f"{child.name()} {state_icon}") + else: + label = f"{child.name()} {state_icon}" + line = f"{prefix}{connector}{label}\n" + self.tree += line + if child.nodes: + extension = " " if i == n_child - 1 else "β”‚ " + self.extract_node_tree(child, prefix + extension) + + def print(self): + """ + Print the formatted node tree and logs using the selected formatter. + """ + self.formatter.tree(self.tree) + self.formatter.logs(self.content) + + +def get_formatter(format_type): + """ + Return a formatter instance for the given format type. + + Args: + format_type (str): Output format ('raw', 'md', or 'html'). + + Returns: + Formatter: Formatter instance. + + Raises: + ValueError: If the format type is unknown. + """ + if format_type == "raw": + return RawFormatter() + elif format_type == "md": + return MarkdownFormatter() + elif format_type == "html": + return HTMLFormatter() + else: + raise ValueError(f"Unknown format type: {format_type}") + - for child in node.nodes: - print_node_status(child, indent + "--") +class Formatter(): + """ + Abstract base class for output formatters. + """ + + def tree(self, tree): + """ + Print the formatted node tree. + + Args: + tree (str): The formatted tree string. + """ + raise NotImplementedError("Header method not implemented") + + def logs(self, logs): + """ + Print the logs for each node. + + Args: + logs (dict): Mapping from node path to log content. + """ + raise NotImplementedError("Logs method not implemented") + + def label(self, anchor, label): + """ + Format a label for a node. + + Args: + anchor (str): Anchor or reference for the node. + label (str): Display label. + + Returns: + str: Formatted label. + """ + raise NotImplementedError("Label method not implemented") + + +class RawFormatter(Formatter): + """ + Formatter for plain text output. + """ + + def tree(self, tree): + print(tree) + + def logs(self, logs): + print("Logs cannot be printed in raw format.") + + def label(self, anchor, label): + return label + + +class MarkdownFormatter(RawFormatter): + """ + Formatter for Markdown output. + """ + + def tree(self, tree): + print("```text") + print(tree) + print("```") + + def logs(self, logs): + for name, content in logs.items(): + print(f"
\n") + print(f"{name}\n\n") + print(f"{content}\n\n") + print(f"
\n") + + +class HTMLFormatter(Formatter): + """ + Formatter for HTML output. + """ + + def tree(self, tree): + print("
")
+        print(tree)
+        print("
\n") + + def logs(self, logs): + for name, content in logs.items(): + print(f"
\n") + print(f"{name}\n\n") + print(f"{content}\n\n") + print(f"
\n") + + def label(self, anchor, label): + return f'{label}' def get_parser(): - description = "Replace suite on server and keep some attributes from the old one" + """ + Return an argument parser for the CLI. + + Returns: + argparse.ArgumentParser: The argument parser. + """ + description = "Print ecFlow node tree with states" parser = argparse.ArgumentParser(description=description) parser.add_argument("node", help="Ecflow node on server to print") parser.add_argument("--host", default=os.getenv("HOSTNAME"), help="Target host") parser.add_argument("--port", default=3141, help="Ecflow port") + parser.add_argument("-f", "--format", default="raw", help="Output format (md, html, raw)") return parser def main(args=None): + """ + CLI entry point for printing ecFlow node trees. + + Args: + args (list, optional): Command-line arguments. + """ parser = get_parser() args = parser.parse_args() - log.info("Revert options:") - log.info(f" - node: {args.node}") - log.info(f" - host: {args.host}") - log.info(f" - port: {args.port}") - client = EcflowClient(args.host, args.port) - - # stage the suite running on the server defs = client.get_defs() node = defs.find_abs_node(args.node) - # node = client.get_node(args.node) - print("| Node | State |") - print("| --- | --- |") - print_node_status(node) + processor = SuiteDisplay(args.format) + processor.extract_node_tree(node) + processor.print() if __name__ == "__main__": From 80aabd97b9a69fa34b5270775bd7645bd8ab32d9 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Thu, 10 Jul 2025 15:44:37 +0000 Subject: [PATCH 3/8] use ecflow client object in print tool --- tracksuite/print.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tracksuite/print.py b/tracksuite/print.py index c332c58..acbbcb5 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -268,8 +268,7 @@ def main(args=None): args = parser.parse_args() client = EcflowClient(args.host, args.port) - defs = client.get_defs() - node = defs.find_abs_node(args.node) + node = client.get_node(args.node) processor = SuiteDisplay(args.format) processor.extract_node_tree(node) From a2c6a67a3ff32ec0457b6866c475d60d90e7024f Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Wed, 3 Sep 2025 16:00:14 +0000 Subject: [PATCH 4/8] update readme and print tool --- README.md | 15 +++++++++++++++ tracksuite/ecflow_client.py | 9 ++++++++- tracksuite/print.py | 24 +++++++++++++++--------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ceb10c8..7cf4496 100644 --- a/README.md +++ b/README.md @@ -131,5 +131,20 @@ To install tracksuite using pip (requires python, ecFlow (optional) and pip): --skip-attributes Don't synchronise attributes --skip-repeat Don't synchronise repeat +**To print the status of the suite (useful to create small html or md summary):** + + usage: print.py [-h] [--host HOST] [--port PORT] [-f FORMAT] node + + Print ecFlow node tree with states + + positional arguments: + node Ecflow node on server to print + + options: + -h, --help show this help message and exit + --host HOST Target host + --port PORT Ecflow port + -f FORMAT, --format FORMAT + Output format (md, html, raw) ## Overview ![](workflow.png) \ No newline at end of file diff --git a/tracksuite/ecflow_client.py b/tracksuite/ecflow_client.py index db347b8..72db053 100644 --- a/tracksuite/ecflow_client.py +++ b/tracksuite/ecflow_client.py @@ -206,7 +206,14 @@ def sync_node_recursive( for new_child in new_node.nodes: for old_child in old_node.nodes: if new_child.name() == old_child.name(): - self.sync_node_recursive(new_child, old_child, attributes) + self.sync_node_recursive( + new_child, + old_child, + attributes, + skip_status, + skip_attributes, + skip_repeat, + ) break else: log.warning( diff --git a/tracksuite/print.py b/tracksuite/print.py index acbbcb5..92f832a 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -22,11 +22,13 @@ tracksuite-print [--host HOST] [--port PORT] [-f FORMAT] """ -import os import argparse -from tracksuite.ecflow_client import EcflowClient +import os + import ecflow +from tracksuite.ecflow_client import EcflowClient + def get_state_icon(node): """ @@ -150,7 +152,7 @@ def get_formatter(format_type): raise ValueError(f"Unknown format type: {format_type}") -class Formatter(): +class Formatter: """ Abstract base class for output formatters. """ @@ -163,7 +165,7 @@ def tree(self, tree): tree (str): The formatted tree string. """ raise NotImplementedError("Header method not implemented") - + def logs(self, logs): """ Print the logs for each node. @@ -214,10 +216,10 @@ def tree(self, tree): def logs(self, logs): for name, content in logs.items(): - print(f"
\n") + print(f'
\n') print(f"{name}\n\n") print(f"{content}\n\n") - print(f"
\n") + print("
\n") class HTMLFormatter(Formatter): @@ -232,10 +234,12 @@ def tree(self, tree): def logs(self, logs): for name, content in logs.items(): - print(f"
\n") + print(f'
\n') print(f"{name}\n\n") + print("
\n")
             print(f"{content}\n\n")
-            print(f"
\n") + print("\n") + print("
\n") def label(self, anchor, label): return f'{label}' @@ -253,7 +257,9 @@ def get_parser(): parser.add_argument("node", help="Ecflow node on server to print") parser.add_argument("--host", default=os.getenv("HOSTNAME"), help="Target host") parser.add_argument("--port", default=3141, help="Ecflow port") - parser.add_argument("-f", "--format", default="raw", help="Output format (md, html, raw)") + parser.add_argument( + "-f", "--format", default="raw", help="Output format (md, html, raw)" + ) return parser From 41cfbd0bbf7c2e7a71585107c19ebe9561d13fc6 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Thu, 4 Sep 2025 09:28:23 +0000 Subject: [PATCH 5/8] add fix for alias --- tracksuite/print.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tracksuite/print.py b/tracksuite/print.py index 92f832a..6accc2c 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -117,7 +117,8 @@ def extract_node_tree(self, node, prefix=""): label = f"{child.name()} {state_icon}" line = f"{prefix}{connector}{label}\n" self.tree += line - if child.nodes: + # check if the attribute exists first, otherwise it fails for aliases + if hasattr(child, "nodes") and child.nodes: extension = " " if i == n_child - 1 else "β”‚ " self.extract_node_tree(child, prefix + extension) From 75d72c3adb823cf9c82aee4c4df60b50ab8adfd5 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Thu, 4 Sep 2025 09:28:43 +0000 Subject: [PATCH 6/8] formatting --- tracksuite/print.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracksuite/print.py b/tracksuite/print.py index 6accc2c..2bdcb5b 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -117,7 +117,7 @@ def extract_node_tree(self, node, prefix=""): label = f"{child.name()} {state_icon}" line = f"{prefix}{connector}{label}\n" self.tree += line - # check if the attribute exists first, otherwise it fails for aliases + # check if the attribute exists first, otherwise it fails for aliases if hasattr(child, "nodes") and child.nodes: extension = " " if i == n_child - 1 else "β”‚ " self.extract_node_tree(child, prefix + extension) From 7462f38068d5eb09f5006086b7a8836193822b67 Mon Sep 17 00:00:00 2001 From: Corentin Carton De Wiart Date: Thu, 4 Sep 2025 09:31:20 +0000 Subject: [PATCH 7/8] add fix for alias --- tracksuite/print.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tracksuite/print.py b/tracksuite/print.py index 2bdcb5b..5739dd2 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -118,9 +118,10 @@ def extract_node_tree(self, node, prefix=""): line = f"{prefix}{connector}{label}\n" self.tree += line # check if the attribute exists first, otherwise it fails for aliases - if hasattr(child, "nodes") and child.nodes: - extension = " " if i == n_child - 1 else "β”‚ " - self.extract_node_tree(child, prefix + extension) + if hasattr(child, "nodes"): + if child.nodes: + extension = " " if i == n_child - 1 else "β”‚ " + self.extract_node_tree(child, prefix + extension) def print(self): """ From 2dec130b0c34e0f3ae0ae00835099602e4891850 Mon Sep 17 00:00:00 2001 From: Juan Colonese Date: Thu, 4 Sep 2025 09:45:27 +0000 Subject: [PATCH 8/8] pr review. fix printing Alias tasks --- README.md | 2 +- pyproject.toml | 4 ++-- tracksuite/print.py | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7cf4496..d4ac0d5 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ To install tracksuite using pip (requires python, ecFlow (optional) and pip): **To print the status of the suite (useful to create small html or md summary):** - usage: print.py [-h] [--host HOST] [--port PORT] [-f FORMAT] node + usage: tracksuite-print [-h] [--host HOST] [--port PORT] [-f FORMAT] node Print ecFlow node tree with states diff --git a/pyproject.toml b/pyproject.toml index fc3e45a..bfdfdef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,8 @@ exclude = ["tests"] tracksuite-print = "tracksuite.print:main" [tool.setuptools_scm] -write_to = "tracksuite/_version.py" -write_to_template = ''' +version_file = "tracksuite/_version.py" +version_file_template = ''' # Do not change! Do not track in version control! __version__ = "{version}" ''' diff --git a/tracksuite/print.py b/tracksuite/print.py index 5739dd2..0a25fc8 100644 --- a/tracksuite/print.py +++ b/tracksuite/print.py @@ -117,11 +117,13 @@ def extract_node_tree(self, node, prefix=""): label = f"{child.name()} {state_icon}" line = f"{prefix}{connector}{label}\n" self.tree += line - # check if the attribute exists first, otherwise it fails for aliases - if hasattr(child, "nodes"): + try: if child.nodes: extension = " " if i == n_child - 1 else "β”‚ " self.extract_node_tree(child, prefix + extension) + except Exception: + # hit in case of Alias type, but maybe others too + pass def print(self): """