rushti 2.2.3__tar.gz → 2.3.1__tar.gz
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.
- {rushti-2.2.3/src/rushti.egg-info → rushti-2.3.1}/PKG-INFO +6 -1
- {rushti-2.2.3 → rushti-2.3.1}/README.md +5 -0
- {rushti-2.2.3 → rushti-2.3.1}/pyproject.toml +1 -1
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/__init__.py +1 -1
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/app_paths.py +22 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/cli.py +22 -4
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/build.py +12 -6
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/resume.py +5 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/__init__.py +12 -3
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/dashboard.py +61 -17
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/execution.py +222 -16
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/logging.py +17 -6
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/messages.py +25 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/optimizer.py +4 -2
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/parsing.py +19 -8
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/dynamodb.py +9 -2
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/repository.py +1 -0
- rushti-2.3.1/src/rushti/stats/signature.py +42 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/sqlite.py +29 -11
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/task.py +66 -14
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/taskfile.py +87 -10
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/taskfile_ops.py +21 -9
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/templates/visualization.html +20 -5
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_integration.py +26 -7
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_objects.py +10 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/visualization_template.py +20 -5
- {rushti-2.2.3 → rushti-2.3.1/src/rushti.egg-info}/PKG-INFO +6 -1
- rushti-2.2.3/src/rushti/stats/signature.py +0 -29
- {rushti-2.2.3 → rushti-2.3.1}/LICENSE +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/setup.cfg +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/_shlex_utils.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/checkpoint.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/__init__.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/db.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/__init__.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/analyze.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/export.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/list.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/optimize.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/stats/visualize.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/expand.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/export.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/push.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/validate.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/commands/tasks/visualize.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/contention_analyzer.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/dag.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/db_admin.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/exclusive.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/logging_setup.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/optimization_report.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/results_writer.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/settings.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/__init__.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/stats/paths.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/templates/__init__.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/tm1_build.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti/utils.py +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/SOURCES.txt +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/dependency_links.txt +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/entry_points.txt +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/requires.txt +0 -0
- {rushti-2.2.3 → rushti-2.3.1}/src/rushti.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rushti
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: RushTI - Parallel TM1 TurboIntegrator Process Execution
|
|
5
5
|
Author-email: nicolasbisurgi <nbisurgi@cubewise.com>, Marius Wirtz <mwirtz@cubewise.com>
|
|
6
6
|
Maintainer: Cubewise CODE
|
|
@@ -109,6 +109,11 @@ user = admin
|
|
|
109
109
|
password = apple
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
RushTI looks for `config.ini` under `config/` (or `RUSHTI_DIR`). To point a
|
|
113
|
+
single run at a different file — e.g. a `config.ini` shared with other tm1py
|
|
114
|
+
utilities — pass `--config PATH` to any TM1-connecting command (`run`, `build`,
|
|
115
|
+
`tasks`, `resume`).
|
|
116
|
+
|
|
112
117
|
**2. Create a task file**
|
|
113
118
|
|
|
114
119
|
```json
|
|
@@ -65,6 +65,11 @@ user = admin
|
|
|
65
65
|
password = apple
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
RushTI looks for `config.ini` under `config/` (or `RUSHTI_DIR`). To point a
|
|
69
|
+
single run at a different file — e.g. a `config.ini` shared with other tm1py
|
|
70
|
+
utilities — pass `--config PATH` to any TM1-connecting command (`run`, `build`,
|
|
71
|
+
`tasks`, `resume`).
|
|
72
|
+
|
|
68
73
|
**2. Create a task file**
|
|
69
74
|
|
|
70
75
|
```json
|
|
@@ -5,7 +5,7 @@ This package provides tools for executing TM1 TI processes in parallel
|
|
|
5
5
|
with dependency management, checkpoint/resume support, and execution logging.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "2.
|
|
8
|
+
__version__ = "2.3.1"
|
|
9
9
|
__app_name__ = "RushTI"
|
|
10
10
|
|
|
11
11
|
# Core exports for programmatic use
|
|
@@ -6,6 +6,7 @@ and tracks which legacy paths have been used so a deprecation warning
|
|
|
6
6
|
can be emitted once logging is initialized.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import argparse
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ __all__ = [
|
|
|
15
16
|
"CURRENT_DIRECTORY",
|
|
16
17
|
"resolve_config_path",
|
|
17
18
|
"log_legacy_path_warnings",
|
|
19
|
+
"add_config_arg",
|
|
18
20
|
]
|
|
19
21
|
|
|
20
22
|
|
|
@@ -73,6 +75,26 @@ def resolve_config_path(filename: str, warn_on_legacy: bool = True, cli_path: st
|
|
|
73
75
|
return new_path
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
def add_config_arg(parser: argparse.ArgumentParser) -> None:
|
|
79
|
+
"""Add the ``--config`` argument to a parser.
|
|
80
|
+
|
|
81
|
+
Long-form only (``-c`` is taken by ``resume --checkpoint``). Relocates
|
|
82
|
+
only ``config.ini`` (TM1 connection parameters); ``settings.ini`` and
|
|
83
|
+
``logging_config.ini`` keep their own resolution. The resolved value is
|
|
84
|
+
fed to :func:`resolve_config_path` as ``cli_path``.
|
|
85
|
+
"""
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--config",
|
|
88
|
+
dest="config",
|
|
89
|
+
default=None,
|
|
90
|
+
metavar="FILE",
|
|
91
|
+
help=(
|
|
92
|
+
"Path to a config.ini file (TM1 connection parameters). "
|
|
93
|
+
"Overrides RUSHTI_DIR/default location."
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
76
98
|
def log_legacy_path_warnings(logger: logging.Logger) -> None:
|
|
77
99
|
"""Log deprecation warnings for any legacy paths that were used.
|
|
78
100
|
|
|
@@ -19,6 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from TM1py.Utils import integerize_version
|
|
20
20
|
|
|
21
21
|
from rushti.app_paths import (
|
|
22
|
+
add_config_arg,
|
|
22
23
|
log_legacy_path_warnings,
|
|
23
24
|
resolve_config_path,
|
|
24
25
|
)
|
|
@@ -249,11 +250,14 @@ Examples:
|
|
|
249
250
|
%(prog)s -t tasks.txt -w 4 -r 2 -o results.csv
|
|
250
251
|
%(prog)s --tasks tasks.json --max-workers 8 --retries 3
|
|
251
252
|
%(prog)s --tm1-instance tm1srv01 --workflow Sample --max-workers 4
|
|
253
|
+
%(prog)s --tm1-instance tm1srv01 --workflow Sample_Optimal_Mode --mode opt
|
|
252
254
|
|
|
253
255
|
Configuration:
|
|
254
256
|
Default values can be set in settings.ini (see settings.ini.template).
|
|
255
257
|
Settings precedence: CLI args > JSON task file > settings.ini > defaults
|
|
256
|
-
File
|
|
258
|
+
File sources auto-detect the mode (--mode is ignored). Cube reads
|
|
259
|
+
(--tm1-instance) cannot, and default to norm; pass --mode opt to honour
|
|
260
|
+
the 'predecessors' measure.
|
|
257
261
|
""",
|
|
258
262
|
)
|
|
259
263
|
|
|
@@ -270,6 +274,9 @@ Configuration:
|
|
|
270
274
|
# Add taskfile source arguments
|
|
271
275
|
add_taskfile_source_args(parser, required=False, include_settings=True)
|
|
272
276
|
|
|
277
|
+
# Add --config (relocates config.ini only)
|
|
278
|
+
add_config_arg(parser)
|
|
279
|
+
|
|
273
280
|
parser.add_argument(
|
|
274
281
|
"--max-workers",
|
|
275
282
|
"-w",
|
|
@@ -286,7 +293,7 @@ Configuration:
|
|
|
286
293
|
dest="execution_mode",
|
|
287
294
|
choices=["norm", "opt"],
|
|
288
295
|
default=None,
|
|
289
|
-
help="
|
|
296
|
+
help="Execution mode: norm or opt. Auto-detected (and ignored) for file sources. For cube reads (--tm1-instance) it defaults to norm; pass 'opt' to honour the 'predecessors' measure.",
|
|
290
297
|
)
|
|
291
298
|
|
|
292
299
|
parser.add_argument(
|
|
@@ -423,6 +430,7 @@ def parse_named_arguments(argv: list):
|
|
|
423
430
|
"workflow": workflow,
|
|
424
431
|
"log_level": args.log_level,
|
|
425
432
|
"detailed_results": args.detailed_results,
|
|
433
|
+
"config": args.config,
|
|
426
434
|
}
|
|
427
435
|
|
|
428
436
|
return tasks_file_path, cli_args
|
|
@@ -468,6 +476,7 @@ def parse_arguments(argv: list):
|
|
|
468
476
|
"exclusive": None,
|
|
469
477
|
"no_checkpoint": False,
|
|
470
478
|
"log_level": None, # Not supported in positional style
|
|
479
|
+
"config": None, # Legacy positional style does not support --config
|
|
471
480
|
}
|
|
472
481
|
return tasks_file, cli_args
|
|
473
482
|
|
|
@@ -623,6 +632,14 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
|
|
|
623
632
|
if resume_context:
|
|
624
633
|
cli_args.update(resume_context)
|
|
625
634
|
|
|
635
|
+
# Resolve config.ini location, threading the resolved path explicitly into
|
|
636
|
+
# the TM1 connection layer (CLI --config > RUSHTI_DIR > legacy CWD > config/).
|
|
637
|
+
# Fail fast with a clean message (no traceback) on an explicit bad path.
|
|
638
|
+
try:
|
|
639
|
+
config_path = resolve_config_path("config.ini", cli_path=cli_args.get("config"))
|
|
640
|
+
except FileNotFoundError:
|
|
641
|
+
sys.exit(f"RushTI: --config file not found: {cli_args.get('config')}")
|
|
642
|
+
|
|
626
643
|
# Apply log level override if specified
|
|
627
644
|
apply_log_level(cli_args.get("log_level"))
|
|
628
645
|
|
|
@@ -642,7 +659,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
|
|
|
642
659
|
try:
|
|
643
660
|
from rushti.tm1_integration import read_taskfile_from_tm1, connect_to_tm1_instance
|
|
644
661
|
|
|
645
|
-
tm1_source = connect_to_tm1_instance(tm1_instance,
|
|
662
|
+
tm1_source = connect_to_tm1_instance(tm1_instance, config_path)
|
|
646
663
|
try:
|
|
647
664
|
# Resolve execution mode for TM1 read (affects predecessor parsing)
|
|
648
665
|
tm1_mode = "opt" if cli_args.get("execution_mode") == ExecutionMode.OPT else "norm"
|
|
@@ -749,6 +766,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
|
|
|
749
766
|
tasks_file_path=tasks_file_path_for_services if not tm1_taskfile else None,
|
|
750
767
|
workflow=workflow,
|
|
751
768
|
exclusive=exclusive_mode,
|
|
769
|
+
config_path=config_path,
|
|
752
770
|
tm1_instances=tm1_instances_needed,
|
|
753
771
|
)
|
|
754
772
|
|
|
@@ -1103,7 +1121,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
|
|
|
1103
1121
|
)
|
|
1104
1122
|
tm1_upload = connect_to_tm1_instance(
|
|
1105
1123
|
upload_instance,
|
|
1106
|
-
|
|
1124
|
+
config_path,
|
|
1107
1125
|
)
|
|
1108
1126
|
try:
|
|
1109
1127
|
results_df = build_results_dataframe(
|
|
@@ -10,6 +10,7 @@ import sys
|
|
|
10
10
|
|
|
11
11
|
from TM1py import TM1Service
|
|
12
12
|
|
|
13
|
+
from rushti.app_paths import add_config_arg, resolve_config_path
|
|
13
14
|
from rushti.logging_setup import add_log_level_arg, apply_log_level
|
|
14
15
|
from rushti.settings import load_settings
|
|
15
16
|
from rushti.tm1_build import build_logging_objects, get_build_status
|
|
@@ -25,8 +26,6 @@ def run_build_command(argv: list) -> None:
|
|
|
25
26
|
|
|
26
27
|
:param argv: Command line arguments
|
|
27
28
|
"""
|
|
28
|
-
from rushti.cli import CONFIG
|
|
29
|
-
|
|
30
29
|
parser = argparse.ArgumentParser(
|
|
31
30
|
prog=f"{APP_NAME} build",
|
|
32
31
|
description="Create TM1 dimensions and cube for RushTI execution logging.",
|
|
@@ -60,6 +59,7 @@ Examples:
|
|
|
60
59
|
metavar="FILE",
|
|
61
60
|
help="Path to settings.ini file",
|
|
62
61
|
)
|
|
62
|
+
add_config_arg(parser)
|
|
63
63
|
add_log_level_arg(parser)
|
|
64
64
|
|
|
65
65
|
args = parser.parse_args(argv[2:]) # Skip "rushti" and "build"
|
|
@@ -68,16 +68,22 @@ Examples:
|
|
|
68
68
|
# Load settings
|
|
69
69
|
settings = load_settings(args.settings_file)
|
|
70
70
|
|
|
71
|
+
# Resolve config.ini location (CLI --config > RUSHTI_DIR > legacy CWD > config/)
|
|
72
|
+
try:
|
|
73
|
+
config_path = resolve_config_path("config.ini", cli_path=args.config)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
sys.exit(f"RushTI: --config file not found: {args.config}")
|
|
76
|
+
|
|
71
77
|
# Load config to get TM1 connection details
|
|
72
|
-
if not os.path.isfile(
|
|
73
|
-
print(f"Error: {
|
|
78
|
+
if not os.path.isfile(config_path):
|
|
79
|
+
print(f"Error: {config_path} does not exist")
|
|
74
80
|
sys.exit(1)
|
|
75
81
|
|
|
76
82
|
config = configparser.ConfigParser()
|
|
77
|
-
config.read(
|
|
83
|
+
config.read(config_path, encoding="utf-8")
|
|
78
84
|
|
|
79
85
|
if args.tm1_instance not in config.sections():
|
|
80
|
-
print(f"Error: Instance '{args.tm1_instance}' not found in {
|
|
86
|
+
print(f"Error: Instance '{args.tm1_instance}' not found in {config_path}")
|
|
81
87
|
print(f"Available instances: {', '.join(config.sections())}")
|
|
82
88
|
sys.exit(1)
|
|
83
89
|
|
|
@@ -10,6 +10,7 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Optional
|
|
12
12
|
|
|
13
|
+
from rushti.app_paths import add_config_arg
|
|
13
14
|
from rushti.checkpoint import find_checkpoint_for_taskfile, load_checkpoint
|
|
14
15
|
from rushti.logging_setup import add_log_level_arg, apply_log_level
|
|
15
16
|
from rushti.settings import load_settings
|
|
@@ -86,6 +87,7 @@ Examples:
|
|
|
86
87
|
default=False,
|
|
87
88
|
help="Force resume even if checkpoint doesn't match taskfile",
|
|
88
89
|
)
|
|
90
|
+
add_config_arg(parser)
|
|
89
91
|
add_log_level_arg(parser)
|
|
90
92
|
|
|
91
93
|
args = parser.parse_args(argv[2:])
|
|
@@ -200,6 +202,9 @@ Examples:
|
|
|
200
202
|
if args.settings_file:
|
|
201
203
|
resume_argv.extend(["--settings", args.settings_file])
|
|
202
204
|
|
|
205
|
+
if args.config:
|
|
206
|
+
resume_argv.extend(["--config", args.config])
|
|
207
|
+
|
|
203
208
|
if args.force:
|
|
204
209
|
resume_argv.append("--force")
|
|
205
210
|
|
|
@@ -9,8 +9,9 @@ Subcommands:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
|
+
import sys
|
|
12
13
|
|
|
13
|
-
from rushti.app_paths import resolve_config_path
|
|
14
|
+
from rushti.app_paths import add_config_arg, resolve_config_path
|
|
14
15
|
from rushti.commands.tasks.expand import handle_tasks_expand
|
|
15
16
|
from rushti.commands.tasks.export import handle_tasks_export
|
|
16
17
|
from rushti.commands.tasks.push import handle_tasks_push
|
|
@@ -98,6 +99,7 @@ Examples:
|
|
|
98
99
|
help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
|
|
99
100
|
"Ignored for file sources (auto-detected). Default: opt",
|
|
100
101
|
)
|
|
102
|
+
add_config_arg(export_parser)
|
|
101
103
|
add_log_level_arg(export_parser)
|
|
102
104
|
|
|
103
105
|
# --- push subcommand ---
|
|
@@ -122,6 +124,7 @@ Examples:
|
|
|
122
124
|
help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
|
|
123
125
|
"Ignored for file sources (auto-detected). Default: opt",
|
|
124
126
|
)
|
|
127
|
+
add_config_arg(push_parser)
|
|
125
128
|
add_log_level_arg(push_parser)
|
|
126
129
|
|
|
127
130
|
# --- expand subcommand ---
|
|
@@ -155,6 +158,7 @@ Examples:
|
|
|
155
158
|
help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
|
|
156
159
|
"Ignored for file sources (auto-detected). Default: opt",
|
|
157
160
|
)
|
|
161
|
+
add_config_arg(expand_parser)
|
|
158
162
|
add_log_level_arg(expand_parser)
|
|
159
163
|
|
|
160
164
|
# --- visualize subcommand ---
|
|
@@ -188,6 +192,7 @@ Examples:
|
|
|
188
192
|
help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
|
|
189
193
|
"Ignored for file sources (auto-detected). Default: opt",
|
|
190
194
|
)
|
|
195
|
+
add_config_arg(visualize_parser)
|
|
191
196
|
add_log_level_arg(visualize_parser)
|
|
192
197
|
|
|
193
198
|
# --- validate subcommand ---
|
|
@@ -219,14 +224,18 @@ Examples:
|
|
|
219
224
|
help="[TM1 sources only] 'norm' for wait-based sequencing, 'opt' for explicit predecessors. "
|
|
220
225
|
"Ignored for file sources (auto-detected). Default: opt",
|
|
221
226
|
)
|
|
227
|
+
add_config_arg(validate_parser)
|
|
222
228
|
add_log_level_arg(validate_parser)
|
|
223
229
|
|
|
224
230
|
# Parse arguments (skip script name and 'tasks' command)
|
|
225
231
|
args = parser.parse_args(argv[2:])
|
|
226
232
|
apply_log_level(args.log_level)
|
|
227
233
|
|
|
228
|
-
# Resolve config
|
|
229
|
-
|
|
234
|
+
# Resolve config.ini location (CLI --config > RUSHTI_DIR > legacy CWD > config/)
|
|
235
|
+
try:
|
|
236
|
+
config_path = resolve_config_path("config.ini", cli_path=args.config)
|
|
237
|
+
except FileNotFoundError:
|
|
238
|
+
sys.exit(f"RushTI: --config file not found: {args.config}")
|
|
230
239
|
|
|
231
240
|
# Dispatch to appropriate handler based on subcommand
|
|
232
241
|
if args.subcommand == "export":
|
|
@@ -234,16 +234,26 @@ def _prepare_dashboard_data(
|
|
|
234
234
|
}
|
|
235
235
|
)
|
|
236
236
|
|
|
237
|
-
# Build per-task aggregate data (across all runs)
|
|
237
|
+
# Build per-task aggregate data (across all runs). Each row carries
|
|
238
|
+
# both ``process`` (may be empty for chore rows) and ``chore`` so the
|
|
239
|
+
# dashboard can render a unified "Task target" column with a kind
|
|
240
|
+
# indicator. ``task_target``/``task_kind`` are the canonical fields
|
|
241
|
+
# for display; the raw kind fields stay populated for backward
|
|
242
|
+
# compatibility with downstream consumers.
|
|
238
243
|
task_data: Dict[str, Dict[str, Any]] = {}
|
|
239
244
|
for tr in task_results:
|
|
240
245
|
sig = tr["task_signature"]
|
|
246
|
+
process_val = tr.get("process") or ""
|
|
247
|
+
chore_val = tr.get("chore") or ""
|
|
241
248
|
if sig not in task_data:
|
|
242
249
|
task_data[sig] = {
|
|
243
250
|
"task_signature": sig,
|
|
244
251
|
"task_id": tr["task_id"],
|
|
245
252
|
"instance": tr["instance"],
|
|
246
|
-
"process":
|
|
253
|
+
"process": process_val,
|
|
254
|
+
"chore": chore_val,
|
|
255
|
+
"task_target": chore_val or process_val,
|
|
256
|
+
"task_kind": "chore" if chore_val else "process",
|
|
247
257
|
"durations": [],
|
|
248
258
|
"successes": 0,
|
|
249
259
|
"total": 0,
|
|
@@ -263,6 +273,9 @@ def _prepare_dashboard_data(
|
|
|
263
273
|
"task_id": data["task_id"],
|
|
264
274
|
"instance": data["instance"],
|
|
265
275
|
"process": data["process"],
|
|
276
|
+
"chore": data["chore"],
|
|
277
|
+
"task_target": data["task_target"],
|
|
278
|
+
"task_kind": data["task_kind"],
|
|
266
279
|
"executions": data["total"],
|
|
267
280
|
"success_rate": (
|
|
268
281
|
round(data["successes"] / data["total"] * 100, 1) if data["total"] > 0 else 0
|
|
@@ -290,10 +303,15 @@ def _prepare_dashboard_data(
|
|
|
290
303
|
outliers = []
|
|
291
304
|
for tr in task_results:
|
|
292
305
|
if tr["duration_seconds"] is not None:
|
|
306
|
+
process_val = tr.get("process") or ""
|
|
307
|
+
chore_val = tr.get("chore") or ""
|
|
293
308
|
outliers.append(
|
|
294
309
|
{
|
|
295
310
|
"task_id": tr["task_id"],
|
|
296
|
-
"process":
|
|
311
|
+
"process": process_val,
|
|
312
|
+
"chore": chore_val,
|
|
313
|
+
"task_target": chore_val or process_val,
|
|
314
|
+
"task_kind": "chore" if chore_val else "process",
|
|
297
315
|
"instance": tr["instance"],
|
|
298
316
|
"run_id": tr["run_id"],
|
|
299
317
|
"duration": round(tr["duration_seconds"], 2),
|
|
@@ -308,10 +326,15 @@ def _prepare_dashboard_data(
|
|
|
308
326
|
failures = []
|
|
309
327
|
for tr in task_results:
|
|
310
328
|
if tr["status"] != "Success":
|
|
329
|
+
process_val = tr.get("process") or ""
|
|
330
|
+
chore_val = tr.get("chore") or ""
|
|
311
331
|
failures.append(
|
|
312
332
|
{
|
|
313
333
|
"task_id": tr["task_id"],
|
|
314
|
-
"process":
|
|
334
|
+
"process": process_val,
|
|
335
|
+
"chore": chore_val,
|
|
336
|
+
"task_target": chore_val or process_val,
|
|
337
|
+
"task_kind": "chore" if chore_val else "process",
|
|
315
338
|
"instance": tr["instance"],
|
|
316
339
|
"run_id": tr["run_id"],
|
|
317
340
|
"duration": round(tr["duration_seconds"], 2) if tr["duration_seconds"] else 0,
|
|
@@ -322,13 +345,18 @@ def _prepare_dashboard_data(
|
|
|
322
345
|
# Slim task_results for JS (only fields needed for interactive filtering)
|
|
323
346
|
slim_task_results = []
|
|
324
347
|
for tr in task_results:
|
|
348
|
+
process_val = tr.get("process") or ""
|
|
349
|
+
chore_val = tr.get("chore") or ""
|
|
325
350
|
slim_task_results.append(
|
|
326
351
|
{
|
|
327
352
|
"run_id": tr["run_id"],
|
|
328
353
|
"task_id": tr["task_id"],
|
|
329
354
|
"task_signature": tr["task_signature"],
|
|
330
355
|
"instance": tr["instance"],
|
|
331
|
-
"process":
|
|
356
|
+
"process": process_val,
|
|
357
|
+
"chore": chore_val,
|
|
358
|
+
"task_target": chore_val or process_val,
|
|
359
|
+
"task_kind": "chore" if chore_val else "process",
|
|
332
360
|
"status": tr["status"],
|
|
333
361
|
"duration_seconds": tr["duration_seconds"],
|
|
334
362
|
"error_message": tr.get("error_message"),
|
|
@@ -578,6 +606,11 @@ def generate_dashboard(
|
|
|
578
606
|
.status-partial {{ color: #D97706; font-weight: 600; }}
|
|
579
607
|
.high-cv {{ background: #FFF7ED; }}
|
|
580
608
|
.duration-slow {{ color: #DC2626; font-weight: 600; }}
|
|
609
|
+
.kind-tag {{
|
|
610
|
+
display: inline-block; padding: 1px 5px; border-radius: 4px;
|
|
611
|
+
font-size: 0.7rem; font-weight: 700; color: #475569;
|
|
612
|
+
background: #E2E8F0; margin-right: 4px;
|
|
613
|
+
}}
|
|
581
614
|
|
|
582
615
|
/* Accordion */
|
|
583
616
|
.accordion {{ margin-bottom: 20px; }}
|
|
@@ -752,7 +785,7 @@ def generate_dashboard(
|
|
|
752
785
|
<thead><tr>
|
|
753
786
|
<th onclick="sortTaskTable(0)">Task ID <span class="sort-arrow"></span></th>
|
|
754
787
|
<th onclick="sortTaskTable(1)">Instance <span class="sort-arrow"></span></th>
|
|
755
|
-
<th onclick="sortTaskTable(2)">
|
|
788
|
+
<th onclick="sortTaskTable(2)">Task target <span class="sort-arrow"></span></th>
|
|
756
789
|
<th onclick="sortTaskTable(3)">Avg (s) <span class="sort-arrow"></span></th>
|
|
757
790
|
<th onclick="sortTaskTable(4)">Min (s) <span class="sort-arrow"></span></th>
|
|
758
791
|
<th onclick="sortTaskTable(5)">Max (s) <span class="sort-arrow"></span></th>
|
|
@@ -768,7 +801,7 @@ def generate_dashboard(
|
|
|
768
801
|
<h3>Top 10 Slowest Executions <span class="help-icon">?<span class="help-tip">How to read: These are the 10 longest-running individual task executions across the selected runs. The "vs Median" column tells you how many times slower each one was compared to the typical task — a high multiplier (e.g., 5×) means that execution was significantly slower than usual and may be worth investigating. Check whether the same task appears multiple times — if so, it is a consistent bottleneck.</span></span></h3>
|
|
769
802
|
<table id="outlierTable">
|
|
770
803
|
<thead><tr>
|
|
771
|
-
<th>Task ID</th><th>
|
|
804
|
+
<th>Task ID</th><th>Task target</th><th>Instance</th>
|
|
772
805
|
<th>Run</th><th>Duration (s)</th><th>vs Median</th><th>Status</th>
|
|
773
806
|
</tr></thead>
|
|
774
807
|
<tbody id="outlierTableBody"></tbody>
|
|
@@ -779,7 +812,7 @@ def generate_dashboard(
|
|
|
779
812
|
<h3>Failed Processes <span class="help-icon">?<span class="help-tip">How to read: Every task execution that did not succeed is listed here. Look for patterns — does the same task fail repeatedly across runs? That points to a systemic issue. If failures are scattered across different tasks, the problem may be environmental (server load, connectivity). Hover over the Error column to see the full error message for each failure.</span></span></h3>
|
|
780
813
|
<table id="failureTable">
|
|
781
814
|
<thead><tr>
|
|
782
|
-
<th>Task ID</th><th>
|
|
815
|
+
<th>Task ID</th><th>Task target</th><th>Instance</th>
|
|
783
816
|
<th>Run</th><th>Duration (s)</th><th>Error</th>
|
|
784
817
|
</tr></thead>
|
|
785
818
|
<tbody id="failureTableBody"></tbody>
|
|
@@ -1260,14 +1293,14 @@ def generate_dashboard(
|
|
|
1260
1293
|
// Also store a lookup for tooltip labels
|
|
1261
1294
|
const sigLabel = {{}};
|
|
1262
1295
|
relevant.forEach(tr => {{
|
|
1263
|
-
if (!sigLabel[tr.task_signature]) sigLabel[tr.task_signature] = tr.process;
|
|
1296
|
+
if (!sigLabel[tr.task_signature]) sigLabel[tr.task_signature] = tr.task_target || tr.process || tr.chore;
|
|
1264
1297
|
}});
|
|
1265
1298
|
|
|
1266
1299
|
const datasets = runs.map((run, idx) => {{
|
|
1267
1300
|
const runTasks = relevant.filter(tr => tr.run_id === run.run_id);
|
|
1268
1301
|
return {{
|
|
1269
1302
|
label: formatDate(run.start_time),
|
|
1270
|
-
data: runTasks.map(t => ({{ x: sigIndex[t.task_signature], y: t.duration_seconds || 0, _sig: t.task_signature, _task: t.task_id, _proc: t.process }})),
|
|
1303
|
+
data: runTasks.map(t => ({{ x: sigIndex[t.task_signature], y: t.duration_seconds || 0, _sig: t.task_signature, _task: t.task_id, _proc: t.task_target || t.process || t.chore }})),
|
|
1271
1304
|
backgroundColor: RUN_COLORS[idx % RUN_COLORS.length] + '80',
|
|
1272
1305
|
borderColor: RUN_COLORS[idx % RUN_COLORS.length],
|
|
1273
1306
|
pointRadius: 3,
|
|
@@ -1470,7 +1503,10 @@ def generate_dashboard(
|
|
|
1470
1503
|
const sig = tr.task_signature;
|
|
1471
1504
|
if (!taskData[sig]) {{
|
|
1472
1505
|
taskData[sig] = {{
|
|
1473
|
-
task_id: tr.task_id, instance: tr.instance,
|
|
1506
|
+
task_id: tr.task_id, instance: tr.instance,
|
|
1507
|
+
process: tr.process, chore: tr.chore,
|
|
1508
|
+
task_target: tr.task_target || tr.process || tr.chore,
|
|
1509
|
+
task_kind: tr.task_kind || (tr.chore ? 'chore' : 'process'),
|
|
1474
1510
|
durations: [], successes: 0, total: 0
|
|
1475
1511
|
}};
|
|
1476
1512
|
}}
|
|
@@ -1492,7 +1528,7 @@ def generate_dashboard(
|
|
|
1492
1528
|
}}
|
|
1493
1529
|
|
|
1494
1530
|
function applyTaskSort(rows) {{
|
|
1495
|
-
const fields = ['task_id', 'instance', '
|
|
1531
|
+
const fields = ['task_id', 'instance', 'task_target', 'avg', 'mn', 'mx', 'stdDev', 'successRate', 'total'];
|
|
1496
1532
|
const field = fields[taskTableSortCol];
|
|
1497
1533
|
rows.sort((a, b) => {{
|
|
1498
1534
|
const av = a[field], bv = b[field];
|
|
@@ -1556,10 +1592,12 @@ def generate_dashboard(
|
|
|
1556
1592
|
tbody.innerHTML = pageRows.map(r => {{
|
|
1557
1593
|
const cvClass = r.cv > 0.5 ? 'high-cv' : '';
|
|
1558
1594
|
const statusCls = r.successRate < 100 ? 'status-fail' : 'status-success';
|
|
1595
|
+
const kindTag = r.task_kind === 'chore' ? '[C]' : '[P]';
|
|
1596
|
+
const target = r.task_target || r.process || r.chore || '';
|
|
1559
1597
|
return `<tr class="${{cvClass}}">
|
|
1560
1598
|
<td class="mono">${{r.task_id}}</td>
|
|
1561
1599
|
<td>${{r.instance}}</td>
|
|
1562
|
-
<td>${{
|
|
1600
|
+
<td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
|
|
1563
1601
|
<td class="mono">${{r.avg.toFixed(2)}}</td>
|
|
1564
1602
|
<td class="mono">${{r.mn.toFixed(2)}}</td>
|
|
1565
1603
|
<td class="mono">${{r.mx.toFixed(2)}}</td>
|
|
@@ -1585,9 +1623,11 @@ def generate_dashboard(
|
|
|
1585
1623
|
tbody.innerHTML = sorted.map(o => {{
|
|
1586
1624
|
const statusCls = o.status === 'Success' ? 'status-success' : 'status-fail';
|
|
1587
1625
|
const vsMedian = o.duration_seconds - median;
|
|
1626
|
+
const kindTag = o.task_kind === 'chore' || o.chore ? '[C]' : '[P]';
|
|
1627
|
+
const target = o.task_target || o.process || o.chore || '';
|
|
1588
1628
|
return `<tr>
|
|
1589
1629
|
<td class="mono">${{o.task_id}}</td>
|
|
1590
|
-
<td>${{
|
|
1630
|
+
<td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
|
|
1591
1631
|
<td>${{o.instance}}</td>
|
|
1592
1632
|
<td class="mono">${{o.run_id}}</td>
|
|
1593
1633
|
<td class="mono duration-slow">${{o.duration_seconds.toFixed(2)}}</td>
|
|
@@ -1613,15 +1653,19 @@ def generate_dashboard(
|
|
|
1613
1653
|
}}
|
|
1614
1654
|
|
|
1615
1655
|
panel.style.display = 'block';
|
|
1616
|
-
tbody.innerHTML = filtered.map(f =>
|
|
1656
|
+
tbody.innerHTML = filtered.map(f => {{
|
|
1657
|
+
const kindTag = f.task_kind === 'chore' || f.chore ? '[C]' : '[P]';
|
|
1658
|
+
const target = f.task_target || f.process || f.chore || '';
|
|
1659
|
+
return `<tr>
|
|
1617
1660
|
<td class="mono">${{f.task_id}}</td>
|
|
1618
|
-
<td>${{
|
|
1661
|
+
<td><span class="kind-tag">${{kindTag}}</span> ${{target}}</td>
|
|
1619
1662
|
<td>${{f.instance}}</td>
|
|
1620
1663
|
<td class="mono">${{f.run_id}}</td>
|
|
1621
1664
|
<td class="mono">${{f.duration_seconds != null ? f.duration_seconds.toFixed(2) : '-'}}</td>
|
|
1622
1665
|
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
|
|
1623
1666
|
title="${{(f.error_message || '').replace(/"/g, '"')}}">${{f.error_message || '-'}}</td>
|
|
1624
|
-
</tr
|
|
1667
|
+
</tr>`;
|
|
1668
|
+
}}).join('');
|
|
1625
1669
|
}}
|
|
1626
1670
|
|
|
1627
1671
|
function updateConfigDetails(runs) {{
|