diff --git a/bento_wes/backends/_wes_backend.py b/bento_wes/backends/_wes_backend.py index 70bbb7d..1304835 100644 --- a/bento_wes/backends/_wes_backend.py +++ b/bento_wes/backends/_wes_backend.py @@ -339,7 +339,7 @@ def _initialize_run_and_get_command( f"{workflow_id}.project_id": run.request.tags.project_id, f"{workflow_id}.dataset_id": run.request.tags.dataset_id, # Don't use data_type from workflow metadata here - instead, workflows can say what they're ingesting - + f"{workflow_id}.service_url": run.request.tags.service_url, # TODO: more special parameters: service URLs, system__run_dir... } diff --git a/bento_wes/models.py b/bento_wes/models.py index 09c94d2..73575bb 100644 --- a/bento_wes/models.py +++ b/bento_wes/models.py @@ -4,6 +4,7 @@ __all__ = [ "BentoWorkflowInput", + "BentoWorkflowInputWithValue", "BentoWorkflowOutput", "BentoWorkflowMetadata", "BentoRunRequestTags", diff --git a/bento_wes/runs.py b/bento_wes/runs.py index 967b19c..aa38f79 100644 --- a/bento_wes/runs.py +++ b/bento_wes/runs.py @@ -31,6 +31,7 @@ from .logger import logger from .models import RunRequest from .runner import run_workflow +from .states import STATE_COMPLETE from .types import RunStream from .workflows import ( WorkflowType, @@ -47,7 +48,7 @@ def _check_runs_permission(run_requests: list[RunRequest], permission: str) -> tuple[bool, ...]: if not current_app.config["AUTHZ_ENABLED"]: return tuple([True] * len(run_requests)) # Assume we have permission for everything if authz disabled - + authz_response = authz_middleware.authz_post(request, "/policy/evaluate", body={ "requested_resource": [ { @@ -183,6 +184,31 @@ def _create_run(db: sqlite3.Connection, c: sqlite3.Cursor) -> Response: return jsonify({"run_id": str(run_id)}) +PUBLIC_RUN_DETAILS_SHAPE = { + "request": { + "workflow_type": True, + "tags": { + "workflow_id": True, + "workflow_metadata": { + "data_type": True, + }, + "project_id": True, + "dataset_id": True, + }, + }, + "run_log": { + "start_time": True, + "end_time": True, + }, +} + +PRIVATE_RUN_DETAILS_SHAPE = { + "request": True, + "run_log": True, + "task_logs": True, +} + + @bp_runs.route("/runs", methods=["GET", "POST"]) def run_list(): db = get_db() @@ -200,6 +226,8 @@ def run_list(): return flask_bad_request_error("Value error") # GET + # Bento Extension: Include run public details with /runs request + public_endpoint = request.args.get("public", "false").lower() == "true" # Bento Extension: Include run details with /runs request with_details = request.args.get("with_details", "false").lower() == "true" @@ -209,13 +237,26 @@ def run_list(): for r in c.execute("SELECT * FROM runs").fetchall(): run = run_with_details_and_output_from_row(c, r, stream_content=False) perms_list.append(run.request) - res_list.append({ - **run.model_dump(mode="json", include={"run_id", "state"}), - **({"details": run.model_dump(mode="json", exclude={"outputs"})} if with_details else {}), - }) - p_res = _check_runs_permission(perms_list, PERMISSION_VIEW_RUNS) - res_list = [v for v, p in zip(res_list, p_res) if p] + if not public_endpoint or run.state == STATE_COMPLETE: + res_list.append({ + **run.model_dump(mode="json", include={"run_id", "state"}), + **( + { + "details": run.model_dump(mode="json", include={ + "run_id": True, + "state": True, + **(PUBLIC_RUN_DETAILS_SHAPE if public_endpoint else PRIVATE_RUN_DETAILS_SHAPE), + }), + } + if with_details else {} + ), + }) + + if not public_endpoint: + # Filter runs to just those which we have permission to view + p_res = _check_runs_permission(perms_list, PERMISSION_VIEW_RUNS) + res_list = [v for v, p in zip(res_list, p_res) if p] authz_middleware.mark_authz_done(request) diff --git a/tests/constants.py b/tests/constants.py index 796dc46..1ffe5fd 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -45,6 +45,7 @@ }, "project_id": EXAMPLE_PROJECT_ID, "dataset_id": EXAMPLE_DATASET_ID, + "service_url": "http://metadata.local", }, } diff --git a/tests/test_runs.py b/tests/test_runs.py index 7b05b06..13fab98 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -3,10 +3,10 @@ import responses import uuid -from bento_wes.states import STATE_QUEUED - from .constants import EXAMPLE_RUN, EXAMPLE_RUN_BODY +from bento_wes.states import STATE_QUEUED, STATE_COMPLETE + def _add_workflow_response(r): with open(os.path.join(os.path.dirname(__file__), "phenopackets_json.wdl"), "r") as wf: @@ -168,3 +168,46 @@ def test_run_cancel_endpoint(client, mocked_responses): # error = rv.get_json() # assert len(error["errors"]) == 1 # assert error["errors"][0]["message"] == "Run already canceled" + + +def test_runs_public_endpoint(client, mocked_responses): + from bento_wes.db import get_db, update_run_state_and_commit + from bento_lib.events import EventBus + + event_bus = EventBus(allow_fake=True) # mock event bus + + _add_workflow_response(mocked_responses) + + # first, create a run, so we have something to fetch + rv = client.post("/runs", data=EXAMPLE_RUN_BODY) + assert rv.status_code == 200 # 200 is WES spec, even though 201 would be better (?) + + # make sure the run is complete, otherwise the public endpoint won't list it + db = get_db() + c = db.cursor() + update_run_state_and_commit(db, c, rv.get_json()["run_id"], STATE_COMPLETE, event_bus) + + # validate the public runs endpoint + rv = client.get("/runs?with_details=true&public=true") + assert rv.status_code == 200 + data = rv.get_json() + + expected_keys = ["run_id", "state", "details"] + expected_details_keys = ["request", "run_id", "run_log", "state"] + expected_request_keys = ["tags", "workflow_type"] + expected_tags_keys = ["workflow_id", "workflow_metadata", "project_id", "dataset_id"] + expected_metadata_keys = ["data_type"] + expected_run_log_keys = ["end_time", "start_time"] + + for run in data: + assert set(run.keys()) == set(expected_keys) + details = run["details"] + assert set(details.keys()) == set(expected_details_keys) + request = details["request"] + assert set(request.keys()) == set(expected_request_keys) + tags = request["tags"] + assert set(tags.keys()) == set(expected_tags_keys) + metadata = tags["workflow_metadata"] + assert set(metadata.keys()) == set(expected_metadata_keys) + run_log = details["run_log"] + assert set(run_log.keys()) == set(expected_run_log_keys)