From 69141e5fdff05a67a6d6fe069367d870893a1fc7 Mon Sep 17 00:00:00 2001 From: Sahil Lenka Date: Wed, 4 Feb 2026 21:26:36 +0530 Subject: [PATCH 1/2] add workflow inspect command with rich output and json export --- concore_cli/cli.py | 18 ++- concore_cli/commands/inspect.py | 259 ++++++++++++++++++++++++++++++++ concore_cli/commands/status.py | 1 + concore_cli/commands/stop.py | 1 + pyproject.toml | 30 ++++ 5 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 concore_cli/commands/inspect.py create mode 100644 pyproject.toml diff --git a/concore_cli/cli.py b/concore_cli/cli.py index f714453..49c78cb 100644 --- a/concore_cli/cli.py +++ b/concore_cli/cli.py @@ -1,17 +1,13 @@ import click from rich.console import Console -from rich.table import Table -from rich.panel import Panel -from rich import print as rprint import sys -import os -from pathlib import Path from .commands.init import init_project from .commands.run import run_workflow from .commands.validate import validate_workflow from .commands.status import show_status from .commands.stop import stop_all +from .commands.inspect import inspect_workflow console = Console() @@ -55,6 +51,18 @@ def validate(workflow_file): console.print(f"[red]Error:[/red] {str(e)}") sys.exit(1) +@cli.command() +@click.argument('workflow_file', type=click.Path(exists=True)) +@click.option('--source', '-s', default='src', help='Source directory') +@click.option('--json', 'output_json', is_flag=True, help='Output in JSON format') +def inspect(workflow_file, source, output_json): + """Inspect a workflow file and show its structure""" + try: + inspect_workflow(workflow_file, source, output_json, console) + except Exception as e: + console.print(f"[red]Error:[/red] {str(e)}") + sys.exit(1) + @cli.command() def status(): """Show running concore processes""" diff --git a/concore_cli/commands/inspect.py b/concore_cli/commands/inspect.py new file mode 100644 index 0000000..e5ce383 --- /dev/null +++ b/concore_cli/commands/inspect.py @@ -0,0 +1,259 @@ +from pathlib import Path +from bs4 import BeautifulSoup +from rich.table import Table +from rich.tree import Tree +from rich.panel import Panel +from collections import defaultdict +import re + +def inspect_workflow(workflow_file, console, output_json=False): + workflow_path = Path(workflow_file) + + if output_json: + return _inspect_json(workflow_path) + + _inspect_rich(workflow_path, console) + +def _inspect_rich(workflow_path, console): + console.print() + console.print(f"[bold cyan]Workflow:[/bold cyan] {workflow_path.name}") + console.print() + + try: + with open(workflow_path, 'r') as f: + content = f.read() + + soup = BeautifulSoup(content, 'xml') + + if not soup.find('graphml'): + console.print("[red]Not a valid GraphML file[/red]") + return + + nodes = soup.find_all('node') + edges = soup.find_all('edge') + + tree = Tree("📊 [bold]Workflow Overview[/bold]") + + lang_counts = defaultdict(int) + node_files = [] + missing_files = [] + + for node in nodes: + label_tag = node.find('y:NodeLabel') + if label_tag and label_tag.text: + label = label_tag.text.strip() + if ':' in label: + _, filename = label.split(':', 1) + node_files.append(filename) + + ext = Path(filename).suffix + if ext == '.py': + lang_counts['Python'] += 1 + elif ext == '.m': + lang_counts['MATLAB'] += 1 + elif ext == '.java': + lang_counts['Java'] += 1 + elif ext == '.cpp' or ext == '.hpp': + lang_counts['C++'] += 1 + elif ext == '.v': + lang_counts['Verilog'] += 1 + else: + lang_counts['Other'] += 1 + + src_dir = workflow_path.parent / 'src' + if not (src_dir / filename).exists(): + missing_files.append(filename) + + nodes_branch = tree.add(f"Nodes: [bold]{len(nodes)}[/bold]") + if lang_counts: + for lang, count in sorted(lang_counts.items(), key=lambda x: -x[1]): + nodes_branch.add(f"{lang}: {count}") + + edges_branch = tree.add(f"Edges: [bold]{len(edges)}[/bold]") + + edge_label_regex = re.compile(r"0x([a-fA-F0-9]+)_(\S+)") + zmq_count = 0 + file_count = 0 + + for edge in edges: + label_tag = edge.find('y:EdgeLabel') + if label_tag and label_tag.text: + if edge_label_regex.match(label_tag.text.strip()): + zmq_count += 1 + else: + file_count += 1 + + if zmq_count > 0: + edges_branch.add(f"ZMQ: {zmq_count}") + if file_count > 0: + edges_branch.add(f"File-based: {file_count}") + + comm_type = "ZMQ (0mq)" if zmq_count > 0 else "File-based" if file_count > 0 else "None" + tree.add(f"Communication: [bold]{comm_type}[/bold]") + + if missing_files: + missing_branch = tree.add(f"[yellow]Missing files: {len(missing_files)}[/yellow]") + for f in missing_files[:5]: + missing_branch.add(f"[yellow]{f}[/yellow]") + if len(missing_files) > 5: + missing_branch.add(f"[dim]...and {len(missing_files) - 5} more[/dim]") + + console.print(tree) + console.print() + + if nodes: + table = Table(title="Node Details", show_header=True, header_style="bold magenta") + table.add_column("ID", style="cyan", width=12) + table.add_column("File", style="white") + table.add_column("Language", style="green") + table.add_column("Status", style="yellow") + + for node in nodes[:10]: + label_tag = node.find('y:NodeLabel') + if label_tag and label_tag.text: + label = label_tag.text.strip() + if ':' in label: + node_id, filename = label.split(':', 1) + + ext = Path(filename).suffix + lang_map = { + '.py': 'Python', + '.m': 'MATLAB', + '.java': 'Java', + '.cpp': 'C++', + '.hpp': 'C++', + '.v': 'Verilog' + } + lang = lang_map.get(ext, 'Other') + + src_dir = workflow_path.parent / 'src' + status = "✓" if (src_dir / filename).exists() else "✗" + + table.add_row(node_id, filename, lang, status) + + if len(nodes) > 10: + table.caption = f"Showing 10 of {len(nodes)} nodes" + + console.print(table) + console.print() + + if edges: + edge_table = Table(title="Edge Connections", show_header=True, header_style="bold magenta") + edge_table.add_column("From", style="cyan", width=12) + edge_table.add_column("To", style="cyan", width=12) + edge_table.add_column("Type", style="green") + + for edge in edges[:10]: + source = edge.get('source', 'unknown') + target = edge.get('target', 'unknown') + + label_tag = edge.find('y:EdgeLabel') + edge_type = "File" + if label_tag and label_tag.text: + if edge_label_regex.match(label_tag.text.strip()): + edge_type = "ZMQ" + + edge_table.add_row(source, target, edge_type) + + if len(edges) > 10: + edge_table.caption = f"Showing 10 of {len(edges)} edges" + + console.print(edge_table) + console.print() + + except FileNotFoundError: + console.print(f"[red]File not found:[/red] {workflow_path}") + except Exception as e: + console.print(f"[red]Inspection failed:[/red] {str(e)}") + +def _inspect_json(workflow_path): + import json + + try: + with open(workflow_path, 'r') as f: + content = f.read() + + soup = BeautifulSoup(content, 'xml') + + nodes = soup.find_all('node') + edges = soup.find_all('edge') + + lang_counts = defaultdict(int) + node_list = [] + edge_list = [] + missing_files = [] + + for node in nodes: + label_tag = node.find('y:NodeLabel') + if label_tag and label_tag.text: + label = label_tag.text.strip() + if ':' in label: + node_id, filename = label.split(':', 1) + + ext = Path(filename).suffix + lang_map = { + '.py': 'python', + '.m': 'matlab', + '.java': 'java', + '.cpp': 'cpp', + '.hpp': 'cpp', + '.v': 'verilog' + } + lang = lang_map.get(ext, 'other') + lang_counts[lang] += 1 + + src_dir = workflow_path.parent / 'src' + exists = (src_dir / filename).exists() + if not exists: + missing_files.append(filename) + + node_list.append({ + 'id': node_id, + 'file': filename, + 'language': lang, + 'exists': exists + }) + + edge_label_regex = re.compile(r"0x([a-fA-F0-9]+)_(\S+)") + zmq_count = 0 + file_count = 0 + + for edge in edges: + source = edge.get('source') + target = edge.get('target') + + label_tag = edge.find('y:EdgeLabel') + edge_type = 'file' + if label_tag and label_tag.text: + if edge_label_regex.match(label_tag.text.strip()): + edge_type = 'zmq' + zmq_count += 1 + else: + file_count += 1 + + edge_list.append({ + 'source': source, + 'target': target, + 'type': edge_type + }) + + result = { + 'workflow': str(workflow_path.name), + 'nodes': { + 'total': len(nodes), + 'by_language': dict(lang_counts), + 'list': node_list + }, + 'edges': { + 'total': len(edges), + 'zmq': zmq_count, + 'file': file_count, + 'list': edge_list + }, + 'missing_files': missing_files + } + + print(json.dumps(result, indent=2)) + + except Exception as e: + print(json.dumps({'error': str(e)}, indent=2)) diff --git a/concore_cli/commands/status.py b/concore_cli/commands/status.py index e407224..7d5e526 100644 --- a/concore_cli/commands/status.py +++ b/concore_cli/commands/status.py @@ -56,6 +56,7 @@ def show_status(console): 'memory': mem_str }) except (psutil.NoSuchProcess, psutil.AccessDenied): + # Process may have exited or be inaccessible; safe to ignore continue except Exception as e: diff --git a/concore_cli/commands/stop.py b/concore_cli/commands/stop.py index ad1f38f..1943a1e 100644 --- a/concore_cli/commands/stop.py +++ b/concore_cli/commands/stop.py @@ -31,6 +31,7 @@ def stop_all(console): if is_concore: processes_to_kill.append(proc) except (psutil.NoSuchProcess, psutil.AccessDenied): + # Process may have exited or be inaccessible; safe to ignore continue except Exception as e: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8c79b66 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "concore" +version = "1.0.0" +description = "Concore workflow management CLI" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +dependencies = [ + "click>=8.0.0", + "rich>=10.0.0", + "beautifulsoup4>=4.9.0", + "lxml>=4.6.0", + "psutil>=5.8.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=6.0.0", + "pytest-cov>=2.10.0", +] + +[project.scripts] +concore = "concore_cli.cli:cli" + +[tool.setuptools] +packages = ["concore_cli", "concore_cli.commands"] From 673feed9abe5d4a960c836d550263107a5c27e67 Mon Sep 17 00:00:00 2001 From: Sahil Lenka Date: Wed, 4 Feb 2026 23:10:15 +0530 Subject: [PATCH 2/2] fix copilot review issues: add init files, fix function signatures, add comments to except clauses, improve edge counting --- concore_cli/commands/inspect.py | 42 ++++++++++++++++++--------------- concore_cli/commands/status.py | 2 ++ concore_cli/commands/stop.py | 2 +- pyproject.toml | 1 + 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/concore_cli/commands/inspect.py b/concore_cli/commands/inspect.py index e5ce383..c84fbed 100644 --- a/concore_cli/commands/inspect.py +++ b/concore_cli/commands/inspect.py @@ -6,15 +6,15 @@ from collections import defaultdict import re -def inspect_workflow(workflow_file, console, output_json=False): +def inspect_workflow(workflow_file, source_dir, output_json, console): workflow_path = Path(workflow_file) if output_json: - return _inspect_json(workflow_path) + return _inspect_json(workflow_path, source_dir) - _inspect_rich(workflow_path, console) + _inspect_rich(workflow_path, source_dir, console) -def _inspect_rich(workflow_path, console): +def _inspect_rich(workflow_path, source_dir, console): console.print() console.print(f"[bold cyan]Workflow:[/bold cyan] {workflow_path.name}") console.print() @@ -60,7 +60,7 @@ def _inspect_rich(workflow_path, console): else: lang_counts['Other'] += 1 - src_dir = workflow_path.parent / 'src' + src_dir = workflow_path.parent / source_dir if not (src_dir / filename).exists(): missing_files.append(filename) @@ -77,11 +77,11 @@ def _inspect_rich(workflow_path, console): for edge in edges: label_tag = edge.find('y:EdgeLabel') - if label_tag and label_tag.text: - if edge_label_regex.match(label_tag.text.strip()): - zmq_count += 1 - else: - file_count += 1 + label_text = label_tag.text.strip() if label_tag and label_tag.text else "" + if label_text and edge_label_regex.match(label_text): + zmq_count += 1 + else: + file_count += 1 if zmq_count > 0: edges_branch.add(f"ZMQ: {zmq_count}") @@ -126,7 +126,7 @@ def _inspect_rich(workflow_path, console): } lang = lang_map.get(ext, 'Other') - src_dir = workflow_path.parent / 'src' + src_dir = workflow_path.parent / source_dir status = "✓" if (src_dir / filename).exists() else "✗" table.add_row(node_id, filename, lang, status) @@ -166,7 +166,7 @@ def _inspect_rich(workflow_path, console): except Exception as e: console.print(f"[red]Inspection failed:[/red] {str(e)}") -def _inspect_json(workflow_path): +def _inspect_json(workflow_path, source_dir): import json try: @@ -175,6 +175,10 @@ def _inspect_json(workflow_path): soup = BeautifulSoup(content, 'xml') + if not soup.find('graphml'): + print(json.dumps({'error': 'Not a valid GraphML file'}, indent=2)) + return + nodes = soup.find_all('node') edges = soup.find_all('edge') @@ -202,7 +206,7 @@ def _inspect_json(workflow_path): lang = lang_map.get(ext, 'other') lang_counts[lang] += 1 - src_dir = workflow_path.parent / 'src' + src_dir = workflow_path.parent / source_dir exists = (src_dir / filename).exists() if not exists: missing_files.append(filename) @@ -223,13 +227,13 @@ def _inspect_json(workflow_path): target = edge.get('target') label_tag = edge.find('y:EdgeLabel') + label_text = label_tag.text.strip() if label_tag and label_tag.text else "" edge_type = 'file' - if label_tag and label_tag.text: - if edge_label_regex.match(label_tag.text.strip()): - edge_type = 'zmq' - zmq_count += 1 - else: - file_count += 1 + if label_text and edge_label_regex.match(label_text): + edge_type = 'zmq' + zmq_count += 1 + else: + file_count += 1 edge_list.append({ 'source': source, diff --git a/concore_cli/commands/status.py b/concore_cli/commands/status.py index 7d5e526..6eaf6c8 100644 --- a/concore_cli/commands/status.py +++ b/concore_cli/commands/status.py @@ -38,12 +38,14 @@ def show_status(console): minutes, seconds = divmod(remainder, 60) uptime_str = f"{hours}h {minutes}m {seconds}s" except: + # Failed to calculate uptime uptime_str = "unknown" try: mem_mb = proc.info['memory_info'].rss / 1024 / 1024 mem_str = f"{mem_mb:.1f} MB" except: + # Failed to get memory info mem_str = "unknown" command = ' '.join(cmdline[:3]) if len(cmdline) >= 3 else cmdline_str[:50] diff --git a/concore_cli/commands/stop.py b/concore_cli/commands/stop.py index 1943a1e..0b2f530 100644 --- a/concore_cli/commands/stop.py +++ b/concore_cli/commands/stop.py @@ -31,7 +31,7 @@ def stop_all(console): if is_concore: processes_to_kill.append(proc) except (psutil.NoSuchProcess, psutil.AccessDenied): - # Process may have exited or be inaccessible; safe to ignore + # Process already exited or access denied; continue continue except Exception as e: diff --git a/pyproject.toml b/pyproject.toml index 8c79b66..63a8f6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,3 +28,4 @@ concore = "concore_cli.cli:cli" [tool.setuptools] packages = ["concore_cli", "concore_cli.commands"] +py-modules = ["mkconcore"]