rushti 2.3.0__tar.gz → 2.3.2__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.
Files changed (62) hide show
  1. {rushti-2.3.0/src/rushti.egg-info → rushti-2.3.2}/PKG-INFO +6 -1
  2. {rushti-2.3.0 → rushti-2.3.2}/README.md +5 -0
  3. {rushti-2.3.0 → rushti-2.3.2}/pyproject.toml +1 -1
  4. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/__init__.py +1 -1
  5. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/app_paths.py +22 -0
  6. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/cli.py +17 -2
  7. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/build.py +12 -6
  8. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/resume.py +5 -0
  9. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/__init__.py +12 -3
  10. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/execution.py +2 -4
  11. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/tm1_objects.py +87 -11
  12. {rushti-2.3.0 → rushti-2.3.2/src/rushti.egg-info}/PKG-INFO +6 -1
  13. {rushti-2.3.0 → rushti-2.3.2}/LICENSE +0 -0
  14. {rushti-2.3.0 → rushti-2.3.2}/setup.cfg +0 -0
  15. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/_shlex_utils.py +0 -0
  16. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/checkpoint.py +0 -0
  17. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/__init__.py +0 -0
  18. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/db.py +0 -0
  19. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/__init__.py +0 -0
  20. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/analyze.py +0 -0
  21. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/export.py +0 -0
  22. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/list.py +0 -0
  23. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/optimize.py +0 -0
  24. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/stats/visualize.py +0 -0
  25. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/expand.py +0 -0
  26. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/export.py +0 -0
  27. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/push.py +0 -0
  28. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/validate.py +0 -0
  29. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/commands/tasks/visualize.py +0 -0
  30. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/contention_analyzer.py +0 -0
  31. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/dag.py +0 -0
  32. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/dashboard.py +0 -0
  33. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/db_admin.py +0 -0
  34. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/exclusive.py +0 -0
  35. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/logging.py +0 -0
  36. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/logging_setup.py +0 -0
  37. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/messages.py +0 -0
  38. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/optimization_report.py +0 -0
  39. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/optimizer.py +0 -0
  40. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/parsing.py +0 -0
  41. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/results_writer.py +0 -0
  42. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/settings.py +0 -0
  43. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/__init__.py +0 -0
  44. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/dynamodb.py +0 -0
  45. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/paths.py +0 -0
  46. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/repository.py +0 -0
  47. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/signature.py +0 -0
  48. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/stats/sqlite.py +0 -0
  49. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/task.py +0 -0
  50. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/taskfile.py +0 -0
  51. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/taskfile_ops.py +0 -0
  52. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/templates/__init__.py +0 -0
  53. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/templates/visualization.html +0 -0
  54. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/tm1_build.py +0 -0
  55. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/tm1_integration.py +0 -0
  56. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/utils.py +0 -0
  57. {rushti-2.3.0 → rushti-2.3.2}/src/rushti/visualization_template.py +0 -0
  58. {rushti-2.3.0 → rushti-2.3.2}/src/rushti.egg-info/SOURCES.txt +0 -0
  59. {rushti-2.3.0 → rushti-2.3.2}/src/rushti.egg-info/dependency_links.txt +0 -0
  60. {rushti-2.3.0 → rushti-2.3.2}/src/rushti.egg-info/entry_points.txt +0 -0
  61. {rushti-2.3.0 → rushti-2.3.2}/src/rushti.egg-info/requires.txt +0 -0
  62. {rushti-2.3.0 → rushti-2.3.2}/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.0
3
+ Version: 2.3.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rushti"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  description = "RushTI - Parallel TM1 TurboIntegrator Process Execution"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.3.0"
8
+ __version__ = "2.3.2"
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
  )
@@ -273,6 +274,9 @@ Configuration:
273
274
  # Add taskfile source arguments
274
275
  add_taskfile_source_args(parser, required=False, include_settings=True)
275
276
 
277
+ # Add --config (relocates config.ini only)
278
+ add_config_arg(parser)
279
+
276
280
  parser.add_argument(
277
281
  "--max-workers",
278
282
  "-w",
@@ -426,6 +430,7 @@ def parse_named_arguments(argv: list):
426
430
  "workflow": workflow,
427
431
  "log_level": args.log_level,
428
432
  "detailed_results": args.detailed_results,
433
+ "config": args.config,
429
434
  }
430
435
 
431
436
  return tasks_file_path, cli_args
@@ -471,6 +476,7 @@ def parse_arguments(argv: list):
471
476
  "exclusive": None,
472
477
  "no_checkpoint": False,
473
478
  "log_level": None, # Not supported in positional style
479
+ "config": None, # Legacy positional style does not support --config
474
480
  }
475
481
  return tasks_file, cli_args
476
482
 
@@ -626,6 +632,14 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
626
632
  if resume_context:
627
633
  cli_args.update(resume_context)
628
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
+
629
643
  # Apply log level override if specified
630
644
  apply_log_level(cli_args.get("log_level"))
631
645
 
@@ -645,7 +659,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
645
659
  try:
646
660
  from rushti.tm1_integration import read_taskfile_from_tm1, connect_to_tm1_instance
647
661
 
648
- tm1_source = connect_to_tm1_instance(tm1_instance, CONFIG)
662
+ tm1_source = connect_to_tm1_instance(tm1_instance, config_path)
649
663
  try:
650
664
  # Resolve execution mode for TM1 read (affects predecessor parsing)
651
665
  tm1_mode = "opt" if cli_args.get("execution_mode") == ExecutionMode.OPT else "norm"
@@ -752,6 +766,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
752
766
  tasks_file_path=tasks_file_path_for_services if not tm1_taskfile else None,
753
767
  workflow=workflow,
754
768
  exclusive=exclusive_mode,
769
+ config_path=config_path,
755
770
  tm1_instances=tm1_instances_needed,
756
771
  )
757
772
 
@@ -1106,7 +1121,7 @@ Use '{APP_NAME} <command> --help' for command-specific options and examples.
1106
1121
  )
1107
1122
  tm1_upload = connect_to_tm1_instance(
1108
1123
  upload_instance,
1109
- CONFIG,
1124
+ config_path,
1110
1125
  )
1111
1126
  try:
1112
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(CONFIG):
73
- print(f"Error: {CONFIG} does not exist")
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(CONFIG, encoding="utf-8")
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 {CONFIG}")
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 path
229
- config_path = resolve_config_path("config.ini", cli_path=None)
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":
@@ -148,14 +148,12 @@ def setup_tm1_services(
148
148
  :param tasks_file_path: Path to the tasks file (can be None if tm1_instances provided)
149
149
  :param workflow: Workflow identifier for session context
150
150
  :param exclusive: Whether running in exclusive mode
151
- :param config_path: Optional path to config.ini file (defaults to resolved CONFIG)
151
+ :param config_path: Path to config.ini file (resolved by the caller; required)
152
152
  :param tm1_instances: Optional set of TM1 instance names (used when reading from TM1)
153
153
  :return: Dictionary server_names and TM1py.TM1Service instances pairs
154
154
  """
155
155
  if config_path is None:
156
- from rushti.cli import CONFIG
157
-
158
- config_path = CONFIG
156
+ raise ValueError("config_path is required")
159
157
 
160
158
  config_file = config_path
161
159
  if not os.path.isfile(config_file):
@@ -150,6 +150,8 @@ nProcessReturnCode = 0;
150
150
  nErrors = 0;
151
151
  nMetadataRecordCount= 0;
152
152
  nDataRecordCount = 0;
153
+ nHeaderChecked = 0;
154
+ nDataHeaderChecked = 0;
153
155
 
154
156
  ### Process Specific Constants
155
157
  cFileSrc = pSourceFile;
@@ -220,7 +222,9 @@ CubeSetLogChanges( cCubeTgt, 0 );
220
222
  If( nErrors = 0 );
221
223
  DatasourceType = 'CHARACTERDELIMITED';
222
224
  DatasourceNameForServer = cFileSrc;
223
- DatasourceASCIIHeaderRecords = 1;
225
+ # 0 (not 1): the header row is consumed as the first record so the
226
+ # metadata tab can validate it against the expected columns (issue #169).
227
+ DatasourceASCIIHeaderRecords = 0;
224
228
  DatasourceASCIIDelimiter=',';
225
229
  DatasourceASCIIDecimalSeparator='.';
226
230
  DatasourceASCIIThousandSeparator='';
@@ -290,6 +294,8 @@ nProcessReturnCode = 0;
290
294
  nErrors = 0;
291
295
  nMetadataRecordCount= 0;
292
296
  nDataRecordCount = 0;
297
+ nHeaderChecked = 0;
298
+ nDataHeaderChecked = 0;
293
299
 
294
300
  ### Process Specific Constants
295
301
  cFileSrc = pSourceFile;
@@ -356,7 +362,9 @@ EndIf;
356
362
  If( nErrors = 0 );
357
363
  DatasourceType = 'CHARACTERDELIMITED';
358
364
  DatasourceNameForServer = cFileSrc;
359
- DatasourceASCIIHeaderRecords = 1;
365
+ # 0 (not 1): the header row is consumed as the first record so the
366
+ # metadata tab can validate it against the expected columns (issue #169).
367
+ DatasourceASCIIHeaderRecords = 0;
360
368
  DatasourceASCIIDelimiter=',';
361
369
  DatasourceASCIIDecimalSeparator='.';
362
370
  DatasourceASCIIThousandSeparator='';
@@ -366,10 +374,17 @@ EndIf;
366
374
  #################################################################################################
367
375
  """
368
376
 
369
- PROCESS_METADATA = r"""#****Begin: Generated Statements***
377
+ # Marker pair TM1 Architect uses to inject auto-generated variable/parameter
378
+ # code at the top of a tab. Kept as the first lines of every generated
379
+ # procedure so the process round-trips cleanly if later opened in Architect.
380
+ _GENERATED_STATEMENTS_MARKER = """#****Begin: Generated Statements***
370
381
  #****End: Generated Statements****
382
+ """
371
383
 
372
- If( pLogOutput >= 1 );
384
+ # Body of the metadata tab. The header-validation preamble is generated from
385
+ # PROCESS_VARIABLES and prepended in ``build_metadata_procedure`` so it stays
386
+ # in lockstep with the CSV column contract (see issue #169).
387
+ _PROCESS_METADATA_BODY = r"""If( pLogOutput >= 1 );
373
388
  nMetadataRecordCount = nMetadataRecordCount + 1;
374
389
  EndIf;
375
390
 
@@ -388,10 +403,9 @@ if (DimensionElementExists(pRunId_Dim, vrun_id) = 0);
388
403
  DimensionElementInsertDirect(pRunId_Dim, '', vrun_id, 'N');
389
404
  endif;"""
390
405
 
391
- PROCESS_DATA = r"""#****Begin: Generated Statements***
392
- #****End: Generated Statements****
393
-
394
- If( pLogOutput >= 1 );
406
+ # Body of the data tab. The header-skip preamble is prepended in
407
+ # ``build_data_procedure`` so the CSV header row is not written to the cube.
408
+ _PROCESS_DATA_BODY = r"""If( pLogOutput >= 1 );
395
409
  nDataRecordCount = nDataRecordCount + 1;
396
410
  EndIf;
397
411
 
@@ -544,8 +558,9 @@ PROCESS_PARAMETERS = [
544
558
  # declaration list. Must match the column order produced by
545
559
  # ``rushti.tm1_integration.upload_results_to_tm1`` (which inserts
546
560
  # ``workflow`` and ``run_id`` ahead of the columns built by
547
- # ``build_results_dataframe``). Mismatched ordering silently scrambles
548
- # the loaded payload.
561
+ # ``build_results_dataframe``). Mismatched ordering scrambles the loaded
562
+ # payload; the metadata-tab header check generated from this list turns a
563
+ # stale-process mismatch into a hard error instead (see issue #169).
549
564
  PROCESS_VARIABLES = [
550
565
  "vworkflow",
551
566
  "vrun_id",
@@ -571,12 +586,73 @@ PROCESS_VARIABLES = [
571
586
  "vsucceed_on_minor_errors",
572
587
  ]
573
588
 
589
+
590
+ def build_metadata_procedure() -> str:
591
+ """Assemble the ``}rushti.load.results`` metadata tab.
592
+
593
+ Prepends a header-validation preamble generated from ``PROCESS_VARIABLES``
594
+ so it stays in lockstep with the CSV column contract. The loader maps CSV
595
+ columns to variables positionally; with ``asciiHeaderRecords=0`` the CSV
596
+ header row is the first source record, so validating each column name
597
+ against the variable it feeds makes a process left stale by an in-place
598
+ rushti upgrade fail loudly instead of silently writing values to the wrong
599
+ measures (issue #169). Re-run ``rushti build`` to refresh a stale process.
600
+ """
601
+ checks = "\n".join(
602
+ f" If( {var} @<> '{var[1:]}' );\n"
603
+ f" sHeaderError = sHeaderError | ' {var[1:]}';\n"
604
+ f" EndIf;"
605
+ for var in PROCESS_VARIABLES
606
+ )
607
+ preamble = f"""### Validate the CSV header against the columns this process expects.
608
+ ### The loader maps columns to variables positionally, so a process built by
609
+ ### an older rushti scrambles measures (issue #169). Fail loudly on mismatch
610
+ ### and skip the header so it is not loaded as data.
611
+ If( nHeaderChecked = 0 );
612
+ nHeaderChecked = 1;
613
+ sHeaderError = '';
614
+ {checks}
615
+ If( Trim( sHeaderError ) @<> '' );
616
+ nErrors = nErrors + 1;
617
+ sMessage = Expand( 'Results CSV header does not match the columns }}rushti.load.results expects (mismatched:%sHeaderError% ). This process is out of date with the rushti version that produced the file - re-run: rushti build --tm1-instance <instance>.' );
618
+ LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
619
+ ProcessQuit;
620
+ EndIf;
621
+ ItemSkip;
622
+ EndIf;
623
+
624
+ """
625
+ return _GENERATED_STATEMENTS_MARKER + "\n" + preamble + _PROCESS_METADATA_BODY
626
+
627
+
628
+ def build_data_procedure() -> str:
629
+ """Assemble the ``}rushti.load.results`` data tab.
630
+
631
+ Skips the CSV header row (validated in the metadata tab) so it is not
632
+ written to the cube, then delegates to the static body. See issue #169.
633
+ """
634
+ preamble = """### Skip the CSV header row (asciiHeaderRecords=0; validated in the
635
+ ### metadata tab). See issue #169.
636
+ If( nDataHeaderChecked = 0 );
637
+ nDataHeaderChecked = 1;
638
+ ItemSkip;
639
+ EndIf;
640
+
641
+ """
642
+ return _GENERATED_STATEMENTS_MARKER + "\n" + preamble + _PROCESS_DATA_BODY
643
+
644
+
645
+ PROCESS_METADATA = build_metadata_procedure()
646
+ PROCESS_DATA = build_data_procedure()
647
+
574
648
  PROCESS_DATASOURCE = {
575
649
  "Type": "ASCII",
576
650
  "asciiDecimalSeparator": ".",
577
651
  "asciiDelimiterChar": ",",
578
652
  "asciiDelimiterType": "Character",
579
- "asciiHeaderRecords": 1,
653
+ # 0: the header row is validated (not skipped) by the metadata tab; see
654
+ # the DatasourceASCIIHeaderRecords note in the prolog and issue #169.
655
+ "asciiHeaderRecords": 0,
580
656
  "asciiQuoteCharacter": '"',
581
657
  "asciiThousandSeparator": ",",
582
658
  "dataSourceNameForClient": "",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rushti
3
- Version: 2.3.0
3
+ Version: 2.3.2
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes