runnable 0.37.0__py3-none-any.whl → 1.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.

Potentially problematic release.


This version of runnable might be problematic. Click here for more details.

runnable/cli.py CHANGED
@@ -1,10 +1,13 @@
1
1
  import logging
2
+ import os
2
3
  from enum import Enum
4
+ from pathlib import Path
3
5
  from typing import Annotated
4
6
 
5
7
  import typer
6
8
 
7
9
  from runnable import defaults, entrypoints
10
+ from runnable.gantt import SimpleVisualizer, generate_html_timeline, visualize_simple
8
11
 
9
12
  logger = logging.getLogger(defaults.LOGGER_NAME)
10
13
 
@@ -274,5 +277,158 @@ def execute_job(
274
277
  )
275
278
 
276
279
 
280
+ @app.command()
281
+ def timeline(
282
+ run_id_or_path: Annotated[
283
+ str, typer.Argument(help="Run ID to visualize, or path to JSON run log file")
284
+ ],
285
+ output: Annotated[
286
+ str,
287
+ typer.Option("--output", "-o", help="Output HTML file path"),
288
+ ] = "",
289
+ console: Annotated[
290
+ bool,
291
+ typer.Option(
292
+ "--console/--no-console",
293
+ help="Show console timeline output (default: true)",
294
+ ),
295
+ ] = True,
296
+ open_browser: Annotated[
297
+ bool,
298
+ typer.Option(
299
+ "--open/--no-open",
300
+ help="Automatically open the generated file in default browser",
301
+ ),
302
+ ] = True,
303
+ log_level: Annotated[
304
+ LogLevel,
305
+ typer.Option(
306
+ "--log-level",
307
+ help="The log level",
308
+ show_default=True,
309
+ case_sensitive=False,
310
+ ),
311
+ ] = LogLevel.WARNING,
312
+ ):
313
+ """
314
+ Visualize pipeline execution as an interactive timeline.
315
+
316
+ This command creates lightweight timeline visualizations that effectively
317
+ show composite nodes (parallel, map, conditional) with hierarchical structure,
318
+ timing information, and execution metadata.
319
+
320
+ The new visualization system provides:
321
+ - Clean console output with hierarchical display
322
+ - Interactive HTML with hover tooltips and expandable sections
323
+ - Proper support for all composite pipeline types
324
+ - Rich metadata including commands, parameters, and catalog operations
325
+
326
+ By default, shows console output AND generates HTML file with browser opening.
327
+
328
+ Input Options:
329
+ - Run ID: Looks up JSON file in .run_log_store/ directory
330
+ - JSON Path: Direct path to run log JSON file (flexible for any config)
331
+
332
+ Examples:
333
+ # Using Run ID (looks in .run_log_store/)
334
+ runnable timeline forgiving-joliot-0645 # Console + HTML + browser
335
+ runnable timeline parallel-run --output custom.html # Console + custom HTML + browser
336
+
337
+ # Using JSON file path (any location)
338
+ runnable timeline /path/to/my-run.json # Console + HTML + browser
339
+ runnable timeline ../logs/pipeline-run.json --no-open # Console + HTML, no browser
340
+ runnable timeline ~/experiments/run.json --no-console # HTML + browser only
341
+
342
+ # Other options
343
+ runnable timeline complex-pipeline --no-open # Console + HTML, no browser
344
+ runnable timeline simple-run --no-console --no-open # HTML only, no browser
345
+ """
346
+ logger.setLevel(log_level.value)
347
+
348
+ # Determine if input is a file path or run ID
349
+ if os.path.exists(run_id_or_path) or run_id_or_path.endswith(".json"):
350
+ # Input is a file path
351
+ json_file_path = Path(run_id_or_path)
352
+ if not json_file_path.exists():
353
+ print(f"❌ JSON file not found: {json_file_path}")
354
+ return
355
+
356
+ # Extract run ID from the file for default naming
357
+ run_id = json_file_path.stem
358
+ mode = "file"
359
+ else:
360
+ # Input is a run ID - use existing behavior
361
+ run_id = run_id_or_path
362
+ json_file_path = None
363
+ mode = "run_id"
364
+
365
+ # Default console behavior: always show console output
366
+ show_console = console if console is not None else True
367
+
368
+ if output:
369
+ # Generate HTML file with console output
370
+ output_file = output
371
+ print(f"🌐 Generating timeline: {output_file}")
372
+
373
+ if show_console:
374
+ # Show console output first, then generate HTML
375
+ if mode == "file":
376
+ _visualize_simple_from_file(json_file_path, show_summary=False)
377
+ else:
378
+ visualize_simple(run_id, show_summary=False)
379
+ print(f"\n🌐 Generating HTML timeline: {output_file}")
380
+
381
+ if mode == "file":
382
+ _generate_html_timeline_from_file(json_file_path, output_file, open_browser)
383
+ else:
384
+ generate_html_timeline(run_id, output_file, open_browser)
385
+ else:
386
+ # Default behavior: show console + generate HTML with browser
387
+ if show_console:
388
+ if mode == "file":
389
+ _visualize_simple_from_file(json_file_path, show_summary=False)
390
+ else:
391
+ visualize_simple(run_id, show_summary=False)
392
+
393
+ # Always generate HTML file and open browser by default
394
+ output_file = f"{run_id}_timeline.html"
395
+ print(f"\n🌐 Generating HTML timeline: {output_file}")
396
+ if mode == "file":
397
+ _generate_html_timeline_from_file(json_file_path, output_file, open_browser)
398
+ else:
399
+ generate_html_timeline(run_id, output_file, open_browser)
400
+
401
+
402
+ def _visualize_simple_from_file(json_file_path, show_summary: bool = False) -> None:
403
+ """Visualize timeline from JSON file path."""
404
+
405
+ try:
406
+ viz = SimpleVisualizer(json_file_path)
407
+ viz.print_simple_timeline()
408
+ if show_summary:
409
+ viz.print_execution_summary()
410
+ except Exception as e:
411
+ print(f"❌ Error reading JSON file: {e}")
412
+
413
+
414
+ def _generate_html_timeline_from_file(
415
+ json_file_path, output_file: str, open_browser: bool = True
416
+ ) -> None:
417
+ """Generate HTML timeline from JSON file path."""
418
+
419
+ try:
420
+ viz = SimpleVisualizer(json_file_path)
421
+ viz.generate_html_timeline(output_file)
422
+
423
+ if open_browser:
424
+ import webbrowser
425
+
426
+ file_path = Path(output_file).absolute()
427
+ print(f"🌐 Opening timeline in browser: {file_path.name}")
428
+ webbrowser.open(file_path.as_uri())
429
+ except Exception as e:
430
+ print(f"❌ Error generating HTML: {e}")
431
+
432
+
277
433
  if __name__ == "__main__":
278
434
  app()