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.
Files changed (269) hide show
  1. {twopy-0.4.0 → twopy-0.4.1}/PKG-INFO +1 -1
  2. {twopy-0.4.0 → twopy-0.4.1}/pyproject.toml +1 -1
  3. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/api.py +5 -11
  4. twopy-0.4.1/src/twopy/database/date_filters.py +294 -0
  5. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/modeled.py +89 -94
  6. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/search.py +12 -22
  7. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_favorite_editor.py +3 -0
  8. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_favorites.py +29 -28
  9. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/database_search.py +3 -0
  10. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/PKG-INFO +1 -1
  11. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/SOURCES.txt +2 -0
  12. {twopy-0.4.0 → twopy-0.4.1}/tests/test_api.py +57 -12
  13. {twopy-0.4.0 → twopy-0.4.1}/tests/test_database.py +8 -34
  14. twopy-0.4.1/tests/test_database_date_filters.py +275 -0
  15. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_database_search.py +78 -12
  16. {twopy-0.4.0 → twopy-0.4.1}/README.md +0 -0
  17. {twopy-0.4.0 → twopy-0.4.1}/setup.cfg +0 -0
  18. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/__init__.py +0 -0
  19. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/__init__.pyi +0 -0
  20. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/_segments.py +0 -0
  21. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/_version.py +0 -0
  22. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/__init__.py +0 -0
  23. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/background_subtraction.py +0 -0
  24. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/dff.py +0 -0
  25. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/dff_options.py +0 -0
  26. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/epoch_mapping.py +0 -0
  27. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/group_matching.py +0 -0
  28. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/__init__.py +0 -0
  29. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/_fitting.py +0 -0
  30. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  31. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/motion.py +0 -0
  32. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/persistence.py +0 -0
  33. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_context.py +0 -0
  34. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_maps.py +0 -0
  35. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_plotting.py +0 -0
  36. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/__init__.py +0 -0
  37. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/apply.py +0 -0
  38. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/options.py +0 -0
  39. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/persistence.py +0 -0
  40. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/qc.py +0 -0
  41. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_processing/signals.py +0 -0
  42. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/response_window_options.py +0 -0
  43. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/responses.py +0 -0
  44. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/timing.py +0 -0
  45. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/trials.py +0 -0
  46. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis/workflow.py +0 -0
  47. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/analysis_cache.py +0 -0
  48. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/cache_inventory.py +0 -0
  49. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  50. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/calibrations/pixel_size.csv +0 -0
  51. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/config.example.yml +0 -0
  52. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/config.py +0 -0
  53. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/__init__.py +0 -0
  54. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/alignment_crop.py +0 -0
  55. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/api.py +0 -0
  56. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/hdf5_writing.py +0 -0
  57. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/matlab_values.py +0 -0
  58. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/orientation.py +0 -0
  59. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/source_loading.py +0 -0
  60. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/stimulus_code.py +0 -0
  61. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/conversion/types.py +0 -0
  62. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/converted.py +0 -0
  63. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/__init__.py +0 -0
  64. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/context.py +0 -0
  65. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/discovery.py +0 -0
  66. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/metadata.py +0 -0
  67. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native.py +0 -0
  68. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/__init__.py +0 -0
  69. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/direction_selectivity.py +0 -0
  70. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
  71. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/parameters.py +0 -0
  72. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/provenance.py +0 -0
  73. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/registry.py +0 -0
  74. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/results.py +0 -0
  75. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/runner.py +0 -0
  76. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/custom/workflows.py +0 -0
  77. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/__init__.py +0 -0
  78. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/cache.py +0 -0
  79. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/catalog.py +0 -0
  80. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/connection.py +0 -0
  81. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/generic.py +0 -0
  82. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/sql.py +0 -0
  83. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/database/types.py +0 -0
  84. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/filenames.py +0 -0
  85. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/frame_ranges.py +0 -0
  86. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/hdf5_utils.py +0 -0
  87. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/inspection.py +0 -0
  88. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/matlab.py +0 -0
  89. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/__init__.py +0 -0
  90. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/__main__.py +0 -0
  91. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-128.png +0 -0
  92. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-16.png +0 -0
  93. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-256.png +0 -0
  94. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-32.png +0 -0
  95. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-512.png +0 -0
  96. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon-64.png +0 -0
  97. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon.icns +0 -0
  98. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/assets/twopy-app-icon.png +0 -0
  99. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/controls.py +0 -0
  100. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/custom_tab.py +0 -0
  101. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/dims.py +0 -0
  102. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/display.py +0 -0
  103. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/display_paths.py +0 -0
  104. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/empty_state.py +0 -0
  105. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/errors.py +0 -0
  106. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/__init__.py +0 -0
  107. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/cards.py +0 -0
  108. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/fov_assignment.py +0 -0
  109. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/fov_cards.py +0 -0
  110. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/images.py +0 -0
  111. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/protocols.py +0 -0
  112. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/response_preview.py +0 -0
  113. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/responses.py +0 -0
  114. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/roi_assignment.py +0 -0
  115. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/roi_cards.py +0 -0
  116. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/style.py +0 -0
  117. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/tables.py +0 -0
  118. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/group_matching/window.py +0 -0
  119. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/interactive.py +0 -0
  120. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/latest_worker.py +0 -0
  121. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/launcher.py +0 -0
  122. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/live_analysis.py +0 -0
  123. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_controller.py +0 -0
  124. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_picker.py +0 -0
  125. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_progress.py +0 -0
  126. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/load_workflow.py +0 -0
  127. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/loaded_recordings_csv.py +0 -0
  128. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/loading.py +0 -0
  129. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/movie.py +0 -0
  130. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/output_routing.py +0 -0
  131. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/paths.py +0 -0
  132. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/__init__.py +0 -0
  133. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/data.py +0 -0
  134. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/dff_options.py +0 -0
  135. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/__init__.py +0 -0
  136. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/api.py +0 -0
  137. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
  138. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/options_panel.py +0 -0
  139. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/paths.py +0 -0
  140. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
  141. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
  142. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/response_plot_widget.py +0 -0
  143. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  144. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
  145. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/export.py +0 -0
  146. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/export_controls.py +0 -0
  147. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/form_controls.py +0 -0
  148. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/label_visibility.py +0 -0
  149. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/motion_summary.py +0 -0
  150. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/normalization_options.py +0 -0
  151. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/options.py +0 -0
  152. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/panels.py +0 -0
  153. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/preview_strip.py +0 -0
  154. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/processing_options.py +0 -0
  155. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  156. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_display.py +0 -0
  157. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_map_options.py +0 -0
  158. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/response_window_options.py +0 -0
  159. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
  160. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
  161. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
  162. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
  163. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/plotting/widgets.py +0 -0
  164. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/protocols.py +0 -0
  165. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/responses.py +0 -0
  166. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/roi.py +0 -0
  167. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/roi_signatures.py +0 -0
  168. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/session.py +0 -0
  169. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/sidebar.py +0 -0
  170. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/state.py +0 -0
  171. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/text.py +0 -0
  172. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/theme.py +0 -0
  173. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/trial_timeline.py +0 -0
  174. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/types.py +0 -0
  175. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/version_check.py +0 -0
  176. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/napari/viewer.py +0 -0
  177. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/__init__.py +0 -0
  178. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/dff.py +0 -0
  179. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/psycho5.py +0 -0
  180. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/roi.py +0 -0
  181. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/roi_extraction.py +0 -0
  182. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/parity/saved_analysis.py +0 -0
  183. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/photodiode.py +0 -0
  184. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/photodiode_classification.py +0 -0
  185. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/pixel_calibration.py +0 -0
  186. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/pixel_calibration_profiles.py +0 -0
  187. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/response_roi_extraction.py +0 -0
  188. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi.py +0 -0
  189. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi_extraction.py +0 -0
  190. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/roi_mask_cleanup.py +0 -0
  191. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/session.py +0 -0
  192. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/spatial.py +0 -0
  193. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/spatial_orientation.py +0 -0
  194. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/stimulus.py +0 -0
  195. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/synchronization.py +0 -0
  196. {twopy-0.4.0 → twopy-0.4.1}/src/twopy/typing_guards.py +0 -0
  197. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/dependency_links.txt +0 -0
  198. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/entry_points.txt +0 -0
  199. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/requires.txt +0 -0
  200. {twopy-0.4.0 → twopy-0.4.1}/src/twopy.egg-info/top_level.txt +0 -0
  201. {twopy-0.4.0 → twopy-0.4.1}/tests/test_background_subtraction.py +0 -0
  202. {twopy-0.4.0 → twopy-0.4.1}/tests/test_cache_inventory.py +0 -0
  203. {twopy-0.4.0 → twopy-0.4.1}/tests/test_config.py +0 -0
  204. {twopy-0.4.0 → twopy-0.4.1}/tests/test_conversion.py +0 -0
  205. {twopy-0.4.0 → twopy-0.4.1}/tests/test_converted.py +0 -0
  206. {twopy-0.4.0 → twopy-0.4.1}/tests/test_custom_workflows.py +0 -0
  207. {twopy-0.4.0 → twopy-0.4.1}/tests/test_dff.py +0 -0
  208. {twopy-0.4.0 → twopy-0.4.1}/tests/test_epoch_mapping.py +0 -0
  209. {twopy-0.4.0 → twopy-0.4.1}/tests/test_filenames.py +0 -0
  210. {twopy-0.4.0 → twopy-0.4.1}/tests/test_frame_ranges.py +0 -0
  211. {twopy-0.4.0 → twopy-0.4.1}/tests/test_function_inventory.py +0 -0
  212. {twopy-0.4.0 → twopy-0.4.1}/tests/test_group_matching.py +0 -0
  213. {twopy-0.4.0 → twopy-0.4.1}/tests/test_hdf5_utils.py +0 -0
  214. {twopy-0.4.0 → twopy-0.4.1}/tests/test_import.py +0 -0
  215. {twopy-0.4.0 → twopy-0.4.1}/tests/test_inspection.py +0 -0
  216. {twopy-0.4.0 → twopy-0.4.1}/tests/test_kernels.py +0 -0
  217. {twopy-0.4.0 → twopy-0.4.1}/tests/test_load_workflow.py +0 -0
  218. {twopy-0.4.0 → twopy-0.4.1}/tests/test_matlab.py +0 -0
  219. {twopy-0.4.0 → twopy-0.4.1}/tests/test_motion.py +0 -0
  220. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari.py +0 -0
  221. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_display.py +0 -0
  222. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_export.py +0 -0
  223. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_group_matching_responses.py +0 -0
  224. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_group_matching_roi_cards.py +0 -0
  225. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_live_controller.py +0 -0
  226. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_controller.py +0 -0
  227. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_picker.py +0 -0
  228. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_progress.py +0 -0
  229. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_load_tab.py +0 -0
  230. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_loaded_recordings.py +0 -0
  231. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_path_resolution.py +0 -0
  232. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_controls.py +0 -0
  233. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_data.py +0 -0
  234. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_plot_widgets.py +0 -0
  235. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_processing_controls.py +0 -0
  236. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_response_map_display.py +0 -0
  237. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_response_workflow.py +0 -0
  238. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_roi_generation.py +0 -0
  239. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_roi_visibility.py +0 -0
  240. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_state.py +0 -0
  241. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_text.py +0 -0
  242. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_theme.py +0 -0
  243. {twopy-0.4.0 → twopy-0.4.1}/tests/test_napari_timeline.py +0 -0
  244. {twopy-0.4.0 → twopy-0.4.1}/tests/test_packaging.py +0 -0
  245. {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity.py +0 -0
  246. {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity_psycho5.py +0 -0
  247. {twopy-0.4.0 → twopy-0.4.1}/tests/test_parity_roi_extraction.py +0 -0
  248. {twopy-0.4.0 → twopy-0.4.1}/tests/test_persistence.py +0 -0
  249. {twopy-0.4.0 → twopy-0.4.1}/tests/test_photodiode_classification.py +0 -0
  250. {twopy-0.4.0 → twopy-0.4.1}/tests/test_pixel_calibration.py +0 -0
  251. {twopy-0.4.0 → twopy-0.4.1}/tests/test_pixel_calibration_profiles.py +0 -0
  252. {twopy-0.4.0 → twopy-0.4.1}/tests/test_real_fixture.py +0 -0
  253. {twopy-0.4.0 → twopy-0.4.1}/tests/test_recording_timing.py +0 -0
  254. {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_map_area.py +0 -0
  255. {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_maps.py +0 -0
  256. {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_processing.py +0 -0
  257. {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_roi_extraction.py +0 -0
  258. {twopy-0.4.0 → twopy-0.4.1}/tests/test_response_window_options.py +0 -0
  259. {twopy-0.4.0 → twopy-0.4.1}/tests/test_responses.py +0 -0
  260. {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi.py +0 -0
  261. {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi_extraction.py +0 -0
  262. {twopy-0.4.0 → twopy-0.4.1}/tests/test_roi_mask_cleanup.py +0 -0
  263. {twopy-0.4.0 → twopy-0.4.1}/tests/test_saved_analysis.py +0 -0
  264. {twopy-0.4.0 → twopy-0.4.1}/tests/test_session.py +0 -0
  265. {twopy-0.4.0 → twopy-0.4.1}/tests/test_stimulus.py +0 -0
  266. {twopy-0.4.0 → twopy-0.4.1}/tests/test_synchronization.py +0 -0
  267. {twopy-0.4.0 → twopy-0.4.1}/tests/test_trials.py +0 -0
  268. {twopy-0.4.0 → twopy-0.4.1}/tests/test_version_check.py +0 -0
  269. {twopy-0.4.0 → twopy-0.4.1}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Two-photon imaging analysis tool with a napari interface.
5
5
  Author-email: Gustavo Madeira Santana <fromweb1@gumadeiras.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Two-photon imaging analysis tool with a napari interface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -1,6 +1,6 @@
1
1
  """Public script-friendly API for twopy.
2
2
 
3
- Inputs: simple Python arguments such as year, genotype, stimulus, and paths.
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
- year: int | None = None,
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
- year: Optional experiment year.
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(year=2023, month=10, genotype="gh146")``
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
- year=year,
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
- date_year: int | None = None,
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
- date_year: Optional four-digit year matched against experiment date.
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
- date_year=date_year,
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
- year: int | None = None,
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
- year: Optional experiment year.
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
- date_year=year,
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
- date_year: int | None,
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
- date_year: Optional experiment year.
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
- _append_date_filters(
226
+ _append_date_filter_expression(
242
227
  where_clauses=where_clauses,
243
228
  parameters=parameters,
244
- year=date_year,
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