mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 07:00:17 -05:00
Change ResInsight to use ri-cloud-api as submodule (#14468)
This commit is contained in:
1 parent
0012a65922
commit
086ab43e0a
17 files changed
+383
-174
No files matched your search
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
+1
Submodule scripts/ri-cloud-api added at 36b996a96e.
@@ -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
|
||||
Whitespace-only changes.
@@ -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.
@@ -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"}
|
||||
Reference in new issue
Block a user