diff --git a/docker-wrappers/OmicsIntegrator2/0001-disable-graph-exports.patch b/docker-wrappers/OmicsIntegrator2/0001-disable-graph-exports.patch new file mode 100644 index 000000000..b482261b4 --- /dev/null +++ b/docker-wrappers/OmicsIntegrator2/0001-disable-graph-exports.patch @@ -0,0 +1,26 @@ +From 5dc0e69fa3d1049ae8e1d8f51859335910245ad7 Mon Sep 17 00:00:00 2001 +From: "Tristan F.-R." +Date: Mon, 26 May 2025 09:52:34 -0700 +Subject: [PATCH] fix: disable graph exports + +this allows OI2 to work offline; plus, SPRAS already has graph visualizers. +--- + src/__main__.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/__main__.py b/src/__main__.py +index 49ef402..b7afbca 100644 +--- a/src/__main__.py ++++ b/src/__main__.py +@@ -80,7 +80,7 @@ def main(): + forest, augmented_forest = graph.output_forest_as_networkx(vertex_indices, edge_indices) + + #oi.output_networkx_graph_as_graphml_for_cytoscape(augmented_forest, args.output_dir) +- oi.output_networkx_graph_as_interactive_html(augmented_forest, args.output_dir, args.filename+'.html') ++ # oi.output_networkx_graph_as_interactive_html(augmented_forest, args.output_dir, args.filename+'.html') + augmented_forest_df = oi.get_networkx_graph_as_dataframe_of_edges(augmented_forest) + output_dataframe_to_tsv(augmented_forest_df, args.output_dir, args.filename+'.tsv') + +-- +2.47.0 + diff --git a/docker-wrappers/OmicsIntegrator2/Dockerfile b/docker-wrappers/OmicsIntegrator2/Dockerfile index e0df2d2db..67ceb23a2 100644 --- a/docker-wrappers/OmicsIntegrator2/Dockerfile +++ b/docker-wrappers/OmicsIntegrator2/Dockerfile @@ -2,5 +2,14 @@ # https://github.com/fraenkel-lab/OmicsIntegrator2 FROM continuumio/miniconda3:4.9.2 +COPY 0001-disable-graph-exports.patch . + +RUN git clone https://github.com/agitter/OmicsIntegrator2 && \ + cd OmicsIntegrator2 && \ + git reset --hard 568f170eae388e42e923c478ac9f3308b487760b && \ + git config user.email "email@example.com" && \ + git config user.name "Non-existent User" && \ + git am /0001-disable-graph-exports.patch + COPY environment.yml . RUN conda env update --name base --file environment.yml --prune diff --git a/docker-wrappers/OmicsIntegrator2/README.md b/docker-wrappers/OmicsIntegrator2/README.md index c7c7d11f9..881ce2d1f 100644 --- a/docker-wrappers/OmicsIntegrator2/README.md +++ b/docker-wrappers/OmicsIntegrator2/README.md @@ -25,6 +25,7 @@ The Docker wrapper can be tested with `pytest`. ## Versions: - v1: Created a named conda environment in the container and used `ENTRYPOINT` to execute commands inside that environment. Not compatible with Singularity. - v2: Used the environment file to update the base conda environment so the `ENTRYPOINT` command was no longer needed. Compatible with Singularity. +- v3: Patch to work offline by never running `output_networkx_graph_as_interactive_html` ([#226](https://github.com/Reed-CompBio/spras/pull/226)) ## TODO - Attribute https://github.com/fraenkel-lab/OmicsIntegrator2 diff --git a/docker-wrappers/OmicsIntegrator2/environment.yml b/docker-wrappers/OmicsIntegrator2/environment.yml index ceb19a546..09e8d1b93 100644 --- a/docker-wrappers/OmicsIntegrator2/environment.yml +++ b/docker-wrappers/OmicsIntegrator2/environment.yml @@ -18,4 +18,4 @@ dependencies: - pcst_fast==1.0.7 - goenrich==1.7.0 - axial==0.1.10 - - git+https://github.com/agitter/OmicsIntegrator2@568f170eae388e42e923c478ac9f3308b487760b + - git+file:///OmicsIntegrator2 diff --git a/docs/contributing/patching.rst b/docs/contributing/patching.rst new file mode 100644 index 000000000..7dc617e73 --- /dev/null +++ b/docs/contributing/patching.rst @@ -0,0 +1,23 @@ +Patching Algorithms +=================== + +Some wrapped algorithms require extra fixes inside their code. For permissively licensed algorithms, +we use ``.patch`` files generated from ``git format-patch``. + +To create patch files using ``git format-patch`` (assuming your wrapped algorithm is in a git repository): + +#. Clone the repository locally. +#. Commit the changes you want to make (with good commit messages and descriptions). + + * Distinct changes should be made in different commits to make patch files easy to read + * For removing code, we prefer to comment out code instead of removing it, to make potential stacktraces less confusing for end users. + +#. Run ``git format-patch HEAD~[N]`` where ``N`` is the number of commits you made. + +To use ``.patch`` files in ``Dockerfiles``, we create a fake user for ``git`` and apply the patch files using ``git am``: + +.. code:: shell + + git config user.email "email@example.com" + git config user.name "Non-existent User" + git am /0001-my-patch.patch diff --git a/docs/index.rst b/docs/index.rst index d12af3157..19e30b0b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,6 +55,7 @@ methods (PRMs) to omics data. contributing/index contributing/maintain + contributing/patching Indices and tables ================== diff --git a/spras/containers.py b/spras/containers.py index 88a02ec58..62782fb16 100644 --- a/spras/containers.py +++ b/spras/containers.py @@ -132,7 +132,7 @@ def env_to_items(environment: dict[str, str]) -> Iterator[str]: # TODO consider a better default environment variable # Follow docker-py's naming conventions (https://docker-py.readthedocs.io/en/stable/containers.html) # Technically the argument is an image, not a container, but we use container here. -def run_container(framework: str, container_suffix: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, out_dir: str | os.PathLike, environment: Optional[dict[str, str]] = None): +def run_container(framework: str, container_suffix: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, out_dir: str | os.PathLike, environment: Optional[dict[str, str]] = None, network_disabled = False): """ Runs a command in the container using Singularity or Docker @param framework: singularity or docker @@ -140,15 +140,16 @@ def run_container(framework: str, container_suffix: str, command: List[str], vol @param command: command to run in the container @param volumes: a list of volumes to mount where each item is a (source, destination) tuple @param working_dir: the working directory in the container - @param environment: environment variables to set in the container @param out_dir: output directory for the rule's artifacts. Only passed into run_container_singularity for the purpose of profiling. + @param environment: environment variables to set in the container + @param network_disabled: Disables the network on the container. Only works for docker for now. This acts as a 'runtime assertion' that a container works w/o networking. @return: output from Singularity execute or Docker run """ normalized_framework = framework.casefold() container = config.config.container_prefix + "/" + container_suffix if normalized_framework == 'docker': - return run_container_docker(container, command, volumes, working_dir, environment) + return run_container_docker(container, command, volumes, working_dir, environment, network_disabled) elif normalized_framework == 'singularity' or normalized_framework == "apptainer": return run_container_singularity(container, command, volumes, working_dir, out_dir, environment) elif normalized_framework == 'dsub': @@ -156,7 +157,7 @@ def run_container(framework: str, container_suffix: str, command: List[str], vol else: raise ValueError(f'{framework} is not a recognized container framework. Choose "docker", "dsub", or "singularity".') -def run_container_and_log(name: str, framework: str, container_suffix: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, out_dir: str | os.PathLike, environment: Optional[dict[str, str]] = None): +def run_container_and_log(name: str, framework: str, container_suffix: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, out_dir: str | os.PathLike, environment: Optional[dict[str, str]] = None, network_disabled=False): """ Runs a command in the container using Singularity or Docker with associated pretty printed messages. @param name: the display name of the running container for logging purposes @@ -166,6 +167,7 @@ def run_container_and_log(name: str, framework: str, container_suffix: str, comm @param volumes: a list of volumes to mount where each item is a (source, destination) tuple @param working_dir: the working directory in the container @param environment: environment variables to set in the container + @param network_disabled: Disables the network on the container. Only works for docker for now. This acts as a 'runtime assertion' that a container works w/o networking. @return: output from Singularity execute or Docker run """ if not environment: @@ -173,7 +175,7 @@ def run_container_and_log(name: str, framework: str, container_suffix: str, comm print('Running {} on container framework "{}" on env {} with command: {}'.format(name, framework, list(env_to_items(environment)), ' '.join(command)), flush=True) try: - out = run_container(framework=framework, container_suffix=container_suffix, command=command, volumes=volumes, working_dir=working_dir, out_dir=out_dir, environment=environment) + out = run_container(framework=framework, container_suffix=container_suffix, command=command, volumes=volumes, working_dir=working_dir, out_dir=out_dir, environment=environment, network_disabled=network_disabled) if out is not None: if isinstance(out, list): out = ''.join(out) @@ -199,7 +201,7 @@ def run_container_and_log(name: str, framework: str, container_suffix: str, comm raise err # TODO any issue with creating a new client each time inside this function? -def run_container_docker(container: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, environment: Optional[dict[str, str]] = None): +def run_container_docker(container: str, command: List[str], volumes: List[Tuple[PurePath, PurePath]], working_dir: str, environment: Optional[dict[str, str]] = None, network_disabled=False): """ Runs a command in the container using Docker. Attempts to automatically correct file owner and group for new files created by the container, setting them to the @@ -244,6 +246,7 @@ def run_container_docker(container: str, command: List[str], volumes: List[Tuple stderr=True, volumes=bind_paths, working_dir=working_dir, + network_disabled=network_disabled, environment=environment).decode('utf-8') # TODO does this cleanup need to still run even if there was an error in the above run command? @@ -278,6 +281,7 @@ def run_container_docker(container: str, command: List[str], volumes: List[Tuple stderr=True, volumes=bind_paths, working_dir=working_dir, + network_disabled=network_disabled, environment=environment).decode('utf-8') # Raised on non-Unix systems diff --git a/spras/omicsintegrator2.py b/spras/omicsintegrator2.py index b038baec2..8b97fa2d1 100644 --- a/spras/omicsintegrator2.py +++ b/spras/omicsintegrator2.py @@ -128,14 +128,15 @@ def run(edges=None, prizes=None, output_file=None, w=None, b=None, g=None, noise if seed is not None: command.extend(['--seed', str(seed)]) - container_suffix = "omics-integrator-2:v2" + container_suffix = "omics-integrator-2:v3" run_container_and_log('Omics Integrator 2', container_framework, container_suffix, command, volumes, work_dir, - out_dir) + out_dir, + network_disabled=True) # TODO do we want to retain other output files? # TODO if deleting other output files, write them all to a tmp directory and copy