TwoPy 0.4.0__tar.gz → 0.4.1__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.4.0 → twopy-0.4.1}/PKG-INFO +1 -1
- {twopy-0.4.0 → twopy-0.4.1}/pyproject.toml +1 -1
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/api.py +5 -11
- twopy-0.4.1/src/twopy/database/date_filters.py +294 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/modeled.py +89 -94
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/search.py +12 -22
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_favorite_editor.py +3 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_favorites.py +29 -28
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_search.py +3 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/PKG-INFO +1 -1
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/SOURCES.txt +2 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_api.py +57 -12
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_database.py +8 -34
- twopy-0.4.1/tests/test_database_date_filters.py +275 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_database_search.py +78 -12
- {twopy-0.4.0 → twopy-0.4.1}/README.md +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/setup.cfg +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/__init__.pyi +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/_segments.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/_version.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/background_subtraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/dff.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/dff_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/epoch_mapping.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/group_matching.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/_fitting.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/_stimulus.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/motion.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/persistence.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_context.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_maps.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_plotting.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/apply.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/persistence.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/qc.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/signals.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_window_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/responses.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/timing.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/trials.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/workflow.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis_cache.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/cache_inventory.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/calibrations/pixel_size.csv +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/config.example.yml +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/config.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/alignment_crop.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/api.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/hdf5_writing.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/matlab_values.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/orientation.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/source_loading.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/stimulus_code.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/types.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/converted.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/context.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/discovery.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/metadata.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/direction_selectivity.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/parameters.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/provenance.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/registry.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/results.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/runner.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/workflows.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/cache.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/catalog.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/connection.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/generic.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/sql.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/types.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/filenames.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/frame_ranges.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/hdf5_utils.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/inspection.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/matlab.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/__main__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-128.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-16.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-256.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-32.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-512.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-64.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon.icns +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon.png +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/custom_tab.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/dims.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/display.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/display_paths.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/empty_state.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/errors.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/cards.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/fov_assignment.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/fov_cards.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/images.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/protocols.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/response_preview.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/responses.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/roi_assignment.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/roi_cards.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/style.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/tables.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/window.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/interactive.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/latest_worker.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/launcher.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/live_analysis.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_controller.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_picker.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_progress.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_workflow.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/loaded_recordings_csv.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/loading.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/movie.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/output_routing.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/paths.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/data.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/dff_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/api.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/options_panel.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/paths.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/response_plot_widget.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/export.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/export_controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/form_controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/label_visibility.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/motion_summary.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/normalization_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/panels.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/preview_strip.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/processing_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_colors.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_display.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_window_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/widgets.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/protocols.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/responses.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/roi.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/roi_signatures.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/session.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/sidebar.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/state.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/text.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/theme.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/trial_timeline.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/types.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/version_check.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/viewer.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/__init__.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/dff.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/psycho5.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/roi.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/saved_analysis.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/photodiode.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/photodiode_classification.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/pixel_calibration.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/pixel_calibration_profiles.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/response_roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi_mask_cleanup.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/session.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/spatial.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/spatial_orientation.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/stimulus.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/synchronization.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy/typing_guards.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/dependency_links.txt +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/entry_points.txt +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/requires.txt +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/top_level.txt +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_background_subtraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_cache_inventory.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_config.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_conversion.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_converted.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_custom_workflows.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_dff.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_epoch_mapping.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_filenames.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_frame_ranges.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_function_inventory.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_group_matching.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_hdf5_utils.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_import.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_inspection.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_kernels.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_load_workflow.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_matlab.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_motion.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_display.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_export.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_group_matching_responses.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_group_matching_roi_cards.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_live_controller.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_controller.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_picker.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_progress.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_tab.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_loaded_recordings.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_path_resolution.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_data.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_widgets.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_processing_controls.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_response_map_display.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_response_workflow.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_roi_generation.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_roi_visibility.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_state.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_text.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_theme.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_timeline.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_packaging.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity_psycho5.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity_roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_persistence.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_photodiode_classification.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_pixel_calibration.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_pixel_calibration_profiles.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_real_fixture.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_recording_timing.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_map_area.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_maps.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_processing.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_window_options.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_responses.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi_extraction.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi_mask_cleanup.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_saved_analysis.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_session.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_stimulus.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_synchronization.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_trials.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_version_check.py +0 -0
- {twopy-0.4.0 → twopy-0.4.1}/tests/test_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Public script-friendly API for twopy.
|
|
2
2
|
|
|
3
|
-
Inputs: simple Python arguments such as
|
|
3
|
+
Inputs: simple Python arguments such as date, genotype, stimulus, and paths.
|
|
4
4
|
Outputs: typed twopy objects returned by lower-level modules.
|
|
5
5
|
|
|
6
6
|
This module is the stable place for scripts to import from. It keeps scripts away
|
|
@@ -19,9 +19,7 @@ __all__ = ["find_recordings", "finite_mean_and_sem"]
|
|
|
19
19
|
|
|
20
20
|
def find_recordings(
|
|
21
21
|
*,
|
|
22
|
-
|
|
23
|
-
month: int | None = None,
|
|
24
|
-
day: int | None = None,
|
|
22
|
+
date: str | None = None,
|
|
25
23
|
genotype: str | None = None,
|
|
26
24
|
stimulus: str | None = None,
|
|
27
25
|
sensor: str | None = None,
|
|
@@ -36,9 +34,7 @@ def find_recordings(
|
|
|
36
34
|
"""Find recording experiments using the configured lab database path.
|
|
37
35
|
|
|
38
36
|
Args:
|
|
39
|
-
|
|
40
|
-
month: Optional experiment month.
|
|
41
|
-
day: Optional experiment day-of-month.
|
|
37
|
+
date: Optional date expression with selectors and inclusive ranges.
|
|
42
38
|
genotype: Optional genotype substring.
|
|
43
39
|
stimulus: Optional stimulus-function substring.
|
|
44
40
|
sensor: Optional sensor substring from fly ``fluorescentProtein``.
|
|
@@ -58,7 +54,7 @@ def find_recordings(
|
|
|
58
54
|
Typed recording experiment rows.
|
|
59
55
|
|
|
60
56
|
Example:
|
|
61
|
-
``find_recordings(
|
|
57
|
+
``find_recordings(date="2023-10", genotype="gh146")``
|
|
62
58
|
|
|
63
59
|
The function loads twopy config by default so scripts can start with a
|
|
64
60
|
single import and call after ``twopy config setup``.
|
|
@@ -66,9 +62,7 @@ def find_recordings(
|
|
|
66
62
|
config = load_config(config_path)
|
|
67
63
|
return _find_recordings(
|
|
68
64
|
config.database_path,
|
|
69
|
-
|
|
70
|
-
month=month,
|
|
71
|
-
day=day,
|
|
65
|
+
date=date,
|
|
72
66
|
genotype=genotype,
|
|
73
67
|
stimulus=stimulus,
|
|
74
68
|
sensor=sensor,
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Parse explicit date expressions for recording database searches.
|
|
2
|
+
|
|
3
|
+
Inputs: date filter text from the search dialog or scripting API.
|
|
4
|
+
Outputs: validated selectors for safe, parameterized database queries.
|
|
5
|
+
|
|
6
|
+
The parser keeps date meaning separate from SQLite text matching. This makes
|
|
7
|
+
lists, partial dates, and inclusive ranges clear before a query starts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import date
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
EXPERIMENT_DATE_FILTER_HELP = (
|
|
18
|
+
"Use YYYY, YYYY-MM, MM-DD, or YYYY-MM-DD.\n"
|
|
19
|
+
"Separate selections with commas. Use two full dates with '..' for an "
|
|
20
|
+
"inclusive range.\n"
|
|
21
|
+
"Date parts accept '-', '/', '\\', '.', or '_'."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DATE_SEPARATOR = r"[-/\\._]"
|
|
25
|
+
_YEAR_PATTERN = re.compile(r"^(\d{4})$")
|
|
26
|
+
_YEAR_MONTH_PATTERN = re.compile(rf"^(\d{{4}}){_DATE_SEPARATOR}(\d{{1,2}})$")
|
|
27
|
+
_MONTH_DAY_PATTERN = re.compile(rf"^(\d{{1,2}}){_DATE_SEPARATOR}(\d{{1,2}})$")
|
|
28
|
+
_FULL_DATE_PATTERN = re.compile(
|
|
29
|
+
rf"^(\d{{4}}){_DATE_SEPARATOR}(\d{{1,2}}){_DATE_SEPARATOR}(\d{{1,2}})$"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
DateSelectorKind = Literal["year", "year_month", "month_day", "date"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ExperimentDateSelector:
|
|
37
|
+
"""One validated date or partial-date selection.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
kind: Date parts that take part in the database comparison.
|
|
41
|
+
year: Four-digit year when ``kind`` includes a year.
|
|
42
|
+
month: Month number when ``kind`` includes a month.
|
|
43
|
+
day: Day number when ``kind`` includes a day.
|
|
44
|
+
|
|
45
|
+
Outputs:
|
|
46
|
+
Immutable date parts with a canonical user-facing representation.
|
|
47
|
+
|
|
48
|
+
The explicit kind prevents a partial date from becoming an arbitrary text
|
|
49
|
+
match in the database.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
kind: DateSelectorKind
|
|
53
|
+
year: int | None = None
|
|
54
|
+
month: int | None = None
|
|
55
|
+
day: int | None = None
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
"""Validate the date parts required by the selector kind."""
|
|
59
|
+
expected_parts = {
|
|
60
|
+
"year": (True, False, False),
|
|
61
|
+
"year_month": (True, True, False),
|
|
62
|
+
"month_day": (False, True, True),
|
|
63
|
+
"date": (True, True, True),
|
|
64
|
+
}[self.kind]
|
|
65
|
+
actual_parts = (
|
|
66
|
+
self.year is not None,
|
|
67
|
+
self.month is not None,
|
|
68
|
+
self.day is not None,
|
|
69
|
+
)
|
|
70
|
+
if actual_parts != expected_parts:
|
|
71
|
+
msg = f"Date selector {self.kind!r} has incorrect date parts."
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
|
|
74
|
+
validation_year = self.year if self.year is not None else 2000
|
|
75
|
+
validation_month = self.month if self.month is not None else 1
|
|
76
|
+
validation_day = self.day if self.day is not None else 1
|
|
77
|
+
date(validation_year, validation_month, validation_day)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def normalized(self) -> str:
|
|
81
|
+
"""Return the selector with zero-padded date parts.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Canonical text for display, comparison, and favorite storage.
|
|
85
|
+
|
|
86
|
+
The canonical text makes equivalent separators and padding compare as
|
|
87
|
+
one saved filter.
|
|
88
|
+
"""
|
|
89
|
+
if self.kind == "year":
|
|
90
|
+
assert self.year is not None
|
|
91
|
+
return f"{self.year:04d}"
|
|
92
|
+
if self.kind == "year_month":
|
|
93
|
+
assert self.year is not None and self.month is not None
|
|
94
|
+
return f"{self.year:04d}-{self.month:02d}"
|
|
95
|
+
if self.kind == "month_day":
|
|
96
|
+
assert self.month is not None and self.day is not None
|
|
97
|
+
return f"{self.month:02d}-{self.day:02d}"
|
|
98
|
+
assert self.year is not None and self.month is not None and self.day is not None
|
|
99
|
+
return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class ExperimentDateRange:
|
|
104
|
+
"""One inclusive range between two full recording dates.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
start: First accepted calendar date.
|
|
108
|
+
end: Last accepted calendar date.
|
|
109
|
+
|
|
110
|
+
Outputs:
|
|
111
|
+
Immutable ordered endpoints with a canonical representation.
|
|
112
|
+
|
|
113
|
+
The full-date endpoints make inclusion of the final recording day
|
|
114
|
+
unambiguous.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
start: date
|
|
118
|
+
end: date
|
|
119
|
+
|
|
120
|
+
def __post_init__(self) -> None:
|
|
121
|
+
"""Reject a range whose start follows its end."""
|
|
122
|
+
if self.start > self.end:
|
|
123
|
+
msg = (
|
|
124
|
+
f"Date range start {self.start.isoformat()} follows "
|
|
125
|
+
f"{self.end.isoformat()}."
|
|
126
|
+
)
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def normalized(self) -> str:
|
|
131
|
+
"""Return the range with canonical full-date endpoints.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
``YYYY-MM-DD..YYYY-MM-DD`` text.
|
|
135
|
+
|
|
136
|
+
This text preserves the inclusive range operator used by the search
|
|
137
|
+
field.
|
|
138
|
+
"""
|
|
139
|
+
return f"{self.start.isoformat()}..{self.end.isoformat()}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
ExperimentDateFilterItem = ExperimentDateSelector | ExperimentDateRange
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class ExperimentDateFilter:
|
|
147
|
+
"""One validated list of date selectors and inclusive ranges.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
items: Non-empty tuple of date selections joined with logical ``OR``.
|
|
151
|
+
|
|
152
|
+
Outputs:
|
|
153
|
+
Immutable query input with canonical text for the search field.
|
|
154
|
+
|
|
155
|
+
The filter groups all items into one database condition. Other search
|
|
156
|
+
fields can then combine with that condition using logical ``AND``.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
items: tuple[ExperimentDateFilterItem, ...]
|
|
160
|
+
|
|
161
|
+
def __post_init__(self) -> None:
|
|
162
|
+
"""Require at least one date selection."""
|
|
163
|
+
if len(self.items) == 0:
|
|
164
|
+
msg = "A date filter must contain at least one selector."
|
|
165
|
+
raise ValueError(msg)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def normalized(self) -> str:
|
|
169
|
+
"""Return canonical comma-separated filter text.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Normalized selectors in their entered order.
|
|
173
|
+
|
|
174
|
+
Favorites use this text so equivalent separators and spacing compare
|
|
175
|
+
equally.
|
|
176
|
+
"""
|
|
177
|
+
return ", ".join(item.normalized for item in self.items)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_experiment_date_filter(value: str | None) -> ExperimentDateFilter | None:
|
|
181
|
+
r"""Parse one database-search date expression.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
value: Optional date filter text from the dialog or scripting API.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Validated selectors, or ``None`` when the input is blank.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If any selector, date, or range is invalid.
|
|
191
|
+
|
|
192
|
+
This parser accepts ``-``, ``/``, ``\``, ``.``, and ``_`` separators. It
|
|
193
|
+
rejects invalid input before any database query starts.
|
|
194
|
+
"""
|
|
195
|
+
if value is None or value.strip() == "":
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
items: list[ExperimentDateFilterItem] = []
|
|
199
|
+
for index, raw_item in enumerate(value.strip().split(","), start=1):
|
|
200
|
+
item = raw_item.strip()
|
|
201
|
+
if item == "":
|
|
202
|
+
raise _date_filter_error(item, f"selector {index} is empty")
|
|
203
|
+
items.append(_parse_date_filter_item(item))
|
|
204
|
+
return ExperimentDateFilter(tuple(items))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_date_filter_item(value: str) -> ExperimentDateFilterItem:
|
|
208
|
+
"""Return one validated partial date, full date, or range."""
|
|
209
|
+
if ".." not in value:
|
|
210
|
+
return _parse_date_selector(value)
|
|
211
|
+
if value.count("..") != 1:
|
|
212
|
+
raise _date_filter_error(value, "a range must contain one '..' operator")
|
|
213
|
+
|
|
214
|
+
raw_start, raw_end = (part.strip() for part in value.split("..", maxsplit=1))
|
|
215
|
+
if raw_start == "" or raw_end == "":
|
|
216
|
+
raise _date_filter_error(value, "a range must contain two full dates")
|
|
217
|
+
try:
|
|
218
|
+
start = _parse_full_date(raw_start)
|
|
219
|
+
end = _parse_full_date(raw_end)
|
|
220
|
+
except ValueError as error:
|
|
221
|
+
reason = f"range endpoints must be full dates: {error}"
|
|
222
|
+
raise _date_filter_error(value, reason) from error
|
|
223
|
+
if start > end:
|
|
224
|
+
reason = f"range start {start.isoformat()} follows {end.isoformat()}"
|
|
225
|
+
raise _date_filter_error(value, reason)
|
|
226
|
+
return ExperimentDateRange(start=start, end=end)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _parse_date_selector(value: str) -> ExperimentDateSelector:
|
|
230
|
+
"""Return one validated year, year-month, month-day, or full date."""
|
|
231
|
+
match = _YEAR_PATTERN.fullmatch(value)
|
|
232
|
+
if match is not None:
|
|
233
|
+
return _validated_selector(value, "year", year=int(match.group(1)))
|
|
234
|
+
|
|
235
|
+
match = _YEAR_MONTH_PATTERN.fullmatch(value)
|
|
236
|
+
if match is not None:
|
|
237
|
+
return _validated_selector(
|
|
238
|
+
value,
|
|
239
|
+
"year_month",
|
|
240
|
+
year=int(match.group(1)),
|
|
241
|
+
month=int(match.group(2)),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
match = _MONTH_DAY_PATTERN.fullmatch(value)
|
|
245
|
+
if match is not None:
|
|
246
|
+
return _validated_selector(
|
|
247
|
+
value,
|
|
248
|
+
"month_day",
|
|
249
|
+
month=int(match.group(1)),
|
|
250
|
+
day=int(match.group(2)),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
match = _FULL_DATE_PATTERN.fullmatch(value)
|
|
254
|
+
if match is not None:
|
|
255
|
+
return _validated_selector(
|
|
256
|
+
value,
|
|
257
|
+
"date",
|
|
258
|
+
year=int(match.group(1)),
|
|
259
|
+
month=int(match.group(2)),
|
|
260
|
+
day=int(match.group(3)),
|
|
261
|
+
)
|
|
262
|
+
raise _date_filter_error(value, "the selector has an unsupported form")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _parse_full_date(value: str) -> date:
|
|
266
|
+
"""Return one full date or raise a date validation error."""
|
|
267
|
+
match = _FULL_DATE_PATTERN.fullmatch(value)
|
|
268
|
+
if match is None:
|
|
269
|
+
msg = f"{value!r} is not a full date"
|
|
270
|
+
raise ValueError(msg)
|
|
271
|
+
year, month, day = (int(part) for part in match.groups())
|
|
272
|
+
return date(year, month, day)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _validated_selector(
|
|
276
|
+
source: str,
|
|
277
|
+
kind: DateSelectorKind,
|
|
278
|
+
*,
|
|
279
|
+
year: int | None = None,
|
|
280
|
+
month: int | None = None,
|
|
281
|
+
day: int | None = None,
|
|
282
|
+
) -> ExperimentDateSelector:
|
|
283
|
+
"""Build one selector and add source text to validation errors."""
|
|
284
|
+
try:
|
|
285
|
+
return ExperimentDateSelector(kind=kind, year=year, month=month, day=day)
|
|
286
|
+
except ValueError as error:
|
|
287
|
+
raise _date_filter_error(source, str(error)) from error
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _date_filter_error(selector: str, reason: str) -> ValueError:
|
|
291
|
+
"""Return one complete user-facing date filter error."""
|
|
292
|
+
return ValueError(
|
|
293
|
+
f"Invalid date selector {selector!r}: {reason}.\n{EXPERIMENT_DATE_FILTER_HELP}"
|
|
294
|
+
)
|
|
@@ -15,6 +15,12 @@ from pathlib import Path
|
|
|
15
15
|
from twopy.config import DatabaseAccess
|
|
16
16
|
from twopy.database.catalog import read_database_catalog
|
|
17
17
|
from twopy.database.connection import connect_read_only
|
|
18
|
+
from twopy.database.date_filters import (
|
|
19
|
+
ExperimentDateFilter,
|
|
20
|
+
ExperimentDateRange,
|
|
21
|
+
ExperimentDateSelector,
|
|
22
|
+
parse_experiment_date_filter,
|
|
23
|
+
)
|
|
18
24
|
from twopy.database.types import DatabaseExperiment, DatabaseValue
|
|
19
25
|
|
|
20
26
|
__all__ = ["find_recordings", "find_stimulus_presentations"]
|
|
@@ -23,10 +29,7 @@ __all__ = ["find_recordings", "find_stimulus_presentations"]
|
|
|
23
29
|
def find_stimulus_presentations(
|
|
24
30
|
database_dir: Path,
|
|
25
31
|
*,
|
|
26
|
-
|
|
27
|
-
date_month: int | None = None,
|
|
28
|
-
date_day: int | None = None,
|
|
29
|
-
date_contains: str | None = None,
|
|
32
|
+
date: str | None = None,
|
|
30
33
|
path_contains: str | None = None,
|
|
31
34
|
stimulus_contains: str | None = None,
|
|
32
35
|
genotype_contains: str | None = None,
|
|
@@ -42,11 +45,7 @@ def find_stimulus_presentations(
|
|
|
42
45
|
|
|
43
46
|
Args:
|
|
44
47
|
database_dir: Folder containing lab SQLite DB files.
|
|
45
|
-
|
|
46
|
-
date_month: Optional month number matched against experiment date.
|
|
47
|
-
date_day: Optional day-of-month matched against experiment date.
|
|
48
|
-
date_contains: Optional substring matched against full experiment date
|
|
49
|
-
text.
|
|
48
|
+
date: Optional date expression with selectors and inclusive ranges.
|
|
50
49
|
path_contains: Optional substring matched against ``relativeDataPath``.
|
|
51
50
|
stimulus_contains: Optional substring matched against ``stimulusFunction``.
|
|
52
51
|
genotype_contains: Optional substring matched against fly ``genotype``.
|
|
@@ -73,6 +72,7 @@ def find_stimulus_presentations(
|
|
|
73
72
|
if limit < 1:
|
|
74
73
|
msg = "Database query limit must be at least 1"
|
|
75
74
|
raise ValueError(msg)
|
|
75
|
+
date_filter = parse_experiment_date_filter(date)
|
|
76
76
|
|
|
77
77
|
catalog = read_database_catalog(
|
|
78
78
|
database_dir,
|
|
@@ -89,10 +89,7 @@ def find_stimulus_presentations(
|
|
|
89
89
|
|
|
90
90
|
rows = _query_stimulus_presentations(
|
|
91
91
|
connection=connection,
|
|
92
|
-
|
|
93
|
-
date_month=date_month,
|
|
94
|
-
date_day=date_day,
|
|
95
|
-
date_contains=date_contains,
|
|
92
|
+
date_filter=date_filter,
|
|
96
93
|
path_contains=path_contains,
|
|
97
94
|
stimulus_contains=stimulus_contains,
|
|
98
95
|
genotype_contains=genotype_contains,
|
|
@@ -116,9 +113,7 @@ def find_stimulus_presentations(
|
|
|
116
113
|
def find_recordings(
|
|
117
114
|
database_dir: Path,
|
|
118
115
|
*,
|
|
119
|
-
|
|
120
|
-
month: int | None = None,
|
|
121
|
-
day: int | None = None,
|
|
116
|
+
date: str | None = None,
|
|
122
117
|
genotype: str | None = None,
|
|
123
118
|
stimulus: str | None = None,
|
|
124
119
|
sensor: str | None = None,
|
|
@@ -133,9 +128,7 @@ def find_recordings(
|
|
|
133
128
|
|
|
134
129
|
Args:
|
|
135
130
|
database_dir: Folder containing lab SQLite DB files.
|
|
136
|
-
|
|
137
|
-
month: Optional experiment month.
|
|
138
|
-
day: Optional experiment day-of-month.
|
|
131
|
+
date: Optional date expression with selectors and inclusive ranges.
|
|
139
132
|
genotype: Optional genotype substring.
|
|
140
133
|
stimulus: Optional stimulus-function substring.
|
|
141
134
|
sensor: Optional sensor substring from fly ``fluorescentProtein``.
|
|
@@ -158,9 +151,7 @@ def find_recordings(
|
|
|
158
151
|
"""
|
|
159
152
|
return find_stimulus_presentations(
|
|
160
153
|
database_dir,
|
|
161
|
-
|
|
162
|
-
date_month=month,
|
|
163
|
-
date_day=day,
|
|
154
|
+
date=date,
|
|
164
155
|
stimulus_contains=stimulus,
|
|
165
156
|
genotype_contains=genotype,
|
|
166
157
|
sensor_contains=sensor,
|
|
@@ -199,10 +190,7 @@ def _has_tables(connection: sqlite3.Connection, table_names: tuple[str, ...]) ->
|
|
|
199
190
|
def _query_stimulus_presentations(
|
|
200
191
|
*,
|
|
201
192
|
connection: sqlite3.Connection,
|
|
202
|
-
|
|
203
|
-
date_month: int | None,
|
|
204
|
-
date_day: int | None,
|
|
205
|
-
date_contains: str | None,
|
|
193
|
+
date_filter: ExperimentDateFilter | None,
|
|
206
194
|
path_contains: str | None,
|
|
207
195
|
stimulus_contains: str | None,
|
|
208
196
|
genotype_contains: str | None,
|
|
@@ -216,10 +204,7 @@ def _query_stimulus_presentations(
|
|
|
216
204
|
|
|
217
205
|
Args:
|
|
218
206
|
connection: Open SQLite connection.
|
|
219
|
-
|
|
220
|
-
date_month: Optional experiment month.
|
|
221
|
-
date_day: Optional experiment day-of-month.
|
|
222
|
-
date_contains: Optional substring matched against experiment date.
|
|
207
|
+
date_filter: Optional explicit date selectors and inclusive ranges.
|
|
223
208
|
path_contains: Optional relative path substring.
|
|
224
209
|
stimulus_contains: Optional stimulus-function substring.
|
|
225
210
|
genotype_contains: Optional genotype substring.
|
|
@@ -238,19 +223,16 @@ def _query_stimulus_presentations(
|
|
|
238
223
|
where_clauses: list[str] = []
|
|
239
224
|
parameters: list[DatabaseValue] = []
|
|
240
225
|
|
|
241
|
-
|
|
226
|
+
_append_date_filter_expression(
|
|
242
227
|
where_clauses=where_clauses,
|
|
243
228
|
parameters=parameters,
|
|
244
|
-
|
|
245
|
-
month=date_month,
|
|
246
|
-
day=date_day,
|
|
229
|
+
date_filter=date_filter,
|
|
247
230
|
)
|
|
248
231
|
_append_optional_text_filters(
|
|
249
232
|
where_clauses=where_clauses,
|
|
250
233
|
parameters=parameters,
|
|
251
234
|
filters={
|
|
252
235
|
'CAST(sp."relativeDataPath" AS TEXT) LIKE ?': path_contains,
|
|
253
|
-
'CAST(sp."date" AS TEXT) LIKE ?': date_contains,
|
|
254
236
|
'CAST(sp."stimulusFunction" AS TEXT) LIKE ?': stimulus_contains,
|
|
255
237
|
'CAST(f."genotype" AS TEXT) LIKE ?': genotype_contains,
|
|
256
238
|
'CAST(f."fluorescentProtein" AS TEXT) LIKE ?': sensor_contains,
|
|
@@ -312,6 +294,78 @@ def _append_optional_text_filters(
|
|
|
312
294
|
parameters.append(f"%{value}%")
|
|
313
295
|
|
|
314
296
|
|
|
297
|
+
def _append_date_filter_expression(
|
|
298
|
+
*,
|
|
299
|
+
where_clauses: list[str],
|
|
300
|
+
parameters: list[DatabaseValue],
|
|
301
|
+
date_filter: ExperimentDateFilter | None,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Add one grouped date expression to a modeled query.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
where_clauses: SQL where clauses being built.
|
|
307
|
+
parameters: SQL parameter values being built.
|
|
308
|
+
date_filter: Validated selectors joined with logical ``OR``.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
None. The function appends one grouped clause and its parameters.
|
|
312
|
+
|
|
313
|
+
All comparisons use the fixed date portion of the stored timestamp. This
|
|
314
|
+
includes every recording time on an inclusive range end date.
|
|
315
|
+
"""
|
|
316
|
+
if date_filter is None:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
item_clauses: list[str] = []
|
|
320
|
+
for item in date_filter.items:
|
|
321
|
+
if isinstance(item, ExperimentDateRange):
|
|
322
|
+
item_clauses.append(
|
|
323
|
+
'(substr(CAST(sp."date" AS TEXT), 1, 10) BETWEEN ? AND ?)'
|
|
324
|
+
)
|
|
325
|
+
parameters.extend((item.start.isoformat(), item.end.isoformat()))
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
clause, values = _date_selector_condition(item)
|
|
329
|
+
item_clauses.append(clause)
|
|
330
|
+
parameters.extend(values)
|
|
331
|
+
where_clauses.append(f"({' OR '.join(item_clauses)})")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _date_selector_condition(
|
|
335
|
+
selector: ExperimentDateSelector,
|
|
336
|
+
) -> tuple[str, tuple[str, ...]]:
|
|
337
|
+
"""Return one SQL condition and its bound date values."""
|
|
338
|
+
if selector.kind == "year":
|
|
339
|
+
assert selector.year is not None
|
|
340
|
+
return (
|
|
341
|
+
'(substr(CAST(sp."date" AS TEXT), 1, 4) = ?)',
|
|
342
|
+
(f"{selector.year:04d}",),
|
|
343
|
+
)
|
|
344
|
+
if selector.kind == "year_month":
|
|
345
|
+
assert selector.year is not None and selector.month is not None
|
|
346
|
+
return (
|
|
347
|
+
"("
|
|
348
|
+
'substr(CAST(sp."date" AS TEXT), 1, 4) = ? AND '
|
|
349
|
+
'substr(CAST(sp."date" AS TEXT), 6, 2) = ?'
|
|
350
|
+
")",
|
|
351
|
+
(f"{selector.year:04d}", f"{selector.month:02d}"),
|
|
352
|
+
)
|
|
353
|
+
if selector.kind == "month_day":
|
|
354
|
+
assert selector.month is not None and selector.day is not None
|
|
355
|
+
return (
|
|
356
|
+
"("
|
|
357
|
+
'substr(CAST(sp."date" AS TEXT), 6, 2) = ? AND '
|
|
358
|
+
'substr(CAST(sp."date" AS TEXT), 9, 2) = ?'
|
|
359
|
+
")",
|
|
360
|
+
(f"{selector.month:02d}", f"{selector.day:02d}"),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
'(substr(CAST(sp."date" AS TEXT), 1, 10) = ?)',
|
|
365
|
+
(selector.normalized,),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
315
369
|
def _experiment_from_row(database_path: Path, row: sqlite3.Row) -> DatabaseExperiment:
|
|
316
370
|
"""Convert one joined SQLite row into a typed experiment.
|
|
317
371
|
|
|
@@ -338,65 +392,6 @@ def _experiment_from_row(database_path: Path, row: sqlite3.Row) -> DatabaseExper
|
|
|
338
392
|
)
|
|
339
393
|
|
|
340
394
|
|
|
341
|
-
def _append_date_filters(
|
|
342
|
-
*,
|
|
343
|
-
where_clauses: list[str],
|
|
344
|
-
parameters: list[DatabaseValue],
|
|
345
|
-
year: int | None,
|
|
346
|
-
month: int | None,
|
|
347
|
-
day: int | None,
|
|
348
|
-
) -> None:
|
|
349
|
-
"""Add flexible year, month, and day filters for experiment dates.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
where_clauses: SQL where clauses being built.
|
|
353
|
-
parameters: SQL parameter values being built.
|
|
354
|
-
year: Optional experiment year.
|
|
355
|
-
month: Optional experiment month.
|
|
356
|
-
day: Optional experiment day-of-month.
|
|
357
|
-
|
|
358
|
-
Returns:
|
|
359
|
-
None. The function appends clauses and parameters in place.
|
|
360
|
-
|
|
361
|
-
The lab DB stores dates as ``YYYY-MM-DD HH:MM:SS`` text. ``substr`` keeps the
|
|
362
|
-
query simple and allows year-only, month-only, day-only, or combined filters.
|
|
363
|
-
"""
|
|
364
|
-
if year is not None:
|
|
365
|
-
_validate_range("year", year, 1, 9999)
|
|
366
|
-
where_clauses.append('substr(CAST(sp."date" AS TEXT), 1, 4) = ?')
|
|
367
|
-
parameters.append(f"{year:04d}")
|
|
368
|
-
|
|
369
|
-
if month is not None:
|
|
370
|
-
_validate_range("month", month, 1, 12)
|
|
371
|
-
where_clauses.append('substr(CAST(sp."date" AS TEXT), 6, 2) = ?')
|
|
372
|
-
parameters.append(f"{month:02d}")
|
|
373
|
-
|
|
374
|
-
if day is not None:
|
|
375
|
-
_validate_range("day", day, 1, 31)
|
|
376
|
-
where_clauses.append('substr(CAST(sp."date" AS TEXT), 9, 2) = ?')
|
|
377
|
-
parameters.append(f"{day:02d}")
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def _validate_range(name: str, value: int, minimum: int, maximum: int) -> None:
|
|
381
|
-
"""Validate one integer filter range.
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
name: Filter name for error messages.
|
|
385
|
-
value: Integer value to check.
|
|
386
|
-
minimum: Smallest accepted value.
|
|
387
|
-
maximum: Largest accepted value.
|
|
388
|
-
|
|
389
|
-
Returns:
|
|
390
|
-
None when the value is valid.
|
|
391
|
-
|
|
392
|
-
Raises:
|
|
393
|
-
ValueError: If the value falls outside the accepted range.
|
|
394
|
-
"""
|
|
395
|
-
if not minimum <= value <= maximum:
|
|
396
|
-
msg = f"{name} must be between {minimum} and {maximum}. Got {value}"
|
|
397
|
-
raise ValueError(msg)
|
|
398
|
-
|
|
399
|
-
|
|
400
395
|
def _optional_string(value: object) -> str | None:
|
|
401
396
|
"""Convert nullable SQLite values into optional strings.
|
|
402
397
|
|