diff --git a/pyflask/apis/neuroconv.py b/pyflask/apis/neuroconv.py index e719625c1..ea9ade85e 100644 --- a/pyflask/apis/neuroconv.py +++ b/pyflask/apis/neuroconv.py @@ -13,7 +13,11 @@ validate_metadata, listen_to_neuroconv_events, generate_dataset, + inspect_nwb_file, + inspect_nwb_folder, + inspect_multiple_filesystem_objects, ) + from errorHandlers import notBadRequestException neuroconv_api = Namespace("neuroconv", description="Neuroconv neuroconv_api for the NWB GUIDE.") @@ -141,25 +145,7 @@ class InspectNWBFile(Resource): @neuroconv_api.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"}) def post(self): try: - import json - from nwbinspector import inspect_nwbfile - from nwbinspector.nwbinspector import InspectorOutputJSONEncoder - - return json.loads( - json.dumps( - list( - inspect_nwbfile( - ignore=[ - "check_description", - "check_data_orientation", - ], # TODO: remove when metadata control is exposed - **neuroconv_api.payload, - ) - ), - cls=InspectorOutputJSONEncoder, - ) - ) - + return inspect_nwb_file(neuroconv_api.payload) except Exception as e: if notBadRequestException(e): neuroconv_api.abort(500, str(e)) @@ -170,24 +156,30 @@ class InspectNWBFolder(Resource): @neuroconv_api.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"}) def post(self): try: - import json - from nwbinspector import inspect_all - from nwbinspector.nwbinspector import InspectorOutputJSONEncoder - - messages = list( - inspect_all( - n_jobs=-2, # uses number of CPU - 1 - ignore=[ - "check_description", - "check_data_orientation", - ], # TODO: remove when metadata control is exposed - **neuroconv_api.payload, - ) - ) - - # messages = organize_messages(messages, levels=["importance", "message"]) - - return json.loads(json.dumps(messages, cls=InspectorOutputJSONEncoder)) + return inspect_nwb_folder(neuroconv_api.payload) + + except Exception as e: + if notBadRequestException(e): + neuroconv_api.abort(500, str(e)) + + +@neuroconv_api.route("/inspect") +class InspectNWBFolder(Resource): + @neuroconv_api.doc(responses={200: "Success", 400: "Bad Request", 500: "Internal server error"}) + def post(self): + from os.path import isfile + + try: + paths = neuroconv_api.payload["paths"] + + if len(paths) == 1: + if isfile(paths[0]): + return inspect_nwb_file({"path": paths[0]}) + else: + return inspect_nwb_folder({"path": paths[0]}) + + else: + return inspect_multiple_filesystem_objects(paths) except Exception as e: if notBadRequestException(e): diff --git a/pyflask/manageNeuroconv/__init__.py b/pyflask/manageNeuroconv/__init__.py index 7f84d44c5..b3fae5e89 100644 --- a/pyflask/manageNeuroconv/__init__.py +++ b/pyflask/manageNeuroconv/__init__.py @@ -9,6 +9,9 @@ upload_folder_to_dandi, listen_to_neuroconv_events, generate_dataset, + inspect_nwb_file, + inspect_nwb_folder, + inspect_multiple_filesystem_objects, ) diff --git a/pyflask/manageNeuroconv/info/__init__.py b/pyflask/manageNeuroconv/info/__init__.py index a3d9f9d41..f1fa1a023 100644 --- a/pyflask/manageNeuroconv/info/__init__.py +++ b/pyflask/manageNeuroconv/info/__init__.py @@ -1 +1,7 @@ -from .urls import resource_path, STUB_SAVE_FOLDER_PATH, CONVERSION_SAVE_FOLDER_PATH, TUTORIAL_SAVE_FOLDER_PATH +from .urls import ( + resource_path, + GUIDE_ROOT_FOLDER, + STUB_SAVE_FOLDER_PATH, + CONVERSION_SAVE_FOLDER_PATH, + TUTORIAL_SAVE_FOLDER_PATH, +) diff --git a/pyflask/manageNeuroconv/info/urls.py b/pyflask/manageNeuroconv/info/urls.py index 9672160e6..1715c02cd 100644 --- a/pyflask/manageNeuroconv/info/urls.py +++ b/pyflask/manageNeuroconv/info/urls.py @@ -20,6 +20,7 @@ def resource_path(relative_path): ) # NOTE: Must have pyflask for running the GUIDE as a whole, but errors for just the server f = path_config.open() data = json.load(f) +GUIDE_ROOT_FOLDER = Path(Path.home(), data["root"]) STUB_SAVE_FOLDER_PATH = Path(Path.home(), data["root"], *data["subfolders"]["preview"]) CONVERSION_SAVE_FOLDER_PATH = Path(Path.home(), data["root"], *data["subfolders"]["conversions"]) TUTORIAL_SAVE_FOLDER_PATH = Path(Path.home(), data["root"], *data["subfolders"]["tutorial"]) diff --git a/pyflask/manageNeuroconv/manage_neuroconv.py b/pyflask/manageNeuroconv/manage_neuroconv.py index 1888a0f10..8ff0c6d11 100644 --- a/pyflask/manageNeuroconv/manage_neuroconv.py +++ b/pyflask/manageNeuroconv/manage_neuroconv.py @@ -9,7 +9,7 @@ from pathlib import Path from sse import MessageAnnouncer -from .info import STUB_SAVE_FOLDER_PATH, CONVERSION_SAVE_FOLDER_PATH, TUTORIAL_SAVE_FOLDER_PATH +from .info import GUIDE_ROOT_FOLDER, STUB_SAVE_FOLDER_PATH, CONVERSION_SAVE_FOLDER_PATH, TUTORIAL_SAVE_FOLDER_PATH announcer = MessageAnnouncer() @@ -507,3 +507,89 @@ def generate_dataset(test_data_directory_path: str): phy_output_dir.symlink_to(phy_base_directory, True) return {"output_directory": str(output_directory)} + + +def inspect_nwb_file(payload): + from nwbinspector import inspect_nwbfile + from nwbinspector.nwbinspector import InspectorOutputJSONEncoder + + return json.loads( + json.dumps( + list( + inspect_nwbfile( + ignore=[ + "check_description", + "check_data_orientation", + ], # TODO: remove when metadata control is exposed + **payload, + ) + ), + cls=InspectorOutputJSONEncoder, + ) + ) + + +def inspect_nwb_file(payload): + from nwbinspector import inspect_nwbfile + from nwbinspector.nwbinspector import InspectorOutputJSONEncoder + + return json.loads( + json.dumps( + list( + inspect_nwbfile( + ignore=[ + "check_description", + "check_data_orientation", + ], # TODO: remove when metadata control is exposed + **payload, + ) + ), + cls=InspectorOutputJSONEncoder, + ) + ) + + +def inspect_nwb_folder(payload): + from nwbinspector import inspect_all + from nwbinspector.nwbinspector import InspectorOutputJSONEncoder + + messages = list( + inspect_all( + n_jobs=-2, # uses number of CPU - 1 + ignore=[ + "check_description", + "check_data_orientation", + ], # TODO: remove when metadata control is exposed + **payload, + ) + ) + + # messages = organize_messages(messages, levels=["importance", "message"]) + + return json.loads(json.dumps(messages, cls=InspectorOutputJSONEncoder)) + + +def aggregate_symlinks_in_new_directory(paths, reason="", folder_path=None): + if folder_path is None: + folder_path = GUIDE_ROOT_FOLDER / ".temp" / reason / f"temp_{datetime.now().strftime('%Y%m%d-%H%M%S')}" + + folder_path.mkdir(parents=True) + + for path in paths: + path = Path(path) + new_path = folder_path / path.name + if path.is_dir(): + aggregate_symlinks_in_new_directory( + list(map(lambda name: os.path.join(path, name), os.listdir(path))), None, new_path + ) + else: + new_path.symlink_to(path, path.is_dir()) + + return folder_path + + +def inspect_multiple_filesystem_objects(paths): + tmp_folder_path = aggregate_symlinks_in_new_directory(paths, "inspect") + result = inspect_nwb_folder({"path": tmp_folder_path}) + rmtree(tmp_folder_path) + return result diff --git a/src/renderer/src/stories/FileSystemSelector.js b/src/renderer/src/stories/FileSystemSelector.js index 8a2ffb725..d86c873e4 100644 --- a/src/renderer/src/stories/FileSystemSelector.js +++ b/src/renderer/src/stories/FileSystemSelector.js @@ -10,7 +10,7 @@ function getObjectTypeReferenceString(type, multiple, { nested, native } = {}) { .join(" / ")}`; const isDir = type === "directory"; - return multiple && (!isDir || (isDir && !native)) + return multiple && (!isDir || (isDir && !native) || dialog) ? type === "directory" ? "directories" : "files" @@ -122,7 +122,10 @@ export class FilesystemSelector extends LitElement { }; #onCancel = () => { - this.#onThrow(`No ${this.type} selected`, "The request was cancelled by the user"); + this.#onThrow( + `No ${getObjectTypeReferenceString(this.type, this.multiple, { native: true })} selected`, + "The request was cancelled by the user" + ); }; #checkType = (value) => { @@ -133,12 +136,18 @@ export class FilesystemSelector extends LitElement { #handleFiles = async (pathOrPaths, type) => { if (!pathOrPaths) - this.#onThrow("No paths detected", `Unable to parse ${this.type} path${this.multiple ? "s" : ""}`); + this.#onThrow( + "No paths detected", + `Unable to parse ${getObjectTypeReferenceString(this.type, false, { native: true })} path${ + this.multiple ? "s" : "" + }` + ); if (Array.isArray(pathOrPaths)) pathOrPaths.forEach(this.#checkType); else if (!type) this.#checkType(pathOrPaths); let resolvedValue = pathOrPaths; + if (Array.isArray(resolvedValue) && !this.multiple) { if (resolvedValue.length > 1) this.#onThrow( @@ -158,9 +167,9 @@ export class FilesystemSelector extends LitElement { async selectFormat(type = this.type) { if (dialog) { - const file = await this.#useElectronDialog(type); - const path = file.filePath ?? file.filePaths?.[0]; - this.#handleFiles(path, type); + const results = await this.#useElectronDialog(type); + // const path = file.filePath ?? file.filePaths?.[0]; + this.#handleFiles(results.filePath ?? results.filePaths, type); } else { let handles = await (type === "directory" ? window.showDirectoryPicker() @@ -236,7 +245,7 @@ export class FilesystemSelector extends LitElement { native: true, })}`}${this.multiple && - (this.type === "directory" || (isMultipleTypes && this.type.includes("directory"))) + (this.type === "directory" || (isMultipleTypes && this.type.includes("directory") && !dialog)) ? html`
Multiple directory support only available using drag-and-drop.` diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index f39afc4dd..67e2920a5 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -149,6 +149,8 @@ export class JSONSchemaInput extends LitElement { `; } + #onThrow = (...args) => (this.onThrow ? this.onThrow(...args) : this.form?.onThrow(...args)); + #render() { const { validateOnChange, info, path: fullPath } = this; @@ -158,7 +160,8 @@ export class JSONSchemaInput extends LitElement { const isArray = info.type === "array"; // Handle string (and related) formats / types const hasItemsRef = "items" in info && "$ref" in info.items; - if (!("items" in info) || (!("type" in info.items) && !hasItemsRef)) info.items = { type: "string" }; + if (!("items" in info)) info.items = {}; + if (!("type" in info.items) && !hasItemsRef) info.items.type = "string"; // Handle file and directory formats const createFilesystemSelector = (format) => { @@ -167,7 +170,7 @@ export class JSONSchemaInput extends LitElement { value: this.value, onSelect: (filePath) => this.#updateData(fullPath, filePath), onChange: (filePath) => validateOnChange && this.#triggerValidation(name, el, path), - onThrow: (...args) => this.form?.onThrow(...args), + onThrow: (...args) => this.#onThrow(...args), dialogOptions: this.form?.dialogOptions, dialogType: this.form?.dialogType, multiple: isArray, @@ -211,7 +214,7 @@ export class JSONSchemaInput extends LitElement { this.form.checkAllLoaded(); } }, - onThrow: (...args) => this.form?.onThrow(...args), + onThrow: (...args) => this.#onThrow(...args), }; return (this.form.tables[name] = diff --git a/src/renderer/src/stories/pages/inspect/InspectPage.js b/src/renderer/src/stories/pages/inspect/InspectPage.js index 5b5164a2c..b2889987a 100644 --- a/src/renderer/src/stories/pages/inspect/InspectPage.js +++ b/src/renderer/src/stories/pages/inspect/InspectPage.js @@ -6,7 +6,7 @@ import { Button } from "../../Button.js"; import { run } from "../guided-mode/options/utils.js"; import { JSONSchemaInput } from "../../JSONSchemaInput.js"; import { Modal } from "../../Modal"; -import { truncateFilePaths } from "../../preview/NWBFilePreview.js"; +import { getSharedPath, truncateFilePaths } from "../../preview/NWBFilePreview.js"; import { InspectorList } from "../../preview/inspector/InspectorList.js"; export class InspectPage extends Page { @@ -16,24 +16,29 @@ export class InspectPage extends Page { showReport = async (value) => { if (!value) { - const message = "Please provide a folder to inspect."; + const message = "Please provide filesystem entries to inspect."; onThrow(message); throw new Error(message); } - const items = truncateFilePaths( - await run("inspect_folder", { path: value }, { title: "Inspecting your files" }).catch((e) => { - this.notify(e.message, "error"); - throw e; - }), - value - ); + const result = await run( + "inspect", + { paths: value }, + { title: "Inspecting selected filesystem entries." } + ).catch((e) => { + this.notify(e.message, "error"); + throw e; + }); + + if (!result.length) return this.notify("No messages received from the NWB Inspector"); + + const items = truncateFilePaths(result, getSharedPath(result.map((o) => o.file_path))); const list = new InspectorList({ items }); list.style.padding = "25px"; const modal = new Modal({ - header: value, + header: value.length === 1 ? value : `Selected Filesystem Entries`, }); modal.append(list); document.body.append(modal); @@ -42,17 +47,20 @@ export class InspectPage extends Page { }; input = new JSONSchemaInput({ - path: ["folder_path"], + path: ["filesystem_paths"], info: { - type: "string", - format: "directory", + type: "array", + items: { + format: ["file", "directory"], + multiple: true, + }, }, onThrow, }); render() { const button = new Button({ - label: "Inspect Files", + label: "Start Inspection", onClick: async () => this.showReport(this.input.value), });