Source code for reana_workflow_controller.rest.workflows

# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2020, 2021, 2022, 2023, 2024, 2025, 2026 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""REANA Workflow Controller workflows REST API."""

import datetime
import json
import logging
import gc
from typing import Optional
from uuid import uuid4

from flask import Blueprint, jsonify, request
from sqlalchemy import and_, nullslast, or_, select
from sqlalchemy.orm import aliased
from sqlalchemy.exc import IntegrityError
import marshmallow
from webargs import fields, validate
from webargs.flaskparser import use_args, use_kwargs
from reana_commons.config import WORKFLOW_TIME_FORMAT
from reana_commons.utils import build_unique_component_name, get_dask_component_name
from reana_db.database import Session
from reana_db.models import (
    RunStatus,
    User,
    UserWorkflow,
    Workflow,
    WorkflowResource,
    Service,
    ServiceType,
    ServiceStatus,
)
from reana_db.utils import (
    _get_workflow_by_uuid,
    _get_workflow_with_uuid_or_name,
    build_workspace_path,
    get_default_quota_resource,
)
from reana_workflow_controller.config import (
    REANA_URL,
    DEFAULT_NAME_FOR_WORKFLOWS,
    MAX_WORKFLOW_SHARING_MESSAGE_LENGTH,
    FORCE_GARBAGE_COLLECTION,
)
from reana_workflow_controller.errors import (
    REANAWorkflowControllerError,
    REANAWorkflowNameError,
)
from reana_workflow_controller.rest.utils import (
    create_workflow_workspace,
    get_specification_diff,
    get_workflow_name,
    get_workflow_progress,
    get_workspace_diff,
    is_uuid_v4,
    use_paginate_args,
)

from reana_workflow_controller.k8s import (
    check_pod_status_by_prefix,
    check_pod_readiness_by_prefix,
)
from reana_workflow_controller.dask import requires_dask

START = "start"
STOP = "stop"
DELETED = "deleted"
STATUSES = {START, STOP, DELETED}

blueprint = Blueprint("workflows", __name__)


[docs] @blueprint.route("/workflows", methods=["GET"]) @use_paginate_args() @use_args( { "include_progress": fields.Bool(), "include_workspace_size": fields.Bool(), "search": fields.String(load_default=""), "sort": fields.String(load_default="desc"), "status": fields.String(load_default=""), "type": fields.String(required=True), "user": fields.String(required=True), "verbose": fields.Bool(load_default=False), "workflow_id_or_name": fields.String(), "shared": fields.Bool(load_default=False), "shared_by": fields.String(), "shared_with": fields.String(), }, location="query", unknown=marshmallow.EXCLUDE, ) def get_workflows(args, paginate=None): # noqa r"""Get all workflows. --- get: summary: Returns all workflows. description: >- This resource is expecting a user UUID. The information related to all workflows for a given user will be served as JSON operationId: get_workflows produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: type in: query description: Required. Type of workflows. required: true type: string - name: verbose in: query description: Optional flag to show more information. required: false type: boolean - name: search in: query description: Filter workflows by name. required: false type: string - name: sort in: query description: Sort workflows by creation date (asc, desc). required: false type: string - name: status in: query description: Filter workflows by list of statuses. required: false type: array items: type: string - name: page in: query description: Results page number (pagination). required: false type: integer - name: size in: query description: Number of results per page (pagination). required: false type: integer - name: include_progress in: query description: Include progress information of the workflows. required: false type: boolean - name: include_workspace_size in: query description: Include size information of the workspace. required: false type: boolean - name: workflow_id_or_name in: query description: Optional analysis UUID or name to filter. required: false type: string - name: shared in: query description: Optional flag to list all shared (owned and unowned) workflows. required: false type: boolean - name: shared_by in: query description: Optional argument to list workflows shared by the specified user. required: false type: string - name: shared_with in: query description: Optional argument to list workflows shared with the specified user. required: false type: string responses: 200: description: >- Requests succeeded. The response contains the current workflows for a given user. schema: type: object properties: total: type: integer items: type: array items: type: object properties: id: type: string name: type: string status: type: string size: type: object properties: raw: type: number human_readable: type: string user: type: string created: type: string progress: type: object launcher_url: type: string x-nullable: true owner_email: type: string shared_with: type: array items: type: string examples: application/json: [ { "id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", "name": "mytest.1", "status": "running", "size":{ "raw": 10490000, "human_readable": "10 MB" }, "user": "00000000-0000-0000-0000-000000000000", "created": "2018-06-13T09:47:35.66097", "launcher_url": "https://github.com/reanahub/reana-demo-helloworld.git", }, { "id": "3c9b117c-d40a-49e3-a6de-5f89fcada5a3", "name": "mytest.2", "status": "finished", "size":{ "raw": 12580000, "human_readable": "12 MB" }, "user": "00000000-0000-0000-0000-000000000000", "created": "2018-06-13T09:47:35.66097", "launcher_url": "https://example.org/specs/reana-snakemake.yaml", }, { "id": "72e3ee4f-9cd3-4dc7-906c-24511d9f5ee3", "name": "mytest.3", "status": "created", "size":{ "raw": 184320, "human_readable": "180 KB" }, "user": "00000000-0000-0000-0000-000000000000", "created": "2018-06-13T09:47:35.66097", "launcher_url": "https://zenodo.org/record/1/reana.yaml", }, { "id": "c4c0a1a6-beef-46c7-be04-bf4b3beca5a1", "name": "mytest.4", "status": "created", "size": { "raw": 1074000000, "human_readable": "1 GB" }, "user": "00000000-0000-0000-0000-000000000000", "created": "2018-06-13T09:47:35.66097", "launcher_url": null, } ] 400: description: >- Request failed. The incoming data specification seems malformed. 404: description: >- Request failed. User does not exist. examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 does not exist" } 500: description: >- Request failed. Internal controller error. examples: application/json: { "message": "Internal workflow controller error." } """ user_uuid: str = args["user"] type_: str = args["type"] verbose: bool = args["verbose"] sort: str = args["sort"] search: str = args["search"] status_list: str = args["status"] include_progress: bool = args.get("include_progress", verbose) include_workspace_size: bool = args.get("include_workspace_size", verbose) workflow_id_or_name: Optional[str] = args.get("workflow_id_or_name") shared: bool = args.get("shared") shared_by: Optional[str] = args.get("shared_by") shared_with: Optional[str] = args.get("shared_with") if "list" in FORCE_GARBAGE_COLLECTION: gc.collect() if shared_by and shared_with: message = "You cannot filter by shared_by and shared_with at the same time." return (jsonify({"message": message}), 400) try: user = Session.query(User).filter(User.id_ == user_uuid).first() if not user: return jsonify({"message": "User {} does not exist".format(user_uuid)}), 404 owned_workflows = Session.query(Workflow).filter(Workflow.owner_id == user.id_) shared_with_me = ( Session.query(Workflow) .join(UserWorkflow, UserWorkflow.workflow_id == Workflow.id_) .filter(UserWorkflow.user_id == user.id_) ) # default case: retrieve owned workflows query = owned_workflows if shared_with: if shared_with == "nobody": # retrieve owned unshared workflows query = owned_workflows.filter( Workflow.id_.notin_(select(UserWorkflow.workflow_id)) ) elif shared_with == "anybody": # retrieve exclusively owned shared workflows query = owned_workflows.filter( Workflow.id_.in_(select(UserWorkflow.workflow_id)) ) else: # retrieve owned workflows shared with specific user query = owned_workflows.filter( Workflow.users_it_is_shared_with.any(User.email == shared_with) ) elif shared_by: if shared_by == "anybody": # retrieve unowned workflows shared by anyone query = shared_with_me else: # retrieve unowned workflows shared by specific user query = shared_with_me.filter( Workflow.owner.has(User.email == shared_by) ) elif shared: # retrieve all workflows, owned and shared with user query = owned_workflows.union_all(shared_with_me) if search: search = json.loads(search) search_val = search.get("name")[0] query = query.filter(Workflow.name.ilike("%{}%".format(search_val))) if status_list: workflow_status = [RunStatus[status] for status in status_list.split(",")] query = query.filter(Workflow.status.in_(workflow_status)) if workflow_id_or_name: query = ( query.filter(Workflow.id_ == workflow_id_or_name) if is_uuid_v4(workflow_id_or_name) else query.filter(Workflow.name == workflow_id_or_name) ) column_sorted = Workflow.created.desc() if sort in ["disk-desc", "cpu-desc"]: resource_type = sort.split("-")[0] resource = get_default_quota_resource(resource_type) query = query.join( WorkflowResource, and_( Workflow.id_ == WorkflowResource.workflow_id, WorkflowResource.resource_id == resource.id_, ), isouter=True, ) column_sorted = nullslast(WorkflowResource.quota_used.desc()) elif sort in ["asc", "desc"]: column_sorted = getattr(Workflow.created, sort)() if type_ == "interactive": # Keep only workflows that have at least one interactive session. query = query.filter(Workflow.sessions.any()) pagination_dict = paginate(query.order_by(column_sorted)) owner_ids = {workflow.owner_id for workflow in pagination_dict["items"]} owners = dict( Session.query(User.id_, User.email).filter(User.id_.in_(owner_ids)).all() ) workflows = [] for workflow in pagination_dict["items"]: owner_email = owners[workflow.owner_id] if workflow.owner_id == user.id_: shared_with = [ shared_user.email for shared_user in workflow.users_it_is_shared_with ] else: shared_with = [] workflow_response = { "id": workflow.id_, "name": get_workflow_name(workflow), "status": workflow.status.name, "user": user_uuid, "launcher_url": workflow.launcher_url, "created": workflow.created.strftime(WORKFLOW_TIME_FORMAT), "progress": get_workflow_progress( workflow, include_progress=include_progress ), "owner_email": owner_email, "shared_with": shared_with, } if requires_dask(workflow): dask_service = workflow.services[0] if workflow.services else None if dask_service and dask_service.status == ServiceStatus.created: pod_readiness = check_pod_readiness_by_prefix( pod_name_prefix=get_dask_component_name(workflow.id_, "cluster") ) if pod_readiness == "Ready": dask_service.status = ServiceStatus.running db_session = Session.object_session(dask_service) db_session.commit() services = workflow.services services_serialized = [ { "name": service.name, "type": service.type_.name, "status": service.status.name, } for service in services ] workflow_response["services"] = services_serialized if type_ == "interactive" or verbose: int_session = workflow.sessions[0] if workflow.sessions else None if int_session: workflow_response["session_type"] = int_session.type_.name workflow_response["session_uri"] = int_session.path int_session_pod_name_prefix = build_unique_component_name( "run-session", int_session.workflow[0].id_ ) if int_session.status == RunStatus.created: pod_status = check_pod_status_by_prefix( pod_name_prefix=int_session_pod_name_prefix ) if pod_status == "Running": int_session.status = RunStatus.running db_session = Session.object_session(int_session) db_session.commit() workflow_response["session_status"] = int_session.status.name # Skip workflow if type is interactive and there is no session elif type_ == "interactive": continue empty_disk_usage = { "human_readable": "", "raw": -1, } if include_workspace_size: workflow_response["size"] = ( workflow.get_quota_usage() .get("disk", {}) .get("usage", empty_disk_usage) ) else: workflow_response["size"] = empty_disk_usage workflows.append(workflow_response) pagination_dict["items"] = workflows pagination_dict["user_has_workflows"] = ( Session.query(Workflow.id_).filter(Workflow.owner_id == user.id_).first() is not None ) return jsonify(pagination_dict), 200 except (ValueError, KeyError): return jsonify({"message": "Malformed request."}), 400 except json.JSONDecodeError: return jsonify({"message": "Your request contains not valid JSON."}), 400 except Exception as e: return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows", methods=["POST"]) def create_workflow(): # noqa r"""Create workflow and its workspace. --- post: summary: Create workflow and its workspace. description: >- This resource expects all necessary data to represent a workflow so it is stored in database and its workspace is created. operationId: create_workflow produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workspace_root_path in: query description: A root path under which the workflow workspaces are stored. required: false type: string - name: workflow in: body description: >- JSON object including workflow parameters and workflow specification in JSON format (`yadageschemas.load()` output) with necessary data to instantiate a yadage workflow. required: true schema: type: object properties: operational_options: type: object description: Operational options. reana_specification: type: object description: Workflow specification in JSON format. workflow_name: type: string description: Workflow name. If empty name will be generated. git_data: type: object description: GitLab data. launcher_url: type: string description: Launcher URL. retention_rules: type: array title: Retention rules list for the files in the workspace. items: title: Retention rule for the files in the workspace. type: object additionalProperties: false properties: workspace_files: type: string retention_days: type: integer required: [reana_specification, workflow_name, operational_options, retention_rules] responses: 201: description: >- Request succeeded. The workflow has been created along with its workspace schema: type: object properties: message: type: string workflow_id: type: string workflow_name: type: string examples: application/json: { "message": "Workflow workspace has been created.", "workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac", "workflow_name": "mytest-1" } 400: description: >- Request failed. The incoming data specification seems malformed 404: description: >- Request failed. User does not exist. examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 does not exist" } """ try: user_uuid = request.args["user"] user = Session.query(User).filter(User.id_ == user_uuid).first() if not user: return ( jsonify( {"message": "User with id:{} does not exist".format(user_uuid)} ), 404, ) workflow_uuid = str(uuid4()) # Use name prefix user specified or use default name prefix # Actual name is prefix + autoincremented run_number. workflow_name = request.json.get("workflow_name", "") if workflow_name == "": workflow_name = DEFAULT_NAME_FOR_WORKFLOWS else: try: workflow_name.encode("ascii") except UnicodeEncodeError: # `workflow_name` contains something else than just ASCII. raise REANAWorkflowNameError( "Workflow name {} is not valid.".format(workflow_name) ) git_ref = "" git_repo = "" if "git_data" in request.json: git_data = request.json["git_data"] git_ref = git_data["git_commit_sha"] git_repo = git_data["git_url"] # add spec and params to DB as JSON workspace_root_path = request.args.get("workspace_root_path", None) reana_specification = request.json["reana_specification"] workflow = Workflow( id_=workflow_uuid, name=workflow_name, owner_id=request.args["user"], reana_specification=reana_specification, operational_options=request.json.get("operational_options", {}), type_=reana_specification["workflow"]["type"], logs="", git_ref=git_ref, git_repo=git_repo, workspace_path=build_workspace_path( request.args["user"], workflow_uuid, workspace_root_path ), launcher_url=request.json.get("launcher_url"), ) if requires_dask(workflow): dask_service = Service( name=get_dask_component_name(workflow.id_, "database_model_service"), uri=f"{REANA_URL}/{workflow_uuid}/dashboard/status", type_=ServiceType.dask, status=ServiceStatus.created, ) workflow.services.append(dask_service) Session.add(workflow) Session.object_session(workflow).commit() retention_rules = request.json.get("retention_rules", []) if retention_rules: workflow.set_workspace_retention_rules(retention_rules) if git_ref: create_workflow_workspace( workflow.workspace_path, user_id=user.id_, git_url=git_data["git_url"], git_branch=git_data["git_branch"], git_ref=git_ref, ) else: create_workflow_workspace(workflow.workspace_path) return ( jsonify( { "message": "Workflow workspace created", "workflow_id": workflow.id_, "workflow_name": get_workflow_name(workflow), } ), 201, ) except (REANAWorkflowNameError, KeyError) as e: return jsonify({"message": str(e)}), 400 except Exception as e: return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows/<workflow_id_or_name>/parameters", methods=["GET"]) def get_workflow_parameters(workflow_id_or_name): # noqa r"""Get workflow input parameters. --- get: summary: Get workflow parameters. description: >- This resource reports the input parameters of workflow. operationId: get_workflow_parameters produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name in: path description: Required. Workflow UUID or name. required: true type: string responses: 200: description: >- Request succeeded. Workflow input parameters, including the status are returned. schema: type: object properties: id: type: string name: type: string type: type: string parameters: type: object examples: application/json: { 'id': 'dd4e93cf-e6d0-4714-a601-301ed97eec60', 'name': 'workflow.24', 'type': 'serial', 'parameters': {'helloworld': 'code/helloworld.py', 'inputfile': 'data/names.txt', 'outputfile': 'results/greetings.txt', 'sleeptime': 2} } 400: description: >- Request failed. The incoming data specification seems malformed. examples: application/json: { "message": "Malformed request." } 403: description: >- Request failed. User is not allowed to access workflow. examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 is not allowed to access workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1" } 404: description: >- Request failed. Either User or Workflow does not exist. examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 does not exist" } application/json: { "message": "Workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1 does not exist" } 500: description: >- Request failed. Internal controller error. """ try: user_uuid = request.args["user"] workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid, True) workflow_parameters = workflow.get_input_parameters() return ( jsonify( { "id": workflow.id_, "name": get_workflow_name(workflow), "type": workflow.reana_specification["workflow"]["type"], "parameters": workflow_parameters, } ), 200, ) except ValueError: return ( jsonify( { "message": "REANA_WORKON is set to {0}, but " "that workflow does not exist. " "Please set your REANA_WORKON environment " "variable appropriately.".format(workflow_id_or_name) } ), 404, ) except KeyError as e: return jsonify({"message": str(e)}), 400 except Exception as e: return jsonify({"message": str(e)}), 500
[docs] @blueprint.route( "/workflows/<workflow_id_or_name_a>/diff/" "<workflow_id_or_name_b>", methods=["GET"], ) def get_workflow_diff(workflow_id_or_name_a, workflow_id_or_name_b): # noqa r"""Get differences between two workflows. --- get: summary: Get diff between two workflows. description: >- This resource shows the differences between the assets of two workflows. Resource is expecting two workflow UUIDs or names. operationId: get_workflow_diff produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name_a in: path description: Required. Analysis UUID or name of the first workflow. required: true type: string - name: workflow_id_or_name_b in: path description: Required. Analysis UUID or name of the second workflow. required: true type: string - name: brief in: query description: Optional flag. If set, file contents are examined. required: false type: boolean default: false - name: context_lines in: query description: Optional parameter. Sets number of context lines for workspace diff output. required: false type: string default: '5' responses: 200: description: >- Request succeeded. Info about a workflow, including the status is returned. schema: type: object properties: reana_specification: type: string workspace_listing: type: string examples: application/json: { "reana_specification": ["- nevents: 100000\n+ nevents: 200000"], "workspace_listing": {"Only in workspace a: code"} } 400: description: >- Request failed. The incoming payload seems malformed. examples: application/json: { "message": "Malformed request." } 403: description: >- Request failed. User is not allowed to access workflow. examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 is not allowed to access workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1" } 404: description: >- Request failed. Either user or workflow does not exist. examples: application/json: { "message": "Workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1 does not exist." } 500: description: >- Request failed. Internal controller error. """ try: user_uuid = request.args["user"] brief = json.loads(request.args.get("brief", "false").lower()) context_lines = request.args.get("context_lines", 5) workflow_a_exists = False workflow_a = _get_workflow_with_uuid_or_name( workflow_id_or_name_a, user_uuid, True ) workflow_a_exists = True workflow_b = _get_workflow_with_uuid_or_name( workflow_id_or_name_b, user_uuid, True ) if not workflow_id_or_name_a or not workflow_id_or_name_b: raise ValueError("Workflow id or name is not supplied") specification_diff = get_specification_diff(workflow_a, workflow_b) try: workspace_diff = get_workspace_diff( workflow_a, workflow_b, brief, context_lines ) except ValueError as e: workspace_diff = str(e) response = { "reana_specification": json.dumps(specification_diff), "workspace_listing": json.dumps(workspace_diff), } return jsonify(response) except REANAWorkflowControllerError as e: return jsonify({"message": str(e)}), 409 except ValueError: wrong_workflow = ( workflow_id_or_name_b if workflow_a_exists else workflow_id_or_name_a ) return ( jsonify({"message": "Workflow {0} does not exist.".format(wrong_workflow)}), 404, ) except KeyError as e: return jsonify({"message": str(e)}), 400 except ValueError as e: return jsonify({"message": str(e)}), 400 except json.JSONDecodeError: return jsonify({"message": "Your request contains not valid JSON."}), 400 except Exception as e: return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows/<workflow_id_or_name>/retention_rules") @use_kwargs( {"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE ) def get_workflow_retention_rules(workflow_id_or_name: str, user: str): r"""Get the retention rules of a workflow. --- get: summary: Get the retention rules of a workflow. description: >- This resource returns all the retention rules of a given workflow. operationId: get_workflow_retention_rules produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. required: true type: string responses: 200: description: >- Request succeeded. The response contains the list of all the retention rules. schema: type: object properties: workflow_id: type: string workflow_name: type: string retention_rules: type: array items: type: object properties: id: type: string workspace_files: type: string retention_days: type: integer apply_on: type: string x-nullable: true status: type: string examples: application/json: { "workflow_id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", "workflow_name": "mytest.1", "retention_rules": [ { "id": "851da5cf-0b26-40c5-97a1-9acdbb35aac7", "workspace_files": "**/*.tmp", "retention_days": 1, "apply_on": "2022-11-24T23:59:59", "status": "active" } ] } 404: description: >- Request failed. User or workflow do not exist. schema: type: object properties: message: type: string examples: application/json: { "message": "User 00000000-0000-0000-0000-000000000000 does not exist." } 500: description: >- Request failed. Internal server error. schema: type: object properties: message: type: string examples: application/json: { "message": "Something went wrong." } """ try: workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user, True) rules = workflow.retention_rules response = { "workflow_id": workflow.id_, "workflow_name": workflow.get_full_workflow_name(), "retention_rules": [rule.serialize() for rule in rules], } return jsonify(response), 200 except ValueError as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 404 except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows/<workflow_id_or_name>/share", methods=["POST"]) @use_kwargs( {"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE ) @use_kwargs( { "user_email_to_share_with": fields.Str(required=True), "message": fields.Str( validate=validate.Length( max=MAX_WORKFLOW_SHARING_MESSAGE_LENGTH, error="Message is too long. Please keep it under {max} characters.", ) ), "valid_until": fields.Date( error_messages={ "invalid": "Date format is not valid. Please use YYYY-MM-DD format." } ), }, location="json", ) def share_workflow( workflow_id_or_name: str, user: str, user_email_to_share_with: str, **kwargs ): r"""Share a workflow with other users. --- post: summary: Share a workflow with other users. description: >- This resource allows to share a workflow with other users. operationId: share_workflow produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. required: true type: string - name: share_details in: body description: JSON object with details of the share. required: true schema: type: object properties: user_email_to_share_with: type: string description: User to share the workflow with. message: type: string description: Optional. Message to include when sharing the workflow. valid_until: type: string description: Optional. Date when access to the workflow will expire (format YYYY-MM-DD). required: [user_email_to_share_with] responses: 200: description: >- Request succeeded. The workflow has been shared with the user. schema: type: object properties: message: type: string workflow_id: type: string workflow_name: type: string examples: application/json: { "message": "The workflow has been shared with the user.", "workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac", "workflow_name": "mytest.1" } 400: description: >- Request failed. The incoming data seems malformed. 404: description: >- Request failed. Workflow does not exist or user does not exist. examples: application/json: { "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist", } 409: description: >- Request failed. The workflow is already shared with the user. examples: application/json: { "message": "The workflow is already shared with the user.", } 500: description: >- Request failed. Internal controller error. examples: application/json: { "message": "Internal controller error.", } """ message = kwargs.get("message") valid_until = kwargs.get("valid_until") try: sharer = Session.query(User).filter(User.id_ == user).first() if not sharer: return ( jsonify({"message": f"User with id '{user}' does not exist."}), 404, ) if sharer.email == user_email_to_share_with: raise ValueError("Unable to share a workflow with yourself.") user_to_share_with = ( Session.query(User) .filter(User.email == user_email_to_share_with) .one_or_none() ) if not user_to_share_with: return ( jsonify( { "message": f"User with email '{user_email_to_share_with}' does not exist." } ), 404, ) if valid_until and valid_until < datetime.date.today(): raise ValueError("The 'valid_until' date cannot be in the past.") workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, sharer.id_) try: Session.add( UserWorkflow( user_id=user_to_share_with.id_, workflow_id=workflow.id_, message=message, valid_until=valid_until, ) ) Session.commit() except IntegrityError: Session.rollback() return ( jsonify( { "message": f"{workflow.get_full_workflow_name()} is already shared with {user_email_to_share_with}." } ), 409, ) response = { "message": "The workflow has been shared with the user.", "workflow_id": workflow.id_, "workflow_name": workflow.get_full_workflow_name(), } return jsonify(response), 200 except ValueError as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 400 except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows/<workflow_id_or_name>/unshare", methods=["POST"]) @use_kwargs( { "user": fields.Str(required=True), "user_email_to_unshare_with": fields.Str(required=True), }, location="query", unknown=marshmallow.EXCLUDE, ) def unshare_workflow( workflow_id_or_name: str, user: str, user_email_to_unshare_with: str ): r"""Unshare a workflow with other users. --- post: summary: Unshare a workflow with other users. description: >- This resource allows to unshare a workflow with other users. operationId: unshare_workflow produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. required: true type: string - name: user_email_to_unshare_with in: query description: >- Required. User to unshare the workflow with. required: true type: string responses: 200: description: >- Request succeeded. The workflow has been unshared with the user. schema: type: object properties: message: type: string workflow_id: type: string workflow_name: type: string examples: application/json: { "message": "The workflow has been unsahred with the user.", "workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac", "workflow_name": "mytest.1" } 400: description: >- Request failed. The incoming data specification seems malformed. schema: type: object properties: message: type: string examples: application/json: { "message": "Malformed request.", } 403: description: >- Request failed. User is not allowed to unshare the workflow. schema: type: object properties: message: type: string examples: application/json: { "message": "User is not allowed to unshare the workflow." } 404: description: >- Request failed. Workflow does not exist or user does not exist. schema: type: object properties: message: type: string examples: application/json: { "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist" } 409: description: >- Request failed. The workflow is not shared with the user. schema: type: object properties: message: type: string examples: application/json: { "message": "The workflow is not shared with the user." } 500: description: >- Request failed. Internal controller error. schema: type: object properties: message: type: string examples: application/json: { "message": "Internal controller error." } """ try: sharer = Session.query(User).filter(User.id_ == user).first() if not sharer: return ( jsonify({"message": f"User with id '{sharer}' does not exist."}), 404, ) if sharer.email == user_email_to_unshare_with: raise ValueError("Unable to unshare a workflow with yourself.") user_to_unshare_with = ( Session.query(User).filter(User.email == user_email_to_unshare_with).first() ) if not user_to_unshare_with: message = f"User with email '{user_email_to_unshare_with}' does not exist." return jsonify({"message": message}), 404 workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, str(sharer.id_)) existing_share = ( Session.query(UserWorkflow) .filter_by(user_id=user_to_unshare_with.id_, workflow_id=workflow.id_) .first() ) if not existing_share: message = f"{workflow.get_full_workflow_name()} is not shared with {user_email_to_unshare_with}." return (jsonify({"message": message}), 409) Session.delete(existing_share) Session.commit() response = { "message": "The workflow has been unshared with the user.", "workflow_id": workflow.id_, "workflow_name": workflow.get_full_workflow_name(), } return jsonify(response), 200 except ValueError as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 400 except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500
[docs] @blueprint.route("/workflows/<workflow_id_or_name>/share-status", methods=["GET"]) @use_kwargs( {"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE ) def get_workflow_share_status( workflow_id_or_name: str, user: str, ): r"""Get the share status of a workflow. --- get: summary: Get the share status of a workflow. description: >- This resource returns the share status of a given workflow. operationId: get_workflow_share_status produces: - application/json parameters: - name: user in: query description: Required. UUID of workflow owner. required: true type: string - name: workflow_id_or_name in: path description: Required. Workflow UUID or name. required: true type: string responses: 200: description: >- Request succeeded. The response contains the share status of the workflow. schema: type: object properties: workflow_id: type: string workflow_name: type: string shared_with: type: array items: type: object properties: user_email: type: string valid_until: type: string x-nullable: true examples: application/json: { "workflow_id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", "workflow_name": "mytest.1", "shared_with": [ { "user_email": "bob@example.org", "valid_until": "2022-11-24T23:59:59" } ] } 404: description: >- Request failed. Workflow does not exist. schema: type: object properties: message: type: string examples: application/json: { "message": "Workflow mytest.1 does not exist." } 500: description: >- Request failed. Internal server error. schema: type: object properties: message: type: string examples: application/json: { "message": "Something went wrong." } """ try: workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user) shared_with = ( Session.query(UserWorkflow) .filter_by(workflow_id=workflow.id_) .join(User, User.id_ == UserWorkflow.user_id) .with_entities(User.email, UserWorkflow.valid_until) .all() ) response = { "workflow_id": workflow.id_, "workflow_name": workflow.get_full_workflow_name(), "shared_with": [ { "user_email": share.email, "valid_until": ( share.valid_until.strftime(WORKFLOW_TIME_FORMAT) if share.valid_until else None ), } for share in shared_with ], } return jsonify(response), 200 except ValueError as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 404 except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500