Commit 1a3fac87 authored by Duggan, John's avatar Duggan, John
Browse files

Overhaul input file management

parent a969382b
Loading
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -25,6 +25,10 @@ class IPSFastranTool(BasicTool):
        self.store = connection.get_data_store("ips_fastran")

    def prepare_tool(self) -> Tuple[Tool, Parameters]:
        # TODO: This method needs to be rewritten to support the SF API for use when not connected to Galaxy.
        # Need to talk with others about how they want to authenticate (access token provided or generated) before
        # doing this.

        # Prepare file ingestion into Galaxy
        self.inputs_dataset = Dataset(name="inputs.zip")
        with zipfile.ZipFile("input.zip", "w") as zip_obj:
@@ -32,7 +36,7 @@ class IPSFastranTool(BasicTool):
                zip_obj.writestr(file["name"], file["content"].encode("latin1"))
        self.inputs_dataset.path = "input.zip"
        self.config_dataset = Dataset(name="config.txt")
        self.config_dataset.set_content(self.model.config.file_contents)
        # self.config_dataset.set_content(self.model.config.file_contents)

        # Create the tool instance
        tool_params = Parameters()
+22 −7
Original line number Diff line number Diff line
"""Module for the main model."""

import json
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, List

@@ -23,7 +25,6 @@ class Config(BaseModel):
    input_files: List[Dict[str, Any]] = Field(default=[], title="Input Files")
    shot_number: str = Field(default="000001", title="Shot Number")
    time_id: str = Field(default="00001", title="Time ID")
    file_contents: str = Field(default="")
    result_file: str = Field(default="", title="File to Plot")
    result_files: List[str] = Field(default=[])

@@ -31,6 +32,7 @@ class Config(BaseModel):
class ResourceParameters(BaseModel):
    """Contains resource parameters for running the job."""

    access_token: str = Field(default="", title="Superfacility API Access Token")
    partition: str = Field(default="debug", title="Partition")
    number_of_nodes: int = Field(default=1, ge=1, le=2, title="Number of Nodes")
    time_limit: int = Field(default=10, ge=1, le=10080, title="Time Limit (minutes)")
@@ -51,19 +53,32 @@ class MainModel:
        self.plot_json = PlotJSON()
        self.resource_params = ResourceParameters()

    def get_config_file(self) -> str:
        return self.config.file_contents
    def download_files(self) -> bytes:
        archive = BytesIO()
        with zipfile.ZipFile(archive, "w") as zip_obj:
            for file in self.config.input_files:
                zip_obj.writestr(file["relative_path"], file["content"])
        archive.seek(0)

    def set_config(self, json_data: str) -> None:
        self.config.file_contents = json_data
        return archive.read()

    def set_files(self, files: List[Dict[str, Any]]) -> None:
    def set_file_contents(self, index: int, json_data: str) -> None:
        self.config.input_files[index]["content"] = json_data

    def set_file_path(self, index: int, path: str) -> None:
        self.config.input_files[index]["name"] = path.split("/")[-1]
        self.config.input_files[index]["relative_path"] = path

    def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None:
        if not files:
            self.config.input_files = []

        for file in files:
        for index, file in enumerate(files):
            relative_path = relative_paths[index]

            decoded_file = file.copy()
            decoded_file["content"] = decoded_file["content"].decode("latin1")
            decoded_file["relative_path"] = "/".join(relative_path.split("/")[1:])
            self.config.input_files.append(decoded_file)

    def set_plot(self, json_data: str) -> None:
+3 −0
Original line number Diff line number Diff line
@@ -43,3 +43,6 @@ class ExecutionViewModel:

    def store_factory(self) -> str:
        return "ips_fastran"

    def download_files(self) -> bytes:
        return self.model.download_files()
+28 −8
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ class ViewState(BaseModel):
    active_tab: str = Field(default="0")
    errors: List[str] = Field(default=[])
    results_disabled: bool = Field(default=False)
    editor_index: int = Field(default=0)
    editor_path: str = Field(default="", title="File Path")
    editor_content: str = Field(default="")


class MainViewModel:
@@ -42,7 +45,7 @@ class MainViewModel:
            self.model.resource_params, callback_after_update=self.on_change_resource_params
        )
        self.plot_json_bind = binding.new_bind()
        self.view_state_bind = binding.new_bind(self.view_state)
        self.view_state_bind = binding.new_bind(self.view_state, callback_after_update=self.on_change_view_state)

        # Signals to process events from other view models
        self.execution_signal = signal(get_signal_id("ips_fastran", Signal.TOOL_COMMAND))
@@ -50,9 +53,6 @@ class MainViewModel:
        self.completion_signal = signal("ips_fastran_complete")
        self.completion_signal.connect(self.on_completion, weak=False)

    def get_config_file(self) -> str:
        return self.model.get_config_file()

    def get_figure(self) -> Figure:
        return self.figure

@@ -60,14 +60,15 @@ class MainViewModel:
        self.config_bind.update_in_view(self.model.config)
        self.plot_json_bind.update_in_view(self.model.plot_json.model_dump_json(indent=2))

    def on_change_config_file(self, json_data: str) -> None:
    def on_change_file(self, json_data: str) -> None:
        # Monaco fires input events with internal data that need to be ignored.
        if "_vts" in json_data:
            return

        self.view_state.errors = []
        try:
            self.model.set_config(json_data)
            self.view_state.editor_content = json_data
            self.model.set_file_contents(self.view_state.editor_index, json_data)
        except ValidationError as e:
            for error in e.errors():
                msg = ""
@@ -98,6 +99,14 @@ class MainViewModel:
            # You can take actions in Python when specific fields are changed here.
            pass

    def on_change_view_state(self, results: Dict[str, Any]) -> None:
        updated = results.get("updated", [])
        for update in updated:
            match update:
                case "editor_path":
                    self.model.set_file_path(self.view_state.editor_index, self.view_state.editor_path)
                    self.config_bind.update_in_view(self.model.config)

    def on_completion(self, _sender: Any) -> None:
        self.config_bind.update_in_view(self.model.config)

@@ -112,10 +121,21 @@ class MainViewModel:
            self.view_state.active_tab = "2"
            self.view_state_bind.update_in_view(self.view_state)

    def set_files(self, files: List[Dict[str, Any]]) -> None:
        self.model.set_files(files)
    def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None:
        self.model.set_files(files, relative_paths)
        self.config_bind.update_in_view(self.model.config)

    def edit_file(self, index: int) -> None:
        if index >= len(self.model.config.input_files):
            return

        file = self.model.config.input_files[index]
        self.view_state.editor_index = index
        self.view_state.editor_path = file["relative_path"]
        self.view_state.editor_content = file["content"]

        self.view_state_bind.update_in_view(self.view_state)

    def update_figure(self) -> None:
        plotter = PlotFastran(self.figure, self.model.plot_json.input_params)
        plotter.load_fastran(Path(self.model.config.result_file))
+22 −2
Original line number Diff line number Diff line
"""Module for the Execution panel."""

from nova.trame.view.components import ExecutionButtons
from nova.trame.view.layouts import HBoxLayout
from trame.widgets import vuetify3 as vuetify
from trame_server.core import Server

from ips_fastran_gui.app.view_models.execution import ExecutionViewModel


class ExecutionPanel:
    """Execution panel view class. Responsible for displaying execution buttons."""

    def __init__(self) -> None:
    def __init__(self, server: Server, view_model: ExecutionViewModel) -> None:
        self.server = server
        self.view_model = view_model

        self.create_ui()

    def create_ui(self) -> None:
        @self.server.controller.trigger("download_files")
        def download_files() -> bytes:
            return self.view_model.download_files()

        with HBoxLayout(gap="2em", halign="center", valign="center"):
            vuetify.VBtn(
                "Download Files",
                prepend_icon="mdi-file-download",
                click="utils.download('ips_fastran_files.zip', trigger('download_files'), 'application/zip')",
            )
            vuetify.VDivider(classes="my-2", vertical=True)
            with HBoxLayout():
                ExecutionButtons("ips_fastran", download_btn=True)
Loading