From f015af98eeddc849000298946018fb71b8b7ec8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Herje?= <82032112+jorgenherje@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:44:33 +0200 Subject: [PATCH] Dedicated python env for ri-cloud-api (#14831) - Add ri-cloud-api python env preferences - Prevent loop of restart/reconnect when manually clicking "start"/"Authenticate" --- .../Application/CMakeLists_files.cmake | 1 + .../Application/RiaApplication.cpp | 19 +++- .../Application/RiaApplication.h | 1 + .../Application/RiaPreferences.cpp | 26 +++++ .../Application/RiaPreferences.h | 6 ++ .../Application/RiaPreferencesCloudApi.cpp | 83 ++++++++++++++++ .../Application/RiaPreferencesCloudApi.h | 46 +++++++++ .../Tools/Cloud/RiaCloudApiService.cpp | 97 +++++++++++++++++-- .../Tools/Cloud/RiaCloudApiService.h | 31 +++++- docs/agents/build.md | 7 +- docs/cloud-service-api.md | 29 +++--- 11 files changed, 320 insertions(+), 26 deletions(-) create mode 100644 ApplicationLibCode/Application/RiaPreferencesCloudApi.cpp create mode 100644 ApplicationLibCode/Application/RiaPreferencesCloudApi.h diff --git a/ApplicationLibCode/Application/CMakeLists_files.cmake b/ApplicationLibCode/Application/CMakeLists_files.cmake index f107d9748e..ece3a18eb7 100644 --- a/ApplicationLibCode/Application/CMakeLists_files.cmake +++ b/ApplicationLibCode/Application/CMakeLists_files.cmake @@ -14,6 +14,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesOsdu.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesOpm.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesSumo.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaPreferencesCloudApi.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaPorosityModel.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaCurveSetDefinition.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaRftPltCurveDefinition.cpp diff --git a/ApplicationLibCode/Application/RiaApplication.cpp b/ApplicationLibCode/Application/RiaApplication.cpp index f3f9390e28..23d87b7b7e 100644 --- a/ApplicationLibCode/Application/RiaApplication.cpp +++ b/ApplicationLibCode/Application/RiaApplication.cpp @@ -35,6 +35,7 @@ #include "RiaOpenMPTools.h" #include "RiaPlotWindowRedrawScheduler.h" #include "RiaPreferences.h" +#include "RiaPreferencesCloudApi.h" #include "RiaPreferencesOsdu.h" #include "RiaPreferencesSumo.h" #include "RiaPreferencesSystem.h" @@ -1382,6 +1383,22 @@ QString RiaApplication::pythonPath() const return m_preferences->pythonExecutable(); } +//-------------------------------------------------------------------------------------------------- +/// The ri-cloud-api service uses a dedicated Python environment when enabled under +/// Preferences -> RI Cloud API, and otherwise falls back to the scripting environment so the two do +/// not have to share a single interpreter and risk conflicting package versions. +//-------------------------------------------------------------------------------------------------- +QString RiaApplication::cloudApiPythonPath() const +{ + auto cloudApiPreferences = m_preferences->cloudApiPreferences(); + if ( cloudApiPreferences->useDedicatedPythonEnvironment() ) + { + return cloudApiPreferences->pythonExecutable(); + } + + return pythonPath(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -1984,7 +2001,7 @@ RiaSumoConnector* RiaApplication::makeSumoConnector() QObject::connect( m_sumoConnector, &RiaSumoConnector::tokenReady, cloudApiService(), - [this]( const QString& ) { cloudApiService()->start(); } ); + [this]( const QString& ) { cloudApiService()->start( RiaCloudApiService::StartTrigger::Automatic ); } ); } return m_sumoConnector; diff --git a/ApplicationLibCode/Application/RiaApplication.h b/ApplicationLibCode/Application/RiaApplication.h index 78c64615af..2aa835cbc0 100644 --- a/ApplicationLibCode/Application/RiaApplication.h +++ b/ApplicationLibCode/Application/RiaApplication.h @@ -162,6 +162,7 @@ public: QProcessEnvironment octaveProcessEnvironment() const; QString pythonPath() const; + QString cloudApiPythonPath() const; virtual QProcessEnvironment pythonProcessEnvironment() const; virtual std::optional activeGrpcPortNumber() const; diff --git a/ApplicationLibCode/Application/RiaPreferences.cpp b/ApplicationLibCode/Application/RiaPreferences.cpp index f8f42ae08a..0b422475ae 100644 --- a/ApplicationLibCode/Application/RiaPreferences.cpp +++ b/ApplicationLibCode/Application/RiaPreferences.cpp @@ -25,6 +25,7 @@ #include "RiaColorTables.h" #include "RiaGuiApplication.h" #include "RiaLogging.h" +#include "RiaPreferencesCloudApi.h" #include "RiaPreferencesGeoMech.h" #include "RiaPreferencesGrid.h" #include "RiaPreferencesOpenTelemetry.h" @@ -287,6 +288,9 @@ RiaPreferences::RiaPreferences() CAF_PDM_InitFieldNoDefault( &m_openTelemetryPreferences, "openTelemetryPreferences", "openTelemetryPreferences" ); m_openTelemetryPreferences = new RiaPreferencesOpenTelemetry; + + CAF_PDM_InitFieldNoDefault( &m_cloudApiPreferences, "cloudApiPreferences", "cloudApiPreferences" ); + m_cloudApiPreferences = new RiaPreferencesCloudApi; } //-------------------------------------------------------------------------------------------------- @@ -482,6 +486,11 @@ void RiaPreferences::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& scriptGroup->add( &m_maxScriptFoldersDepth ); scriptGroup->add( &scriptEditorExecutable ); } + else if ( uiConfigName == RiaPreferences::tabNameCloudApi() ) + { + caf::PdmUiGroup* pythonGroup = uiOrdering.addNewGroup( "Python Environment" ); + m_cloudApiPreferences()->appendItems( *pythonGroup ); + } else if ( uiConfigName == RiaPreferences::tabNameOpmFlow() ) { m_opmPreferences()->appendItems( uiOrdering ); @@ -647,6 +656,14 @@ QString RiaPreferences::tabNameScripting() return "Scripting"; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaPreferences::tabNameCloudApi() +{ + return "RI Cloud API"; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -707,6 +724,7 @@ QStringList RiaPreferences::tabNames() names << tabNameSummary(); names << tabNamePlotting(); names << tabNameScripting(); + names << tabNameCloudApi(); names << tabNameOpmFlow(); #ifdef USE_ODB_API names << tabNameGeomech(); @@ -1084,6 +1102,14 @@ RiaPreferencesOpenTelemetry* RiaPreferences::openTelemetryPreferences() const return m_openTelemetryPreferences(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaPreferencesCloudApi* RiaPreferences::cloudApiPreferences() const +{ + return m_cloudApiPreferences(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/RiaPreferences.h b/ApplicationLibCode/Application/RiaPreferences.h index 036cbc4575..5cacc31ea8 100644 --- a/ApplicationLibCode/Application/RiaPreferences.h +++ b/ApplicationLibCode/Application/RiaPreferences.h @@ -48,6 +48,7 @@ class RiaPreferencesGrid; class RiaPreferencesSumo; class RiaPreferencesOpm; class RiaPreferencesOpenTelemetry; +class RiaPreferencesCloudApi; //-------------------------------------------------------------------------------------------------- /// @@ -132,6 +133,7 @@ public: RiaPreferencesGrid* gridPreferences() const; RiaPreferencesOpm* opmPreferences() const; RiaPreferencesOpenTelemetry* openTelemetryPreferences() const; + RiaPreferencesCloudApi* cloudApiPreferences() const; void importPreferenceValuesFromFile( const QString& fileName ); void exportPreferenceValuesToFile( const QString& fileName ); @@ -171,6 +173,7 @@ private: static QString tabNameGeomech(); static QString tabNamePlotting(); static QString tabNameScripting(); + static QString tabNameCloudApi(); static QString tabNameSystem(); static QString tabNameImportExport(); static QString tabNameOpmFlow(); @@ -245,6 +248,9 @@ private: // OpenTelemetry settings caf::PdmChildField m_openTelemetryPreferences; + // RI Cloud API settings + caf::PdmChildField m_cloudApiPreferences; + // 3d view caf::PdmField> m_defaultMeshModeType; caf::PdmField> m_navigationPolicy; diff --git a/ApplicationLibCode/Application/RiaPreferencesCloudApi.cpp b/ApplicationLibCode/Application/RiaPreferencesCloudApi.cpp new file mode 100644 index 0000000000..baf62bab51 --- /dev/null +++ b/ApplicationLibCode/Application/RiaPreferencesCloudApi.cpp @@ -0,0 +1,83 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RiaPreferencesCloudApi.h" + +#include "RiaApplication.h" +#include "RiaPreferences.h" + +#include "cafPdmUiCheckBoxEditor.h" +#include "cafPdmUiFilePathEditor.h" +#include "cafPdmUiOrdering.h" + +CAF_PDM_SOURCE_INIT( RiaPreferencesCloudApi, "RiaPreferencesCloudApi" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaPreferencesCloudApi::RiaPreferencesCloudApi() +{ + CAF_PDM_InitField( &m_enableDedicatedPythonEnvironment, + "enableDedicatedPythonEnvironment", + false, + "Enable for Dedicated ri-cloud-api Environment" ); + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_enableDedicatedPythonEnvironment ); + + CAF_PDM_InitField( &m_pythonExecutable, "pythonExecutable", QString( "python" ), "Python Executable Location" ); + m_pythonExecutable.uiCapability()->setUiEditorTypeName( caf::PdmUiFilePathEditor::uiEditorTypeName() ); + m_pythonExecutable.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaPreferencesCloudApi* RiaPreferencesCloudApi::current() +{ + return RiaApplication::instance()->preferences()->cloudApiPreferences(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiaPreferencesCloudApi::appendItems( caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_enableDedicatedPythonEnvironment ); + + // Only show the dedicated interpreter field when it is actually in use. When disabled, the + // service falls back to Preferences -> Scripting -> Python Executable Location. + if ( m_enableDedicatedPythonEnvironment() ) + { + uiOrdering.add( &m_pythonExecutable ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RiaPreferencesCloudApi::useDedicatedPythonEnvironment() const +{ + return m_enableDedicatedPythonEnvironment(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaPreferencesCloudApi::pythonExecutable() const +{ + return m_pythonExecutable().trimmed(); +} diff --git a/ApplicationLibCode/Application/RiaPreferencesCloudApi.h b/ApplicationLibCode/Application/RiaPreferencesCloudApi.h new file mode 100644 index 0000000000..b3a61836ee --- /dev/null +++ b/ApplicationLibCode/Application/RiaPreferencesCloudApi.h @@ -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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObject.h" + +//-------------------------------------------------------------------------------------------------- +/// Preferences for the ri-cloud-api service. By default the service runs with the same Python +/// interpreter as the scripting engine (Preferences -> Scripting -> Python Executable Location). A +/// dedicated environment can be enabled here to avoid package/version conflicts between the two. +//-------------------------------------------------------------------------------------------------- +class RiaPreferencesCloudApi : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RiaPreferencesCloudApi(); + + static RiaPreferencesCloudApi* current(); + + void appendItems( caf::PdmUiOrdering& uiOrdering ); + + bool useDedicatedPythonEnvironment() const; + QString pythonExecutable() const; + +private: + caf::PdmField m_enableDedicatedPythonEnvironment; + caf::PdmField m_pythonExecutable; +}; diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp index b4cf06269f..2fe0dc7ee4 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp @@ -61,6 +61,13 @@ constexpr int readinessPollIntervalMs = 250; // Number of ports scanned, starting at the wanted port, when looking for a free port to bind to. constexpr int portRangeLength = 100; + +// Shared hint appended to errors caused by a service that never responded, pointing at the most likely +// fix without claiming to have diagnosed the exact cause. +const std::string pythonEnvironmentHint = + "This could be caused by the configured Python environment (Preferences -> RI Cloud API, or " + "Preferences -> Scripting) missing the packages required by ri-cloud-api. Check the log above for " + "details, and fix the environment if needed."; } // namespace //-------------------------------------------------------------------------------------------------- @@ -74,7 +81,10 @@ RiaCloudApiService::RiaCloudApiService( const QString& serverAddress, int wanted , m_wantedPort( wantedPort ) , m_port( -1 ) , m_consecutiveFailures( 0 ) + , m_consecutiveLaunchFailures( 0 ) , m_isResponding( false ) + , m_hasRespondedSinceStart( false ) + , m_hasGivenUp( false ) { // Never route loopback requests through a system/corporate proxy, which can add several seconds // per request. @@ -143,14 +153,31 @@ QString RiaCloudApiService::serverUrl() const //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RiaCloudApiService::start() +void RiaCloudApiService::start( StartTrigger trigger ) { + // An Automatic call (e.g. Sumo's tokenReady, which can fire well after the give-up, such as on a + // background token refresh) must not silently re-arm a retry loop that already gave up. Only an + // explicit Manual start/restart may do that. + if ( trigger == StartTrigger::Automatic && m_hasGivenUp ) return; + if ( isRunning() ) return; + m_consecutiveLaunchFailures = 0; + m_hasRespondedSinceStart = false; + m_hasGivenUp = false; + + launchProcess(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiaCloudApiService::launchProcess() +{ // Clean up any stale QProcess instance (e.g. after an unexpected exit) before re-starting. if ( m_process ) stop(); - const QString pythonExecutable = RiaApplication::instance()->pythonPath(); + const QString pythonExecutable = RiaApplication::instance()->cloudApiPythonPath(); if ( pythonExecutable.isEmpty() ) { RiaLogging::error( "Cloud API service: no Python executable configured, cannot start service." ); @@ -209,7 +236,8 @@ void RiaCloudApiService::start() // A non-zero exit means the service never came up, or died. Anything else is an // ordinary shutdown. - if ( exitCode != 0 || exitStatus != QProcess::NormalExit ) + const bool failed = exitCode != 0 || exitStatus != QProcess::NormalExit; + if ( failed ) { RiaLogging::error( message ); } @@ -220,6 +248,15 @@ void RiaCloudApiService::start() m_isResponding = false; emit statusChanged(); + + // React immediately rather than waiting for a timer. Deferred via singleShot, since + // restarting the process here, during its own finished() signal, would not be safe. + if ( failed ) + { + m_startupTimer.stop(); + m_healthTimer.stop(); + QTimer::singleShot( 0, this, &RiaCloudApiService::autoRestart ); + } } ); m_consecutiveFailures = 0; @@ -248,7 +285,7 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs ) { if ( m_isResponding ) return true; - if ( !isRunning() ) start(); + if ( !isRunning() ) start( StartTrigger::Automatic ); // Nested event loops below let the periodic health check fire and restart() the service while this // call is still waiting for it to boot. Stop both timers for the duration of the wait to avoid that. @@ -277,7 +314,9 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs ) if ( answered ) { - m_consecutiveFailures = 0; + m_consecutiveFailures = 0; + m_consecutiveLaunchFailures = 0; + m_hasRespondedSinceStart = true; if ( !m_isResponding ) { @@ -305,7 +344,14 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs ) delayLoop.exec(); } - RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) ); + if ( m_hasGivenUp ) + { + RiaLogging::error( std::format( "Cloud API service: not available, giving up. {}", pythonEnvironmentHint ) ); + } + else + { + RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) ); + } // Resume background health-checking if the process is still running, so it can still recover on // its own without another blocking wait. @@ -351,6 +397,35 @@ void RiaCloudApiService::restart() start(); } +//-------------------------------------------------------------------------------------------------- +/// Called when the process exits/crashes, or when a health check times out. If the service has never +/// answered a health check since the current start (see m_hasRespondedSinceStart), this is treated as a +/// configuration problem rather than a transient crash: retrying is unlikely to help, so this gives up +/// immediately instead of restarting, and sets m_hasGivenUp so a later Automatic start() (e.g. Sumo's +/// tokenReady) does not quietly re-arm the loop. If the service HAD been responding, this is instead +/// treated as an ordinary crash of an otherwise working server, and is retried no matter how many times it +/// takes to come back. +//-------------------------------------------------------------------------------------------------- +void RiaCloudApiService::autoRestart() +{ + m_consecutiveLaunchFailures++; + + if ( !m_hasRespondedSinceStart ) + { + RiaLogging::error( std::format( "Cloud API service: giving up after {} failed launch attempt{}. {}", + m_consecutiveLaunchFailures, + m_consecutiveLaunchFailures == 1 ? "" : "s", + pythonEnvironmentHint ) ); + m_hasGivenUp = true; + stop(); + return; + } + + RiaLogging::warning( "Cloud API service: not responding, restarting." ); + stop(); + launchProcess(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -370,7 +445,7 @@ void RiaCloudApiService::onHealthCheck() { if ( !isRunning() || m_port < 0 ) { - restart(); + autoRestart(); return; } @@ -390,8 +465,10 @@ void RiaCloudApiService::onHealthCheck() const int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); if ( reply->error() == QNetworkReply::NoError && statusCode == 200 ) { - m_consecutiveFailures = 0; - m_isResponding = true; + m_consecutiveFailures = 0; + m_consecutiveLaunchFailures = 0; + m_hasRespondedSinceStart = true; + m_isResponding = true; } else { @@ -404,7 +481,7 @@ void RiaCloudApiService::onHealthCheck() if ( !m_isResponding && m_consecutiveFailures >= maxConsecutiveFailures ) { - restart(); + autoRestart(); } } ); } diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.h b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.h index 08a0fc8b87..c5d8f2f599 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.h +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.h @@ -48,7 +48,18 @@ public: RiaCloudApiService( const QString& serverAddress, int wantedPort, QObject* parent = nullptr ); ~RiaCloudApiService() override; - void start(); + // Manual: an explicit user action (Start/Restart button). Automatic: triggered by application logic + // (e.g. Sumo authentication completing, or waitUntilResponding()). + // + // Does not affect whether autoRestart() gives up (see autoRestart()), only what happens after it has: + // a later Automatic call is then a no-op (see m_hasGivenUp), while a Manual one tries again. + enum class StartTrigger + { + Manual, + Automatic + }; + + void start( StartTrigger trigger = StartTrigger::Manual ); void stop(); void restart(); @@ -84,6 +95,15 @@ private slots: void onReadyReadStandardOutput(); private: + // Called after a crash or an unresponsive health check. Gives up immediately if the service has never + // responded since this start (see m_hasRespondedSinceStart) -- likely a config problem, not worth + // retrying. Otherwise retries indefinitely, treating it as a crash of a previously working server. + void autoRestart(); + + // Shared process-launch implementation used by start() and by autoRestart()'s retry. Unlike start(), + // leaves m_consecutiveLaunchFailures and m_hasRespondedSinceStart untouched. + void launchProcess(); + static int findAvailablePortNumber( int firstPort ); static QString serviceWorkingDirectory(); static QProcessEnvironment buildProcessEnvironment( const QString& workingDirectory ); @@ -99,5 +119,14 @@ private: int m_port; int m_consecutiveFailures; + int m_consecutiveLaunchFailures; bool m_isResponding; + + // True once the current run has answered a health check; autoRestart() then retries indefinitely + // instead of giving up on the first failure. Reset to false in start(). + bool m_hasRespondedSinceStart; + + // True once autoRestart() has given up on a run that never responded. Blocks a later Automatic + // start() (e.g. Sumo's tokenReady) from re-arming the loop; a Manual start/restart clears it. + bool m_hasGivenUp; }; diff --git a/docs/agents/build.md b/docs/agents/build.md index f5f10cea9c..0ed18c1ae5 100644 --- a/docs/agents/build.md +++ b/docs/agents/build.md @@ -48,10 +48,11 @@ ThirdParty\vcpkg\bootstrap-vcpkg.bat ### Cloud Service API -The local Cloud Service API (`scripts/ri-cloud-api`, a git submodule) runs in the Python +The local Cloud Service API (`scripts/ri-cloud-api`, a git submodule) runs, by default, in the Python interpreter configured under *Preferences -> Scripting -> Python Executable Location*, the same one -used for `rips`. ResInsight does not install its dependencies, so they have to be installed into -that environment up front. See [cloud-service-api.md](../cloud-service-api.md). +used for `rips`. A dedicated interpreter can instead be enabled under *Preferences -> RI Cloud API*. +ResInsight does not install its dependencies, so they have to be installed into whichever environment +is in use, up front. See [cloud-service-api.md](../cloud-service-api.md). ## Build Commands diff --git a/docs/cloud-service-api.md b/docs/cloud-service-api.md index a86a85ab69..3e1920f02b 100644 --- a/docs/cloud-service-api.md +++ b/docs/cloud-service-api.md @@ -7,11 +7,15 @@ the `scripts/ri-cloud-api` submodule, and ResInsight starts it after you sign in -m uvicorn ri_cloud_api.main:app --host 127.0.0.1 --port ``` -`` is the interpreter from *Preferences -> Scripting -> Python Executable Location*, the same -one used for `rips`. ResInsight installs nothing itself, so that environment has to be ready up -front. The service code always runs from its source folder -- a development build uses the submodule -in place, an installed build a copy of it under `CloudServiceApi` -- so only the dependencies are -strictly required in the environment. +`` is, by default, the interpreter from *Preferences -> Scripting -> Python Executable +Location*, the same one used for `rips`. A dedicated interpreter can be configured instead under +*Preferences -> RI Cloud API*: tick *Enable for Dedicated ri-cloud-api Environment* and point +*Python Executable Location* in that tab at it -- useful when `rips` and `ri-cloud-api` need +different, possibly conflicting, package versions. Leave the checkbox unticked to keep using the +scripting environment. Either way, ResInsight installs nothing itself, so the chosen environment has +to be ready up front. The service code always runs from its source folder -- a development build +uses the submodule in place, an installed build a copy of it under `CloudServiceApi` -- so only the +dependencies are strictly required in the environment. ## Prerequisites @@ -25,10 +29,10 @@ strictly required in the environment. ## Python Environment - **Komodo**: the release already provides an environment with the required packages. Point *Python - Executable Location* at it and the service starts on its own; the rest of this document does not - apply. -- **Custom**: install into the environment you already use for `rips`, or into a dedicated one that - *Python Executable Location* points at. The next section covers both. + Executable Location* (either tab, see above) at it and the service starts on its own; the rest of + this document does not apply. +- **Custom**: install into the environment you already use for `rips`, or into a dedicated one + configured under *Preferences -> RI Cloud API*. The next section covers both. Use [uv](https://docs.astral.sh/uv/getting-started/installation/) and `uv sync` for an environment dedicated to the service, either of the other two recipes when it is shared with `rips`. All three @@ -40,7 +44,9 @@ C:\venvs\venvRiCloudApi\Scripts\activate ``` On Linux, `source ~/venvs/venvRiCloudApi/bin/activate`. Then set *Python Executable Location* to -`C:\venvs\venvRiCloudApi\Scripts\python.exe`, or `~/venvs/venvRiCloudApi/bin/python`. +`C:\venvs\venvRiCloudApi\Scripts\python.exe`, or `~/venvs/venvRiCloudApi/bin/python` -- under +*Preferences -> RI Cloud API* with *Enable for Dedicated ri-cloud-api Environment* ticked for a +dedicated environment, or under *Preferences -> Scripting* when it is shared with `rips`. Keep the environment out of the repository so `git clean` does not take it, and out of the install tree. @@ -221,7 +227,8 @@ reports no problem even when the service cannot import. Service output is forwarded to the ResInsight log, prefixed `Cloud API service:`. -- `no Python executable configured` -- *Python Executable Location* is empty. +- `no Python executable configured` -- *Python Executable Location* is empty, in whichever tab is + active (*RI Cloud API* if its dedicated environment is enabled, otherwise *Scripting*). - `'ri_cloud_api' not found in any of: ...` -- submodule missing, or not reachable from *Shared Script Folder(s)*. The message lists every location probed. On an installed build it means `CloudServiceApi` is empty, because the submodule was not checked out when ResInsight was installed.