TwoPy 0.1.5__tar.gz → 0.1.6__tar.gz
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.
- {twopy-0.1.5 → twopy-0.1.6}/PKG-INFO +1 -1
- {twopy-0.1.5 → twopy-0.1.6}/pyproject.toml +1 -1
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/__init__.py +16 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/modeled.py +7 -0
- twopy-0.1.6/src/twopy/database/search.py +433 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/controls.py +224 -45
- twopy-0.1.6/src/twopy/napari/database_search.py +491 -0
- twopy-0.1.6/src/twopy/napari/errors.py +24 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/interactive.py +2 -2
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/data.py +43 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/dynamic_options.py +7 -1
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/response_plot_widget.py +87 -22
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/widgets.py +23 -7
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/roi.py +41 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/PKG-INFO +1 -1
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/SOURCES.txt +3 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_database.py +204 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_napari.py +589 -0
- {twopy-0.1.5 → twopy-0.1.6}/README.md +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/setup.cfg +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/_segments.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/_version.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/background_subtraction.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/dff.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/dff_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/epoch_mapping.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/motion.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/persistence.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/apply.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/persistence.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/qc.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_processing/signals.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/response_window_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/responses.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/trials.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/analysis/workflow.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/api.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/config.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/alignment_crop.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/api.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/hdf5_writing.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/matlab_values.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/source_loading.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/stimulus_code.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/conversion/types.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/converted.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/cache.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/catalog.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/connection.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/generic.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/sql.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/database/types.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/frame_ranges.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/inspection.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/matlab.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/__main__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/constants.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/display.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/display_paths.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/launcher.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/loading.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/movie.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/paths.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/dff_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/api.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/options_panel.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/paths.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/export.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/export_controls.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/form_controls.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/label_visibility.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/panels.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/processing_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/plotting/response_window_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/protocols.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/responses.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/session.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/sidebar.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/state.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/text.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/types.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/napari/viewer.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/parity/__init__.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/parity/dff.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/parity/psycho5.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/parity/roi.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/parity/saved_analysis.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/photodiode.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/photodiode_classification.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/roi.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/session.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/spatial.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/stimulus.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/synchronization.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy/typing_guards.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/dependency_links.txt +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/entry_points.txt +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/requires.txt +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/src/twopy.egg-info/top_level.txt +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_api.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_background_subtraction.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_config.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_conversion.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_converted.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_dff.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_epoch_mapping.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_frame_ranges.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_import.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_inspection.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_matlab.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_motion.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_packaging.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_parity.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_parity_psycho5.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_persistence.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_photodiode_classification.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_real_fixture.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_response_processing.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_response_window_options.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_responses.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_roi.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_saved_analysis.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_session.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_stimulus.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_synchronization.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_trials.py +0 -0
- {twopy-0.1.5 → twopy-0.1.6}/tests/test_workflow.py +0 -0
|
@@ -10,6 +10,15 @@ focused modules.
|
|
|
10
10
|
from twopy.database.catalog import read_database_catalog
|
|
11
11
|
from twopy.database.generic import find_experiments
|
|
12
12
|
from twopy.database.modeled import find_recordings, find_stimulus_presentations
|
|
13
|
+
from twopy.database.search import (
|
|
14
|
+
ExperimentHierarchyField,
|
|
15
|
+
ExperimentSearchFilters,
|
|
16
|
+
ExperimentSearchNode,
|
|
17
|
+
build_experiment_search_tree,
|
|
18
|
+
find_recording_search_results,
|
|
19
|
+
recording_path_for_database_experiment,
|
|
20
|
+
recording_paths_for_database_experiments,
|
|
21
|
+
)
|
|
13
22
|
from twopy.database.types import (
|
|
14
23
|
DatabaseAccess,
|
|
15
24
|
DatabaseCatalog,
|
|
@@ -26,8 +35,15 @@ __all__ = [
|
|
|
26
35
|
"DatabaseExperiment",
|
|
27
36
|
"DatabaseRecord",
|
|
28
37
|
"DatabaseTable",
|
|
38
|
+
"ExperimentHierarchyField",
|
|
39
|
+
"ExperimentSearchFilters",
|
|
40
|
+
"ExperimentSearchNode",
|
|
41
|
+
"build_experiment_search_tree",
|
|
29
42
|
"find_experiments",
|
|
30
43
|
"find_recordings",
|
|
44
|
+
"find_recording_search_results",
|
|
31
45
|
"find_stimulus_presentations",
|
|
32
46
|
"read_database_catalog",
|
|
47
|
+
"recording_path_for_database_experiment",
|
|
48
|
+
"recording_paths_for_database_experiments",
|
|
33
49
|
]
|
|
@@ -25,6 +25,7 @@ def find_stimulus_presentations(
|
|
|
25
25
|
date_year: int | None = None,
|
|
26
26
|
date_month: int | None = None,
|
|
27
27
|
date_day: int | None = None,
|
|
28
|
+
date_contains: str | None = None,
|
|
28
29
|
path_contains: str | None = None,
|
|
29
30
|
stimulus_contains: str | None = None,
|
|
30
31
|
genotype_contains: str | None = None,
|
|
@@ -43,6 +44,8 @@ def find_stimulus_presentations(
|
|
|
43
44
|
date_year: Optional four-digit year matched against experiment date.
|
|
44
45
|
date_month: Optional month number matched against experiment date.
|
|
45
46
|
date_day: Optional day-of-month matched against experiment date.
|
|
47
|
+
date_contains: Optional substring matched against full experiment date
|
|
48
|
+
text.
|
|
46
49
|
path_contains: Optional substring matched against ``relativeDataPath``.
|
|
47
50
|
stimulus_contains: Optional substring matched against ``stimulusFunction``.
|
|
48
51
|
genotype_contains: Optional substring matched against fly ``genotype``.
|
|
@@ -87,6 +90,7 @@ def find_stimulus_presentations(
|
|
|
87
90
|
date_year=date_year,
|
|
88
91
|
date_month=date_month,
|
|
89
92
|
date_day=date_day,
|
|
93
|
+
date_contains=date_contains,
|
|
90
94
|
path_contains=path_contains,
|
|
91
95
|
stimulus_contains=stimulus_contains,
|
|
92
96
|
genotype_contains=genotype_contains,
|
|
@@ -195,6 +199,7 @@ def _query_stimulus_presentations(
|
|
|
195
199
|
date_year: int | None,
|
|
196
200
|
date_month: int | None,
|
|
197
201
|
date_day: int | None,
|
|
202
|
+
date_contains: str | None,
|
|
198
203
|
path_contains: str | None,
|
|
199
204
|
stimulus_contains: str | None,
|
|
200
205
|
genotype_contains: str | None,
|
|
@@ -211,6 +216,7 @@ def _query_stimulus_presentations(
|
|
|
211
216
|
date_year: Optional experiment year.
|
|
212
217
|
date_month: Optional experiment month.
|
|
213
218
|
date_day: Optional experiment day-of-month.
|
|
219
|
+
date_contains: Optional substring matched against experiment date.
|
|
214
220
|
path_contains: Optional relative path substring.
|
|
215
221
|
stimulus_contains: Optional stimulus-function substring.
|
|
216
222
|
genotype_contains: Optional genotype substring.
|
|
@@ -241,6 +247,7 @@ def _query_stimulus_presentations(
|
|
|
241
247
|
parameters=parameters,
|
|
242
248
|
filters={
|
|
243
249
|
'CAST(sp."relativeDataPath" AS TEXT) LIKE ?': path_contains,
|
|
250
|
+
'CAST(sp."date" AS TEXT) LIKE ?': date_contains,
|
|
244
251
|
'CAST(sp."stimulusFunction" AS TEXT) LIKE ?': stimulus_contains,
|
|
245
252
|
'CAST(f."genotype" AS TEXT) LIKE ?': genotype_contains,
|
|
246
253
|
'CAST(f."fluorescentProtein" AS TEXT) LIKE ?': sensor_contains,
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Database search helpers for browsing recording experiments.
|
|
2
|
+
|
|
3
|
+
Inputs: configured lab database paths, user-facing text filters, and typed
|
|
4
|
+
``DatabaseExperiment`` rows.
|
|
5
|
+
Outputs: filtered experiment rows, browsable hierarchy nodes, and source
|
|
6
|
+
recording paths.
|
|
7
|
+
|
|
8
|
+
The napari search dialog uses this module so database querying, grouping, and
|
|
9
|
+
path resolution stay testable without Qt imports.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path, PurePosixPath
|
|
17
|
+
from typing import TYPE_CHECKING, Literal
|
|
18
|
+
|
|
19
|
+
from twopy.database.modeled import find_stimulus_presentations
|
|
20
|
+
from twopy.database.types import DatabaseExperiment
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from twopy.config import TwopyConfig
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ExperimentHierarchyField",
|
|
27
|
+
"ExperimentSearchFilters",
|
|
28
|
+
"ExperimentSearchNode",
|
|
29
|
+
"build_experiment_search_tree",
|
|
30
|
+
"date_label_for_experiment",
|
|
31
|
+
"find_recording_search_results",
|
|
32
|
+
"recording_path_for_database_experiment",
|
|
33
|
+
"recording_paths_for_database_experiments",
|
|
34
|
+
"time_label_for_experiment",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
ExperimentHierarchyField = Literal[
|
|
38
|
+
"user",
|
|
39
|
+
"cell_type",
|
|
40
|
+
"sensor",
|
|
41
|
+
"stimulus",
|
|
42
|
+
"date",
|
|
43
|
+
"time",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
UNKNOWN_LABEL = "(unknown)"
|
|
47
|
+
DEFAULT_EXPERIMENT_SEARCH_LIMIT = 50_000
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ExperimentSearchFilters:
|
|
52
|
+
"""User-facing filters for recording database search.
|
|
53
|
+
|
|
54
|
+
Inputs: optional text from the database-search dialog.
|
|
55
|
+
Outputs: immutable normalized filter values passed to database search.
|
|
56
|
+
|
|
57
|
+
Blank strings mean no filtering, matching the GUI contract.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
user: str | None = None
|
|
61
|
+
cell_type: str | None = None
|
|
62
|
+
sensor: str | None = None
|
|
63
|
+
stimulus: str | None = None
|
|
64
|
+
date: str | None = None
|
|
65
|
+
limit: int = DEFAULT_EXPERIMENT_SEARCH_LIMIT
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ExperimentSearchNode:
|
|
70
|
+
"""One node in the recording search hierarchy.
|
|
71
|
+
|
|
72
|
+
Inputs: a hierarchy label, the field it represents, child nodes, and the
|
|
73
|
+
experiments below it.
|
|
74
|
+
Outputs: an immutable tree the Qt dialog can render directly.
|
|
75
|
+
|
|
76
|
+
Every non-root node carries all descendant experiments so selecting a user,
|
|
77
|
+
cell type, stimulus, date, or time can load the matching recordings.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
label: str
|
|
81
|
+
field: ExperimentHierarchyField | None
|
|
82
|
+
experiments: tuple[DatabaseExperiment, ...]
|
|
83
|
+
children: tuple[ExperimentSearchNode, ...] = ()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def find_recording_search_results(
|
|
87
|
+
config: TwopyConfig,
|
|
88
|
+
filters: ExperimentSearchFilters,
|
|
89
|
+
*,
|
|
90
|
+
cache_dir: Path | None = None,
|
|
91
|
+
) -> tuple[DatabaseExperiment, ...]:
|
|
92
|
+
"""Find recording experiments for the database-search dialog.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
config: Loaded twopy configuration.
|
|
96
|
+
filters: User-facing text filters. Blank values are ignored.
|
|
97
|
+
cache_dir: Optional local database-copy cache.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Typed database experiments matching all active filters.
|
|
101
|
+
|
|
102
|
+
The SQL query stays in the database package. This helper maps GUI labels
|
|
103
|
+
such as ``user`` and ``sensor`` onto the stable modeled query fields.
|
|
104
|
+
"""
|
|
105
|
+
experiments = find_stimulus_presentations(
|
|
106
|
+
config.database_path,
|
|
107
|
+
date_contains=_clean_filter(filters.date),
|
|
108
|
+
stimulus_contains=_clean_filter(filters.stimulus),
|
|
109
|
+
sensor_contains=_clean_filter(filters.sensor),
|
|
110
|
+
cell_type_contains=_clean_filter(filters.cell_type),
|
|
111
|
+
person_contains=_clean_filter(filters.user),
|
|
112
|
+
limit=filters.limit,
|
|
113
|
+
database_access=config.database_access,
|
|
114
|
+
cache_dir=cache_dir,
|
|
115
|
+
)
|
|
116
|
+
return _unique_experiments_by_relative_path(experiments)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_experiment_search_tree(
|
|
120
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
121
|
+
) -> ExperimentSearchNode:
|
|
122
|
+
"""Build the folder-like hierarchy for database search results.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
experiments: Database rows to group.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Root node with descendants grouped by user, cell type, sensor, stimulus,
|
|
129
|
+
date, then experiment time.
|
|
130
|
+
|
|
131
|
+
Nodes are sorted by display label so repeated searches render in a stable
|
|
132
|
+
order. Unknown values are grouped explicitly instead of disappearing.
|
|
133
|
+
"""
|
|
134
|
+
return ExperimentSearchNode(
|
|
135
|
+
label="Results",
|
|
136
|
+
field=None,
|
|
137
|
+
experiments=experiments,
|
|
138
|
+
children=_group_nodes(
|
|
139
|
+
experiments,
|
|
140
|
+
field="user",
|
|
141
|
+
label_for_experiment=lambda experiment: _optional_label(experiment.person),
|
|
142
|
+
next_builder=_cell_type_nodes,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def recording_path_for_database_experiment(
|
|
148
|
+
config: TwopyConfig,
|
|
149
|
+
experiment: DatabaseExperiment,
|
|
150
|
+
) -> Path:
|
|
151
|
+
"""Resolve one database experiment into a source recording folder.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
config: Loaded twopy configuration with ``data_path``.
|
|
155
|
+
experiment: Database row with a relative data path.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Source recording folder under the configured data root.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If the database row does not contain a safe relative path.
|
|
162
|
+
|
|
163
|
+
The database stores Windows-style separators in observed rows. Twopy treats
|
|
164
|
+
the value as relative path text and resolves it beneath ``data_path`` before
|
|
165
|
+
passing it to the existing recording loader.
|
|
166
|
+
"""
|
|
167
|
+
relative_path = _normalized_relative_path(experiment.relative_data_path)
|
|
168
|
+
if relative_path == "":
|
|
169
|
+
msg = (
|
|
170
|
+
"Database experiment has an empty relativeDataPath: "
|
|
171
|
+
f"{experiment.stimulus_presentation_id}"
|
|
172
|
+
)
|
|
173
|
+
raise ValueError(msg)
|
|
174
|
+
|
|
175
|
+
path_parts = PurePosixPath(relative_path).parts
|
|
176
|
+
if any(part in {"", ".", ".."} for part in path_parts):
|
|
177
|
+
msg = (
|
|
178
|
+
"Database experiment relativeDataPath must stay under data_path: "
|
|
179
|
+
f"{experiment.relative_data_path!r}"
|
|
180
|
+
)
|
|
181
|
+
raise ValueError(msg)
|
|
182
|
+
|
|
183
|
+
return config.data_path.expanduser().joinpath(*path_parts)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def recording_paths_for_database_experiments(
|
|
187
|
+
config: TwopyConfig,
|
|
188
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
189
|
+
) -> tuple[Path, ...]:
|
|
190
|
+
"""Resolve database experiments into unique source recording folders.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
config: Loaded twopy configuration with ``data_path``.
|
|
194
|
+
experiments: Database rows to load.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Unique source recording folders in search-result order.
|
|
198
|
+
|
|
199
|
+
Parent hierarchy nodes can contain multiple database rows. Deduplicating
|
|
200
|
+
here prevents the GUI from loading the same folder twice when duplicate
|
|
201
|
+
rows point at one recording.
|
|
202
|
+
"""
|
|
203
|
+
paths: list[Path] = []
|
|
204
|
+
seen: set[Path] = set()
|
|
205
|
+
for experiment in experiments:
|
|
206
|
+
path = recording_path_for_database_experiment(config, experiment)
|
|
207
|
+
if path not in seen:
|
|
208
|
+
paths.append(path)
|
|
209
|
+
seen.add(path)
|
|
210
|
+
return tuple(paths)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def date_label_for_experiment(experiment: DatabaseExperiment) -> str:
|
|
214
|
+
"""Return the date folder label for one experiment.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
experiment: Database experiment row.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
``YYYY-MM-DD`` when available, otherwise ``(unknown)``.
|
|
221
|
+
"""
|
|
222
|
+
date_parts = _date_time_parts_from_relative_path(experiment.relative_data_path)
|
|
223
|
+
if date_parts is not None:
|
|
224
|
+
year, month_day, _time = date_parts
|
|
225
|
+
return f"{year}-{month_day.replace('_', '-')}"
|
|
226
|
+
|
|
227
|
+
if experiment.date is not None and len(experiment.date) >= 10:
|
|
228
|
+
return experiment.date[:10]
|
|
229
|
+
return UNKNOWN_LABEL
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def time_label_for_experiment(experiment: DatabaseExperiment) -> str:
|
|
233
|
+
"""Return the experiment-time label beneath one date node.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
experiment: Database experiment row.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
``HH:MM:SS`` when available, otherwise a stable experiment id label.
|
|
240
|
+
"""
|
|
241
|
+
date_parts = _date_time_parts_from_relative_path(experiment.relative_data_path)
|
|
242
|
+
if date_parts is not None:
|
|
243
|
+
_year, _month_day, time = date_parts
|
|
244
|
+
return time.replace("_", ":")
|
|
245
|
+
|
|
246
|
+
if experiment.date is not None and len(experiment.date) >= 19:
|
|
247
|
+
return experiment.date[11:19]
|
|
248
|
+
return f"id {experiment.stimulus_presentation_id}"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _unique_experiments_by_relative_path(
|
|
252
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
253
|
+
) -> tuple[DatabaseExperiment, ...]:
|
|
254
|
+
"""Return the first experiment row for each source recording path.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
experiments: Database rows from all configured SQLite files.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Deduplicated rows in original query order.
|
|
261
|
+
|
|
262
|
+
The lab keeps the same stimulus rows in ``experimentLog.db`` and
|
|
263
|
+
``experimentInitLog.db``. The GUI browses source recordings, so duplicate DB
|
|
264
|
+
rows for one ``relativeDataPath`` should appear as one experiment.
|
|
265
|
+
"""
|
|
266
|
+
unique: list[DatabaseExperiment] = []
|
|
267
|
+
seen: set[str] = set()
|
|
268
|
+
for experiment in experiments:
|
|
269
|
+
key = _normalized_relative_path(experiment.relative_data_path)
|
|
270
|
+
if key in seen:
|
|
271
|
+
continue
|
|
272
|
+
unique.append(experiment)
|
|
273
|
+
seen.add(key)
|
|
274
|
+
return tuple(unique)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _date_time_parts_from_relative_path(
|
|
278
|
+
relative_data_path: str,
|
|
279
|
+
) -> tuple[str, str, str] | None:
|
|
280
|
+
"""Return ``YYYY``, ``MM_DD``, and ``HH_MM_SS`` from a source path."""
|
|
281
|
+
parts = PurePosixPath(_normalized_relative_path(relative_data_path)).parts
|
|
282
|
+
for index in range(len(parts) - 2):
|
|
283
|
+
year = parts[index]
|
|
284
|
+
month_day = parts[index + 1]
|
|
285
|
+
time = parts[index + 2]
|
|
286
|
+
if (
|
|
287
|
+
len(year) == 4
|
|
288
|
+
and year.isdigit()
|
|
289
|
+
and _is_underscore_date(month_day)
|
|
290
|
+
and _is_underscore_time(time)
|
|
291
|
+
):
|
|
292
|
+
return year, month_day, time
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _cell_type_nodes(
|
|
297
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
298
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
299
|
+
"""Group user children by cell type."""
|
|
300
|
+
return _group_nodes(
|
|
301
|
+
experiments,
|
|
302
|
+
field="cell_type",
|
|
303
|
+
label_for_experiment=lambda experiment: _optional_label(experiment.cell_type),
|
|
304
|
+
next_builder=_sensor_nodes,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _sensor_nodes(
|
|
309
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
310
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
311
|
+
"""Group cell-type children by sensor."""
|
|
312
|
+
return _group_nodes(
|
|
313
|
+
experiments,
|
|
314
|
+
field="sensor",
|
|
315
|
+
label_for_experiment=lambda experiment: _optional_label(
|
|
316
|
+
experiment.fluorescent_protein
|
|
317
|
+
),
|
|
318
|
+
next_builder=_stimulus_nodes,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _stimulus_nodes(
|
|
323
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
324
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
325
|
+
"""Group sensor children by stimulus function."""
|
|
326
|
+
return _group_nodes(
|
|
327
|
+
experiments,
|
|
328
|
+
field="stimulus",
|
|
329
|
+
label_for_experiment=lambda experiment: _optional_label(
|
|
330
|
+
experiment.stimulus_function
|
|
331
|
+
),
|
|
332
|
+
next_builder=_date_nodes,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _date_nodes(
|
|
337
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
338
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
339
|
+
"""Group stimulus children by date."""
|
|
340
|
+
return _group_nodes(
|
|
341
|
+
experiments,
|
|
342
|
+
field="date",
|
|
343
|
+
label_for_experiment=date_label_for_experiment,
|
|
344
|
+
next_builder=_time_nodes,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _time_nodes(
|
|
349
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
350
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
351
|
+
"""Group date children by experiment time."""
|
|
352
|
+
return _group_nodes(
|
|
353
|
+
experiments,
|
|
354
|
+
field="time",
|
|
355
|
+
label_for_experiment=time_label_for_experiment,
|
|
356
|
+
next_builder=lambda grouped: (),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _group_nodes(
|
|
361
|
+
experiments: tuple[DatabaseExperiment, ...],
|
|
362
|
+
*,
|
|
363
|
+
field: ExperimentHierarchyField,
|
|
364
|
+
label_for_experiment: Callable[[DatabaseExperiment], str],
|
|
365
|
+
next_builder: Callable[
|
|
366
|
+
[tuple[DatabaseExperiment, ...]],
|
|
367
|
+
tuple[ExperimentSearchNode, ...],
|
|
368
|
+
],
|
|
369
|
+
) -> tuple[ExperimentSearchNode, ...]:
|
|
370
|
+
"""Group experiments for one hierarchy level."""
|
|
371
|
+
grouped: dict[str, list[DatabaseExperiment]] = {}
|
|
372
|
+
for experiment in experiments:
|
|
373
|
+
grouped.setdefault(label_for_experiment(experiment), []).append(experiment)
|
|
374
|
+
|
|
375
|
+
nodes: list[ExperimentSearchNode] = []
|
|
376
|
+
for label in sorted(grouped, key=_sort_label):
|
|
377
|
+
node_experiments = tuple(grouped[label])
|
|
378
|
+
nodes.append(
|
|
379
|
+
ExperimentSearchNode(
|
|
380
|
+
label=label,
|
|
381
|
+
field=field,
|
|
382
|
+
experiments=node_experiments,
|
|
383
|
+
children=next_builder(node_experiments),
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
return tuple(nodes)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _optional_label(value: str | None) -> str:
|
|
390
|
+
"""Return a display label for a nullable database value."""
|
|
391
|
+
if value is None or value == "":
|
|
392
|
+
return UNKNOWN_LABEL
|
|
393
|
+
return value
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _normalized_relative_path(relative_data_path: str) -> str:
|
|
397
|
+
"""Normalize a database source path for grouping and deduplication."""
|
|
398
|
+
return relative_data_path.replace("\\", "/").strip("/")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _is_underscore_date(value: str) -> bool:
|
|
402
|
+
"""Return whether text looks like a source-folder ``MM_DD`` date."""
|
|
403
|
+
return (
|
|
404
|
+
len(value) == 5
|
|
405
|
+
and value[2] == "_"
|
|
406
|
+
and value[:2].isdigit()
|
|
407
|
+
and value[3:].isdigit()
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _is_underscore_time(value: str) -> bool:
|
|
412
|
+
"""Return whether text looks like a source-folder ``HH_MM_SS`` time."""
|
|
413
|
+
return (
|
|
414
|
+
len(value) == 8
|
|
415
|
+
and value[2] == "_"
|
|
416
|
+
and value[5] == "_"
|
|
417
|
+
and value[:2].isdigit()
|
|
418
|
+
and value[3:5].isdigit()
|
|
419
|
+
and value[6:].isdigit()
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _clean_filter(value: str | None) -> str | None:
|
|
424
|
+
"""Return a non-empty stripped filter string or ``None``."""
|
|
425
|
+
if value is None:
|
|
426
|
+
return None
|
|
427
|
+
cleaned = value.strip()
|
|
428
|
+
return cleaned or None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _sort_label(label: str) -> tuple[bool, str]:
|
|
432
|
+
"""Sort unknown values last and all other labels case-insensitively."""
|
|
433
|
+
return (label == UNKNOWN_LABEL, label.casefold())
|