Change ResInsight to use ri-cloud-api as submodule (#14468)

This commit is contained in:
Jørgen Herje authored and GitHub committed 2026-08-06 08:28:00 +02:00
1 parent 0012a65922
commit 086ab43e0a
17 files changed
+383 -174

No files matched your search

+3
View File
@@ -28,3 +28,6 @@
[submodule "ThirdParty/custom-opm-flowdiag-app/opm-flowdiagnostics-applications"]
path = ThirdParty/custom-opm-flowdiag-app/opm-flowdiagnostics-applications
url = https://github.com/CeetronSolutions/opm-flowdiagnostics-applications
[submodule "scripts/ri-cloud-api"]
path = scripts/ri-cloud-api
url = https://github.com/OPM/ri-cloud-api
@@ -55,6 +55,7 @@
#include "RicfCommandFileExecutor.h"
#include "RicfCommandObject.h"
#include "Cloud/RimCloudDataSourceCollection.h"
#include "Formations/RimFormationNamesCollection.h"
#include "PlotTemplates/RimPlotTemplateFolderItem.h"
#include "Polygons/RimPolygonCollection.h"
@@ -1951,6 +1952,23 @@ RiaCloudApiService* RiaApplication::cloudApiService()
// Ensure the service process is killed while the event loop is still alive at shutdown.
QObject::connect( QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_cloudApiService.get(), &RiaCloudApiService::stop );
// The server status is shown in the property editor for RimCloudDataSourceCollection, and
// changes without any user interaction: the health check confirms the service is up, and the
// process can exit on its own. Refresh the editors so the displayed status keeps up.
QObject::connect( m_cloudApiService.get(),
&RiaCloudApiService::statusChanged,
m_cloudApiService.get(),
[]()
{
auto* project = RimProject::current();
if ( !project || !project->activeOilField() ) return;
if ( auto* collection = RimCloudDataSourceCollection::instance() )
{
collection->updateConnectedEditors();
}
} );
}
return m_cloudApiService.get();
@@ -39,8 +39,9 @@
namespace
{
// Grace period after starting the process before the first health check, to allow for the
// initial uvicorn boot and module imports.
constexpr int startupGraceMs = 10 * 1000;
// initial uvicorn boot and module imports. Kept short so that the reported status settles quickly;
// a service that needs longer is covered by the health check retries rather than by waiting here.
constexpr int startupGraceMs = 4 * 1000;
// Poll the /alive endpoint at this interval.
constexpr int healthCheckIntervalMs = 10 * 1000;
@@ -65,6 +66,7 @@ RiaCloudApiService::RiaCloudApiService( QObject* parent )
, m_networkAccessManager( new QNetworkAccessManager( this ) )
, m_port( -1 )
, m_consecutiveFailures( 0 )
, m_isResponding( false )
{
m_healthTimer.setInterval( healthCheckIntervalMs );
connect( &m_healthTimer, &QTimer::timeout, this, &RiaCloudApiService::onHealthCheck );
@@ -72,7 +74,16 @@ RiaCloudApiService::RiaCloudApiService( QObject* parent )
// After the startup grace period has elapsed, begin periodic health checks.
m_startupTimer.setSingleShot( true );
m_startupTimer.setInterval( startupGraceMs );
connect( &m_startupTimer, &QTimer::timeout, this, [this]() { m_healthTimer.start(); } );
connect( &m_startupTimer,
&QTimer::timeout,
this,
[this]()
{
// A QTimer does not fire on start, so check once here instead of waiting a full
// interval. Start the timer first, since the check may restart the service.
m_healthTimer.start();
onHealthCheck();
} );
}
//--------------------------------------------------------------------------------------------------
@@ -91,6 +102,14 @@ bool RiaCloudApiService::isRunning() const
return m_process && m_process->state() != QProcess::NotRunning;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaCloudApiService::isResponding() const
{
return isRunning() && m_isResponding;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -157,10 +176,27 @@ void RiaCloudApiService::start()
connect( m_process,
&QProcess::finished,
this,
[]( int exitCode, QProcess::ExitStatus )
{ RiaLogging::info( std::format( "Cloud API service: process finished (exit code {}).", exitCode ) ); } );
[this]( int exitCode, QProcess::ExitStatus exitStatus )
{
const std::string message = std::format( "Cloud API service: process finished (exit code {}).", exitCode );
// A non-zero exit means the service never came up, or died. Anything else is an
// ordinary shutdown.
if ( exitCode != 0 || exitStatus != QProcess::NormalExit )
{
RiaLogging::error( message );
}
else
{
RiaLogging::info( message );
}
m_isResponding = false;
emit statusChanged();
} );
m_consecutiveFailures = 0;
m_isResponding = false;
RiaLogging::info( std::format( "Cloud API service: launching '{} {}' (working directory '{}').",
pythonExecutable,
@@ -171,6 +207,8 @@ void RiaCloudApiService::start()
RiaLogging::info( std::format( "Cloud API service: starting on http://127.0.0.1:{}.", m_port ) );
emit statusChanged();
// Delay the first health check by the startup grace period to allow the server to boot.
m_startupTimer.start();
}
@@ -196,7 +234,10 @@ void RiaCloudApiService::stop()
m_process = nullptr;
}
m_port = -1;
m_port = -1;
m_isResponding = false;
emit statusChanged();
}
//--------------------------------------------------------------------------------------------------
@@ -243,18 +284,26 @@ void RiaCloudApiService::onHealthCheck()
{
reply->deleteLater();
const bool wasResponding = m_isResponding;
const int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
if ( reply->error() == QNetworkReply::NoError && statusCode == 200 )
{
m_consecutiveFailures = 0;
m_isResponding = true;
}
else
{
m_isResponding = false;
m_consecutiveFailures++;
if ( m_consecutiveFailures >= maxConsecutiveFailures )
{
restart();
}
}
// Notify before a possible restart, so the transition out of "responding" is seen.
if ( m_isResponding != wasResponding ) emit statusChanged();
if ( !m_isResponding && m_consecutiveFailures >= maxConsecutiveFailures )
{
restart();
}
} );
}
@@ -283,32 +332,41 @@ int RiaCloudApiService::findAvailablePortNumber()
//--------------------------------------------------------------------------------------------------
QString RiaCloudApiService::serviceWorkingDirectory()
{
// The 'ri_cloud_api' package lives under the Python examples folder. Probe a set of candidate
// locations and return the first one that actually contains the package. The configured shared
// script folder(s) are probed first, since in a development build the executable lives in the
// build tree and not next to the Python examples.
// Probe a set of candidate locations and return the first one that actually contains the
// 'ri_cloud_api' package.
const QString appDir = QCoreApplication::applicationDirPath();
QStringList candidates;
// Configured "Shared Script Folder(s)" (semicolon-separated, defaults to the Python examples folder).
// Location relative to the executable, used for an installed ResInsight. Probed first so that
// an installed build always uses the copy it shipped with. The preferences are shared by every
// ResInsight on the machine, so "Shared Script Folder(s)" may well point at a source checkout
// set up for a development build, and would otherwise shadow the installed copy.
candidates << appDir + "/CloudServiceApi/";
// Configured "Shared Script Folder(s)" (semicolon-separated, defaults to the Python examples
// folder). This is what a development build relies on, since its executable lives in the build
// tree and not next to the Python examples.
const QStringList scriptDirectories = RiaApplication::instance()->scriptDirectories().split( ';', Qt::SkipEmptyParts );
for ( const QString& scriptDir : scriptDirectories )
{
candidates << scriptDir.trimmed();
}
// Locations relative to the executable, used for an installed ResInsight.
candidates << appDir + "/CloudServiceApi/";
for ( const QString& candidate : candidates )
{
if ( candidate.isEmpty() ) continue;
QDir dir( candidate );
if ( dir.exists( "ri_cloud_api" ) )
// Probe the candidate itself and a nested 'ri-cloud-api' repository folder, so a shared
// script folder can point either at the repository or at the folder containing it.
const QStringList probePaths = { candidate, candidate + "/ri-cloud-api" };
for ( const QString& path : probePaths )
{
return dir.absolutePath();
QDir dir( path );
if ( dir.exists( "ri_cloud_api" ) )
{
return dir.absolutePath();
}
}
}
@@ -324,7 +382,7 @@ QProcessEnvironment RiaCloudApiService::buildProcessEnvironment( const QString&
{
// The service runs in the configured Python interpreter (typically a virtual environment), so
// uvicorn/fastapi are resolved from that environment. The 'ri_cloud_api' package additionally
// depends on local workspace libraries laid out as 'ri_cloud_api/libs/<lib>/src'. Add the
// depends on local workspace libraries laid out as 'libs/<lib>/src', beside the package. Add the
// discovered 'src' folders to PYTHONPATH so the service can be imported without a separate
// install step.
//
@@ -336,7 +394,7 @@ QProcessEnvironment RiaCloudApiService::buildProcessEnvironment( const QString&
QStringList pythonPaths;
QDir libsDir( workingDirectory + "/ri_cloud_api/libs" );
QDir libsDir( workingDirectory + "/libs" );
const QFileInfoList libEntries = libsDir.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot );
for ( const QFileInfo& libEntry : libEntries )
{
@@ -47,8 +47,20 @@ public:
void stop();
void restart();
// True as soon as the process has been launched, which is not the same as the service being
// usable: uvicorn may still be booting, or may be about to exit on a missing dependency.
bool isRunning() const;
int port() const;
// True once the service has answered a health check, i.e. it is actually serving requests.
bool isResponding() const;
int port() const;
signals:
// Emitted when isRunning() or isResponding() changes, so that user interface elements showing
// the server status can refresh. The status changes without any user interaction, driven by the
// health check and by the process exiting on its own.
void statusChanged();
private slots:
void onHealthCheck();
@@ -65,6 +77,7 @@ private:
QTimer m_startupTimer;
QTimer m_healthTimer;
int m_port;
int m_consecutiveFailures;
int m_port;
int m_consecutiveFailures;
bool m_isResponding;
};
@@ -249,13 +249,21 @@ void RimCloudDataSourceCollection::defineUiOrdering( QString uiConfigName, caf::
// Cloud server group
auto serverGroup = uiOrdering.addNewGroup( QString( "Cloud API Server %1" ).arg( RiaDefines::betaFeaturePostfix() ) );
auto* cloudApiService = RiaApplication::instance()->cloudApiService();
bool isServerRunning = cloudApiService && cloudApiService->isRunning();
QString serverStatus = "Server Status: ";
if ( isServerRunning )
auto* cloudApiService = RiaApplication::instance()->cloudApiService();
bool isServerRunning = cloudApiService && cloudApiService->isRunning();
// A launched process is not yet a working server: uvicorn may still be booting, or may be about
// to exit on a missing dependency. Only report "Running" once a health check has succeeded.
bool isServerResponding = cloudApiService && cloudApiService->isResponding();
QString serverStatus = "Server Status: ";
if ( isServerResponding )
{
serverStatus += QString( "<font color='#228B22'>✔ Running (port %1)</font>" ).arg( cloudApiService->port() );
}
else if ( isServerRunning )
{
serverStatus += QString( "<font color='#FFA500'>… Starting (port %1)</font>" ).arg( cloudApiService->port() );
}
else
{
serverStatus += "<font color='#FFA500'>❌ Stopped</font>";
+14 -4
View File
@@ -441,7 +441,9 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
mark_as_advanced(RESINSIGHT_UPDATE_SUBMODULES)
if(RESINSIGHT_UPDATE_SUBMODULES)
set(SUBDIR "${PROJECT_SOURCE_DIR}/ThirdParty")
set(SUBDIR "${PROJECT_SOURCE_DIR}/ThirdParty"
"${PROJECT_SOURCE_DIR}/scripts/ri-cloud-api"
)
message(STATUS "Initializing GIT submodules")
execute_process(
COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive -- ${SUBDIR}
@@ -1335,9 +1337,17 @@ endif(RESINSIGHT_BUILD_DOCUMENTATION)
# Installation packaging Cloud Service API
# ##############################################################################
set(CLOUD_SERVICE_API_PYTHON_SOURCE_PATH
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/ri_cloud_api"
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/ri-cloud-api"
)
install(DIRECTORY ${CLOUD_SERVICE_API_PYTHON_SOURCE_PATH}/
DESTINATION ${RESINSIGHT_INSTALL_FOLDER}/CloudServiceApi/ri_cloud_api
# The contents of the repository are installed side by side, so that the service can be started
# with 'CloudServiceApi' as working directory, importing the 'ri_cloud_api' package and resolving
# the workspace libraries in 'libs'.
install(
DIRECTORY ${CLOUD_SERVICE_API_PYTHON_SOURCE_PATH}/
DESTINATION ${RESINSIGHT_INSTALL_FOLDER}/CloudServiceApi
PATTERN ".git*" EXCLUDE
PATTERN "__pycache__" EXCLUDE
PATTERN ".venv" EXCLUDE
PATTERN ".*_cache" EXCLUDE
)
+7
View File
@@ -46,6 +46,13 @@ git submodule update --init
ThirdParty\vcpkg\bootstrap-vcpkg.bat
```
### Cloud Service API
The local Cloud Service API (`scripts/ri-cloud-api`, a git submodule) runs 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).
## Build Commands
### Linux
+231
View File
@@ -0,0 +1,231 @@
# Cloud Service API -- Developer Python Setup
The Cloud Service API is a local FastAPI service that bridges cloud data into ResInsight. It lives in
the `scripts/ri-cloud-api` submodule, and ResInsight starts it after you sign in to Sumo:
```
<python> -m uvicorn ri_cloud_api.main:app --host 127.0.0.1 --port <free port>
```
`<python>` 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.
## Prerequisites
- Python 3.11 or later, which is what `ri-cloud-api` requires.
- The submodule checked out: `git submodule update --init`.
## 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.
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
need an environment to install into -- an existing one, or a new virtual environment:
```powershell
python -m venv C:\venvs\venvRiCloudApi
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`.
Keep the environment out of the repository so `git clean` does not take it, and out of the install
tree.
## Installing the packages
Three ways, all installing `ri-cloud-api` and its two workspace libraries with their dependencies:
| Command | Installs | Effect on the environment |
| --- | --- | --- |
| [`uv sync`](#1-using-uv-sync) | exact versions from `uv.lock` | removes anything not in the lock |
| [`uv pip install`](#2-using-uv-pip-install) | exact versions from `uv.lock` | only adds |
| [`pip install`](#3-using-pip) | version ranges from `pyproject.toml` | only adds |
Each runs from the folder holding `ri_cloud_api`, which differs between a development build and an
[installed build](#installed-builds). The recipes below write `<python>` for the environment's
interpreter -- the one from *Python Executable Location*, on Windows typically
`<venv>\Scripts\python.exe`, on Linux `<venv>/bin/python`.
Installing the three `ri-cloud-*` packages is optional: ResInsight prepends every `libs/*/src` to
`PYTHONPATH` and runs from the source folder regardless. Leaving them out keeps the environment
smaller, at the cost of setting `PYTHONPATH` yourself when
[running the service by hand](#running-the-service-by-hand). Each recipe ends with that variant.
### 1. Using uv sync
```powershell
cd ResInsight\scripts\ri-cloud-api # development
cd <install prefix>\bin\CloudServiceApi # installed
uv sync --active --no-dev --frozen
```
`--active` installs into the activated environment instead of creating a `.venv` beside the package.
`--no-dev` skips the `dev` group -- `ruff`, `mypy`, `pytest` -- which uv includes unless asked not
to; drop it when working on the API itself. `--frozen` installs what `uv.lock` pins without
rewriting it, so the source folder stays clean even when the lock is behind `pyproject.toml`. The
`ri-cloud-*` packages go in editable.
`uv sync` owns the environment: it removes anything not in `uv.lock`, so do not point `--active` at a
shared one. `--inexact` keeps those packages, but [`uv pip install`](#2-using-uv-pip-install) is the
better tool there. And if the interpreter does not satisfy `requires-python = ">=3.11"`, uv deletes
and recreates the environment rather than reporting an error.
**Dependencies only:** add `--no-install-workspace`.
### 2. Using uv pip install
```powershell
cd ResInsight\scripts\ri-cloud-api # development
cd <install prefix>\bin\CloudServiceApi # installed
uv export --frozen --no-dev --no-hashes --format requirements-txt -o <somewhere>\req.txt
uv pip install --python <python> -r <somewhere>\req.txt
```
**`uv pip install` only adds and upgrades; it never removes anything missing from `uv.lock`**, which
makes it the right tool for an environment shared with `rips` or your own tooling.
- The export lists the `ri-cloud-*` packages as paths relative to the package directory, so run the
install from that same folder. They install editable.
- `--python` names the target interpreter. Without it uv picks the activated or discovered
environment. The target needs no `pip` of its own.
- `--frozen` reads `uv.lock` without rewriting it. Write `req.txt` outside the source folder so it
does not show up as an untracked change there.
- Drop `--no-dev` for `ruff`, `mypy` and `pytest`. Do not add `--exact`, which turns on pruning.
**Dependencies only:** add `--no-emit-workspace` to the export.
### 3. Using pip
The required packages are declared in the [ri-cloud-api](https://github.com/OPM/ri-cloud-api)
repository, which is the source of truth for the version ranges:
[`pyproject.toml`](https://github.com/OPM/ri-cloud-api/blob/main/pyproject.toml) for the service,
[`libs/services/pyproject.toml`](https://github.com/OPM/ri-cloud-api/blob/main/libs/services/pyproject.toml)
for the service layer, and
[`libs/core_utils/pyproject.toml`](https://github.com/OPM/ri-cloud-api/blob/main/libs/core_utils/pyproject.toml)
for the shared utilities. Rather than repeating that list, let pip read it -- with the environment
activated:
```powershell
cd ResInsight\scripts\ri-cloud-api # development
cd <install prefix>\bin\CloudServiceApi # installed
pip install ./libs/core_utils ./libs/services .
```
Without activating it, `<python> -m pip install ...` does the same.
The local projects must be given explicitly and in that order, since `ri-cloud-core-utils` and
`ri-cloud-services` are on no index. Installing `.` last brings in the third-party dependencies,
`uvicorn` with the `[standard]` extra included. Unlike the `uv` recipes, pip copies the packages into
`site-packages`, so source edits stop taking effect -- add `-e` to each path for editable installs.
**Dependencies only:** follow with `pip uninstall -y ri-cloud-api ri-cloud-services ri-cloud-core-utils`.
Uninstalling does not remove what a package pulled in, so the dependencies remain.
## Installed builds
`cmake --install` copies the whole submodule to `<install prefix>\bin\CloudServiceApi`, beside the
executable, and an installed ResInsight runs that copy rather than your checkout. It is a plain file
copy that keeps the layout the recipes need -- `ri_cloud_api`, `libs/<lib>/src`, `pyproject.toml`,
`uv.lock` -- with `.git*`, `__pycache__`, `.venv` and `.*_cache` excluded.
Installing ResInsight installs no Python packages, so run one of the recipes above from
`CloudServiceApi`. Two things to keep in mind, since an install tree is often read-only or replaced
by the next install:
- The submodule has to be checked out *before* the install runs, or `CloudServiceApi` ends up empty.
- Keep the environment outside the install tree, and do not drop `--active` or `--frozen` -- without
them uv writes a `.venv` into the install folder and rewrites `uv.lock` there.
## Running the service by hand
With the environment activated, run from the same folder the recipes used -- the working directory
ResInsight uses:
```powershell
uvicorn ri_cloud_api.main:app --host 127.0.0.1 --port 8000
```
Without activating it, `<python> -m uvicorn ri_cloud_api.main:app ...` does the same.
<http://127.0.0.1:8000/alive> is the endpoint ResInsight polls; it should answer `{"status":"alive"}`.
With only the dependencies installed, the workspace libraries under `libs/<lib>/src` are not on
`sys.path`, and the service fails with `ModuleNotFoundError: No module named 'ri_cloud_services'`.
Add them first, colon separated on Linux:
```powershell
$env:PYTHONPATH = "libs\core_utils\src;libs\services\src"
```
ResInsight never needs this -- `RiaCloudApiService` sets it before launching, whichever way the
environment was prepared.
## Where ResInsight looks for the package
A folder containing `ri_cloud_api`, in this order:
1. `<ResInsight executable folder>/CloudServiceApi/`
2. Each entry in *Preferences -> Scripting -> Shared Script Folder(s)* (semicolon-separated), and a
nested `ri-cloud-api` folder inside each
An installed build finds its own copy first without any configuration. A development build has no
`CloudServiceApi` beside the executable and falls through to the preference -- add
`<repo>/scripts/ri-cloud-api`, or the `scripts` folder above it.
The order matters: preferences live under one `Ceetron/ResInsight` key shared by every ResInsight on
the machine, so an entry added for a development build is visible to an installed build as well, and
probing the executable's own folder first keeps it from shadowing the shipped copy. The launch is
logged with the directory it settled on:
```
Cloud API service: launching '... -m uvicorn ri_cloud_api.main:app ...' (working directory '...')
```
## Keeping it up to date
Service code needs nothing -- it runs from the folder, so restarting the service picks it up. For an
installed build, re-running the install refreshes that folder. Dependencies need the install re-run
after `git submodule update` moves the submodule pointer, or after `pyproject.toml` or `uv.lock`
changes. Nothing checks automatically, and a dependency added since you last installed surfaces only
as an import error at startup, so check after updating:
```powershell
uv sync --active --no-dev --check
```
It compares the environment against `uv.lock`, lists what is missing and exits non-zero when the two
have diverged, without changing anything. Pass the same flags you installed with, or it reports the
packages you deliberately left out as missing.
Without uv, `<python> -m pip check` catches a missing dependency only when the `ri-cloud-*` packages
are installed, since they are what declare the requirements -- in a dependencies-only environment it
reports no problem even when the service cannot import.
## Troubleshooting
Service output is forwarded to the ResInsight log, prefixed `Cloud API service:`.
- `no Python executable configured` -- *Python Executable Location* is empty.
- `'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.
- `ModuleNotFoundError: No module named 'ri_cloud_services'` -- `PYTHONPATH` not set for a manual run.
- Any other `ModuleNotFoundError` -- dependencies not installed into the configured interpreter, or
installed into a different one than *Python Executable Location* points at.
- The service starts but runs code you did not expect -- check the working directory in the launch
log. An installed build always prefers its own `CloudServiceApi` over *Shared Script Folder(s)*.
- A `uv sync` or `uv pip install` that fails on a cached package -- `uv cache clean` empties uv's
cache so the next command re-downloads. It discards cached downloads and build artifacts only,
never installed environments; the cost is a slower next install.
Submodule scripts/ri-cloud-api added at 36b996a96e.
-74
View File
@@ -1,74 +0,0 @@
# ResInsight Cloud API service
A localhost FastAPI/uvicorn service whose lifecycle is managed by ResInsight
(`RiaCloudApiService`): it is started when Sumo authentication succeeds, bound to an
auto-selected free port, health-checked on `/alive`, restarted if unresponsive, and
killed when ResInsight closes.
## Location
The package lives at `scripts/ri_cloud_api` in the repository root.
On install, the contents of this folder are copied to `<install folder>/CloudServiceApi`
(see the *Installation packaging Cloud Service API* section in the top-level
`CMakeLists.txt`).
`RiaCloudApiService::serviceWorkingDirectory` locates the package by probing, in order:
1. the configured **Shared Script Folder(s)**
(**Preferences → Scripting → Shared Script Folder(s)**), and
2. `<application dir>/CloudServiceApi/`
and returning the first folder that contains a `ri_cloud_api` directory. For a
development build, point one of the shared script folders at the repository `scripts`
folder (e.g. `C:/Git/ResInsight/scripts`), since the executable lives in the build tree
and not next to the package.
## Deployment status
> **Target state:** this service is intended to be distributed and installed as a
> regular **pip package** into the active Python environment. Once packaged, importing
> `ri_cloud_api` (and any `ri_cloud_*` workspace libraries) will work directly from the
> environment's site-packages, with no path manipulation.
>
> **Current state (interim):** the service is **not yet a pip package**. It runs from the
> source tree inside a virtual environment, with the folder containing `ri_cloud_api` used
> as the process working directory. If workspace libraries are laid out as
> `ri_cloud_api/libs/<lib>/src`, ResInsight adds each such `src` folder to `PYTHONPATH` at
> launch time (see `RiaCloudApiService::buildProcessEnvironment`). This is a temporary
> measure that should be removed once the package is published/installed.
## Requirements (interim setup)
- A Python virtual environment configured in ResInsight under
**Preferences → Scripting → Python Executable**
(e.g. `C:/Git/.venvResInsight/Scripts/python.exe`).
- `uvicorn` and `fastapi` installed in that environment, plus any third-party
dependencies pulled in by the routers.
## Contents
ri_cloud_api/
main.py shim exposing `app` for `uvicorn ri_cloud_api.main:app`
primary/
main.py FastAPI application, router registration
routers/health/router.py `/alive` health endpoint
## Running manually (for debugging)
From the `scripts` folder (the parent of this package):
cd scripts
<venv>/Scripts/python.exe -m uvicorn ri_cloud_api.main:app --host 127.0.0.1 --port 8000
If workspace libraries are present, put them on `PYTHONPATH` first:
set PYTHONPATH=ri_cloud_api/libs/<lib>/src
Then check the health endpoint:
http://127.0.0.1:8000/alive -> {"status": "alive"}
Interactive API docs are available at:
http://127.0.0.1:8000/docs
View File
Whitespace-only changes.
-11
View File
@@ -1,11 +0,0 @@
"""ResInsight Cloud API entrypoint.
Thin shim so ``uvicorn ri_cloud_api.main:app`` keeps working alongside the
real application module at :mod:`ri_cloud_api.primary.main`.
"""
from __future__ import annotations
from .primary.main import app
__all__ = ["app"]
Whitespace-only changes.
-43
View File
@@ -1,43 +0,0 @@
"""ResInsight Cloud API — FastAPI application.
Lifecycle is managed by ResInsight (RiaCloudApiService): the service is started when Sumo
authentication succeeds, bound to an auto-selected free port, health-checked on /alive,
restarted if unresponsive, and killed when ResInsight closes.
Deployment status:
Target state — distributed and installed as a regular pip package, importable directly
from the environment's site-packages.
Current state (interim) — runs from the source tree in a virtual environment; ResInsight
injects each ri_cloud_api/libs/<lib>/src folder onto PYTHONPATH at launch time. This is
temporary and should be removed once the package is published/installed.
See README.md (next to this package) for details.
Setup:
pip install poetry
python -m venv .venv
source .venv/bin/activate
poetry install
Run (from the repository root):
uvicorn ri_cloud_api.primary.main:app --host 0.0.0.0 --port 8000 --reload
Docs:
http://localhost:8000/docs
"""
from __future__ import annotations
import logging
from fastapi import FastAPI
from .routers.health.router import router as health_router
logger = logging.getLogger("ri_cloud_api")
logging.basicConfig(level=logging.INFO)
app = FastAPI(title="ResInsight Cloud API")
app.include_router(health_router)
Whitespace-only changes.
Whitespace-only changes.
@@ -1,12 +0,0 @@
from fastapi import APIRouter
router = APIRouter(tags=["health"])
@router.get("/alive")
def alive() -> dict[str, str]:
"""Health-check endpoint polled by ResInsight for service life cycle management."""
return {"status": "alive"}