Repository: catalystneuro/neuroconv

Issue:
Install neuroconv without dandi
Right now dandi is part of our basic dependencies and naturally it comes itself bundled with a bunch of libraries for handling downloading and uploading. 

For the use case of just transforming data within an analysis pipeline none of those dependencies are really needed. 

Maybe it would be worth to add an option to install neuroconv just as a t**ransformation tool** that other libraries can use without the dandi / web / tools-for-uploading-and-downloading data dependencies.

 



Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8aea59a..e954ce88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Upcoming
 
+### Improvement
+
+* `nwbinspector` has been removed as a minimal dependency. It becomes an extra (optional) dependency with `neuroconv[dandi]`. [PR #672](https://github.com/catalystneuro/neuroconv/pull/672)
+
+
+
 # v0.4.6
 
 ### Features
diff --git a/requirements-minimal.txt b/requirements-minimal.txt
index a665b2b2..33db468e 100644
--- a/requirements-minimal.txt
+++ b/requirements-minimal.txt
@@ -6,9 +6,9 @@ h5py>=3.9.0
 hdmf>=3.11.0
 hdmf_zarr>=0.4.0
 pynwb>=2.3.2;python_version>='3.8'
-nwbinspector>=0.4.31
 pydantic>=1.10.13,<2.0.0
 psutil>=5.8.0
 tqdm>=4.60.0
 pandas
 parse>=1.20.0
+click
diff --git a/src/neuroconv/tools/data_transfers/_globus.py b/src/neuroconv/tools/data_transfers/_globus.py
index 907a89f7..88e0264e 100644
--- a/src/neuroconv/tools/data_transfers/_globus.py
+++ b/src/neuroconv/tools/data_transfers/_globus.py
@@ -5,14 +5,12 @@ from pathlib import Path
 from time import sleep, time
 from typing import Dict, List, Tuple, Union
 
-from nwbinspector.utils import is_module_installed
 from pydantic import DirectoryPath
 from tqdm import tqdm
 
+from ..importing import is_package_installed
 from ..processes import deploy_process
 
-HAVE_GLOBUS = is_module_installed(module_name="globus_cli")
-
 
 def get_globus_dataset_content_sizes(
     globus_endpoint_id: str, path: str, recursive: bool = True, timeout: float = 120.0
@@ -22,7 +20,7 @@ def get_globus_dataset_content_sizes(
 
     Returns dictionary whose keys are file names and values are sizes in bytes.
     """
-    assert HAVE_GLOBUS, "You must install the globus CLI (pip install globus-cli)!"
+    assert is_package_installed(package_name="globus_cli"), "You must install the globus CLI (pip install globus-cli)!"
 
     recursive_flag = " --recursive" if recursive else ""
     contents = json.loads(
diff --git a/src/neuroconv/tools/importing.py b/src/neuroconv/tools/importing.py
index 04e71826..2ef3eb2f 100644
--- a/src/neuroconv/tools/importing.py
+++ b/src/neuroconv/tools/importing.py
@@ -1,6 +1,8 @@
-"""Tool functions for performing imports."""
-import importlib.util
+"""Tool functions related to imports."""
 import sys
+from importlib import import_module
+from importlib.metadata import version as importlib_version
+from importlib.util import find_spec
 from platform import processor, python_version
 from types import ModuleType
 from typing import Dict, List, Optional, Union
@@ -8,6 +10,33 @@ from typing import Dict, List, Optional, Union
 from packaging import version
 
 
+def get_package_version(name: str) -> version.Version:
+    """
+    Retrieve the version of a package.
+
+    Parameters
+    ----------
+    name : str
+        Name of package.
+
+    Returns
+    -------
+    version : Version
+        The package version as an object from packaging.version.Version.
+    """
+    package_version = importlib_version(name)
+    return version.parse(package_version)
+
+
+def is_package_installed(package_name: str) -> bool:
+    """
+    Check if the given package is installed on the system.
+
+    Used for lazy imports.
+    """
+    return find_spec(name=package_name) is not None
+
+
 def get_package(
     package_name: str,
     installation_instructions: Optional[str] = None,
@@ -89,8 +118,8 @@ def get_package(
     if package_name in sys.modules:
         return sys.modules[package_name]
 
-    if importlib.util.find_spec(package_name) is not None:
-        return importlib.import_module(name=package_name)
+    if is_package_installed(package_name=package_name) is not None:
+        return import_module(name=package_name)
 
     raise ModuleNotFoundError(
         f"\nThe required package'{package_name}' is not installed!\n"
diff --git a/src/neuroconv/tools/nwb_helpers/_models/_hdf5_models.py b/src/neuroconv/tools/nwb_helpers/_models/_hdf5_models.py
index b3467115..47c26fdf 100644
--- a/src/neuroconv/tools/nwb_helpers/_models/_hdf5_models.py
+++ b/src/neuroconv/tools/nwb_helpers/_models/_hdf5_models.py
@@ -2,11 +2,11 @@
 from typing import Any, Dict, Literal, Type, Union
 
 import h5py
-from nwbinspector.utils import is_module_installed
 from pydantic import Field
 from pynwb import H5DataIO
 
 from ._base_models import BackendConfiguration, DatasetIOConfiguration
+from ...importing import is_package_installed
 
 _base_hdf5_filters = set(h5py.filters.decode)
 _excluded_hdf5_filters = set(
@@ -18,7 +18,7 @@ _excluded_hdf5_filters = set(
 )
 _available_hdf5_filters = set(_base_hdf5_filters - _excluded_hdf5_filters)
 AVAILABLE_HDF5_COMPRESSION_METHODS = {filter_name: filter_name for filter_name in _available_hdf5_filters}
-if is_module_installed(module_name="hdf5plugin"):
+if is_package_installed(package_name="hdf5plugin"):
     import hdf5plugin
 
     AVAILABLE_HDF5_COMPRESSION_METHODS.update(
@@ -55,7 +55,7 @@ class HDF5DatasetIOConfiguration(DatasetIOConfiguration):
     )
 
     def get_data_io_kwargs(self) -> Dict[str, Any]:
-        if is_module_installed(module_name="hdf5plugin"):
+        if is_package_installed(package_name="hdf5plugin"):
             import hdf5plugin
 
             if self.compression_method in _base_hdf5_filters:
diff --git a/src/neuroconv/tools/spikeinterface/spikeinterface.py b/src/neuroconv/tools/spikeinterface/spikeinterface.py
index 88bb64ac..137154e4 100644
--- a/src/neuroconv/tools/spikeinterface/spikeinterface.py
+++ b/src/neuroconv/tools/spikeinterface/spikeinterface.py
@@ -9,14 +9,13 @@ import psutil
 import pynwb
 from hdmf.backends.hdf5.h5_utils import H5DataIO
 from hdmf.data_utils import AbstractDataChunkIterator, DataChunkIterator
-from nwbinspector.utils import get_package_version
 from packaging.version import Version
-from pynwb import NWBFile
 from spikeinterface import BaseRecording, BaseSorting, WaveformExtractor
 
 from .spikeinterfacerecordingdatachunkiterator import (
     SpikeInterfaceRecordingDataChunkIterator,
 )
+from ..importing import get_package_version
 from ..nwb_helpers import get_module, make_or_load_nwbfile
 from ...utils import (
     DeepDict,


Test patch:
diff --git a/src/neuroconv/tools/testing/mock_ttl_signals.py b/src/neuroconv/tools/testing/mock_ttl_signals.py
index 2b637a22..6b4cd5f2 100644
--- a/src/neuroconv/tools/testing/mock_ttl_signals.py
+++ b/src/neuroconv/tools/testing/mock_ttl_signals.py
@@ -4,10 +4,10 @@ from typing import Optional, Union
 
 import numpy as np
 from numpy.typing import DTypeLike
-from nwbinspector.tools import make_minimal_nwbfile
-from nwbinspector.utils import is_module_installed
 from pynwb import NWBHDF5IO, H5DataIO, TimeSeries
+from pynwb.testing.mock.file import mock_NWBFile
 
+from ..importing import is_package_installed
 from ...utils import ArrayType, FolderPathType
 
 
@@ -144,7 +144,7 @@ def regenerate_test_cases(folder_path: FolderPathType, regenerate_reference_imag
     folder_path = Path(folder_path)
 
     if regenerate_reference_images:
-        assert is_module_installed("plotly") and is_module_installed("kaleido"), (
+        assert is_package_installed("plotly") and is_package_installed("kaleido"), (
             "To regenerate the reference images, " "you must install both plotly and kaleido!"
         )
         import plotly.graph_objects as go
@@ -157,7 +157,7 @@ def regenerate_test_cases(folder_path: FolderPathType, regenerate_reference_imag
     unit = "Volts"
     rate = 1000.0  # For non-default series to produce less data
 
-    nwbfile = make_minimal_nwbfile()
+    nwbfile = mock_NWBFile()
 
     # Test Case 1: Default
     default_ttl_signal = generate_mock_ttl_signal()
diff --git a/tests/test_minimal/test_tools/test_backend_and_dataset_configuration/test_helpers/test_get_default_dataset_io_configurations.py b/tests/test_minimal/test_tools/test_backend_and_dataset_configuration/test_helpers/test_get_default_dataset_io_configurations.py
index 69545adb..df81fa87 100644
--- a/tests/test_minimal/test_tools/test_backend_and_dataset_configuration/test_helpers/test_get_default_dataset_io_configurations.py
+++ b/tests/test_minimal/test_tools/test_backend_and_dataset_configuration/test_helpers/test_get_default_dataset_io_configurations.py
@@ -5,7 +5,6 @@ import numpy as np
 import pytest
 from hdmf.common import VectorData
 from hdmf.data_utils import DataChunkIterator
-from nwbinspector.utils import is_module_installed
 from pynwb.base import DynamicTable
 from pynwb.behavior import CompassDirection
 from pynwb.image import ImageSeries
@@ -15,6 +14,7 @@ from pynwb.testing.mock.behavior import mock_SpatialSeries
 from pynwb.testing.mock.file import mock_NWBFile
 
 from neuroconv.tools.hdmf import SliceableDataChunkIterator
+from neuroconv.tools.importing import is_package_installed
 from neuroconv.tools.nwb_helpers import (
     DATASET_IO_CONFIGURATIONS,
     get_default_dataset_io_configurations,
@@ -234,7 +234,8 @@ def test_configuration_on_compass_direction(iterator: callable, backend: Literal
 
 
 @pytest.mark.skipif(
-    not is_module_installed(module_name="ndx_events"), reason="The extra testing package 'ndx-events' is not installed!"
+    not is_package_installed(package_name="ndx_events"),
+    reason="The extra testing package 'ndx-events' is not installed!",
 )
 @pytest.mark.parametrize("backend", ["hdf5", "zarr"])
 def test_configuration_on_ndx_events(backend: Literal["hdf5", "zarr"]):
diff --git a/tests/test_ophys/test_imagingextractordatachunkiterator.py b/tests/test_ophys/test_imagingextractordatachunkiterator.py
index b8c41278..cf9a3edd 100644
--- a/tests/test_ophys/test_imagingextractordatachunkiterator.py
+++ b/tests/test_ophys/test_imagingextractordatachunkiterator.py
@@ -3,7 +3,6 @@ import math
 import numpy as np
 from hdmf.testing import TestCase
 from numpy.testing import assert_array_equal
-from nwbinspector.utils import get_package_version
 from parameterized import param, parameterized
 from roiextractors.testing import generate_dummy_imaging_extractor