Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

All notable changes to RushTI are documented in this file.

## Unreleased
## Unreleased — `feat/issue-154-v12-load-results`

- Fix: `rushti build` now installs a TM1-version-aware `}rushti.load.results`
TI (closes #154). On v12 targets the body no longer references the removed
`CubeGetLogChanges` / `CubeSetLogChanges` / `ExecuteCommand` functions;
source-file cleanup uses the TM1-native `ASCIIDelete` instead of shelling
out via `cmd /c del`. The v11 body is unchanged.
- **Per-workflow `tm1_instance` setting** for results push and auto-load.
Set it inside a JSON taskfile's `settings` block to override the
`settings.ini` default per workflow. Resolution chain (highest wins):
Expand Down
15 changes: 11 additions & 4 deletions src/rushti/tm1_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@
Process,
Subset,
)
from TM1py.Utils import integerize_version

from rushti.tm1_objects import (
MEASURE_ATTRIBUTES,
MEASURE_ELEMENTS,
PROCESS_DATA,
PROCESS_DATASOURCE,
PROCESS_EPILOG,
PROCESS_EPILOG_V11,
PROCESS_EPILOG_V12,
PROCESS_METADATA,
PROCESS_PARAMETERS,
PROCESS_PROLOG,
PROCESS_PROLOG_V11,
PROCESS_PROLOG_V12,
PROCESS_VARIABLES,
RUN_ID_SEED_ELEMENTS,
SAMPLE_DATA,
Expand Down Expand Up @@ -558,12 +561,16 @@ def _create_process(tm1: TM1Service, process_name: str, force: bool = False) ->
logger.info(f"Replacing existing process: {process_name}")
tm1.processes.delete(process_name)

is_v12 = integerize_version(tm1.version, 2) >= 12
prolog = PROCESS_PROLOG_V12 if is_v12 else PROCESS_PROLOG_V11
epilog = PROCESS_EPILOG_V12 if is_v12 else PROCESS_EPILOG_V11

process = Process(
name=process_name,
prolog_procedure=PROCESS_PROLOG,
prolog_procedure=prolog,
metadata_procedure=PROCESS_METADATA,
data_procedure=PROCESS_DATA,
epilog_procedure=PROCESS_EPILOG,
epilog_procedure=epilog,
datasource_type=PROCESS_DATASOURCE["Type"],
datasource_ascii_decimal_separator=PROCESS_DATASOURCE["asciiDecimalSeparator"],
datasource_ascii_delimiter_char=PROCESS_DATASOURCE["asciiDelimiterChar"],
Expand Down
184 changes: 181 additions & 3 deletions src/rushti/tm1_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
- TASK_ID_ELEMENT_COUNT: Number of elements to generate for task_id dimension (1..N)
- RUN_ID_SEED_ELEMENTS: Seed elements for run_id dimension
- WORKFLOW_SEED_ELEMENTS: Seed elements for workflow dimension
- PROCESS_DEFINITION: Full TI process definition for }rushti.load.results
- PROCESS_PROLOG_V11 / PROCESS_PROLOG_V12: Prolog body per TM1 major version
- PROCESS_EPILOG_V11 / PROCESS_EPILOG_V12: Epilog body per TM1 major version
- SAMPLE_DATA: Sample cube data for demonstration taskfiles

The v12 variants drop calls to functions that were removed or are unsupported
on TM1 v12 (``CubeGetLogChanges`` / ``CubeSetLogChanges`` and
``ExecuteCommand``). Source-file cleanup on v12 is handled by the TM1-native
``ASCIIDelete`` instead of shelling out to ``cmd /c del``.
"""

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -85,7 +91,7 @@
# TI Process: }rushti.load.results
# ---------------------------------------------------------------------------

PROCESS_PROLOG = r"""#################################################################################################
PROCESS_PROLOG_V11 = r"""#################################################################################################
##~~Join the bedrock TM1 community on GitHub https://github.com/cubewise-code/bedrock Ver 4.0~~##
#################################################################################################

Expand Down Expand Up @@ -208,6 +214,142 @@
#Region - DataSource


### Assign data source
If( nErrors = 0 );
DatasourceType = 'CHARACTERDELIMITED';
DatasourceNameForServer = cFileSrc;
DatasourceASCIIHeaderRecords = 1;
DatasourceASCIIDelimiter=',';
DatasourceASCIIDecimalSeparator='.';
DatasourceASCIIThousandSeparator='';
EndIf;

#EndRegion - DataSource
#################################################################################################
"""

# v12 prolog: identical to v11 except the CubeGetLogChanges / CubeSetLogChanges
# block is removed. Those functions are unsupported on TM1 v12 and would cause
# the TI to fail at compile time.
PROCESS_PROLOG_V12 = r"""#################################################################################################
##~~Join the bedrock TM1 community on GitHub https://github.com/cubewise-code/bedrock Ver 4.0~~##
#################################################################################################

#****Begin: Generated Statements***
#****End: Generated Statements****

#################################################################################################
### CHANGE HISTORY:
### MODIFICATION DATE CHANGED BY COMMENT
### 2026-01-15 Nicolas Bisurgi Creation of Process
### YYYY-MM-DD Developer Name Reason for modification here
#################################################################################################
#Region @DOC
# Description:
# This process will load RushTI result files into a TM1 cube

# Use case:
# Storing RushTI execution stats in a TM1 Cube

# Note:
# * This process assumes a there is a rushti compliant cube and dimensions. If you don't have it
# * please run: rushti.py build --tm1-instance tm1srv01
#EndRegion @DOC
#################################################################################################

#################################################################################################
#Region Process Declarations
### Process Parameters
# a short description of what the process does goes here in cAction variable, e.g. "copied data from cube A to cube B". This will be written to the message log if pLogOutput=1
cAction = 'loading rushti stats into ' | pTargetCube;
cParamArray = '';
# to use the parameter array remove the line above and uncomment the line below, adding the needed parameters in the provided format
#cParamArray = 'pLogOutput:%pLogOutput%, pTemp:%pTemp%';

### Global Variables
StringGlobalVariable('sProcessReturnCode');
NumericGlobalVariable('nProcessReturnCode');

### Standard Constants
cThisProcName = GetProcessName();
cUserName = TM1User();
cTimeStamp = TimSt( Now, '\Y\m\d\h\i\s' );
cRandomInt = NumberToString( INT( RAND( ) * 1000 ));
cTempObjName = Expand('%cThisProcName%_%cTimeStamp%_%cRandomInt%');
cViewClr = '}bedrock_clear_' | cTempObjName;
cViewSrc = '}bedrock_source_' | cTempObjName;
cMsgErrorLevel = 'ERROR';
cMsgErrorContent = 'Process:%cThisProcName% ErrorMsg:%sMessage%';
cLogInfo = 'Process:%cThisProcName% run with parameters %cParamArray%';
sDelimEleStart = '¦';
sDelimDim = '&';
sDelimEle = '+';
nProcessReturnCode = 0;
nErrors = 0;
nMetadataRecordCount= 0;
nDataRecordCount = 0;

### Process Specific Constants
cFileSrc = pSourceFile;
cCubeTgt = pTargetCube;

#EndRegion Process Declarations
#################################################################################################

### LogOutput parameters
IF( pLogoutput = 1 );
LogOutput('INFO', Expand( cLogInfo ) );
ENDIF;

#################################################################################################
#Region Validate Parameters

# pLogOutput
If( pLogOutput >= 1 );
pLogOutput = 1;
Else;
pLogOutput = 0;
EndIf;

# Validate source file
If( Trim( cFileSrc ) @= '' );
nErrors = nErrors + 1;
sMessage = 'No source cfileube specified.';
LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
ElseIf( FileExists( cFileSrc ) = 0 );
nErrors = nErrors + 1;
sMessage = Expand( 'Invalid source file specified: %cFileSrc%.');
LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
EndIf;

# Validate target cube
If( Trim( cCubeTgt ) @= '' );
nErrors = nErrors + 1;
sMessage = 'No target cube specified.';
LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
ElseIf( CubeExists( cCubeTgt ) = 0 );
nErrors = nErrors + 1;
sMessage = Expand( 'Invalid target cube specified: %cCubeTgt%.');
LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
EndIf;

# If any parameters fail validation then set data source of process to null and go directly to epilog
If( nErrors > 0 );
DataSourceType = 'NULL';
If( pStrictErrorHandling = 1 );
ProcessQuit;
Else;
ProcessBreak;
EndIf;
EndIf;

#################################################################################################
#EndRegion Validate Parameters

#################################################################################################
#Region - DataSource


### Assign data source
If( nErrors = 0 );
DatasourceType = 'CHARACTERDELIMITED';
Expand Down Expand Up @@ -271,7 +413,7 @@
CellPutS(voriginal_task_id, pTargetCube, vworkflow, vrun_id, vtask_id, 'original_task_id');
"""

PROCESS_EPILOG = r"""#################################################################################################
PROCESS_EPILOG_V11 = r"""#################################################################################################
##~~Join the bedrock TM1 community on GitHub https://github.com/cubewise-code/bedrock Ver 4.0~~##
#################################################################################################

Expand Down Expand Up @@ -308,6 +450,42 @@

### End Epilog ###"""

# v12 epilog: source-file cleanup uses TM1-native ASCIIDelete (no shell-out),
# and the CubeSetLogChanges restore is omitted because the v12 prolog does not
# toggle transaction logging.
PROCESS_EPILOG_V12 = r"""#################################################################################################
##~~Join the bedrock TM1 community on GitHub https://github.com/cubewise-code/bedrock Ver 4.0~~##
#################################################################################################

#****Begin: Generated Statements***
#****End: Generated Statements****

### If required delete the source file
if(pDeleteSourceFile=1);
Sleep(1000);
ASCIIDelete(pSourceFile);
endif;

### Return code & final error message handling
If( nErrors > 0 );
sMessage = 'the process incurred at least 1 error. Please see above lines in this file for more details.';
nProcessReturnCode = 0;
LogOutput( cMsgErrorLevel, Expand( cMsgErrorContent ) );
sProcessReturnCode = Expand( '%sProcessReturnCode% Process:%cThisProcName% completed with errors. Check tm1server.log for details.' );
If( pStrictErrorHandling = 1 );
ProcessQuit;
EndIf;
Else;
sProcessAction = Expand( 'Process:%cThisProcName% successfully %cAction%. %nDataRecordCount% records processed.' );
sProcessReturnCode = Expand( '%sProcessReturnCode% %sProcessAction%' );
nProcessReturnCode = 1;
If( pLogoutput = 1 );
LogOutput('INFO', Expand( sProcessAction ) );
EndIf;
EndIf;

### End Epilog ###"""

PROCESS_PARAMETERS = [
{
"Name": "pSourceFile",
Expand Down
56 changes: 56 additions & 0 deletions tests/integration/test_v11_v12_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,62 @@ def test_execute_with_return(self):
)
self.assertTrue(success)

def test_auto_load_results(self):
"""Push CSV then call }rushti.load.results on v11 (legacy body path)."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name

stats_db = None
try:
stats_db = StatsDatabase(db_path=db_path, enabled=True)
run_id = "test_autoload_v11_" + datetime.now().strftime("%Y%m%d_%H%M%S")
workflow = "test-autoload-v11"

stats_db.start_run(run_id=run_id, workflow=workflow)
stats_db.record_task(
run_id=run_id,
task_id="task1",
instance=self.INSTANCE,
process="}bedrock.server.wait",
parameters={"pWaitSec": "1"},
success=True,
start_time=datetime.now(),
end_time=datetime.now() + timedelta(seconds=1),
retry_count=0,
error_message=None,
workflow=workflow,
)
stats_db.complete_run(run_id=run_id, success_count=1, failure_count=0)

results_df = build_results_dataframe(stats_db, workflow, run_id)
file_name = upload_results_to_tm1(self.tm1, workflow, run_id, results_df)

if not self.tm1.processes.exists("}rushti.load.results"):
self.skipTest("}rushti.load.results process not found on TM1 instance")

# v11 file paths need .blb to be locatable by the TI's CHARACTERDELIMITED source.
version = integerize_version(self.tm1.version, 2)
load_file_name = file_name + ".blb" if version < 12 else file_name

success, status, error_log = self.tm1.processes.execute_with_return(
"}rushti.load.results",
pSourceFile=load_file_name,
pTargetCube="rushti",
)
self.assertTrue(
success,
f"}}rushti.load.results failed on v11 (status={status}, error_log={error_log})",
)

try:
self.tm1.files.delete(file_name)
except Exception:
pass
finally:
if stats_db is not None:
stats_db.close()
os.unlink(db_path)


@pytest.mark.requires_tm1
@pytest.mark.integration
Expand Down
15 changes: 11 additions & 4 deletions tests/integration/tm1_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,29 @@ def ensure_load_results_process(
return False

from TM1py.Objects import Process
from TM1py.Utils import integerize_version
from rushti.tm1_objects import (
PROCESS_DATA,
PROCESS_DATASOURCE,
PROCESS_EPILOG,
PROCESS_EPILOG_V11,
PROCESS_EPILOG_V12,
PROCESS_METADATA,
PROCESS_PARAMETERS,
PROCESS_PROLOG,
PROCESS_PROLOG_V11,
PROCESS_PROLOG_V12,
PROCESS_VARIABLES,
)

is_v12 = integerize_version(tm1.version, 2) >= 12
prolog = PROCESS_PROLOG_V12 if is_v12 else PROCESS_PROLOG_V11
epilog = PROCESS_EPILOG_V12 if is_v12 else PROCESS_EPILOG_V11

process = Process(
name=process_name,
prolog_procedure=PROCESS_PROLOG,
prolog_procedure=prolog,
metadata_procedure=PROCESS_METADATA,
data_procedure=PROCESS_DATA,
epilog_procedure=PROCESS_EPILOG,
epilog_procedure=epilog,
datasource_type=PROCESS_DATASOURCE["Type"],
datasource_ascii_decimal_separator=PROCESS_DATASOURCE["asciiDecimalSeparator"],
datasource_ascii_delimiter_char=PROCESS_DATASOURCE["asciiDelimiterChar"],
Expand Down
Loading
Loading