From 398dc667f7e3bfa937e47c59a12e1f2c4992b95f Mon Sep 17 00:00:00 2001 From: VasuJ <145879890+vjaganat90@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:12:51 -0400 Subject: [PATCH] add required files for rest api (core) --- pyproject.toml | 4 + src/sophios/api/http/__init__.py | 0 src/sophios/api/http/restapi.py | 175 +++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 src/sophios/api/http/__init__.py create mode 100644 src/sophios/api/http/restapi.py diff --git a/pyproject.toml b/pyproject.toml index 68fad553..bc63ed0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,11 +33,15 @@ dependencies = [ # See https://github.com/common-workflow-language/cwl-utils/releases/ "typeguard", "pydantic>=2.6", + "pydantic-settings", "docker", # FYI also need uidmap to run podman rootless "podman", # We are using the official release for these packages for now "toil[cwl]", + "fastapi", + "python-jose", + "uvicorn" ] [project.readme] diff --git a/src/sophios/api/http/__init__.py b/src/sophios/api/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sophios/api/http/restapi.py b/src/sophios/api/http/restapi.py new file mode 100644 index 00000000..82f04dbe --- /dev/null +++ b/src/sophios/api/http/restapi.py @@ -0,0 +1,175 @@ +from pathlib import Path +import argparse +import yaml + +import uvicorn +from fastapi import FastAPI, Request, status +from fastapi.middleware.cors import CORSMiddleware + +from sophios import __version__, compiler +from sophios import run_local, input_output +from sophios.utils_graphs import get_graph_reps +from sophios.utils_yaml import wic_loader +from sophios import utils_cwl +from sophios.cli import get_args +from sophios.wic_types import CompilerInfo, Json, Tool, Tools, StepId, YamlTree, Cwl +# from wic.api.utils import converter +# from .auth.auth import authenticate +# from utils import converter + + +# helper functions + + +def remove_dot_dollar(tree: Cwl) -> Cwl: + """Removes . and $ from dictionary keys, e.g. $namespaces and $schemas. Otherwise, you will get + {'error': {'statusCode': 500, 'message': 'Internal Server Error'}} + This is due to MongoDB: + See https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names + Args: + tree (Cwl): A Cwl document + Returns: + Cwl: A Cwl document with . and $ removed from $namespaces and $schemas + """ + tree_str = str(yaml.dump(tree, sort_keys=False, line_break='\n', indent=2)) + tree_str_no_dd = tree_str.replace('$namespaces', 'namespaces').replace( + '$schemas', 'schemas').replace('.wic', '_wic') + tree_no_dd: Cwl = yaml.load(tree_str_no_dd, Loader=wic_loader()) # This effectively copies tree + return tree_no_dd + + +def get_yaml_tree(req: Json) -> Json: + """ + Get the Sophios yaml tree from incoming JSON + Args: + req (JSON): A raw JSON content of incoming JSON object + Returns: + Cwl: A Cwl document with . and $ removed from $namespaces and $schemas + """ + wkflw_name = "generic_workflow" + # args = converter.get_args(wkflw_name) + # yaml_tree_json: Json = converter.wfb_to_wic(req) + yaml_tree_json: Json = {} + return yaml_tree_json + + +def run_workflow(compiler_info: CompilerInfo, args: argparse.Namespace) -> int: + """ + Get the Sophios yaml tree from incoming JSON + Args: + req (JSON): A raw JSON content of incoming JSON object + Returns: + Cwl: A Cwl document with . and $ removed from $namespaces and $schemas + """ + # ========= WRITE OUT ======================= + input_output.write_to_disk(compiler_info.rose, Path('autogenerated/'), relative_run_path=True) + # ======== TEST RUN ========================= + retval = run_local.run_local(args, compiler_info.rose, args.cachedir, 'cwltool', False) + return retval + + +app = FastAPI() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/", status_code=status.HTTP_200_OK) +# @authenticate +async def root(request: Request) -> Json: + """The api has 1 route: compile + + Returns: + Dict[str, str]: {"message": "The api has 1 route: compile"} + """ + return {"message": "The api has 1 route: compile"} + + +@app.post("/compile") +# @authenticate +async def compile_wf(request: Request) -> Json: + """The compile route compiles the json object from http request object built elsewhere + + Args: + request (Request): request object built elsewhere + + Returns: + compute_workflow (JSON): workflow json object ready to submit to compute + """ + print('---------- Compile Workflow! ---------') + # ========= PROCESS REQUEST OBJECT ========== + req: Json = await request.json() + wkflw_name = "generic_workflow" + args = get_args(wkflw_name) + # yaml_tree_json: Json = converter.wfb_to_wic(req) + # yaml_tree_json = get_yaml_tree(req) + + workflow_temp = req["templates"]["ict"]["cwlScript"] # Assume dict form + workflow_can = utils_cwl.desugar_into_canonical_normal_form(workflow_temp) + + # ========= BUILD WIC COMPILE INPUT ========= + # ict_plugins = req["templates"] + tools_cwl: Tools = {StepId(content["id"], "global"): + Tool(".", content["run"]) for content in workflow_can["steps"]} + # run tag will have the actual CommandLineTool + wic_obj = {'wic': workflow_can.get('wic', {})} + plugin_ns = wic_obj['wic'].get('namespace', 'global') + + graph = get_graph_reps(wkflw_name) + yaml_tree: YamlTree = YamlTree(StepId(wkflw_name, plugin_ns), workflow_can) + + # ========= COMPILE WORKFLOW ================ + compiler_info: CompilerInfo = compiler.compile_workflow(yaml_tree, args, [], [graph], {}, {}, {}, {}, + tools_cwl, True, relative_run_path=True, testing=False) + + # =========== OPTIONAL RUN ============== + print('---------- Run Workflow locally! ---------') + retval = run_workflow(compiler_info, args) + + compute_workflow: Json = {} + compute_workflow.get('retval', str(retval)) + return compute_workflow + + +if __name__ == '__main__': + uvicorn.run(app, host="0.0.0.0", port=3000) + + +# # ========= PROCESS COMPILED OBJECT ========= + # sub_node_data: NodeData = compiler_info.rose.data + # yaml_stem = sub_node_data.name + # cwl_tree = sub_node_data.compiled_cwl + # yaml_inputs = sub_node_data.workflow_inputs_file + + # # ======== OUTPUT PROCESSING ================ + # cwl_tree_no_dd = remove_dot_dollar(cwl_tree) + # yaml_inputs_no_dd = remove_dot_dollar(yaml_inputs) + + # # Convert the compiled yaml file to json for labshare Compute. + # cwl_tree_run = copy.deepcopy(cwl_tree_no_dd) + # for step_key in cwl_tree['steps']: + # step_name_i = step_key + # step_name_i = step_name_i.replace('.yml', '_yml') # Due to calling remove_dot_dollar above + # step_name = '__'.join(step_key.split('__')[3:]) # Remove prefix + + # # Get step CWL from templates + # run_val = next((tval['cwlScript'] + # for _, tval in ict_plugins.items() if step_name == tval['name']), None) + # cwl_tree_run['steps'][step_name_i]['run'] = run_val + + # TODO: set name and driver in workflow builder ui + # compute_workflow: Json = {} + # compute_workflow = { + # "name": yaml_stem, + # "driver": "argo", + # # "driver": "cwltool", + # "cwlJobInputs": yaml_inputs_no_dd, + # **cwl_tree_run + # }