Loading pixi.lock +186 −159 File changed.Preview size limit exceeded, changes collapsed. Show changes pyproject.toml +1 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ ips-fastran-gui = { path = ".", editable = true } hatch = "*" gitpython = ">=3.1.40,<4" python-gitlab = ">=5.6.0,<6" nova-trame = "==1.3.2-dev0" nova-trame = "*" trame-datagrid = ">=0.2.2" trame-code = ">=1.0.2" nova-galaxy = ">=0.11.1" Loading src/ips_fastran_gui/app/models/main_model.py +65 −22 Original line number Diff line number Diff line """Module for the main model.""" import json import os import zipfile from enum import Enum from io import BytesIO Loading Loading @@ -76,11 +77,17 @@ class MainModel: self.file_tree: Dict[str, Any] = {} def _download_files(self, zip_obj: zipfile.ZipFile, files: List[Dict[str, Any]]) -> None: for file in files: if "children" in file: self._download_files(zip_obj, file["children"]) else: zip_obj.writestr(file["relative_path"], file["content"]) 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"]) self._download_files(zip_obj, self.config.input_files) archive.seek(0) return archive.read() Loading @@ -94,32 +101,68 @@ class MainModel: return file def set_file_contents(self, index: int, json_data: str) -> None: self.config.input_files[index]["content"] = json_data def read_files(self, file_path: str) -> None: self.file_tree = {} for dirpath, _, filenames in os.walk(file_path): relative_path = os.path.relpath(dirpath, file_path) if relative_path == ".": relative_path = "" parts = [] else: parts = relative_path.split("/") 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 current_level = self.file_tree for part in parts: if part not in current_level: current_level[part] = {} current_level = current_level[part] def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None: if not files: self.config.input_files = [] for file in filenames: try: path = os.path.join(dirpath, file) with open(path, "r") as file_obj: current_level[file] = { "content": file_obj.read(), "name": file, "path": path, "relative_path": os.path.join(relative_path, file), } except Exception: pass self.file_tree = {} for index, file in enumerate(files): relative_path = relative_paths[index] parts = relative_path.split("/")[1:] self.config.input_files = self.set_files_from_tree(self.file_tree) decoded_file = file.copy() decoded_file["content"] = decoded_file["content"].decode("latin1") decoded_file["path"] = "/".join(parts) def set_file_contents(self, path: str, relative_path: str, json_data: str) -> None: parts = relative_path.split("/") if relative_path != "." else [] current_level = self.file_tree for part in parts: if part: current_level = current_level[part] current_level["content"] = json_data with open(path, "w") as file_obj: file_obj.write(json_data) self.config.input_files = self.set_files_from_tree(self.file_tree) def set_file_path(self, old_path: str, new_path: str) -> None: current_level = self.file_tree parts = old_path.split("/") for part in parts[:-1]: if part not in current_level: current_level[part] = {} current_level = current_level[part] current_level[parts[-1]] = decoded_file file = current_level.pop(parts[-1]) file["path"] = "" parts = new_path.split("/") current_level = self.file_tree for part in parts[:-1]: file["path"] += f"{part}/" current_level = current_level[part] if "path" not in current_level[parts[-1]]: file["path"] += f"{parts[-1]}/" current_level = current_level[parts[-1]] file["path"] += file["name"] current_level[file["name"]] = file self.config.input_files = self.set_files_from_tree(self.file_tree) Loading src/ips_fastran_gui/app/view_models/main_view_model.py +14 −10 Original line number Diff line number Diff line """Module for the main ViewModel.""" import os from pathlib import Path from typing import Any, Dict, List Loading @@ -19,9 +20,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") input_file_path: str = Field(default=os.getenv("HOME", os.getcwd()), title="Path to Input Files") editor_content: str = Field(default="") editor_path: str = Field(default="") class MainViewModel: Loading Loading @@ -68,7 +69,11 @@ class MainViewModel: self.view_state.errors = [] try: self.view_state.editor_content = json_data # self.model.set_file_contents(self.view_state.editor_index, json_data) self.model.set_file_contents( self.view_state.editor_path, os.path.relpath(self.view_state.editor_path, self.view_state.input_file_path), json_data, ) except ValidationError as e: for error in e.errors(): msg = "" Loading Loading @@ -103,8 +108,8 @@ class MainViewModel: 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) case "input_file_path": self.model.read_files(self.view_state.input_file_path) self.config_bind.update_in_view(self.model.config) def on_completion(self, _sender: Any) -> None: Loading @@ -121,18 +126,14 @@ 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]], 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, path: List[str]) -> None: if not path: return file = self.model.get_file_from_path(path[0]) self.view_state.editor_path = file["path"] self.view_state.editor_content = file["content"] self.view_state.editor_path = file["path"] self.view_state_bind.update_in_view(self.view_state) Loading @@ -142,3 +143,6 @@ class MainViewModel: plotter.set_page() self.figure_bind.update_in_view(None) def update_path(self, old_path: str, new_path: str) -> None: self.model.set_file_path(old_path, new_path) src/ips_fastran_gui/app/views/tabs/config_tab.py +30 −27 Original line number Diff line number Diff line """Module for the Config tab.""" from nova.trame.view.components import InputField import os from nova.trame.view.components import InputField, RemoteFileInput from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout from trame.app import get_server from trame.widgets import code from trame.widgets import vuetify3 as vuetify Loading @@ -12,6 +15,7 @@ class ConfigTab: """Config view class.""" def __init__(self, view_model: MainViewModel) -> None: self.server = get_server(None, client_type="vue3") self.view_model = view_model self.create_ui() Loading @@ -22,44 +26,39 @@ class ConfigTab: InputField(v_model="config.time_id") with HBoxLayout(): InputField( v_model="config.input_files", active=True, id="file-upload", multiple=True, raw_attrs=["webkitdirectory"], type="file", update_modelValue=( self.view_model.set_files, # TODO: I would like to better understand why webkitRelativePath is not available in the v-model # by default. ( "[" " config.input_files," " Array.from(window.document.getElementById('file-upload').files).map(" " file => file.webkitRelativePath" ")]" ), ), RemoteFileInput( v_model="state.input_file_path", allow_files=False, allow_folders=True, # TODO: base_paths should be set differently if running on a cluster base_paths=[os.getenv("HOME", os.getcwd())], ) with GridLayout(classes="mb-2", columns=3, stretch=True): with VBoxLayout(): with VBoxLayout(classes="pl-2"): vuetify.VListSubheader("Uploaded Files") vuetify.VTreeview( with vuetify.VTreeview( v_if="config.input_files.length > 0", activatable=True, active_strategy="single-leaf", items=("config.input_files",), item_title="name", item_value="path", update_activated=(self.view_model.edit_file, "[$event]"), ): with vuetify.Template(v_slot_title="{ item }"): vuetify.VListItemTitle( "{{ item.name }}", raw_attrs=[ '''draggable="true"''', '''@dragenter.prevent=""''', '''@dragover.prevent=""''', '''@dragstart="window.ips_drag_path = item.path"''', ], __events=["drop"], drop="trigger('on_drop', [window.ips_drag_path, item.path])", ) vuetify.VListItem("No files available.", v_else=True) with VBoxLayout(column_span=2, stretch=True): with VBoxLayout(): InputField("state.editor_path") code.Editor( ref="input_config", model_value=("state.editor_content",), Loading @@ -67,3 +66,7 @@ class ConfigTab: theme="vs-dark", input=(self.view_model.on_change_file, "[$event]"), ) @self.server.controller.trigger("on_drop") def on_drop(old_path: str, new_path: str) -> None: self.view_model.update_path(old_path, new_path) Loading
pyproject.toml +1 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ ips-fastran-gui = { path = ".", editable = true } hatch = "*" gitpython = ">=3.1.40,<4" python-gitlab = ">=5.6.0,<6" nova-trame = "==1.3.2-dev0" nova-trame = "*" trame-datagrid = ">=0.2.2" trame-code = ">=1.0.2" nova-galaxy = ">=0.11.1" Loading
src/ips_fastran_gui/app/models/main_model.py +65 −22 Original line number Diff line number Diff line """Module for the main model.""" import json import os import zipfile from enum import Enum from io import BytesIO Loading Loading @@ -76,11 +77,17 @@ class MainModel: self.file_tree: Dict[str, Any] = {} def _download_files(self, zip_obj: zipfile.ZipFile, files: List[Dict[str, Any]]) -> None: for file in files: if "children" in file: self._download_files(zip_obj, file["children"]) else: zip_obj.writestr(file["relative_path"], file["content"]) 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"]) self._download_files(zip_obj, self.config.input_files) archive.seek(0) return archive.read() Loading @@ -94,32 +101,68 @@ class MainModel: return file def set_file_contents(self, index: int, json_data: str) -> None: self.config.input_files[index]["content"] = json_data def read_files(self, file_path: str) -> None: self.file_tree = {} for dirpath, _, filenames in os.walk(file_path): relative_path = os.path.relpath(dirpath, file_path) if relative_path == ".": relative_path = "" parts = [] else: parts = relative_path.split("/") 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 current_level = self.file_tree for part in parts: if part not in current_level: current_level[part] = {} current_level = current_level[part] def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None: if not files: self.config.input_files = [] for file in filenames: try: path = os.path.join(dirpath, file) with open(path, "r") as file_obj: current_level[file] = { "content": file_obj.read(), "name": file, "path": path, "relative_path": os.path.join(relative_path, file), } except Exception: pass self.file_tree = {} for index, file in enumerate(files): relative_path = relative_paths[index] parts = relative_path.split("/")[1:] self.config.input_files = self.set_files_from_tree(self.file_tree) decoded_file = file.copy() decoded_file["content"] = decoded_file["content"].decode("latin1") decoded_file["path"] = "/".join(parts) def set_file_contents(self, path: str, relative_path: str, json_data: str) -> None: parts = relative_path.split("/") if relative_path != "." else [] current_level = self.file_tree for part in parts: if part: current_level = current_level[part] current_level["content"] = json_data with open(path, "w") as file_obj: file_obj.write(json_data) self.config.input_files = self.set_files_from_tree(self.file_tree) def set_file_path(self, old_path: str, new_path: str) -> None: current_level = self.file_tree parts = old_path.split("/") for part in parts[:-1]: if part not in current_level: current_level[part] = {} current_level = current_level[part] current_level[parts[-1]] = decoded_file file = current_level.pop(parts[-1]) file["path"] = "" parts = new_path.split("/") current_level = self.file_tree for part in parts[:-1]: file["path"] += f"{part}/" current_level = current_level[part] if "path" not in current_level[parts[-1]]: file["path"] += f"{parts[-1]}/" current_level = current_level[parts[-1]] file["path"] += file["name"] current_level[file["name"]] = file self.config.input_files = self.set_files_from_tree(self.file_tree) Loading
src/ips_fastran_gui/app/view_models/main_view_model.py +14 −10 Original line number Diff line number Diff line """Module for the main ViewModel.""" import os from pathlib import Path from typing import Any, Dict, List Loading @@ -19,9 +20,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") input_file_path: str = Field(default=os.getenv("HOME", os.getcwd()), title="Path to Input Files") editor_content: str = Field(default="") editor_path: str = Field(default="") class MainViewModel: Loading Loading @@ -68,7 +69,11 @@ class MainViewModel: self.view_state.errors = [] try: self.view_state.editor_content = json_data # self.model.set_file_contents(self.view_state.editor_index, json_data) self.model.set_file_contents( self.view_state.editor_path, os.path.relpath(self.view_state.editor_path, self.view_state.input_file_path), json_data, ) except ValidationError as e: for error in e.errors(): msg = "" Loading Loading @@ -103,8 +108,8 @@ class MainViewModel: 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) case "input_file_path": self.model.read_files(self.view_state.input_file_path) self.config_bind.update_in_view(self.model.config) def on_completion(self, _sender: Any) -> None: Loading @@ -121,18 +126,14 @@ 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]], 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, path: List[str]) -> None: if not path: return file = self.model.get_file_from_path(path[0]) self.view_state.editor_path = file["path"] self.view_state.editor_content = file["content"] self.view_state.editor_path = file["path"] self.view_state_bind.update_in_view(self.view_state) Loading @@ -142,3 +143,6 @@ class MainViewModel: plotter.set_page() self.figure_bind.update_in_view(None) def update_path(self, old_path: str, new_path: str) -> None: self.model.set_file_path(old_path, new_path)
src/ips_fastran_gui/app/views/tabs/config_tab.py +30 −27 Original line number Diff line number Diff line """Module for the Config tab.""" from nova.trame.view.components import InputField import os from nova.trame.view.components import InputField, RemoteFileInput from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout from trame.app import get_server from trame.widgets import code from trame.widgets import vuetify3 as vuetify Loading @@ -12,6 +15,7 @@ class ConfigTab: """Config view class.""" def __init__(self, view_model: MainViewModel) -> None: self.server = get_server(None, client_type="vue3") self.view_model = view_model self.create_ui() Loading @@ -22,44 +26,39 @@ class ConfigTab: InputField(v_model="config.time_id") with HBoxLayout(): InputField( v_model="config.input_files", active=True, id="file-upload", multiple=True, raw_attrs=["webkitdirectory"], type="file", update_modelValue=( self.view_model.set_files, # TODO: I would like to better understand why webkitRelativePath is not available in the v-model # by default. ( "[" " config.input_files," " Array.from(window.document.getElementById('file-upload').files).map(" " file => file.webkitRelativePath" ")]" ), ), RemoteFileInput( v_model="state.input_file_path", allow_files=False, allow_folders=True, # TODO: base_paths should be set differently if running on a cluster base_paths=[os.getenv("HOME", os.getcwd())], ) with GridLayout(classes="mb-2", columns=3, stretch=True): with VBoxLayout(): with VBoxLayout(classes="pl-2"): vuetify.VListSubheader("Uploaded Files") vuetify.VTreeview( with vuetify.VTreeview( v_if="config.input_files.length > 0", activatable=True, active_strategy="single-leaf", items=("config.input_files",), item_title="name", item_value="path", update_activated=(self.view_model.edit_file, "[$event]"), ): with vuetify.Template(v_slot_title="{ item }"): vuetify.VListItemTitle( "{{ item.name }}", raw_attrs=[ '''draggable="true"''', '''@dragenter.prevent=""''', '''@dragover.prevent=""''', '''@dragstart="window.ips_drag_path = item.path"''', ], __events=["drop"], drop="trigger('on_drop', [window.ips_drag_path, item.path])", ) vuetify.VListItem("No files available.", v_else=True) with VBoxLayout(column_span=2, stretch=True): with VBoxLayout(): InputField("state.editor_path") code.Editor( ref="input_config", model_value=("state.editor_content",), Loading @@ -67,3 +66,7 @@ class ConfigTab: theme="vs-dark", input=(self.view_model.on_change_file, "[$event]"), ) @self.server.controller.trigger("on_drop") def on_drop(old_path: str, new_path: str) -> None: self.view_model.update_path(old_path, new_path)