flimkit-zstack-explorer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ FLIMKIT_PLUGIN_API = 1
2
+
3
+ from flimkit_zstack_explorer.volumes import build_zstack_volumes
4
+ from flimkit_zstack_explorer.zarr_io import save_ome_zarr, load_ome_zarr
5
+
6
+ registered = False
7
+ register_error = None
8
+ try:
9
+ from flimkit_zstack_explorer import plugin as _plugin
10
+ registered = True
11
+ except ImportError as exc:
12
+ register_error = exc
13
+
14
+ __all__ = [
15
+ 'FLIMKIT_PLUGIN_API',
16
+ 'build_zstack_volumes',
17
+ 'save_ome_zarr',
18
+ 'load_ome_zarr',
19
+ ]
@@ -0,0 +1,101 @@
1
+ from flimkit.plugins import tool
2
+
3
+
4
+ def _parent(app):
5
+ return getattr(app, 'root', None) or app
6
+
7
+
8
+ def _deps_ok():
9
+ try:
10
+ import zarr # noqa: F401
11
+ import ome_zarr # noqa: F401
12
+ except ImportError:
13
+ return False
14
+ return True
15
+
16
+
17
+ @tool(id='zstack_3d_explorer', label='3D Z-stack Explorer...', menu='Tools', order=850)
18
+ def open_3d_explorer(app):
19
+ from tkinter import filedialog, messagebox
20
+
21
+ parent = _parent(app)
22
+ panel = getattr(app, '_fov_preview', None)
23
+ if not getattr(panel, '_zstack', None):
24
+ messagebox.showinfo(
25
+ 'No z-stack loaded',
26
+ 'This tool builds a 3D volume from a loaded z-stack.\n\n'
27
+ 'Load or fit a z-stack first (FOV mode > Z-stack), then run '
28
+ 'this tool again.',
29
+ parent=parent)
30
+ return
31
+
32
+ if not _deps_ok():
33
+ messagebox.showerror(
34
+ 'Missing dependency',
35
+ 'The 3D Z-stack Explorer needs zarr and ome-zarr.\n\n'
36
+ 'Install with:\n\n pip install flimkit-zstack-explorer',
37
+ parent=parent)
38
+ return
39
+
40
+ out_path = filedialog.asksaveasfilename(
41
+ parent=parent, title='Save 3D volume as OME-Zarr',
42
+ defaultextension='.zarr', initialfile='zstack_volume.zarr',
43
+ filetypes=[('OME-Zarr store', '*.zarr'), ('All files', '*')])
44
+ if not out_path:
45
+ return
46
+
47
+ _run_build_and_view(app, parent, out_path)
48
+
49
+
50
+ def _run_build_and_view(app, parent, out_path):
51
+ import threading
52
+ from flimkit.UI.progress_window import ProgressWindow
53
+
54
+ n_slices = len(app._fov_preview._zstack)
55
+ win = ProgressWindow(parent, task_name='Building 3D volume')
56
+ win.set_progress(0, maximum=n_slices)
57
+
58
+ def on_progress(i, n):
59
+ app.root.after(0, lambda: win.set_progress(i, maximum=n))
60
+
61
+ def worker():
62
+ from flimkit_zstack_explorer.volumes import build_zstack_volumes
63
+ from flimkit_zstack_explorer.zarr_io import save_ome_zarr
64
+
65
+ try:
66
+ intensity, lifetime = build_zstack_volumes(app, progress=on_progress)
67
+ app.root.after(0, lambda: win.set_status('Saving OME-Zarr...'))
68
+ save_ome_zarr(out_path, intensity, lifetime)
69
+ except Exception as exc:
70
+ app.root.after(0, win.close)
71
+ app.root.after(0, lambda exc=exc: _show_error(parent, exc))
72
+ return
73
+ app.root.after(0, win.close)
74
+ app.root.after(0, lambda: _launch_viewer(out_path, parent))
75
+
76
+ threading.Thread(target=worker, daemon=True).start()
77
+
78
+
79
+ def _show_error(parent, exc):
80
+ from tkinter import messagebox
81
+ messagebox.showerror('3D Z-stack Explorer failed',
82
+ f'{type(exc).__name__}: {exc}', parent=parent)
83
+
84
+
85
+ def _launch_viewer(zarr_path, parent):
86
+ import subprocess
87
+ import sys
88
+ from tkinter import messagebox
89
+
90
+ try:
91
+ subprocess.Popen([
92
+ sys.executable, '-m', 'flimkit_zstack_explorer.viewer',
93
+ '--zarr', str(zarr_path),
94
+ ])
95
+ except Exception as exc:
96
+ messagebox.showerror(
97
+ '3D Z-stack Explorer',
98
+ f'Volume saved to {zarr_path}, but the viewer failed to start:\n'
99
+ f'{type(exc).__name__}: {exc}\n\n'
100
+ 'Install the viewer with: pip install "flimkit-zstack-explorer[gui]"',
101
+ parent=parent)
@@ -0,0 +1,79 @@
1
+ """Standalone popout 3D viewer. Runs in its own process (see plugin.py),
2
+ never imports tkinter/flimkit, so it doesn't fight FLIMKit's Tk mainloop
3
+ for the main thread.
4
+
5
+ Run directly with: python -m flimkit_zstack_explorer.viewer --zarr <path>
6
+ """
7
+ from __future__ import annotations
8
+ import argparse
9
+ import sys
10
+
11
+ import numpy as np
12
+
13
+
14
+ def _missing_pyvista():
15
+ sys.stderr.write(
16
+ 'flimkit-zstack-explorer: pyvista is required for the 3D viewer.\n'
17
+ 'Install it with:\n\n'
18
+ ' pip install "flimkit-zstack-explorer[gui]"\n\n')
19
+ sys.exit(1)
20
+
21
+
22
+ def show(intensity_stack: np.ndarray, lifetime_stack: np.ndarray, *,
23
+ voxel_size_um=(1.0, 1.0, 1.0), title: str = 'FLIMKit 3D Z-stack Explorer'):
24
+ try:
25
+ import pyvista as pv
26
+ except ImportError:
27
+ _missing_pyvista()
28
+ return
29
+
30
+ spacing = tuple(float(v) for v in voxel_size_um)
31
+
32
+ intensity_grid = pv.ImageData()
33
+ intensity_grid.dimensions = np.array(intensity_stack.shape)[::-1] + 1
34
+ intensity_grid.spacing = spacing[::-1]
35
+ intensity_grid.cell_data['intensity'] = intensity_stack.flatten(order='F')
36
+
37
+ # Background (no-fit / no-signal) voxels are pinned to 0, which is below
38
+ # any real lifetime in the data, so the 'linear' opacity transfer
39
+ # function used below fades them out automatically without needing a
40
+ # second per-voxel opacity field (PyVista's add_volume only accepts an
41
+ # opacity transfer function keyed by the volume's own scalar range).
42
+ lifetime_grid = pv.ImageData()
43
+ lifetime_grid.dimensions = np.array(lifetime_stack.shape)[::-1] + 1
44
+ lifetime_grid.spacing = spacing[::-1]
45
+ finite_lifetime = np.where(np.isfinite(lifetime_stack), lifetime_stack, 0.0)
46
+ lifetime_grid.cell_data['lifetime_ns'] = finite_lifetime.flatten(order='F')
47
+
48
+ plotter = pv.Plotter(shape=(1, 2), title=title)
49
+
50
+ plotter.subplot(0, 0)
51
+ plotter.add_volume(intensity_grid, scalars='intensity', cmap='inferno',
52
+ opacity='linear', name='intensity')
53
+ plotter.add_text('Intensity', font_size=12)
54
+ plotter.add_axes()
55
+
56
+ plotter.subplot(0, 1)
57
+ plotter.add_volume(lifetime_grid, scalars='lifetime_ns', cmap='turbo',
58
+ opacity='linear', name='lifetime')
59
+ plotter.add_scalar_bar('lifetime (ns)')
60
+ plotter.add_text('FLIM (lifetime)', font_size=12)
61
+ plotter.add_axes()
62
+
63
+ plotter.link_views()
64
+ plotter.show()
65
+
66
+
67
+ def main(argv=None):
68
+ parser = argparse.ArgumentParser(description=__doc__)
69
+ parser.add_argument('--zarr', required=True, help='Path to an OME-Zarr store '
70
+ 'written by flimkit_zstack_explorer.zarr_io.save_ome_zarr')
71
+ args = parser.parse_args(argv)
72
+
73
+ from flimkit_zstack_explorer.zarr_io import load_ome_zarr
74
+ intensity, lifetime = load_ome_zarr(args.zarr)
75
+ show(intensity, lifetime, title=f'FLIMKit 3D Z-stack Explorer - {args.zarr}')
76
+
77
+
78
+ if __name__ == '__main__':
79
+ main()
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+ import threading
3
+ from typing import Callable, Optional, Tuple
4
+
5
+ import numpy as np
6
+
7
+
8
+ def _run_on_ui_thread(app, callback: Callable):
9
+ """Run `callback` on the Tkinter main thread and block for its result.
10
+
11
+ FLIMKit's own FOV preview panel is Tkinter/matplotlib state and is not
12
+ thread-safe, so anything that touches it (loading a slice, reading back
13
+ the rendered maps) has to happen on the main thread even when this
14
+ function is called from a worker thread.
15
+ """
16
+ if threading.current_thread() is threading.main_thread():
17
+ return callback()
18
+ root = getattr(app, 'root', None)
19
+ if root is None or not callable(getattr(root, 'after', None)):
20
+ raise RuntimeError('FLIMKit UI is not available')
21
+ done = threading.Event()
22
+ outcome = {}
23
+
24
+ def run():
25
+ try:
26
+ outcome['value'] = callback()
27
+ except BaseException as error:
28
+ outcome['error'] = error
29
+ finally:
30
+ done.set()
31
+
32
+ root.after(0, run)
33
+ done.wait()
34
+ if 'error' in outcome:
35
+ raise outcome['error']
36
+ return outcome.get('value')
37
+
38
+
39
+ def build_zstack_volumes(
40
+ app,
41
+ progress: Optional[Callable[[int, int], None]] = None,
42
+ ) -> Tuple[np.ndarray, np.ndarray]:
43
+ """Build (intensity_stack, lifetime_stack) arrays, each shape (Z, Y, X),
44
+ from the z-stack currently loaded in FLIMKit's FOV preview panel.
45
+
46
+ Each z-slice is displayed through the panel's own `display_fit_results` /
47
+ `load_fov` (the same code path the app uses when you drag the z-slider)
48
+ so the maps match what you'd see on screen, rather than re-deriving the
49
+ intensity/lifetime math here. The panel is left showing whatever slice it
50
+ was on before this ran. Raises RuntimeError if no z-stack is loaded, or
51
+ if slices don't all share one shape.
52
+
53
+ Safe to call from any thread.
54
+ """
55
+ def get_zstack():
56
+ panel = getattr(app, '_fov_preview', None)
57
+ zstack = getattr(panel, '_zstack', None) if panel is not None else None
58
+ return panel, (list(zstack) if zstack else None)
59
+
60
+ panel, zstack = _run_on_ui_thread(app, get_zstack)
61
+ if not zstack:
62
+ raise RuntimeError('No z-stack is loaded in FLIMKit')
63
+
64
+ def show_slice(desc):
65
+ fit_result = desc.get('fit_result')
66
+ if fit_result is not None:
67
+ panel.display_fit_results(desc.get('ptu_path'), fit_result, _keep_zstack=True)
68
+ else:
69
+ panel.load_fov(desc.get('ptu_path'), _keep_zstack=True)
70
+ intensity = getattr(panel, '_intensity_map', None)
71
+ lifetime = getattr(panel, '_lifetime_map', None)
72
+ intensity = (np.array(intensity, dtype=np.float32, copy=True)
73
+ if intensity is not None else None)
74
+ lifetime = (np.array(lifetime, dtype=np.float32, copy=True)
75
+ if lifetime is not None else None)
76
+ return intensity, lifetime
77
+
78
+ original_i = _run_on_ui_thread(app, lambda: getattr(panel, '_z_i', 0))
79
+ intensity_slices = []
80
+ lifetime_slices = []
81
+ try:
82
+ for i, desc in enumerate(zstack):
83
+ intensity, lifetime = _run_on_ui_thread(app, lambda desc=desc: show_slice(desc))
84
+ if intensity is None:
85
+ raise RuntimeError(f'z-slice {i} ({desc.get("z", i)}) has no intensity image')
86
+ intensity_slices.append(intensity)
87
+ lifetime_slices.append(
88
+ lifetime if lifetime is not None
89
+ else np.full(intensity.shape[:2], np.nan, dtype=np.float32))
90
+ if progress is not None:
91
+ progress(i + 1, len(zstack))
92
+ finally:
93
+ def restore():
94
+ panel._z_i = original_i
95
+ panel._show_zstack_slice(original_i)
96
+ sync = getattr(panel, '_sync_z_slider', None)
97
+ if callable(sync):
98
+ sync()
99
+ _run_on_ui_thread(app, restore)
100
+
101
+ shape = intensity_slices[0].shape
102
+ for name, slices in (('intensity', intensity_slices), ('lifetime', lifetime_slices)):
103
+ for i, arr in enumerate(slices):
104
+ if arr.shape != shape:
105
+ raise RuntimeError(
106
+ f'{name} slice {i} has shape {arr.shape}, expected {shape} '
107
+ '(all z-slices must be the same size)')
108
+
109
+ return np.stack(intensity_slices), np.stack(lifetime_slices)
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Sequence
3
+
4
+ import numpy as np
5
+
6
+ CHANNELS = ('intensity', 'lifetime_ns')
7
+
8
+
9
+ def save_ome_zarr(
10
+ path: str,
11
+ intensity_stack: np.ndarray,
12
+ lifetime_stack: np.ndarray,
13
+ *,
14
+ voxel_size_um: Optional[Sequence[float]] = None,
15
+ ) -> str:
16
+ """Write intensity + lifetime volumes as a 2-channel OME-Zarr store.
17
+
18
+ `intensity_stack` and `lifetime_stack` must both be (Z, Y, X) and share
19
+ a shape. Channel 0 is intensity, channel 1 is lifetime in ns (NaN where
20
+ a slice had no fit). `voxel_size_um`, if given, is (z, y, x) physical
21
+ spacing used for the store's scale metadata; otherwise voxels are unit
22
+ spacing. Returns `path`.
23
+ """
24
+ if intensity_stack.shape != lifetime_stack.shape:
25
+ raise ValueError(
26
+ f'intensity {intensity_stack.shape} and lifetime {lifetime_stack.shape} '
27
+ 'stacks must have the same shape')
28
+ if intensity_stack.ndim != 3:
29
+ raise ValueError(f'expected (Z, Y, X) stacks, got shape {intensity_stack.shape}')
30
+
31
+ import zarr
32
+ from ome_zarr.io import parse_url
33
+ from ome_zarr.writer import write_image
34
+
35
+ data = np.stack([
36
+ np.nan_to_num(intensity_stack, nan=0.0).astype(np.float32),
37
+ lifetime_stack.astype(np.float32),
38
+ ])
39
+
40
+ store = parse_url(str(path), mode='w').store
41
+ root = zarr.group(store=store)
42
+
43
+ nz, ny, nx = intensity_stack.shape
44
+ chunks = (1, 1, min(256, ny), min(256, nx))
45
+ write_image(image=data, group=root, axes='czyx', scaler=None,
46
+ storage_options=dict(chunks=chunks))
47
+
48
+ scale = [1.0] + [float(v) for v in (voxel_size_um or (1.0, 1.0, 1.0))]
49
+ root.attrs['flimkit_zstack_explorer'] = {
50
+ 'channels': list(CHANNELS),
51
+ 'voxel_size_um': scale[1:],
52
+ }
53
+ return str(path)
54
+
55
+
56
+ def load_ome_zarr(path: str):
57
+ """Read back a store written by `save_ome_zarr`.
58
+
59
+ Returns (intensity_stack, lifetime_stack), each (Z, Y, X) numpy arrays.
60
+ """
61
+ import zarr
62
+ from ome_zarr.io import parse_url
63
+ from ome_zarr.reader import Reader
64
+
65
+ reader = Reader(parse_url(str(path)))
66
+ node = next(iter(reader()))
67
+ data = np.asarray(node.data[0])
68
+ if data.shape[0] != 2:
69
+ raise ValueError(f'expected a 2-channel store (intensity, lifetime), got shape {data.shape}')
70
+ return data[0], data[1]
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: flimkit-zstack-explorer
3
+ Version: 0.1.0
4
+ Summary: 3D intensity and FLIM volume explorer for FLIMKit z-stacks
5
+ Author-email: Alex Hunt <alexander.hunt@ed.ac.uk>
6
+ License-Expression: MIT
7
+ Keywords: FLIM,fluorescence,lifetime,z-stack,3D,zarr,pyvista
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: zarr>=2.16
13
+ Requires-Dist: ome-zarr>=0.9
14
+ Provides-Extra: gui
15
+ Requires-Dist: pyvista>=0.44; extra == "gui"
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Dynamic: license-file
19
+
20
+ # flimkit-zstack-explorer
21
+
22
+ 3D intensity and FLIM volume explorer for FLIMKit z-stacks. Takes a z-stack
23
+ you've already loaded or fitted in FLIMKit, stacks its per-slice intensity
24
+ and lifetime maps into `(Z, Y, X)` volumes, saves them as an OME-Zarr store,
25
+ and opens a popout 3D viewer with the two volumes side by side.
26
+
27
+ ## What it does
28
+
29
+ - Adds **Tools > 3D Z-stack Explorer...** to FLIMKit.
30
+ - Only does anything once a z-stack is loaded (FOV mode > Z-stack); otherwise
31
+ it tells you to load one first.
32
+ - Walks every slice in the loaded z-stack through the FOV preview panel's own
33
+ display code (the same path used when you drag the z-slider), so the
34
+ volume matches what's on screen slice by slice.
35
+ - Saves a 2-channel OME-Zarr store (channel 0 = intensity, channel 1 =
36
+ lifetime in ns, NaN where a slice had no per-pixel fit).
37
+ - Opens a [PyVista](https://pyvista.org) volume viewer in its own process —
38
+ intensity on the left, FLIM lifetime on the right, camera-linked — so it
39
+ doesn't compete with FLIMKit's Tkinter UI for the main thread.
40
+
41
+ ## Install
42
+
43
+ ```
44
+ pip install flimkit-zstack-explorer[gui]
45
+ ```
46
+
47
+ `[gui]` adds PyVista, which does the actual 3D rendering. Without it, the
48
+ volume-building and OME-Zarr saving still work, but the viewer step will
49
+ tell you it's missing and how to install it.
50
+
51
+ FLIMKit discovers this through the `flimkit.plugins` entry point, so there's
52
+ nothing else to wire up. On start it appears under Tools.
53
+
54
+ ## From code
55
+
56
+ ```python
57
+ from flimkit_zstack_explorer import build_zstack_volumes, save_ome_zarr, load_ome_zarr
58
+
59
+ # app is FLIMKit's running App instance, with a z-stack loaded in its
60
+ # FOV preview panel
61
+ intensity_stack, lifetime_stack = build_zstack_volumes(app)
62
+ save_ome_zarr('volume.zarr', intensity_stack, lifetime_stack,
63
+ voxel_size_um=(2.0, 0.5, 0.5))
64
+
65
+ intensity_stack, lifetime_stack = load_ome_zarr('volume.zarr')
66
+ ```
67
+
68
+ To view a store outside FLIMKit:
69
+
70
+ ```
71
+ python -m flimkit_zstack_explorer.viewer --zarr volume.zarr
72
+ ```
73
+
74
+ ## Why a separate process for the viewer
75
+
76
+ FLIMKit's UI is Tkinter; PyVista's is VTK. Running VTK's render loop on a
77
+ background thread inside a Tkinter process is unreliable, especially on
78
+ macOS, where GUI toolkits generally expect to own the main thread. Building
79
+ the volume and saving to disk happens inside FLIMKit; the popout viewer is a
80
+ plain `python -m flimkit_zstack_explorer.viewer` subprocess reading the
81
+ saved store, so it gets its own main thread and doesn't touch Tkinter at
82
+ all.
83
+
84
+ ## Development
85
+
86
+ ```
87
+ pip install -e .[gui,test]
88
+ pytest
89
+ ```
90
+
91
+ `tests/test_volumes.py` exercises the z-stack-to-array logic against a fake
92
+ FOV preview panel, so it runs without Tkinter or a real FLIMKit install.
93
+ `tests/test_zarr_io.py` round-trips real arrays through zarr/ome-zarr.
@@ -0,0 +1,11 @@
1
+ flimkit_zstack_explorer/__init__.py,sha256=pW_qMjT80Mnb1aMZCHhP6GGPgXBF7ffPka24clno5jY,452
2
+ flimkit_zstack_explorer/plugin.py,sha256=9F2UDuJMUayyj3PskH8bqYZCvffiag4r0QLWaiXIhmk,3298
3
+ flimkit_zstack_explorer/viewer.py,sha256=vKQEvC23HKylaZE6CcC75l5vwFAfukxvhiBm6KHx2jc,2921
4
+ flimkit_zstack_explorer/volumes.py,sha256=n3rP2hcFw0a3fcK-YrnEtp0p2k1rxKAq32toXEubo0Y,4319
5
+ flimkit_zstack_explorer/zarr_io.py,sha256=HAfqCCDeelY8xEnWADdYsvgXSP6mmfoAkXdFT3ekvxw,2338
6
+ flimkit_zstack_explorer-0.1.0.dist-info/licenses/LICENSE,sha256=xRkLMCd6sG_d7rMTncTVtj94vE9hvqc3Dpwl24IpfiU,1064
7
+ flimkit_zstack_explorer-0.1.0.dist-info/METADATA,sha256=hdakiIR5NSLYk1_t4oI5R_K1En4jgIIgyrUwgEN1dUE,3336
8
+ flimkit_zstack_explorer-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ flimkit_zstack_explorer-0.1.0.dist-info/entry_points.txt,sha256=U_gF0roUnvUsu3hlCHabbNbHGvWYv1AaD8Ys58a2134,68
10
+ flimkit_zstack_explorer-0.1.0.dist-info/top_level.txt,sha256=vMm_lu3R3uwr_SEW05dLX2wwSdPeAjbSNL0MeLHvMXI,24
11
+ flimkit_zstack_explorer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [flimkit.plugins]
2
+ flimkit_zstack_explorer = flimkit_zstack_explorer
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FLIMKit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flimkit_zstack_explorer