Workflows: Add UI for taskmaestro workflows

Discover taskmaestro workflows under ~/.taskmaestro/workflows/ and surface
them as a "Workflows" collection in the project tree alongside Scripts.
Each workflow's task input schema is auto-generated by introspecting the
Pydantic models via a Python helper (rips.taskmaestro_helper) and rendered
as PDM field bindings - scalars for str/int/float/bool, and PdmPtrField
pickers for fields typed as rips.Case / rips.WellPath / rips.View.

The Run button on a RimWorkflow writes input.yaml from current bindings
(rips object references serialized as __resinsight_ref__ maps), spawns
the helper's run subcommand, and streams output to a modeless log dialog.
The helper resolves __resinsight_ref__ markers back to live rips objects
via the running ResInsight's gRPC server before driving taskmaestro.
This commit is contained in:
Kristian Bendiksen authored and Magne Sjaastad committed 2026-10-05 19:56:36 +02:00
1 parent 63eaa1266d
commit 3a7b8f6be8
49 files changed
+2861

No files matched your search

+1
View File
@@ -128,6 +128,7 @@ list(
ProjectDataModel/ProcessControl/CMakeLists_files.cmake
ProjectDataModel/Polygons/CMakeLists_files.cmake
ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake
ProjectDataModel/Workflow/CMakeLists_files.cmake
ProjectDataModel/WellLog/CMakeLists_files.cmake
ProjectDataModel/WellLog/WellLogTrack/CMakeLists_files.cmake
ProjectDataModel/WellMeasurement/CMakeLists_files.cmake
@@ -60,6 +60,7 @@ set(COMMAND_REFERENCED_CMAKE_FILES
FractureCommands/CMakeLists_files.cmake
PlotBuilderCommands/CMakeLists_files.cmake
PolygonCommands/CMakeLists_files.cmake
WorkflowCommands/CMakeLists_files.cmake
Sumo/CMakeLists_files.cmake
ToolCommands/CMakeLists_files.cmake
3dView/CMakeLists_files.cmake
@@ -0,0 +1,14 @@
set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RicNewWorkflowJobFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicRunWorkflowJobFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicCancelWorkflowJobFeature.h
)
set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RicNewWorkflowJobFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicRunWorkflowJobFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicCancelWorkflowJobFeature.cpp
)
list(APPEND COMMAND_CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})
list(APPEND COMMAND_CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
@@ -0,0 +1,46 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicCancelWorkflowJobFeature.h"
#include "Workflow/RimWorkflowJob.h"
#include "cafSelectionManagerTools.h"
#include <QAction>
CAF_CMD_SOURCE_INIT( RicCancelWorkflowJobFeature, "RicCancelWorkflowJobFeature" );
void RicCancelWorkflowJobFeature::onActionTriggered( bool isChecked )
{
auto jobs = caf::selectedObjectsByType<RimWorkflowJob*>();
if ( jobs.size() != 1 ) return;
jobs.front()->cancelJob();
}
void RicCancelWorkflowJobFeature::setupActionLook( QAction* actionToSetup )
{
actionToSetup->setText( "Cancel Job" );
actionToSetup->setIcon( QIcon( ":/stop.svg" ) );
}
bool RicCancelWorkflowJobFeature::isCommandEnabled() const
{
auto jobs = caf::selectedObjectsByType<RimWorkflowJob*>();
return jobs.size() == 1 && jobs.front()->isRunning();
}
@@ -0,0 +1,31 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafCmdFeature.h"
class RicCancelWorkflowJobFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
protected:
void onActionTriggered( bool isChecked ) override;
void setupActionLook( QAction* actionToSetup ) override;
bool isCommandEnabled() const override;
};
@@ -0,0 +1,65 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicNewWorkflowJobFeature.h"
#include "Workflow/RimWorkflow.h"
#include "Workflow/RimWorkflowJob.h"
#include "RiaLogging.h"
#include "cafPdmDefaultObjectFactory.h"
#include "cafSelectionManagerTools.h"
#include <QAction>
CAF_CMD_SOURCE_INIT( RicNewWorkflowJobFeature, "RicNewWorkflowJobFeature" );
void RicNewWorkflowJobFeature::onActionTriggered( bool isChecked )
{
auto workflows = caf::selectedObjectsByType<RimWorkflow*>();
if ( workflows.size() != 1 ) return;
RimWorkflow* workflow = workflows.front();
auto jobs = workflow->jobs();
if ( jobs.empty() )
{
RiaLogging::warning( "Cannot create a new job: workflow has no template job to clone." );
return;
}
auto* clone =
dynamic_cast<RimWorkflowJob*>( jobs.front()->xmlCapability()->copyByXmlSerialization( caf::PdmDefaultObjectFactory::instance() ) );
if ( !clone ) return;
clone->setJobName( QString( "Job %1" ).arg( jobs.size() + 1 ) );
workflow->addJob( clone );
clone->resolveReferencesRecursively();
workflow->uiCapability()->updateAllRequiredEditors();
}
void RicNewWorkflowJobFeature::setupActionLook( QAction* actionToSetup )
{
actionToSetup->setText( "New Job" );
actionToSetup->setIcon( QIcon( ":/caf/duplicate.svg" ) );
}
bool RicNewWorkflowJobFeature::isCommandEnabled() const
{
return caf::selectedObjectsByType<RimWorkflow*>().size() == 1;
}
@@ -0,0 +1,31 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafCmdFeature.h"
class RicNewWorkflowJobFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
protected:
void onActionTriggered( bool isChecked ) override;
void setupActionLook( QAction* actionToSetup ) override;
bool isCommandEnabled() const override;
};
@@ -0,0 +1,46 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicRunWorkflowJobFeature.h"
#include "Workflow/RimWorkflowJob.h"
#include "cafSelectionManagerTools.h"
#include <QAction>
CAF_CMD_SOURCE_INIT( RicRunWorkflowJobFeature, "RicRunWorkflowJobFeature" );
void RicRunWorkflowJobFeature::onActionTriggered( bool isChecked )
{
auto jobs = caf::selectedObjectsByType<RimWorkflowJob*>();
if ( jobs.size() != 1 ) return;
jobs.front()->runJob();
}
void RicRunWorkflowJobFeature::setupActionLook( QAction* actionToSetup )
{
actionToSetup->setText( "Run" );
actionToSetup->setIcon( QIcon( ":/Play.svg" ) );
}
bool RicRunWorkflowJobFeature::isCommandEnabled() const
{
auto jobs = caf::selectedObjectsByType<RimWorkflowJob*>();
return jobs.size() == 1 && !jobs.front()->isRunning();
}
@@ -0,0 +1,31 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafCmdFeature.h"
class RicRunWorkflowJobFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
protected:
void onActionTriggered( bool isChecked ) override;
void setupActionLook( QAction* actionToSetup ) override;
bool isCommandEnabled() const override;
};
@@ -102,6 +102,7 @@
#include "Tools/RimAutomationSettings.h"
#include "VerticalFlowPerformance/RimVfpDataCollection.h"
#include "VerticalFlowPerformance/RimVfpPlotCollection.h"
#include "Workflow/RimWorkflowCollection.h"
#include "RiuPlotMainWindow.h"
@@ -137,6 +138,9 @@ RimProject::RimProject()
CAF_PDM_InitFieldNoDefault( &scriptCollection, "ScriptCollection", "Octave Scripts", ":/octave.png" );
scriptCollection.xmlCapability()->disableIO();
CAF_PDM_InitFieldNoDefault( &workflowCollection, "WorkflowCollection", "Workflows", ":/Folder.png" );
workflowCollection.xmlCapability()->disableIO();
CAF_PDM_InitFieldNoDefault( &m_jobCollection, "JobCollection", "Jobs", ":/gear.png" );
CAF_PDM_InitFieldNoDefault( &m_mainPlotCollection, "MainPlotCollection", "Plots" );
@@ -196,6 +200,9 @@ RimProject::RimProject()
scriptCollection->uiCapability()->setUiName( "Scripts" );
scriptCollection->uiCapability()->setUiIconFromResourceString( ":/octave.png" );
workflowCollection = new RimWorkflowCollection();
workflowCollection->uiCapability()->setUiName( "Workflows" );
m_mainPlotCollection = new RimMainPlotCollection();
m_pinnedFieldCollection = new RimQuickAccessCollection();
m_jobCollection = new RimJobCollection();
@@ -1487,6 +1494,7 @@ void RimProject::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, Q
{
uiTreeOrdering.add( scriptCollection() );
uiTreeOrdering.add( jobCollection() );
uiTreeOrdering.add( workflowCollection() );
}
else if ( uiConfigName == "PlotWindow.Templates" )
{
@@ -52,6 +52,7 @@ class RimObservedSummaryData;
class RimOilField;
class RimColorLegendCollection;
class RimScriptCollection;
class RimWorkflowCollection;
class RimSummaryCase;
class RimSummaryEnsemble;
class RimSummaryCaseMainCollection;
@@ -98,6 +99,7 @@ public:
caf::PdmChildArrayField<RimOilField*> oilFields;
caf::PdmChildField<RimColorLegendCollection*> colorLegendCollection;
caf::PdmChildField<RimScriptCollection*> scriptCollection;
caf::PdmChildField<RimWorkflowCollection*> workflowCollection;
caf::PdmChildField<RimViewLinkerCollection*> viewLinkerCollection;
caf::PdmChildField<RimSummaryCalculationCollection*> calculationCollection;
caf::PdmChildField<RimGridCalculationCollection*> gridCalculationCollection;
@@ -0,0 +1,43 @@
set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RimWorkflow.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowCollection.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowJob.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowTaskInput.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFieldBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowStringBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFloatBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowIntBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowBoolBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowCaseBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowWellPathBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowViewBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDateBinding.h
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFilePathBinding.h
)
set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RimWorkflow.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowCollection.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowJob.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowTaskInput.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFieldBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowStringBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFloatBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowIntBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowBoolBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowCaseBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowWellPathBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowViewBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDateBinding.cpp
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFilePathBinding.cpp
)
list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
source_group(
"ProjectDataModel\\Workflow"
FILES ${SOURCE_GROUP_HEADER_FILES} ${SOURCE_GROUP_SOURCE_FILES}
${CMAKE_CURRENT_LIST_DIR}/CMakeLists_files.cmake
)
@@ -0,0 +1,204 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflow.h"
#include "RimWorkflowJob.h"
#include "RimWorkflowTaskInput.h"
#include "RiaLogging.h"
#include "RiaPreferences.h"
#include "cafCmdFeatureMenuBuilder.h"
#include <QDir>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
CAF_PDM_SOURCE_INIT( RimWorkflow, "Workflow" );
RimWorkflow::RimWorkflow()
{
CAF_PDM_InitObject( "Workflow", ":/Folder.png" );
CAF_PDM_InitFieldNoDefault( &m_name, "Name", "Name" );
m_name.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_description, "Description", "Description" );
m_description.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_workflowDirectory, "WorkflowDirectory", "Directory" );
m_workflowDirectory.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_loadError, "LoadError", "Load Error" );
m_loadError.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_jobs, "Jobs", "" );
}
QString RimWorkflow::name() const
{
return m_name();
}
void RimWorkflow::setWorkflowDirectory( const QString& directory )
{
m_workflowDirectory = directory;
QString name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
m_name = name;
setUiName( name );
}
QString RimWorkflow::workflowDirectory() const
{
return m_workflowDirectory().path();
}
std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
{
std::vector<RimWorkflowJob*> result;
result.reserve( m_jobs.size() );
for ( RimWorkflowJob* j : m_jobs.childrenByType() )
{
if ( j ) result.push_back( j );
}
return result;
}
void RimWorkflow::addJob( RimWorkflowJob* job )
{
if ( job ) m_jobs.push_back( job );
}
void RimWorkflow::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
{
menuBuilder << "RicNewWorkflowJobFeature";
}
bool RimWorkflow::loadFromDirectory( QString* errorMessage )
{
m_jobs.deleteChildren();
m_loadError = "";
const QString dir = workflowDirectory();
if ( dir.isEmpty() ) return false;
QString python = RimWorkflow::findPythonExecutable();
if ( python.isEmpty() )
{
m_loadError = "No usable Python interpreter found";
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
if ( errorMessage ) *errorMessage = m_loadError;
return false;
}
QStringList args{ "-m", "rips.taskmaestro_helper", "introspect", dir };
QProcess proc;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
proc.setProcessEnvironment( env );
proc.start( python, args );
if ( !proc.waitForStarted( 10000 ) )
{
m_loadError = QString( "Could not launch '%1'" ).arg( python );
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
if ( errorMessage ) *errorMessage = m_loadError;
return false;
}
if ( !proc.waitForFinished( 30000 ) )
{
proc.kill();
m_loadError = "Introspect helper timed out";
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
if ( errorMessage ) *errorMessage = m_loadError;
return false;
}
const QByteArray stdoutBytes = proc.readAllStandardOutput();
if ( proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0 )
{
m_loadError = QString::fromUtf8( proc.readAllStandardError() ).trimmed();
if ( m_loadError().isEmpty() ) m_loadError = "Introspect helper failed";
RiaLogging::warning( QString( "Workflow '%1' introspect failed: %2" ).arg( dir, m_loadError() ).toStdString() );
if ( errorMessage ) *errorMessage = m_loadError;
return false;
}
QJsonParseError parseErr{};
const QJsonDocument doc = QJsonDocument::fromJson( stdoutBytes, &parseErr );
if ( parseErr.error != QJsonParseError::NoError || !doc.isObject() )
{
m_loadError = QString( "Invalid JSON from introspect: %1" ).arg( parseErr.errorString() );
if ( errorMessage ) *errorMessage = m_loadError;
return false;
}
const QJsonObject root = doc.object();
if ( root.contains( "name" ) ) m_name = root.value( "name" ).toString();
if ( root.contains( "description" ) ) m_description = root.value( "description" ).toString();
setUiName( m_name() );
std::vector<RimWorkflowTaskInput*> taskInputs;
for ( const QJsonValue& tv : root.value( "tasks" ).toArray() )
{
const QJsonObject taskObj = tv.toObject();
auto* input = new RimWorkflowTaskInput;
input->setTaskName( taskObj.value( "name" ).toString() );
input->buildFromSchema( taskObj.value( "config_fields" ).toArray() );
taskInputs.push_back( input );
}
auto* job = new RimWorkflowJob;
job->setJobName( "Default" );
job->setTaskInputs( taskInputs );
m_jobs.push_back( job );
RiaLogging::info(
QString( "Loaded workflow '%1' (%2 tasks with config) from %3" ).arg( m_name() ).arg( taskInputs.size() ).arg( dir ).toStdString() );
return true;
}
QString RimWorkflow::findPythonExecutable()
{
QStringList candidates;
if ( auto* prefs = RiaPreferences::current() )
{
QString configured = prefs->pythonExecutable();
if ( !configured.isEmpty() && configured != "python" ) candidates << configured;
}
candidates << "python3" << "python";
for ( const QString& cand : candidates )
{
if ( cand.contains( '/' ) || cand.contains( '\\' ) )
{
if ( QFileInfo( cand ).isExecutable() ) return cand;
}
else if ( !QStandardPaths::findExecutable( cand ).isEmpty() )
{
return cand;
}
}
return {};
}
@@ -0,0 +1,55 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafFilePath.h"
#include "cafPdmChildArrayField.h"
#include "cafPdmField.h"
#include "cafPdmObject.h"
class RimWorkflowJob;
class RimWorkflow : public caf::PdmObject
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflow();
QString name() const;
void setWorkflowDirectory( const QString& directory );
QString workflowDirectory() const;
bool loadFromDirectory( QString* errorMessage = nullptr );
std::vector<RimWorkflowJob*> jobs() const;
void addJob( RimWorkflowJob* job );
static QString findPythonExecutable();
protected:
void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override;
private:
caf::PdmField<QString> m_name;
caf::PdmField<QString> m_description;
caf::PdmField<caf::FilePath> m_workflowDirectory;
caf::PdmField<QString> m_loadError;
caf::PdmChildArrayField<RimWorkflowJob*> m_jobs;
};
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowBoolBinding.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowBoolBinding, "WorkflowBoolBinding" );
RimWorkflowBoolBinding::RimWorkflowBoolBinding()
{
CAF_PDM_InitField( &m_value, "Value", false, "Value" );
}
void RimWorkflowBoolBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
if ( fieldSchema.contains( "default" ) )
{
m_value = fieldSchema.value( "default" ).toBool( false );
}
}
QString RimWorkflowBoolBinding::toYamlValue() const
{
return m_value() ? "true" : "false";
}
@@ -0,0 +1,37 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
class RimWorkflowBoolBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowBoolBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
private:
caf::PdmField<bool> m_value;
};
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowCaseBinding.h"
#include "RimEclipseCase.h"
#include "RimTools.h"
CAF_PDM_SOURCE_INIT( RimWorkflowCaseBinding, "WorkflowCaseBinding" );
RimWorkflowCaseBinding::RimWorkflowCaseBinding()
{
CAF_PDM_InitFieldNoDefault( &m_case, "Case", "Case" );
}
QString RimWorkflowCaseBinding::toYamlValue() const
{
if ( m_case() == nullptr ) return "null";
return QString( "{__resinsight_ref__: EclipseCase, case_id: %1}" ).arg( m_case()->caseId() );
}
QList<caf::PdmOptionItemInfo> RimWorkflowCaseBinding::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
{
QList<caf::PdmOptionItemInfo> options;
if ( fieldNeedingOptions == &m_case ) RimTools::eclipseCaseOptionItems( &options );
return options;
}
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include "cafPdmPtrField.h"
class RimEclipseCase;
class RimWorkflowCaseBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowCaseBinding();
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_case; }
private:
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
caf::PdmPtrField<RimEclipseCase*> m_case;
};
@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowCollection.h"
#include <QDir>
#include <QFileInfo>
CAF_PDM_SOURCE_INIT( RimWorkflowCollection, "WorkflowCollection" );
RimWorkflowCollection::RimWorkflowCollection()
{
CAF_PDM_InitObject( "Workflows", ":/Folder.png" );
CAF_PDM_InitFieldNoDefault( &m_items, "Workflows", "" );
rescanWorkflows();
}
RimWorkflowCollection::~RimWorkflowCollection() = default;
QString RimWorkflowCollection::discoveryDirectory()
{
return QDir::homePath() + "/.taskmaestro/workflows";
}
void RimWorkflowCollection::rescanWorkflows()
{
deleteAllItems();
QDir dir( discoveryDirectory() );
if ( !dir.exists() ) return;
const QFileInfoList entries = dir.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name );
for ( const QFileInfo& entry : entries )
{
if ( !QFileInfo( entry.absoluteFilePath() + "/workflow.yaml" ).isFile() ) continue;
auto* workflow = new RimWorkflow;
workflow->setWorkflowDirectory( entry.absoluteFilePath() );
workflow->loadFromDirectory();
addItem( workflow );
}
}
@@ -0,0 +1,38 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflow.h"
#include "cafPdmObjectCollection.h"
#include <QString>
class RimWorkflowCollection : public caf::PdmObjectCollection<RimWorkflow>
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowCollection();
~RimWorkflowCollection() override;
void rescanWorkflows();
static QString discoveryDirectory();
};
@@ -0,0 +1,43 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowDateBinding.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowDateBinding, "WorkflowDateBinding" );
RimWorkflowDateBinding::RimWorkflowDateBinding()
{
CAF_PDM_InitField( &m_value, "Value", QDate::currentDate(), "Value" );
}
void RimWorkflowDateBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
if ( fieldSchema.contains( "default" ) )
{
QDate parsed = QDate::fromString( fieldSchema.value( "default" ).toString(), Qt::ISODate );
if ( parsed.isValid() ) m_value = parsed;
}
}
QString RimWorkflowDateBinding::toYamlValue() const
{
return QString( "\"%1\"" ).arg( m_value().toString( Qt::ISODate ) );
}
@@ -0,0 +1,39 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include <QDate>
class RimWorkflowDateBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowDateBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
private:
caf::PdmField<QDate> m_value;
};
@@ -0,0 +1,71 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowFieldBinding.h"
#include <QJsonObject>
CAF_PDM_ABSTRACT_SOURCE_INIT( RimWorkflowFieldBinding, "WorkflowFieldBinding" );
RimWorkflowFieldBinding::RimWorkflowFieldBinding()
{
CAF_PDM_InitObject( "Field", ":/Bullet.png" );
CAF_PDM_InitFieldNoDefault( &m_fieldName, "FieldName", "Field" );
m_fieldName.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_description, "Description", "Description" );
m_description.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitField( &m_required, "Required", false, "Required" );
m_required.uiCapability()->setUiReadOnly( true );
}
QString RimWorkflowFieldBinding::fieldName() const
{
return m_fieldName();
}
void RimWorkflowFieldBinding::setFieldName( const QString& name )
{
m_fieldName = name;
setUiName( name );
}
void RimWorkflowFieldBinding::setDescription( const QString& description )
{
m_description = description;
}
void RimWorkflowFieldBinding::setRequired( bool required )
{
m_required = required;
}
void RimWorkflowFieldBinding::applySchema( const QJsonObject& fieldSchema )
{
setFieldName( fieldSchema.value( "name" ).toString() );
setDescription( fieldSchema.value( "description" ).toString() );
setRequired( fieldSchema.value( "required" ).toBool( false ) );
if ( auto* vf = valueField() )
{
vf->uiCapability()->setUiName( m_fieldName() );
if ( !m_description().isEmpty() ) vf->uiCapability()->setUiToolTip( m_description() );
}
}
@@ -0,0 +1,47 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmField.h"
#include "cafPdmObject.h"
class QJsonObject;
class RimWorkflowFieldBinding : public caf::PdmObject
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowFieldBinding();
QString fieldName() const;
void setFieldName( const QString& name );
void setDescription( const QString& description );
void setRequired( bool required );
virtual void applySchema( const QJsonObject& fieldSchema );
virtual QString toYamlValue() const = 0;
virtual caf::PdmFieldHandle* valueField() = 0;
protected:
caf::PdmField<QString> m_fieldName;
caf::PdmField<QString> m_description;
caf::PdmField<bool> m_required;
};
@@ -0,0 +1,64 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowFilePathBinding.h"
#include "cafPdmUiFilePathEditor.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowFilePathBinding, "WorkflowFilePathBinding" );
RimWorkflowFilePathBinding::RimWorkflowFilePathBinding()
: m_selectDirectory( false )
{
CAF_PDM_InitFieldNoDefault( &m_value, "Value", "Value" );
m_value.uiCapability()->setUiEditorTypeName( caf::PdmUiFilePathEditor::uiEditorTypeName() );
}
void RimWorkflowFilePathBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
const QString fmt = fieldSchema.value( "format" ).toString();
m_selectDirectory = ( fmt == "directory-path" );
if ( fieldSchema.contains( "default" ) )
{
m_value = fieldSchema.value( "default" ).toString();
}
}
QString RimWorkflowFilePathBinding::toYamlValue() const
{
QString p = m_value().path();
p.replace( "\\", "\\\\" );
p.replace( "\"", "\\\"" );
return QString( "\"%1\"" ).arg( p );
}
void RimWorkflowFilePathBinding::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute )
{
if ( field == &m_value )
{
if ( auto* attr = dynamic_cast<caf::PdmUiFilePathEditorAttribute*>( attribute ) )
{
attr->m_selectDirectory = m_selectDirectory;
}
}
}
@@ -0,0 +1,43 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include "cafFilePath.h"
class RimWorkflowFilePathBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowFilePathBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
protected:
void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override;
private:
caf::PdmField<caf::FilePath> m_value;
bool m_selectDirectory;
};
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowFloatBinding.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowFloatBinding, "WorkflowFloatBinding" );
RimWorkflowFloatBinding::RimWorkflowFloatBinding()
{
CAF_PDM_InitField( &m_value, "Value", 0.0, "Value" );
}
void RimWorkflowFloatBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
if ( fieldSchema.contains( "default" ) )
{
m_value = fieldSchema.value( "default" ).toDouble();
}
}
QString RimWorkflowFloatBinding::toYamlValue() const
{
return QString::number( m_value(), 'g', 17 );
}
@@ -0,0 +1,37 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
class RimWorkflowFloatBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowFloatBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
private:
caf::PdmField<double> m_value;
};
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowIntBinding.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowIntBinding, "WorkflowIntBinding" );
RimWorkflowIntBinding::RimWorkflowIntBinding()
{
CAF_PDM_InitField( &m_value, "Value", 0, "Value" );
}
void RimWorkflowIntBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
if ( fieldSchema.contains( "default" ) )
{
m_value = fieldSchema.value( "default" ).toInt();
}
}
QString RimWorkflowIntBinding::toYamlValue() const
{
return QString::number( m_value() );
}
@@ -0,0 +1,37 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
class RimWorkflowIntBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowIntBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
private:
caf::PdmField<int> m_value;
};
@@ -0,0 +1,195 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowJob.h"
#include "RimWorkflow.h"
#include "RiaApplication.h"
#include "RiaLogging.h"
#include "RiuMainWindow.h"
#include "RiuWorkflowJobRunner.h"
#include "cafCmdFeatureMenuBuilder.h"
#include "cafPdmUiGroup.h"
#include "cafPdmUiOrdering.h"
#include "cafPdmUiTreeOrdering.h"
#include <QDir>
#include <QFile>
#include <QProcessEnvironment>
#include <QUuid>
CAF_PDM_SOURCE_INIT( RimWorkflowJob, "WorkflowJob" );
RimWorkflowJob::RimWorkflowJob()
{
CAF_PDM_InitObject( "Job", ":/Bullet.png" );
CAF_PDM_InitFieldNoDefault( &m_name, "Name", "Name" );
CAF_PDM_InitFieldNoDefault( &m_taskInputs, "TaskInputs", "" );
}
void RimWorkflowJob::setJobName( const QString& name )
{
m_name = name;
setUiName( name );
}
void RimWorkflowJob::initAfterRead()
{
setUiName( m_name() );
}
void RimWorkflowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
uiOrdering.add( &m_name );
for ( RimWorkflowTaskInput* task : m_taskInputs.childrenByType() )
{
if ( !task ) continue;
caf::PdmUiGroup* group = uiOrdering.addNewGroup( task->taskName() );
for ( RimWorkflowFieldBinding* b : task->items() )
{
if ( b && b->valueField() ) group->add( b->valueField() );
}
}
uiOrdering.skipRemainingFields( true );
}
void RimWorkflowJob::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
{
uiTreeOrdering.skipRemainingChildren( true );
}
std::vector<RimWorkflowTaskInput*> RimWorkflowJob::taskInputs() const
{
std::vector<RimWorkflowTaskInput*> result;
result.reserve( m_taskInputs.size() );
for ( RimWorkflowTaskInput* t : m_taskInputs.childrenByType() )
{
if ( t ) result.push_back( t );
}
return result;
}
void RimWorkflowJob::setTaskInputs( std::vector<RimWorkflowTaskInput*> inputs )
{
m_taskInputs.deleteChildren();
for ( RimWorkflowTaskInput* t : inputs )
{
if ( t ) m_taskInputs.push_back( t );
}
}
void RimWorkflowJob::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
if ( changedField == &m_name )
{
setUiName( m_name() );
uiCapability()->updateConnectedEditors();
}
}
void RimWorkflowJob::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
{
menuBuilder << "RicRunWorkflowJobFeature";
menuBuilder << "RicCancelWorkflowJobFeature";
}
bool RimWorkflowJob::isRunning() const
{
return m_runner && m_runner->isRunning();
}
void RimWorkflowJob::cancelJob()
{
if ( m_runner ) m_runner->cancel();
}
QString RimWorkflowJob::writeInputYaml( const QString& path ) const
{
QString body;
for ( RimWorkflowTaskInput* t : m_taskInputs.childrenByType() )
{
if ( t ) body += t->toTaskYamlBlock();
}
QFile out( path );
if ( !out.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) return {};
out.write( body.toUtf8() );
out.close();
return path;
}
void RimWorkflowJob::runJob()
{
if ( isRunning() )
{
RiaLogging::warning( "Job is already running." );
return;
}
auto* workflow = firstAncestorOrThisOfType<RimWorkflow>();
if ( !workflow )
{
RiaLogging::warning( "Cannot run job: parent workflow not found." );
return;
}
const QString dir = workflow->workflowDirectory();
if ( dir.isEmpty() ) return;
auto port = RiaApplication::instance()->activeGrpcPortNumber();
if ( !port.has_value() )
{
RiaLogging::warning( "Cannot run workflow: gRPC server is not active. Enable it in Preferences." );
return;
}
QString python = RimWorkflow::findPythonExecutable();
if ( python.isEmpty() )
{
RiaLogging::warning( "Cannot run workflow: no Python interpreter found." );
return;
}
QDir tmp( QDir::tempPath() );
QString runDir = QString( "resinsight_workflow_%1" ).arg( QUuid::createUuid().toString( QUuid::WithoutBraces ) );
if ( !tmp.mkpath( runDir ) )
{
RiaLogging::warning( "Cannot create temp dir for workflow run." );
return;
}
QString inputPath = tmp.absoluteFilePath( runDir + "/input.yaml" );
if ( writeInputYaml( inputPath ).isEmpty() )
{
RiaLogging::warning( "Failed to write input.yaml" );
return;
}
QStringList args{ "-m", "rips.taskmaestro_helper", "run", dir, "--input", inputPath, "--grpc-port", QString::number( port.value() ) };
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
const QString label = workflow->uiName() + " / " + m_name();
m_runner = new RiuWorkflowJobRunner( label, RiuMainWindow::instance() );
m_runner->start( python, args, env );
}
@@ -0,0 +1,59 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowTaskInput.h"
#include "cafPdmChildArrayField.h"
#include "cafPdmField.h"
#include "cafPdmObject.h"
#include <QPointer>
class RiuWorkflowJobRunner;
class RimWorkflowJob : public caf::PdmObject
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowJob();
void setJobName( const QString& name );
std::vector<RimWorkflowTaskInput*> taskInputs() const;
void setTaskInputs( std::vector<RimWorkflowTaskInput*> inputs );
QString writeInputYaml( const QString& path ) const;
void runJob();
void cancelJob();
bool isRunning() const;
protected:
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void initAfterRead() override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override;
private:
caf::PdmField<QString> m_name;
caf::PdmChildArrayField<RimWorkflowTaskInput*> m_taskInputs;
QPointer<RiuWorkflowJobRunner> m_runner;
};
@@ -0,0 +1,45 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowStringBinding.h"
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowStringBinding, "WorkflowStringBinding" );
RimWorkflowStringBinding::RimWorkflowStringBinding()
{
CAF_PDM_InitFieldNoDefault( &m_value, "Value", "Value" );
}
void RimWorkflowStringBinding::applySchema( const QJsonObject& fieldSchema )
{
RimWorkflowFieldBinding::applySchema( fieldSchema );
if ( fieldSchema.contains( "default" ) )
{
m_value = fieldSchema.value( "default" ).toString();
}
}
QString RimWorkflowStringBinding::toYamlValue() const
{
QString v = m_value();
v.replace( "\\", "\\\\" );
v.replace( "\"", "\\\"" );
return QString( "\"%1\"" ).arg( v );
}
@@ -0,0 +1,37 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
class RimWorkflowStringBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowStringBinding();
void applySchema( const QJsonObject& fieldSchema ) override;
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_value; }
private:
caf::PdmField<QString> m_value;
};
@@ -0,0 +1,119 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowTaskInput.h"
#include "RimWorkflowBoolBinding.h"
#include "RimWorkflowCaseBinding.h"
#include "RimWorkflowDateBinding.h"
#include "RimWorkflowFilePathBinding.h"
#include "RimWorkflowFloatBinding.h"
#include "RimWorkflowIntBinding.h"
#include "RimWorkflowStringBinding.h"
#include "RimWorkflowViewBinding.h"
#include "RimWorkflowWellPathBinding.h"
#include "cafPdmUiOrdering.h"
#include "cafPdmUiTreeOrdering.h"
#include <QJsonArray>
#include <QJsonObject>
CAF_PDM_SOURCE_INIT( RimWorkflowTaskInput, "WorkflowTaskInput" );
namespace
{
RimWorkflowFieldBinding* createBinding( const QJsonObject& schema )
{
const QString resinsightType = schema.value( "resinsight_type" ).toString();
if ( resinsightType == "EclipseCase" ) return new RimWorkflowCaseBinding;
if ( resinsightType == "WellPath" ) return new RimWorkflowWellPathBinding;
if ( resinsightType == "View" ) return new RimWorkflowViewBinding;
const QString type = schema.value( "type" ).toString( "string" );
const QString format = schema.value( "format" ).toString();
if ( type == "string" && format == "date" ) return new RimWorkflowDateBinding;
if ( type == "string" && ( format == "path" || format == "file-path" || format == "directory-path" ) )
return new RimWorkflowFilePathBinding;
if ( type == "boolean" ) return new RimWorkflowBoolBinding;
if ( type == "integer" ) return new RimWorkflowIntBinding;
if ( type == "number" ) return new RimWorkflowFloatBinding;
return new RimWorkflowStringBinding;
}
} // namespace
RimWorkflowTaskInput::RimWorkflowTaskInput()
{
CAF_PDM_InitObject( "Task", ":/Bullet.png" );
CAF_PDM_InitFieldNoDefault( &m_items, "Bindings", "" );
CAF_PDM_InitFieldNoDefault( &m_taskName, "TaskName", "Task" );
m_taskName.uiCapability()->setUiReadOnly( true );
}
QString RimWorkflowTaskInput::taskName() const
{
return m_taskName();
}
void RimWorkflowTaskInput::setTaskName( const QString& name )
{
m_taskName = name;
setUiName( name );
}
void RimWorkflowTaskInput::buildFromSchema( const QJsonArray& configFields )
{
deleteAllItems();
for ( const QJsonValue& v : configFields )
{
const QJsonObject schema = v.toObject();
auto* binding = createBinding( schema );
binding->applySchema( schema );
addItem( binding );
}
}
void RimWorkflowTaskInput::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
for ( RimWorkflowFieldBinding* b : items() )
{
if ( b && b->valueField() ) uiOrdering.add( b->valueField() );
}
uiOrdering.skipRemainingFields( true );
}
void RimWorkflowTaskInput::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
{
uiTreeOrdering.skipRemainingChildren( true );
}
QString RimWorkflowTaskInput::toTaskYamlBlock() const
{
QString out = m_taskName() + ":\n";
if ( count() == 0 )
{
out += " {}\n";
return out;
}
for ( const RimWorkflowFieldBinding* b : items() )
{
out += QString( " %1: %2\n" ).arg( b->fieldName(), b->toYamlValue() );
}
return out;
}
@@ -0,0 +1,47 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include "cafPdmObjectCollection.h"
class QJsonArray;
class RimWorkflowTaskInput : public caf::PdmObjectCollection<RimWorkflowFieldBinding>
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowTaskInput();
QString taskName() const;
void setTaskName( const QString& name );
void buildFromSchema( const QJsonArray& configFields );
QString toTaskYamlBlock() const;
protected:
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
private:
caf::PdmField<QString> m_taskName;
};
@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowViewBinding.h"
#include "RimEclipseCase.h"
#include "RimEclipseCaseTools.h"
#include "RimEclipseView.h"
#include "RimProject.h"
CAF_PDM_SOURCE_INIT( RimWorkflowViewBinding, "WorkflowViewBinding" );
RimWorkflowViewBinding::RimWorkflowViewBinding()
{
CAF_PDM_InitFieldNoDefault( &m_view, "View", "View" );
}
QString RimWorkflowViewBinding::toYamlValue() const
{
if ( m_view() == nullptr ) return "null";
return QString( "{__resinsight_ref__: View, view_id: %1}" ).arg( m_view()->id() );
}
QList<caf::PdmOptionItemInfo> RimWorkflowViewBinding::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
{
QList<caf::PdmOptionItemInfo> options;
if ( fieldNeedingOptions != &m_view ) return options;
auto* project = RimProject::current();
if ( project == nullptr ) return options;
for ( RimEclipseCase* eclipseCase : RimEclipseCaseTools::eclipseCases() )
{
if ( eclipseCase == nullptr ) continue;
for ( RimEclipseView* view : eclipseCase->reservoirViews() )
{
if ( view == nullptr ) continue;
QString label = QString( "%1 / %2" ).arg( eclipseCase->caseUserDescription(), view->name() );
options.push_back( caf::PdmOptionItemInfo( label, view ) );
}
}
return options;
}
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include "cafPdmPtrField.h"
class RimEclipseView;
class RimWorkflowViewBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowViewBinding();
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_view; }
private:
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
caf::PdmPtrField<RimEclipseView*> m_view;
};
@@ -0,0 +1,44 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimWorkflowWellPathBinding.h"
#include "RimTools.h"
#include "RimWellPath.h"
CAF_PDM_SOURCE_INIT( RimWorkflowWellPathBinding, "WorkflowWellPathBinding" );
RimWorkflowWellPathBinding::RimWorkflowWellPathBinding()
{
CAF_PDM_InitFieldNoDefault( &m_wellPath, "WellPath", "Well Path" );
}
QString RimWorkflowWellPathBinding::toYamlValue() const
{
if ( m_wellPath() == nullptr ) return "null";
QString name = m_wellPath()->name();
name.replace( "\"", "\\\"" );
return QString( "{__resinsight_ref__: WellPath, well_path_name: \"%1\"}" ).arg( name );
}
QList<caf::PdmOptionItemInfo> RimWorkflowWellPathBinding::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
{
QList<caf::PdmOptionItemInfo> options;
if ( fieldNeedingOptions == &m_wellPath ) RimTools::wellPathOptionItems( &options );
return options;
}
@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimWorkflowFieldBinding.h"
#include "cafPdmPtrField.h"
class RimWellPath;
class RimWorkflowWellPathBinding : public RimWorkflowFieldBinding
{
CAF_PDM_HEADER_INIT;
public:
RimWorkflowWellPathBinding();
QString toYamlValue() const override;
caf::PdmFieldHandle* valueField() override { return &m_wellPath; }
private:
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
caf::PdmPtrField<RimWellPath*> m_wellPath;
};
@@ -1,3 +1,124 @@
set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RiuCadNavigation.h
${CMAKE_CURRENT_LIST_DIR}/RiuCursors.h
${CMAKE_CURRENT_LIST_DIR}/RiuDragDrop.h
${CMAKE_CURRENT_LIST_DIR}/RiuFemResultTextBuilder.h
${CMAKE_CURRENT_LIST_DIR}/RiuGeoQuestNavigation.h
${CMAKE_CURRENT_LIST_DIR}/RiuInterfaceToViewWindow.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotCurveSymbol.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotCurveSymbolImageCreator.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtSymbol.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotCurve.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotCurve.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotCurveDefines.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotItem.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotItem.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotMainWindow.h
${CMAKE_CURRENT_LIST_DIR}/RiuMainWindow.h
${CMAKE_CURRENT_LIST_DIR}/RiuMainWindowBase.h
${CMAKE_CURRENT_LIST_DIR}/RiuMdiArea.h
${CMAKE_CURRENT_LIST_DIR}/RiuMdiSubWindow.h
${CMAKE_CURRENT_LIST_DIR}/RiuProcessMonitor.h
${CMAKE_CURRENT_LIST_DIR}/RiuProjectPropertyView.h
${CMAKE_CURRENT_LIST_DIR}/RiuPropertyViewTabWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuPropertyViewWizard.h
${CMAKE_CURRENT_LIST_DIR}/RiuPvtPlotPanel.h
${CMAKE_CURRENT_LIST_DIR}/RiuPvtPlotUpdater.h
${CMAKE_CURRENT_LIST_DIR}/RiuPvtPlotWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotUpdater.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtLinearScaleEngine.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtScalePicker.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtCurvePointTracker.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotWheelZoomer.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotZoomer.h
${CMAKE_CURRENT_LIST_DIR}/RiuWidgetDragger.h
${CMAKE_CURRENT_LIST_DIR}/RiuRecentFileActionProvider.h
${CMAKE_CURRENT_LIST_DIR}/RiuRelativePermeabilityPlotPanel.h
${CMAKE_CURRENT_LIST_DIR}/RiuRelativePermeabilityPlotUpdater.h
${CMAKE_CURRENT_LIST_DIR}/RiuResultInfoPanel.h
${CMAKE_CURRENT_LIST_DIR}/RiuResultQwtPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuResultTextBuilder.h
${CMAKE_CURRENT_LIST_DIR}/RiuRmsNavigation.h
${CMAKE_CURRENT_LIST_DIR}/RiuSelectionChangedHandler.h
${CMAKE_CURRENT_LIST_DIR}/Riu3dSelectionManager.h
${CMAKE_CURRENT_LIST_DIR}/RiuSimpleHistogramWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuDockedQwtPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuGridCrossQwtPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryQwtPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuTextDialog.h
${CMAKE_CURRENT_LIST_DIR}/RiuTimeStepChangedHandler.h
${CMAKE_CURRENT_LIST_DIR}/RiuTofAccumulatedPhaseFractionsPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuToolTipMenu.h
${CMAKE_CURRENT_LIST_DIR}/RiuTreeViewEventFilter.h
${CMAKE_CURRENT_LIST_DIR}/RiuViewer.h
${CMAKE_CURRENT_LIST_DIR}/RiuViewerToViewInterface.h
${CMAKE_CURRENT_LIST_DIR}/RiuViewerCommands.h
${CMAKE_CURRENT_LIST_DIR}/RiuCellAndNncPickEventHandler.h
${CMAKE_CURRENT_LIST_DIR}/RiuPickItemInfo.h
${CMAKE_CURRENT_LIST_DIR}/RiuWellLogPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuWellLogTrack.h
${CMAKE_CURRENT_LIST_DIR}/RiuMultiPlotPage.h
${CMAKE_CURRENT_LIST_DIR}/RiuMultiPlotBook.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryMultiPlotPage.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryMultiPlotBook.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotAxis.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotLegend.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotAnnotationTool.h
${CMAKE_CURRENT_LIST_DIR}/RiuGeoMechXfTensorResultAccessor.h
${CMAKE_CURRENT_LIST_DIR}/RiuFemTimeHistoryResultAccessor.h
${CMAKE_CURRENT_LIST_DIR}/RiuEditPerforationCollectionWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuAdvancedSnapshotExportWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuWellAllocationPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuFlowCharacteristicsPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuNightchartsWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuMessagePanel.h
${CMAKE_CURRENT_LIST_DIR}/RiuMessageDialog.h
${CMAKE_CURRENT_LIST_DIR}/RiuWorkflowJobRunner.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotObjectPicker.h
${CMAKE_CURRENT_LIST_DIR}/RiuContextMenuLauncher.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryCurveDefinitionKeywords.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryVectorSelectionUi.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryVectorSelectionDialog.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryVectorSelectionWidgetCreator.h
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryQuantityNameInfoProvider.h
${CMAKE_CURRENT_LIST_DIR}/RiuExpressionContextMenuManager.h
${CMAKE_CURRENT_LIST_DIR}/RiuCalculationsContextMenuManager.h
${CMAKE_CURRENT_LIST_DIR}/RiuGridStatisticsHistogramWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuMohrsCirclePlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuPlotMainWindowTools.h
${CMAKE_CURRENT_LIST_DIR}/Riu3DMainWindowTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuDockWidgetTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtCurveSelectorFilter.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotItemGroup.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuDepthQwtPlot.h
${CMAKE_CURRENT_LIST_DIR}/RiuWellPathComponentPlotItem.h
${CMAKE_CURRENT_LIST_DIR}/RiuMeasurementViewEventFilter.h
${CMAKE_CURRENT_LIST_DIR}/RiuDraggableOverlayFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuMdiMaximizeWindowGuard.h
${CMAKE_CURRENT_LIST_DIR}/RiuMainWindowTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuComparisonViewMover.h
${CMAKE_CURRENT_LIST_DIR}/RiuAbstractOverlayContentFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtLegendOverlayContentFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuAbstractLegendFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuSeismicHistogramPanel.h
${CMAKE_CURRENT_LIST_DIR}/RiuCategoryLegendFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuScalarMapperLegendFrame.h
${CMAKE_CURRENT_LIST_DIR}/RiuFileDialogTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuGuiTheme.h
${CMAKE_CURRENT_LIST_DIR}/RiuQssSyntaxHighlighter.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtDateScaleWrapper.h
${CMAKE_CURRENT_LIST_DIR}/RiuMatrixPlotWidget.h
${CMAKE_CURRENT_LIST_DIR}/RiuMenuBarBuildTools.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotRectAnnotation.h
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotZoomerMultiAxes.h
${CMAKE_CURRENT_LIST_DIR}/RiuCellSelectionTool.h
)
set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RiuCadNavigation.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuCursors.cpp
@@ -72,6 +193,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RiuNightchartsWidget.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuMessagePanel.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuMessageDialog.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuWorkflowJobRunner.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuPlotObjectPicker.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuContextMenuLauncher.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuSummaryVectorSelectionUi.cpp
@@ -0,0 +1,104 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiuWorkflowJobRunner.h"
RiuWorkflowJobRunner::RiuWorkflowJobRunner( const QString& label, QObject* parent )
: QObject( parent )
, m_label( label )
{
connect( &m_process, &QProcess::readyReadStandardOutput, this, &RiuWorkflowJobRunner::onReadyReadStdout );
connect( &m_process, &QProcess::readyReadStandardError, this, &RiuWorkflowJobRunner::onReadyReadStderr );
connect( &m_process, QOverload<int, QProcess::ExitStatus>::of( &QProcess::finished ), this, &RiuWorkflowJobRunner::onProcessFinished );
}
void RiuWorkflowJobRunner::start( const QString& program, const QStringList& arguments, const QProcessEnvironment& env )
{
RiaLogging::info( QString( "Running %1" ).arg( m_label ).toStdString() );
RiaLogging::info( QString( "[%1] $ %2 %3" ).arg( m_label, program, arguments.join( ' ' ) ).toStdString() );
m_process.setProcessEnvironment( env );
m_process.start( program, arguments );
}
void RiuWorkflowJobRunner::cancel()
{
if ( m_process.state() == QProcess::NotRunning ) return;
m_process.terminate();
if ( !m_process.waitForFinished( 2000 ) ) m_process.kill();
}
bool RiuWorkflowJobRunner::isRunning() const
{
return m_process.state() != QProcess::NotRunning;
}
void RiuWorkflowJobRunner::onReadyReadStdout()
{
m_stdoutBuf.append( m_process.readAllStandardOutput() );
drainLines( m_stdoutBuf, RILogLevel::RI_LL_INFO );
}
void RiuWorkflowJobRunner::onReadyReadStderr()
{
m_stderrBuf.append( m_process.readAllStandardError() );
drainLines( m_stderrBuf, RILogLevel::RI_LL_WARNING );
}
void RiuWorkflowJobRunner::onProcessFinished( int exitCode, QProcess::ExitStatus status )
{
m_stdoutBuf.append( m_process.readAllStandardOutput() );
m_stderrBuf.append( m_process.readAllStandardError() );
if ( !m_stdoutBuf.endsWith( '\n' ) ) m_stdoutBuf.append( '\n' );
if ( !m_stderrBuf.endsWith( '\n' ) ) m_stderrBuf.append( '\n' );
drainLines( m_stdoutBuf, RILogLevel::RI_LL_INFO );
drainLines( m_stderrBuf, RILogLevel::RI_LL_WARNING );
if ( status == QProcess::NormalExit && exitCode == 0 )
{
RiaLogging::info( QString( "Finished %1 (exit 0)" ).arg( m_label ).toStdString() );
}
else if ( status == QProcess::NormalExit )
{
RiaLogging::error( QString( "Finished %1 (exit %2)" ).arg( m_label ).arg( exitCode ).toStdString() );
}
else
{
RiaLogging::error( QString( "%1 crashed" ).arg( m_label ).toStdString() );
}
deleteLater();
}
void RiuWorkflowJobRunner::drainLines( QByteArray& buffer, RILogLevel level )
{
int nl;
while ( ( nl = buffer.indexOf( '\n' ) ) >= 0 )
{
QString line = QString::fromUtf8( buffer.left( nl ) );
buffer.remove( 0, nl + 1 );
if ( line.endsWith( '\r' ) ) line.chop( 1 );
if ( line.isEmpty() ) continue;
const std::string msg = QString( "[%1] %2" ).arg( m_label, line ).toStdString();
if ( level == RILogLevel::RI_LL_WARNING )
RiaLogging::warning( msg );
else
RiaLogging::info( msg );
}
}
@@ -0,0 +1,51 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RiaLogging.h"
#include <QByteArray>
#include <QObject>
#include <QProcess>
#include <QString>
#include <QStringList>
class RiuWorkflowJobRunner : public QObject
{
Q_OBJECT
public:
RiuWorkflowJobRunner( const QString& label, QObject* parent = nullptr );
void start( const QString& program, const QStringList& arguments, const QProcessEnvironment& env );
void cancel();
bool isRunning() const;
private slots:
void onReadyReadStdout();
void onReadyReadStderr();
void onProcessFinished( int exitCode, QProcess::ExitStatus status );
private:
void drainLines( QByteArray& buffer, RILogLevel level );
QString m_label;
QProcess m_process;
QByteArray m_stdoutBuf;
QByteArray m_stderrBuf;
};
@@ -0,0 +1,6 @@
"""Bridge between ResInsight and the taskmaestro workflow library.
ResInsight invokes this package as a subprocess to introspect workflow
schemas and to execute workflows. The Python side exists so ResInsight
itself does not need to parse YAML or know about Pydantic.
"""
@@ -0,0 +1,34 @@
"""Command-line entry point: `python -m rips.taskmaestro_helper <subcommand>`."""
from __future__ import annotations
import sys
def main() -> int:
if len(sys.argv) < 2:
print(
"usage: python -m rips.taskmaestro_helper <introspect|run> ...",
file=sys.stderr,
)
return 2
subcommand = sys.argv[1]
rest = sys.argv[2:]
if subcommand == "introspect":
from . import introspect
return introspect.main(rest)
if subcommand == "run":
from . import run
return run.main(rest)
print(f"unknown subcommand: {subcommand}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,186 @@
"""Introspect a taskmaestro workflow directory and emit a JSON schema.
The schema is consumed by ResInsight to auto-generate property-editor
fields for each task's config inputs. Only fields listed in the workflow's
`config_fields` are inspected, so models that also reference non-Pydantic
types (e.g. live `rips` objects passed via `depends_on`) do not break
introspection.
"""
from __future__ import annotations
import datetime
import json
import pathlib
import sys
from pathlib import Path
from types import UnionType
from typing import Annotated, Any, Union, get_args, get_origin
from pydantic import TypeAdapter
from pydantic.fields import FieldInfo
from .refs import annotation_to_resinsight_type
_SCALAR_TYPE_MAP: dict[type, str] = {
str: "string",
bool: "boolean",
int: "integer",
float: "number",
}
def _format_for(field_info: FieldInfo) -> str | None:
"""Ask Pydantic for the JSON-schema `format` of a field, if any.
Pydantic stores e.g. `pydantic.DirectoryPath` (which is
`Annotated[Path, PathType('dir')]`) as `annotation=Path` and
`metadata=[PathType('dir')]`. We reassemble both before asking
TypeAdapter, so format markers like `directory-path` survive.
"""
annotation = field_info.annotation
if field_info.metadata:
annotation = Annotated[tuple([annotation, *field_info.metadata])]
try:
return TypeAdapter(annotation).json_schema().get("format")
except Exception:
return None
def _annotation_type(annotation: object) -> str:
"""Map a Pydantic field annotation to a JSON-schema-style type label."""
if isinstance(annotation, type) and annotation in _SCALAR_TYPE_MAP:
return _SCALAR_TYPE_MAP[annotation]
# Optional[T] / T | None — peel the union and map the non-None arm
origin = get_origin(annotation)
if origin in (Union, UnionType):
non_none = [a for a in get_args(annotation) if a is not type(None)]
if len(non_none) == 1:
return _annotation_type(non_none[0])
if origin in (list, tuple, set, frozenset):
return "array"
if isinstance(annotation, type):
# Recognised rips object types are reported as "object" with a separate marker.
if annotation_to_resinsight_type(annotation) is not None:
return "object"
return "string"
_SENTINEL = object()
def _serialize_default(value: Any) -> Any:
"""Convert a Python value into a JSON-friendly form for the schema's `default`."""
if isinstance(value, (datetime.date, datetime.datetime)):
return value.isoformat()
if isinstance(value, pathlib.PurePath):
return str(value)
return value
def _field_schema(
field_name: str, field_info: FieldInfo, input_value: Any = _SENTINEL
) -> dict[str, Any]:
entry: dict[str, Any] = {
"name": field_name,
"type": _annotation_type(field_info.annotation),
"required": field_info.is_required(),
}
if field_info.description:
entry["description"] = field_info.description
# Prefer the value the user already has in input.yaml; fall back to the
# Pydantic-side default so the UI is pre-filled in either case.
if input_value is not _SENTINEL and input_value is not None:
entry["default"] = _serialize_default(input_value)
elif not field_info.is_required():
default = field_info.get_default(call_default_factory=False)
if default is not None:
entry["default"] = _serialize_default(default)
fmt = _format_for(field_info)
if fmt is not None:
entry["format"] = fmt
ri_type = annotation_to_resinsight_type(field_info.annotation)
if ri_type is not None:
entry["resinsight_type"] = ri_type
return entry
def collect_schema(workflow_dir: Path) -> dict[str, Any]:
"""Load the workflow at `workflow_dir` and produce its UI schema."""
workflow_yaml = workflow_dir / "workflow.yaml"
input_yaml = workflow_dir / "input.yaml"
if not workflow_yaml.is_file():
raise FileNotFoundError(f"Missing workflow.yaml in {workflow_dir}")
# Workflow source files (e.g. pipeline.py) live next to workflow.yaml and
# are referenced as plain dotted paths in workflow.yaml — make them importable.
workflow_dir_str = str(workflow_dir)
if workflow_dir_str not in sys.path:
sys.path.insert(0, workflow_dir_str)
from taskmaestro.task import get_input_type
from taskmaestro.yaml_config import _load_workflow_only
wf, jc = _load_workflow_only(
workflow_yaml,
input_yaml if input_yaml.is_file() else None,
)
tasks: list[dict[str, Any]] = []
for task_name, task_cls in wf.topological_order():
config_field_names = wf.get_config_fields(task_name)
if not config_field_names:
continue
task_config = jc.get_config_for_task(task_name) if jc is not None else {}
input_type = get_input_type(task_cls)
config_fields: list[dict[str, Any]] = []
for field_name in sorted(config_field_names):
field_info = input_type.model_fields.get(field_name)
if field_info is None:
config_fields.append(
{
"name": field_name,
"type": "string",
"required": True,
"error": "field not found in input model",
}
)
continue
input_value = task_config.get(field_name, _SENTINEL)
config_fields.append(
_field_schema(field_name, field_info, input_value=input_value)
)
tasks.append({"name": task_name, "config_fields": config_fields})
return {
"name": wf.name,
"description": "",
"tasks": tasks,
}
def main(argv: list[str]) -> int:
if len(argv) != 1:
print("usage: introspect <workflow_dir>", file=sys.stderr)
return 2
workflow_dir = Path(argv[0]).resolve()
try:
schema = collect_schema(workflow_dir)
except Exception as exc:
print(
json.dumps({"error": str(exc), "type": type(exc).__name__}), file=sys.stderr
)
return 1
json.dump(schema, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
@@ -0,0 +1,56 @@
"""Mapping between Pydantic field annotations and ResInsight object types.
The whitelist below is the single source of truth for which `rips` types
the helper recognises as object pickers. It is referenced both by
`introspect` (to add the `resinsight_type` marker on field schemas) and
by `run` (to resolve `__resinsight_ref__` values back to live `rips`
objects).
"""
from __future__ import annotations
# Maps rips class name -> resinsight_type label sent to the C++ side.
# The label is also the C++ binding subclass selector (RimWorkflow{Label}Binding).
# Rips classes live in `rips.generated.generated_classes` but are re-exported as
# `rips.<Name>`; we match by class name only since the module is generated.
RIPS_TYPE_WHITELIST: dict[str, str] = {
"Case": "EclipseCase",
"EclipseCase": "EclipseCase",
"WellPath": "WellPath",
"View": "View",
"EclipseView": "View",
}
_RIPS_MODULE_PREFIXES = ("rips.", "rips")
# Marker used inside input.yaml to denote a ResInsight object reference.
REF_MARKER = "__resinsight_ref__"
def annotation_to_resinsight_type(annotation: object) -> str | None:
"""Return the resinsight_type label for a Pydantic field annotation, or None.
Handles both bare rips classes and ObjectModel[rips.X] wrappers used by
the example workflow (e.g. `class GridCase(ObjectModel[rips.EclipseCase])`).
"""
candidates: list[type] = []
if isinstance(annotation, type):
candidates.append(annotation)
# Walk the MRO for ObjectModel[T] parameterizations: pydantic stores the
# type argument on the intermediate class in __pydantic_generic_metadata__.
for cls in annotation.__mro__:
md = getattr(cls, "__pydantic_generic_metadata__", None)
if not md:
continue
for arg in md.get("args", ()) or ():
if isinstance(arg, type):
candidates.append(arg)
for cls in candidates:
module = getattr(cls, "__module__", "") or ""
if not module.startswith(_RIPS_MODULE_PREFIXES):
continue
label = RIPS_TYPE_WHITELIST.get(cls.__name__)
if label is not None:
return label
return None
@@ -0,0 +1,126 @@
"""Execute a taskmaestro workflow against a running ResInsight.
The helper:
1. Connects back to ResInsight via the gRPC port passed by the caller.
2. Reads input.yaml and walks the dict, replacing every
`{__resinsight_ref__: <type>, ...id...}` map with an `ObjectModel`-style
`{"value": <live rips object>}` so taskmaestro's Pydantic validation
accepts it as a wrapped object.
3. Builds the workflow via taskmaestro's loader and runs it.
4. Streams progress events as newline-delimited JSON to stdout for the
ResInsight log dialog to render.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any, Callable
import yaml
from .refs import REF_MARKER
def _emit(event: str, **fields: Any) -> None:
sys.stdout.write(json.dumps({"event": event, **fields}) + "\n")
sys.stdout.flush()
def resolve_refs(data: Any, resolver: Callable[[str, dict[str, Any]], Any]) -> Any:
"""Recursively replace `{__resinsight_ref__: ..., ...}` maps in `data`.
The replacement is a `{"value": <resolved>}` dict, so Pydantic's
ObjectModel validator can accept it directly.
"""
if isinstance(data, dict):
if REF_MARKER in data:
ri_type = data[REF_MARKER]
extras = {k: v for k, v in data.items() if k != REF_MARKER}
return {"value": resolver(ri_type, extras)}
return {k: resolve_refs(v, resolver) for k, v in data.items()}
if isinstance(data, list):
return [resolve_refs(v, resolver) for v in data]
return data
def make_rips_resolver(rips_instance: Any) -> Callable[[str, dict[str, Any]], Any]:
"""Resolve a `__resinsight_ref__` map to a live `rips` object."""
def resolve(ri_type: str, extras: dict[str, Any]) -> Any:
project = rips_instance.project
if ri_type == "EclipseCase":
return project.case(case_id=extras["case_id"])
if ri_type == "WellPath":
return project.well_path_by_name(extras["well_path_name"])
if ri_type == "View":
for v in project.views():
if getattr(v, "id", None) == extras["view_id"]:
return v
raise LookupError(f"View id {extras['view_id']} not found")
raise LookupError(f"Unknown resinsight_type: {ri_type}")
return resolve
def run_workflow(workflow_dir: Path, input_path: Path, grpc_port: int) -> int:
workflow_dir_str = str(workflow_dir)
if workflow_dir_str not in sys.path:
sys.path.insert(0, workflow_dir_str)
import rips
instance = rips.Instance.find(start_port=grpc_port, end_port=grpc_port + 1)
if instance is None:
_emit("error", message=f"Could not connect to ResInsight on port {grpc_port}")
return 1
_emit("connected", port=grpc_port)
raw_input = yaml.safe_load(input_path.read_text()) or {}
if not isinstance(raw_input, dict):
_emit("error", message="input.yaml must contain a mapping")
return 1
resolver = make_rips_resolver(instance)
try:
resolved = resolve_refs(raw_input, resolver)
except Exception as exc:
_emit("error", message=f"Failed to resolve references: {exc}")
return 1
from taskmaestro import EmptyConfig, ExecutionContext, Job, JobConfiguration, Runner
from taskmaestro.yaml_config import _load_workflow_only
workflow_yaml = workflow_dir / "workflow.yaml"
workflow, _ = _load_workflow_only(workflow_yaml)
job_config = JobConfiguration(resolved)
job: Job[Any] = Job(
workflow=workflow, config=EmptyConfig(), job_configuration=job_config
)
_emit("starting", workflow=workflow.name)
try:
result = Runner().run(job, ctx=ExecutionContext())
except Exception as exc:
_emit("error", message=str(exc))
return 1
_emit("finished", status=str(result.status), error=str(result.error or ""))
return 0 if result.error is None else 1
def main(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(
prog="run", description="Run a taskmaestro workflow."
)
parser.add_argument("workflow_dir", type=Path)
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--grpc-port", type=int, required=True)
args = parser.parse_args(argv)
return run_workflow(
args.workflow_dir.resolve(), args.input.resolve(), args.grpc_port
)
@@ -0,0 +1,186 @@
"""Unit tests for rips.taskmaestro_helper.
These tests do not require a running ResInsight; they exercise the
Pydantic introspection logic in isolation against a synthetic workflow
written into a tmp_path so they stay self-contained.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
pytest.importorskip("taskmaestro")
SYNTHETIC_PIPELINE = '''
"""Synthetic workflow for taskmaestro_helper tests."""
from __future__ import annotations
import datetime
import pathlib
import pydantic
from pydantic import BaseModel, Field
from taskmaestro import EmptyConfig, ExecutionContext, Task
class GreetInput(BaseModel):
name: str = Field(description="Person to greet", default="world")
times: int = Field(description="How many times", ge=1)
when: datetime.date = Field(description="Greet date", default=datetime.date(2024, 1, 1))
out_file: pathlib.Path = Field(description="Where to write greeting", default=pathlib.Path("/tmp/hi.txt"))
out_dir: pydantic.DirectoryPath = Field(description="Where to write logs")
class GreetOutput(BaseModel):
message: str
class Greet(Task[GreetInput, GreetOutput]):
name = "greet"
def run(self, input: GreetInput, ctx: ExecutionContext) -> GreetOutput: # pragma: no cover
return GreetOutput(message=f"hi {input.name}" * input.times)
class StartInput(BaseModel):
pass
class StartOutput(BaseModel):
pass
class Start(Task[StartInput, StartOutput]):
name = "start"
def run(self, input: StartInput, ctx: ExecutionContext) -> StartOutput: # pragma: no cover
return StartOutput()
'''
SYNTHETIC_WORKFLOW_YAML = """workflow:
name: synthetic
tasks:
- task: pipeline.Start
- task: pipeline.Greet
depends_on: pipeline.Start
config_fields: [name, times, when, out_file, out_dir]
"""
SYNTHETIC_INPUT_YAML = """greet:
name: alice
times: 3
out_dir: /tmp
"""
@pytest.fixture
def workflow_dir(tmp_path: Path) -> Path:
(tmp_path / "pipeline.py").write_text(SYNTHETIC_PIPELINE)
(tmp_path / "workflow.yaml").write_text(SYNTHETIC_WORKFLOW_YAML)
(tmp_path / "input.yaml").write_text(SYNTHETIC_INPUT_YAML)
return tmp_path
def test_collect_schema_returns_workflow_name_and_tasks(workflow_dir: Path) -> None:
from rips.taskmaestro_helper.introspect import collect_schema
schema = collect_schema(workflow_dir)
assert schema["name"] == "synthetic"
task_names = [t["name"] for t in schema["tasks"]]
assert task_names == ["greet"] # Start has no config_fields, so omitted
def test_collect_schema_extracts_field_types_and_metadata(workflow_dir: Path) -> None:
from rips.taskmaestro_helper.introspect import collect_schema
schema = collect_schema(workflow_dir)
[greet] = schema["tasks"]
fields_by_name = {f["name"]: f for f in greet["config_fields"]}
# input.yaml has `name: alice`, so that wins over the Pydantic default "world"
assert fields_by_name["name"]["type"] == "string"
assert fields_by_name["name"]["default"] == "alice"
assert fields_by_name["name"]["description"] == "Person to greet"
assert fields_by_name["name"]["required"] is False
# input.yaml has `times: 3`; field has no Pydantic default but is required
assert fields_by_name["times"]["type"] == "integer"
assert fields_by_name["times"]["required"] is True
assert fields_by_name["times"]["default"] == 3
assert "resinsight_type" not in fields_by_name["times"]
# input.yaml has no `when:` block, so Pydantic default kicks in
assert fields_by_name["when"]["type"] == "string"
assert fields_by_name["when"]["format"] == "date"
assert fields_by_name["when"]["default"] == "2024-01-01"
# input.yaml has no out_file; Pydantic default flows through
assert fields_by_name["out_file"]["type"] == "string"
assert fields_by_name["out_file"]["format"] == "path"
assert fields_by_name["out_file"]["default"] == "/tmp/hi.txt"
# input.yaml has `out_dir: /tmp`; pre-fills even though field is required
assert fields_by_name["out_dir"]["type"] == "string"
assert fields_by_name["out_dir"]["format"] == "directory-path"
assert fields_by_name["out_dir"]["required"] is True
assert fields_by_name["out_dir"]["default"] == "/tmp"
def test_resolve_refs_substitutes_object_model_value() -> None:
from rips.taskmaestro_helper.run import resolve_refs
sentinel_case = object()
sentinel_well = object()
def resolver(ri_type: str, extras: dict) -> object:
if ri_type == "EclipseCase" and extras == {"case_id": 0}:
return sentinel_case
if ri_type == "WellPath" and extras == {"well_path_name": "B-2H"}:
return sentinel_well
raise AssertionError(f"unexpected ref: {ri_type} {extras}")
raw = {
"load_model": {
"case": {"__resinsight_ref__": "EclipseCase", "case_id": 0},
"label": "x",
},
"load_well": {
"well": {"__resinsight_ref__": "WellPath", "well_path_name": "B-2H"},
},
"untouched": "stays",
}
out = resolve_refs(raw, resolver)
assert out["load_model"]["case"] == {"value": sentinel_case}
assert out["load_model"]["label"] == "x"
assert out["load_well"]["well"] == {"value": sentinel_well}
assert out["untouched"] == "stays"
def test_main_emits_json_to_stdout(workflow_dir: Path) -> None:
rips_root = Path(__file__).resolve().parents[2]
proc = subprocess.run(
[
sys.executable,
"-m",
"rips.taskmaestro_helper",
"introspect",
str(workflow_dir),
],
cwd=rips_root,
capture_output=True,
text=True,
check=True,
)
payload = json.loads(proc.stdout)
assert payload["name"] == "synthetic"
assert {t["name"] for t in payload["tasks"]} == {"greet"}