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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
GALAXY_PUBLIC_DEFAULT = False
GALAXY_PROGRAMMING_LANGUAGE = "https://galaxyproject.org/"

RRP_PROGRAMMING_LANGUAGE = "https://rrp-eosc.ethz.ch"
RRP_DEFAULT_SERVICE = "https://rrp-eosc.ethz.ch"

BINDER_DEFAULT_SERVICE = "https://mybinder.org"
BINDER_PROGRAMMING_LANGUAGE = "https://jupyter.org/binder/"

Expand Down
88 changes: 88 additions & 0 deletions app/vres/rrp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from .base_vre import VRE, vre_factory
import requests
import logging
from app import exceptions
from app.constants import RRP_PROGRAMMING_LANGUAGE, RRP_DEFAULT_SERVICE

logging.basicConfig(level=logging.INFO)


class VRERrp(VRE):
# XXX: _get_workflow_url is not used yet, it contains the link to the remote ipynb contain a computation workflow.
def _get_workflow_url(self):
"""Extract workflow URL from the crate."""
workflow_url = self.crate.mainEntity.get("url")
if workflow_url is None:
# checked here, as some other vres might be actual files
logging.error(f"{self.__class__.__name__}: Missing url in workflow entity")
raise exceptions.WorkflowURLError("Missing url in workflow entity")
return workflow_url

def post(self):
# NOTE: to align with galaxy:
# this post request creates a rrp project and return the url to the
# project page, not open the project.

# URL of the local Flask proxy see `rrp_vre_proxy.py`
PROXY_URL = self.svc_url

# Backend URL that the proxy should forward to
BACKEND_URL = "https://rrp-eosc.ethz.ch"

# Use a session to store cookies automatically
session = requests.Session()


# Common headers including the X-Backend-Url header
HEADERS = {"X-Backend-Url": BACKEND_URL, "Content-Type": "application/json"}

# FIXME: don't commit with the real username and passward
# login_data = {"user": "xxx", "password": "yyyy"}

resp = session.post(f"{PROXY_URL}/api/login", json=login_data, headers=HEADERS)
print(resp)
print("Login response:", resp.status_code)

# - step 1: create the project and it will fetch the image
# project_data = {
# "type": "createFromExternalCatalog",
# "image": "reproducibleresearchplatform/rrp-tst:q75v54b-cunya",
# "environmentType": "jupyterlab",
# }
#
# resp = session.post(
# f"{PROXY_URL}/api/projects", json=project_data, headers=HEADERS
# )
# print("Create project response:", resp.status_code)
# print(resp.headers)
# project_code = resp.headers.get("Location") # this return api/projects/xxxx, I need to extract the xxxx from it.

# hard code for testing
project_code = "8mihracs"

# trigger the running of the project
# -> get the url?
# polling until the state is ready

# keep on getting state until it is ready
resp = session.get(f"{PROXY_URL}/api/projects/{project_code}", headers=HEADERS)
print("Project status:", resp.status_code)
print(resp.json())

# - step 2: trigger the jupter to start (when the project is ready)
start_req = {
"type": 'start',
"remote": False,
}
resp = session.post(f"{PROXY_URL}/api/projects/{project_code}", json=start_req, headers=HEADERS)
print(resp)

callback_url = f"{BACKEND_URL}/projects/{project_code}"

return callback_url

def get_default_service(self) -> str:
return RRP_DEFAULT_SERVICE


vre_factory.register(RRP_PROGRAMMING_LANGUAGE, VRERrp)
62 changes: 62 additions & 0 deletions test/rrp/ro-crate-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"@context": [
"https://w3id.org/ro/crate/1.1/context",
{ "runsOn": "https://w3id.org/ro/terms/test#runsOn" }
],
"@graph": [
{
"@id": "./",
"@type": "Dataset",
"name": "RRP Example Workflow",
"description": "This is an example of a workflow using the RRP platform.",
"license": "GNU General Public License v3.0",
"datePublished": "2025-05-06T14:35:47+00:00",
"mainEntity" : { "@id": "#workflow" },
"runsOn" : { "@id": "#destination" },
"hasPart": [
{ "@id": "#workflow" }
]
},
{
"@id": "ro-crate-metadata.json",
"@type": "CreativeWork",
"about": {
"@id": "./"
},
"conformsTo": {
"@id": "https://w3id.org/ro/crate/1.1"
}
},
{
"@id": "#workflow",
"@type": [
"File",
"SoftwareSourceCode",
"ComputationalWorkflow"
],
"name": "Example rrp workflow",
"programmingLanguage": {
"@id": "#rrp"
},
"url": "https://raw.githubusercontent.com/rawe0/example_notebook/main/data_analysis_example.ipynb"
},
{
"@id": "#rrp",
"@type": "ComputerLanguage",
"identifier": {
"@id": "https://rrp-eosc.ethz.ch"
},
"name": "RRP Jupyter Notebook",
"url": {
"@id": "https://rrp-eosc.ethz.ch"
}
},
{
"@id": "#destination",
"@type": "Service",
"name": "RRP proxy service",
"url": "http://localhost:7475",
"description": "rrp proxy service"
}
]
}
80 changes: 80 additions & 0 deletions test/rrp/rrp_vre_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)
PORT = int(os.environ.get("PORT", 7475))

# Store session cookies per backend URL
sessions = {}

# Helper to get a requests session for a backend URL
def get_session(backend_url):
if backend_url not in sessions:
sessions[backend_url] = requests.Session()
return sessions[backend_url]

# Generic proxy request
def proxy_request(req, method, path_suffix, data=None):
backend_url = req.headers.get('X-Backend-Url')
if not backend_url:
return jsonify({"error": "Missing X-Backend-Url header"}), 400

session = get_session(backend_url)
url = f"{backend_url.rstrip('/')}{path_suffix}"

try:
response = session.request(
method,
url,
json=data,
verify=False # Allow self-signed certs in dev
)

# Return JSON or raw text
try:
content = response.json()
except ValueError:
content = response.text

headers = {}
if 'Location' in response.headers:
headers['Location'] = response.headers['Location']

return (content, response.status_code, headers)

except requests.RequestException as e:
return jsonify({"error": "Backend request failed", "details": str(e)}), 502

# Login endpoint
@app.route("/api/login", methods=["POST"])
def login():
r = proxy_request(request, "POST", "/api/login/credentials", request.json)
return r

# Logout endpoint
@app.route("/api/logout", methods=["POST"])
def logout():
backend_url = request.headers.get('X-Backend-Url')
if backend_url and backend_url in sessions:
del sessions[backend_url]
return jsonify({"message": "Logged out"}), 200

# Create project
@app.route("/api/projects", methods=["POST"])
def create_project():
return proxy_request(request, "POST", "/api/projects", request.json)

# Project commands (start, etc.)
@app.route("/api/projects/<code>", methods=["POST"])
def project_command(code):
return proxy_request(request, "POST", f"/api/projects/{code}", request.json)

# Get project status
@app.route("/api/projects/<code>", methods=["GET"])
def get_project_status(code):
return proxy_request(request, "GET", f"/api/projects/{code}")

if __name__ == "__main__":
print(f"Dispatcher dev tool running at http://localhost:{PORT}")
app.run(port=PORT)
Loading