From 166ad98832a28e109c9d8208bed6aad460c2f2cf Mon Sep 17 00:00:00 2001 From: "Toni M. Brotons" <10654467+toni-neurosc@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:34:57 +0200 Subject: [PATCH] display Pydantic errors in Settings page --- gui_dev/bun.lockb | Bin 246999 -> 254737 bytes gui_dev/package.json | 21 +- .../src/components/StatusBar/StatusBar.jsx | 56 ++- gui_dev/src/pages/Settings/Dropdown.jsx | 18 +- gui_dev/src/pages/Settings/FrequencyRange.jsx | 8 +- gui_dev/src/pages/Settings/Settings.jsx | 348 +++++++++++------- gui_dev/src/pages/Settings/index.js | 1 - gui_dev/src/stores/createStore.js | 19 +- gui_dev/src/stores/settingsStore.js | 106 +++--- py_neuromodulation/__init__.py | 5 +- py_neuromodulation/gui/backend/app_backend.py | 62 +++- .../utils/pydantic_extensions.py | 48 +-- py_neuromodulation/utils/types.py | 25 +- pyproject.toml | 5 +- 14 files changed, 441 insertions(+), 281 deletions(-) delete mode 100644 gui_dev/src/pages/Settings/index.js diff --git a/gui_dev/bun.lockb b/gui_dev/bun.lockb index cafed1a34c58bcd17f9621a80286e99fce910eea..929e237a89336bd38e10ff03594c899074767620 100755 GIT binary patch delta 67914 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
    VTk5U~Y5Tp>CvH_K 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 km?ub%7 diff --git a/gui_dev/package.json b/gui_dev/package.json index 1a1cf43f..9329eca5 100644 --- a/gui_dev/package.json +++ b/gui_dev/package.json @@ -17,27 +17,26 @@ "plotly.js-basic-dist-min": "^2.35.2", "react": "next", "react-dom": "next", - "react-icons": "^5.3.0", "react-router-dom": "^6.26.2", "zustand": "next" }, "devDependencies": { - "@babel/core": "^7.25.2", - "@babel/eslint-parser": "^7.25.1", - "@babel/preset-env": "^7.25.4", - "@babel/preset-react": "^7.24.7", - "@eslint/compat": "^1.1.1", - "@vitejs/plugin-react": "^4.3.1", + "@babel/core": "^7.25.7", + "@babel/eslint-parser": "^7.25.7", + "@babel/preset-env": "^7.25.7", + "@babel/preset-react": "^7.25.7", + "@eslint/compat": "^1.2.0", + "@vitejs/plugin-react": "^4.3.2", "@welldone-software/why-did-you-render": "^8.0.3", "babel-plugin-react-compiler": "latest", - "eslint": "^9.10.0", + "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsdoc": "^50.2.3", - "eslint-plugin-react": "^7.36.1", + "eslint-plugin-jsdoc": "^50.3.1", + "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-compiler": "latest", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.12", "prettier": "^3.3.3", - "vite": "^5.4.6" + "vite": "^5.4.8" } } diff --git a/gui_dev/src/components/StatusBar/StatusBar.jsx b/gui_dev/src/components/StatusBar/StatusBar.jsx index 36aaf1dc..6bffbe15 100644 --- a/gui_dev/src/components/StatusBar/StatusBar.jsx +++ b/gui_dev/src/components/StatusBar/StatusBar.jsx @@ -1,13 +1,28 @@ +import { useState } from "react"; + import { ResizeHandle } from "./ResizeHandle"; import { SocketStatus } from "./SocketStatus"; import { WebviewStatus } from "./WebviewStatus"; +import { useSettingsStore } from "@/stores"; import { useWebviewStore } from "@/stores"; -import { Stack } from "@mui/material"; +import { Popover, Stack, Typography } from "@mui/material"; export const StatusBar = () => { - const { isWebView } = useWebviewStore((state) => state.isWebView); + const isWebView = useWebviewStore((state) => state.isWebView); + const validationErrors = useSettingsStore((state) => state.validationErrors); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleOpenErrorsPopover = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseErrorsPopover = () => { + setAnchorEl(null); + }; return ( { bgcolor="background.level1" borderTop="2px solid" borderColor="background.level3" + height="2rem" > - + {validationErrors?.length > 0 && ( + <> + + {validationErrors?.length} errors found in Settings + + + + + {validationErrors.map((error, index) => ( + + {index} - [{error.type}] {error.msg} + + ))} + + + + )} + {/* */} {/* Current experiment */} {/* Current stream */} {/* Current activity */} diff --git a/gui_dev/src/pages/Settings/Dropdown.jsx b/gui_dev/src/pages/Settings/Dropdown.jsx index ad662eef..9dbc9833 100644 --- a/gui_dev/src/pages/Settings/Dropdown.jsx +++ b/gui_dev/src/pages/Settings/Dropdown.jsx @@ -1,34 +1,30 @@ import { useState } from "react"; -import "../App.css"; var stringJson = '{"sampling_rate_features_hz":10.0,"segment_length_features_ms":1000.0,"frequency_ranges_hz":{"theta":{"frequency_low_hz":4.0,"frequency_high_hz":8.0},"alpha":{"frequency_low_hz":8.0,"frequency_high_hz":12.0},"low beta":{"frequency_low_hz":13.0,"frequency_high_hz":20.0},"high beta":{"frequency_low_hz":20.0,"frequency_high_hz":35.0},"low gamma":{"frequency_low_hz":60.0,"frequency_high_hz":80.0},"high gamma":{"frequency_low_hz":90.0,"frequency_high_hz":200.0},"HFA":{"frequency_low_hz":200.0,"frequency_high_hz":400.0}},"preprocessing":["raw_resampling","notch_filter","re_referencing"],"raw_resampling_settings":{"resample_freq_hz":1000.0},"preprocessing_filter":{"bandstop_filter":true,"bandpass_filter":true,"lowpass_filter":true,"highpass_filter":true,"bandstop_filter_settings":{"frequency_low_hz":100.0,"frequency_high_hz":160.0},"bandpass_filter_settings":{"frequency_low_hz":2.0,"frequency_high_hz":200.0},"lowpass_filter_cutoff_hz":200.0,"highpass_filter_cutoff_hz":3.0},"raw_normalization_settings":{"normalization_time_s":30.0,"normalization_method":"zscore","clip":3.0},"postprocessing":{"feature_normalization":true,"project_cortex":false,"project_subcortex":false},"feature_normalization_settings":{"normalization_time_s":30.0,"normalization_method":"zscore","clip":3.0},"project_cortex_settings":{"max_dist_mm":20.0},"project_subcortex_settings":{"max_dist_mm":5.0},"features":{"raw_hjorth":true,"return_raw":true,"bandpass_filter":false,"stft":false,"fft":true,"welch":true,"sharpwave_analysis":true,"fooof":false,"nolds":false,"coherence":false,"bursts":true,"linelength":true,"mne_connectivity":false,"bispectrum":false},"fft_settings":{"windowlength_ms":1000,"log_transform":true,"features":{"mean":true,"median":false,"std":false,"max":false},"return_spectrum":false},"welch_settings":{"windowlength_ms":1000,"log_transform":true,"features":{"mean":true,"median":false,"std":false,"max":false},"return_spectrum":false},"stft_settings":{"windowlength_ms":1000,"log_transform":true,"features":{"mean":true,"median":false,"std":false,"max":false},"return_spectrum":false},"bandpass_filter_settings":{"segment_lengths_ms":{"theta":1000,"alpha":500,"low beta":333,"high beta":333,"low gamma":100,"high gamma":100,"HFA":100},"bandpower_features":{"activity":true,"mobility":false,"complexity":false},"log_transform":true,"kalman_filter":false},"kalman_filter_settings":{"Tp":0.1,"sigma_w":0.7,"sigma_v":1.0,"frequency_bands":["theta","alpha","low_beta","high_beta","low_gamma","high_gamma","HFA"]},"burst_settings":{"threshold":75.0,"time_duration_s":30.0,"frequency_bands":["low beta","high beta","low gamma"],"burst_features":{"duration":true,"amplitude":true,"burst_rate_per_s":true,"in_burst":true}},"sharpwave_analysis_settings":{"sharpwave_features":{"peak_left":false,"peak_right":false,"trough":false,"width":false,"prominence":true,"interval":true,"decay_time":false,"rise_time":false,"sharpness":true,"rise_steepness":false,"decay_steepness":false,"slope_ratio":false},"filter_ranges_hz":[{"frequency_low_hz":5.0,"frequency_high_hz":80.0},{"frequency_low_hz":5.0,"frequency_high_hz":30.0}],"detect_troughs":{"estimate":true,"distance_troughs_ms":10.0,"distance_peaks_ms":5.0},"detect_peaks":{"estimate":true,"distance_troughs_ms":10.0,"distance_peaks_ms":5.0},"estimator":{"mean":["interval"],"median":[],"max":["prominence","sharpness"],"min":[],"var":[]},"apply_estimator_between_peaks_and_troughs":true},"mne_connectivity":{"method":"plv","mode":"multitaper"},"coherence":{"features":{"mean_fband":true,"max_fband":true,"max_allfbands":true},"method":{"coh":true,"icoh":true},"channels":[],"frequency_bands":["high beta"]},"fooof":{"aperiodic":{"exponent":true,"offset":true,"knee":true},"periodic":{"center_frequency":false,"band_width":false,"height_over_ap":false},"windowlength_ms":800.0,"peak_width_limits":{"frequency_low_hz":0.5,"frequency_high_hz":12.0},"max_n_peaks":3,"min_peak_height":0.0,"peak_threshold":2.0,"freq_range_hz":{"frequency_low_hz":2.0,"frequency_high_hz":40.0},"knee":true},"nolds_features":{"raw":true,"frequency_bands":["low beta"],"features":{"sample_entropy":false,"correlation_dimension":false,"lyapunov_exponent":true,"hurst_exponent":false,"detrended_fluctuation_analysis":false}},"bispectrum":{"f1s":{"frequency_low_hz":5.0,"frequency_high_hz":35.0},"f2s":{"frequency_low_hz":5.0,"frequency_high_hz":35.0},"compute_features_for_whole_fband_range":true,"frequency_bands":["theta","alpha","low_beta","high_beta"],"components":{"absolute":true,"real":true,"imag":true,"phase":true},"bispectrum_features":{"mean":true,"sum":true,"var":true}}}'; const nm_settings = JSON.parse(stringJson); -const filterByKeys = (dict, keys) => { +const filterByKeys = (obj, keys) => { const filteredDict = {}; keys.forEach((key) => { if (typeof key === "string") { // Top-level key - if (dict.hasOwnProperty(key)) { - filteredDict[key] = dict[key]; + if (obj.hasOwn(key)) { + filteredDict[key] = obj[key]; } } else if (typeof key === "object") { // Nested key const topLevelKey = Object.keys(key)[0]; const nestedKeys = key[topLevelKey]; - if ( - dict.hasOwnProperty(topLevelKey) && - typeof dict[topLevelKey] === "object" - ) { - filteredDict[topLevelKey] = filterByKeys(dict[topLevelKey], nestedKeys); + if (obj.hasOwn(topLevelKey) && typeof obj[topLevelKey] === "object") { + filteredDict[topLevelKey] = filterByKeys(obj[topLevelKey], nestedKeys); } } }); return filteredDict; }; -const Dropdown = ({ onChange, keysToInclude }) => { +export const Dropdown = ({ onChange, keysToInclude }) => { const filteredSettings = filterByKeys(nm_settings, keysToInclude); const [selectedOption, setSelectedOption] = useState(""); @@ -62,5 +58,3 @@ const Dropdown = ({ onChange, keysToInclude }) => { ); }; - -export default Dropdown; diff --git a/gui_dev/src/pages/Settings/FrequencyRange.jsx b/gui_dev/src/pages/Settings/FrequencyRange.jsx index e881b459..83ee09d5 100644 --- a/gui_dev/src/pages/Settings/FrequencyRange.jsx +++ b/gui_dev/src/pages/Settings/FrequencyRange.jsx @@ -138,7 +138,11 @@ export const FrequencyRangeList = ({ const updatedRanges = { ...ranges, - [newName]: { frequency_low_hz: 1, frequency_high_hz: 500 }, + [newName]: { + __field_type__: "FrequencyRange", + frequency_low_hz: 1, + frequency_high_hz: 500, + }, }; onChange(["frequency_ranges_hz"], updatedRanges); @@ -147,7 +151,7 @@ export const FrequencyRangeList = ({ }; return ( - + {rangeOrder.map((name, index) => ( { - // console.log(key); return key .split("_") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) @@ -24,15 +25,32 @@ const formatKey = (key) => { }; // Wrapper components for each type -const BooleanField = ({ value, onChange }) => ( +const BooleanField = ({ value, onChange, error }) => ( onChange(e.target.checked)} /> ); -const StringField = ({ value, onChange, label }) => ( - +const StringField = ({ value, onChange, label, error }) => ( + onChange(e.target.value)} + label={label} + sx={{ + "& .MuiOutlinedInput-root": { + "& fieldset": { + borderColor: error ? "error.main" : "inherit", + }, + "&:hover fieldset": { + borderColor: error ? "error.main" : "primary.main", + }, + "&.Mui-focused fieldset": { + borderColor: error ? "error.main" : "primary.main", + }, + }, + }} + /> ); -const NumberField = ({ value, onChange, label }) => { +const NumberField = ({ value, onChange, label, error }) => { const handleChange = (event) => { const newValue = event.target.value; // Only allow numbers and decimal point @@ -47,6 +65,19 @@ const NumberField = ({ value, onChange, label }) => { value={value} onChange={handleChange} label={label} + sx={{ + "& .MuiOutlinedInput-root": { + "& fieldset": { + borderColor: error ? "error.main" : "inherit", + }, + "&:hover fieldset": { + borderColor: error ? "error.main" : "primary.main", + }, + "&.Mui-focused fieldset": { + borderColor: error ? "error.main" : "primary.main", + }, + }, + }} // InputProps={{ // endAdornment: ( // @@ -66,24 +97,22 @@ const componentRegistry = { boolean: BooleanField, string: StringField, number: NumberField, + Array: Dropdown, }; -const SettingsField = ({ path, Component, label, value, onChange, depth }) => { +const SettingsField = ({ path, Component, label, value, onChange, error }) => { return ( - - {label} - onChange(path, newValue)} - label={label} - /> - + + + {label} + onChange(path, newValue)} + label={label} + error={error} + /> + + ); }; @@ -92,10 +121,23 @@ const SettingsSection = ({ title = null, path = [], onChange, - depth = 0, + errors, }) => { const boxTitle = title ? title : formatKey(path[path.length - 1]); + // Function to get the error corresponding to this field or its children + const getFieldError = (fieldPath) => { + if (!errors) return null; + + return errors.find((error) => { + const errorPath = error.loc.join("."); + const currentPath = fieldPath.join("."); + return ( + errorPath === currentPath || errorPath.startsWith(currentPath + ".") + ); + }); + }; + // If we receive a primitive value, we need to render a component if (typeof settings !== "object") { const Component = componentRegistry[typeof settings]; @@ -103,32 +145,35 @@ const SettingsSection = ({ console.error(`Invalid component type: ${typeof settings}`); return null; } + + const error = getFieldError(path); + return ( - // - // {boxTitle} - // - // ); } // If we receive a nested object, we iterate over it and render recursively return ( - + <> {Object.entries(settings).map(([key, value]) => { if (key === "__field_type__") return null; + if (value === null) return null; const newPath = [...path, key]; const label = key; const isPydanticModel = typeof value === "object" && "__field_type__" in value; + const error = getFieldError(newPath); + const fieldType = isPydanticModel ? value.__field_type__ : typeof value; const Component = componentRegistry[fieldType]; @@ -142,29 +187,34 @@ const SettingsSection = ({ label={formatKey(label)} value={value} onChange={onChange} - depth={depth + 1} + error={error} /> ); } else { return ( - + + + ); } })} - + ); }; -const SettingsContent = () => { - const [selectedFeature, setSelectedFeature] = useState(""); +export const Settings = () => { + // Get all necessary state from the settings store const settings = useSettingsStore((state) => state.settings); - const updateSettings = useSettingsStore((state) => state.updateSettings); + const uploadSettings = useSettingsStore((state) => state.uploadSettings); + const resetSettings = useSettingsStore((state) => state.resetSettings); + const validationErrors = useSettingsStore((state) => state.validationErrors); + + // This is needed so that the frequency ranges stay in order between updates const frequencyRangeOrder = useSettingsStore( (state) => state.frequencyRangeOrder ); @@ -172,20 +222,35 @@ const SettingsContent = () => { (state) => state.updateFrequencyRangeOrder ); + // Here I handle the selected feature in the feature settings component + const [selectedFeature, setSelectedFeature] = useState(""); + + useEffect(() => { + uploadSettings(null, true); // validateOnly = true + }, [settings]); + + // This has to be after all the hooks, otherwise React will complain if (!settings) { return

    Loading settings...
    ; } - console.log(settings); - - const handleChange = (path, value) => { - updateSettings((settings) => { + // This are the callbacks for the different buttons + const handleChangeSettings = async (path, value) => { + uploadSettings((settings) => { let current = settings; for (let i = 0; i < path.length - 1; i++) { current = current[path[i]]; } current[path[path.length - 1]] = value; - }); + }, true); // validateOnly = true + }; + + const handleSaveSettings = () => { + uploadSettings(() => settings); + }; + + const handleResetSettings = async () => { + await resetSettings(); }; const featureSettingsKeys = Object.keys(settings.features) @@ -214,109 +279,102 @@ const SettingsContent = () => { ]; return ( - - - - {generalSettingsKeys.map((key) => ( + + {/* SETTINGS LAYOUT */} + + {/* GENERAL SETTINGS + FREQUENCY RANGES */} + + + {generalSettingsKeys.map((key) => ( + + ))} + + + + + + + + {/* POSTPROCESSING + PREPROCESSING SETTINGS */} + + {preprocessingSettingsKeys.map((key) => ( ))} - - - - - - - {preprocessingSettingsKeys.map((key) => ( - - ))} - - - - {postprocessingSettingsKeys.map((key) => ( - - ))} - - - - - + + {postprocessingSettingsKeys.map((key) => ( - - - {Object.entries(enabledFeatures).map( - ([feature, featureSettings]) => ( - - - - ) - )} + ))} + + + {/* FEATURE SETTINGS */} + + + + + + + {Object.entries(enabledFeatures).map( + ([feature, featureSettings]) => ( + + + + ) + )} + - - - - ); -}; + + {/* END SETTINGS LAYOUT */} + -export const Settings = () => { - return ( - - + {/* BUTTONS */} { borderColor={"divider"} p={1} > - + {/* */} + diff --git a/gui_dev/src/pages/Settings/index.js b/gui_dev/src/pages/Settings/index.js deleted file mode 100644 index 16355023..00000000 --- a/gui_dev/src/pages/Settings/index.js +++ /dev/null @@ -1 +0,0 @@ -export { TextField } from './TextField'; diff --git a/gui_dev/src/stores/createStore.js b/gui_dev/src/stores/createStore.js index 762d9340..e683a35c 100644 --- a/gui_dev/src/stores/createStore.js +++ b/gui_dev/src/stores/createStore.js @@ -2,16 +2,23 @@ import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; import { devtools, persist as persistMiddleware } from "zustand/middleware"; -export const createStore = (name, initializer, persist = false) => { +export const createStore = ( + name, + initializer, + persist = false, + dev = false +) => { const fn = persist ? persistMiddleware(immer(initializer), name) : immer(initializer); - return create( - devtools(fn, { - name: name, - }) - ); + const dev_fn = dev + ? devtools(fn, { + name: name, + }) + : fn; + + return create(dev_fn); }; export const createPersistStore = (name, initializer) => { diff --git a/gui_dev/src/stores/settingsStore.js b/gui_dev/src/stores/settingsStore.js index 7064ec98..634c445f 100644 --- a/gui_dev/src/stores/settingsStore.js +++ b/gui_dev/src/stores/settingsStore.js @@ -4,34 +4,18 @@ const INITIAL_DELAY = 3000; // wait for Flask const RETRY_DELAY = 1000; // ms const MAX_RETRIES = 100; -const uploadSettingsToServer = async (settings) => { - try { - const response = await fetch("/api/settings", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(settings), - }); - if (!response.ok) { - throw new Error("Failed to update settings"); - } - return { success: true }; - } catch (error) { - console.error("Error updating settings:", error); - return { success: false, error }; - } -}; - export const useSettingsStore = createStore("settings", (set, get) => ({ settings: null, + lastValidSettings: null, frequencyRangeOrder: [], isLoading: false, error: null, + validationErrors: null, retryCount: 0, - setSettings: (settings) => set({ settings }), - setFrequencyRangeOrder: (order) => set({ frequencyRangeOrder: order }), + updateLocalSettings: (updater) => { + set((state) => updater(state.settings)); + }, fetchSettingsWithDelay: () => { set({ isLoading: true, error: null }); @@ -40,16 +24,22 @@ export const useSettingsStore = createStore("settings", (set, get) => ({ }, INITIAL_DELAY); }, - fetchSettings: async () => { + fetchSettings: async (reset = false) => { + console.log("Fetching settings..."); try { - console.log("Fetching settings..."); - const response = await fetch("/api/settings"); + const response = await fetch( + `/api/settings${reset ? "?reset=true" : ""}` + ); + if (!response.ok) { throw new Error("Failed to fetch settings"); } + const data = await response.json(); + set({ settings: data, + lastValidSettings: data, frequencyRangeOrder: Object.keys(data.frequency_ranges_hz || {}), retryCount: 0, }); @@ -60,6 +50,8 @@ export const useSettingsStore = createStore("settings", (set, get) => ({ retryCount: state.retryCount + 1, })); + console.log(get().retryCount); + if (get().retryCount < MAX_RETRIES) { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return get().fetchSettings(); @@ -71,38 +63,62 @@ export const useSettingsStore = createStore("settings", (set, get) => ({ resetRetryCount: () => set({ retryCount: 0 }), + resetSettings: async () => { + await get().fetchSettings(true); + }, + updateFrequencyRangeOrder: (newOrder) => { set({ frequencyRangeOrder: newOrder }); }, - updateSettings: async (updater) => { - const currentSettings = get().settings; - const currentOrder = get().frequencyRangeOrder; - - // Apply the update optimistically - set((state) => { - updater(state.settings); - }); - - const newSettings = get().settings; + uploadSettings: async (updater, validateOnly = false) => { + if (updater) { + set((state) => { + updater(state.settings); + }); + } - // Update the frequency range order - const newOrder = Object.keys(newSettings.frequency_ranges_hz || {}); - set({ frequencyRangeOrder: newOrder }); + const currentSettings = get().settings; try { - const result = await uploadSettingsToServer(newSettings); + const response = await fetch( + `/api/settings${validateOnly ? "?validate_only=true" : ""}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(currentSettings), + } + ); - if (!result.success) { - // Revert the local state if the server update failed - set({ settings: currentSettings, frequencyRangeOrder: currentOrder }); + const data = await response.json(); + + if (!response.ok) { + throw new Error("Failed to upload settings to backend"); } - return result; + if (data.valid) { + // Settings are valid + set({ + lastValidSettings: currentSettings, + validationErrors: null, + }); + return true; + } else { + // Settings are invalid + set({ + validationErrors: data.errors, + }); + // Note: We don't revert the settings here, keeping the potentially invalid state + return false; + } } catch (error) { - // Revert the local state if there was an error - set({ settings: currentSettings, frequencyRangeOrder: currentOrder }); - throw error; + console.error( + `Error ${validateOnly ? "validating" : "updating"} settings:`, + error + ); + return false; } }, })); diff --git a/py_neuromodulation/__init__.py b/py_neuromodulation/__init__.py index 688393fd..864df345 100644 --- a/py_neuromodulation/__init__.py +++ b/py_neuromodulation/__init__.py @@ -4,11 +4,12 @@ from importlib.metadata import version from py_neuromodulation.utils.logging import NMLogger + ##################################### # Globals and environment variables # ##################################### -__version__ = version("py_neuromodulation") # get version from pyproject.toml +__version__ = version("py_neuromodulation") # Check if the module is running headless (no display) for tests and doc builds PYNM_HEADLESS: bool = not os.environ.get("DISPLAY") @@ -18,6 +19,7 @@ os.environ["LSLAPICFG"] = str(PYNM_DIR / "lsl_api.cfg") # LSL config file + # Set environment variable MNE_LSL_LIB (required to import Stream below) LSL_DICT = { "windows_32bit": "windows/x86/liblsl.1.16.2.dll", @@ -36,6 +38,7 @@ PLATFORM = platform.system().lower().strip() ARCH = platform.architecture()[0] + match PLATFORM: case "windows": KEY = PLATFORM + "_" + ARCH diff --git a/py_neuromodulation/gui/backend/app_backend.py b/py_neuromodulation/gui/backend/app_backend.py index 114b9f04..9c7b0a88 100644 --- a/py_neuromodulation/gui/backend/app_backend.py +++ b/py_neuromodulation/gui/backend/app_backend.py @@ -15,6 +15,7 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware +from pydantic import ValidationError from . import app_pynm from .app_socket import WebSocketManager @@ -93,23 +94,62 @@ async def healthcheck(): #################### ##### SETTINGS ##### #################### - @self.get("/api/settings") - async def get_settings(): - return self.pynm_state.settings.process_for_frontend() + async def get_settings( + reset: bool = Query(False, description="Reset settings to default"), + ): + if reset: + settings = NMSettings.get_default() + else: + settings = self.pynm_state.settings + + return settings.serialize_with_metadata() @self.post("/api/settings") - async def update_settings(data: dict): - print(data) + async def update_settings(data: dict, validate_only: bool = Query(False)): try: - self.pynm_state.settings = NMSettings.model_validate(data) - self.logger.debug(self.pynm_state.settings.features) - return self.pynm_state.settings.model_dump() - except ValueError as e: - self.logger.error(f"Error updating settings: {e}") + # First, validate with Pydantic + try: + validated_settings = NMSettings.model_validate(data) + except ValidationError as e: + if not validate_only: + # If validation failed but we wanted to upload, return error + self.logger.error(f"Error validating settings: {e}") + raise HTTPException( + status_code=422, + detail={ + "error": "Error validating settings", + "details": str(e), + }, + ) + # Else return list of errors + return { + "valid": False, + "errors": [err for err in e.errors()], + "details": str(e), + } + + # If validation succesful, return or update settings + if validate_only: + return { + "valid": True, + "settings": validated_settings.serialize_with_metadata(), + } + + self.pynm_state.settings = validated_settings + self.logger.info("Settings successfully updated") + + return { + "valid": True, + "settings": self.pynm_state.settings.serialize_with_metadata(), + } + + # If something else than validation went wrong, return error + except Exception as e: + self.logger.error(f"Error validating/updating settings: {e}") raise HTTPException( status_code=422, - detail={"error": "Validation failed", "details": str(e)}, + detail={"error": "Error uploading settings", "details": str(e)}, ) ######################## diff --git a/py_neuromodulation/utils/pydantic_extensions.py b/py_neuromodulation/utils/pydantic_extensions.py index 65d6cb4e..556f751c 100644 --- a/py_neuromodulation/utils/pydantic_extensions.py +++ b/py_neuromodulation/utils/pydantic_extensions.py @@ -1,6 +1,6 @@ from typing import Any, get_type_hints from pydantic.fields import FieldInfo, _FieldInfoInputs, _FromFieldInfoInputs -from pydantic import BaseModel, ConfigDict, model_serializer +from pydantic import BaseModel, ConfigDict, SerializationInfo, model_serializer from pydantic_core import PydanticUndefined from typing_extensions import Unpack, TypedDict from pprint import pformat @@ -9,7 +9,7 @@ class _NMExtraFieldInputs(TypedDict, total=False): """Additional fields to add on top of the pydantic FieldInfo""" - meta: dict[str, Any] + custom_metadata: dict[str, Any] class _NMFieldInfoInputs(_FieldInfoInputs, _NMExtraFieldInputs, total=False): @@ -29,17 +29,7 @@ class NMFieldInfo(FieldInfo): _default_values = {} def __init__(self, **kwargs: Unpack[_NMFieldInfoInputs]) -> None: - extra_fields = get_type_hints(_NMExtraFieldInputs) - for field, field_type in extra_fields.items(): - # If no default value, try to instantiate the field type with no arguments, if it fails, set to None - try: - value = ( - kwargs.pop(field, self._default_values.get(field, field_type())), # type: ignore - ) - except Exception: - value = None - - setattr(self, field, value) + self.custom_metadata = kwargs.pop("custom_metadata", {}) super().__init__(**kwargs) @staticmethod @@ -94,36 +84,34 @@ def __getitem__(self, key): def __setitem__(self, key, value) -> None: setattr(self, key, value) - @classmethod - def get_fields(cls) -> dict[str, NMFieldInfo]: - return cls.model_fields # type: ignore + @property + def fields(self) -> dict[str, FieldInfo | NMFieldInfo]: + return self.model_fields # type: ignore + + def serialize_with_metadata(self): + result: dict[str, Any] = {"__field_type__": self.__class__.__name__} - @model_serializer - def serialize_model(self) -> dict[str, Any]: - result = {"__field_type__": self.__class__.__name__} - for field_name, field_info in self.model_fields.items(): + for field_name, field_info in self.fields.items(): value = getattr(self, field_name) if isinstance(value, NMBaseModel): - result[field_name] = value.serialize_model() + result[field_name] = value.serialize_with_metadata() elif isinstance(value, list): result[field_name] = [ - item.serialize_model() if isinstance(item, NMBaseModel) else item + item.serialize_with_metadata() + if isinstance(item, NMBaseModel) + else item for item in value ] elif isinstance(value, dict): result[field_name] = { - k: v.serialize_model() if isinstance(v, NMBaseModel) else v + k: v.serialize_with_metadata() if isinstance(v, NMBaseModel) else v for k, v in value.items() } else: result[field_name] = value # Extract unit information from Annotated type - if get_origin(field_info.annotation) is Annotated: - metadata = get_args(field_info.annotation)[1:] - for item in metadata: - if isinstance(item, dict) and "unit" in item: - result[f"{field_name}_metadata"] = {"unit": item["unit"]} - break - + if isinstance(field_info, NMFieldInfo): + for tag, value in field_info.custom_metadata.items(): + result[f"__{tag}__"] = value return result diff --git a/py_neuromodulation/utils/types.py b/py_neuromodulation/utils/types.py index 1aad1b08..f5921281 100644 --- a/py_neuromodulation/utils/types.py +++ b/py_neuromodulation/utils/types.py @@ -1,21 +1,14 @@ from os import PathLike from math import isnan -from typing import ( - Literal, - Protocol, - TYPE_CHECKING, - runtime_checkable, -) -from pydantic import ( - BaseModel, - model_validator, -) +from typing import Literal, TYPE_CHECKING +from pydantic import BaseModel, model_validator from pydantic_core import ValidationError, InitErrorDetails from .pydantic_extensions import NMBaseModel, NMField from collections.abc import Sequence from datetime import datetime + if TYPE_CHECKING: import numpy as np from py_neuromodulation import NMSettings @@ -26,7 +19,6 @@ _PathLike = str | PathLike - FeatureName = Literal[ "raw_hjorth", "return_raw", @@ -69,8 +61,7 @@ ################################### -@runtime_checkable -class NMFeature(Protocol): +class NMFeature: def __init__( self, settings: "NMSettings", ch_names: Sequence[str], sfreq: int | float ) -> None: ... @@ -91,17 +82,15 @@ def calc_feature(self, data: "np.ndarray") -> dict: ... -class NMPreprocessor(Protocol): - def __init__(self, sfreq: float, settings: "NMSettings") -> None: ... - +class NMPreprocessor: def process(self, data: "np.ndarray") -> "np.ndarray": ... class FrequencyRange(NMBaseModel): # frequency_low_hz: Annotated[list[float], {"unit": "Hz"}] = Field(gt=0) # frequency_high_hz: FrequencyHz = Field(gt=0) - frequency_low_hz: float = NMField(gt=0, meta={"unit": "Hz"}) - frequency_high_hz: float = NMField(gt=0, meta={"unit": "Hz"}) + frequency_low_hz: float = NMField(gt=0, custom_metadata={"unit": "Hz"}) + frequency_high_hz: float = NMField(gt=0, custom_metadata={"unit": "Hz"}) def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index 095dcf80..395b003b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dependencies = [ "nolds", "numpy >= 1.21.2", "pandas >= 2.0.0", - "scikit-image", "scikit-learn >= 0.24.2", "scikit-optimize", "scipy >= 1.7.1", @@ -55,12 +54,10 @@ dependencies = [ "statsmodels", "mne-lsl>=1.2.0", "pynput", - "pyqt5", "pydantic>=2.7.3", "pywebview", "fastapi", - "uvicorn>=0.30.6", - "websockets>=13.0", + "uvicorn[standard]>=0.30.6", "seaborn >= 0.11", ]