TwoPy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. twopy/__init__.py +168 -0
  2. twopy/_segments.py +53 -0
  3. twopy/analysis/__init__.py +101 -0
  4. twopy/analysis/background_subtraction.py +672 -0
  5. twopy/analysis/dff.py +524 -0
  6. twopy/analysis/epoch_mapping.py +223 -0
  7. twopy/analysis/motion.py +66 -0
  8. twopy/analysis/persistence.py +975 -0
  9. twopy/analysis/response_processing/__init__.py +55 -0
  10. twopy/analysis/response_processing/apply.py +298 -0
  11. twopy/analysis/response_processing/options.py +192 -0
  12. twopy/analysis/response_processing/qc.py +270 -0
  13. twopy/analysis/response_processing/signals.py +200 -0
  14. twopy/analysis/responses.py +345 -0
  15. twopy/analysis/trials.py +341 -0
  16. twopy/analysis/workflow.py +457 -0
  17. twopy/api.py +78 -0
  18. twopy/config.py +200 -0
  19. twopy/conversion/__init__.py +40 -0
  20. twopy/conversion/alignment_crop.py +329 -0
  21. twopy/conversion/api.py +113 -0
  22. twopy/conversion/frame_ranges.py +35 -0
  23. twopy/conversion/hdf5_writing.py +362 -0
  24. twopy/conversion/matlab_values.py +123 -0
  25. twopy/conversion/source_loading.py +403 -0
  26. twopy/conversion/stimulus_code.py +248 -0
  27. twopy/conversion/types.py +295 -0
  28. twopy/converted.py +681 -0
  29. twopy/database/__init__.py +33 -0
  30. twopy/database/cache.py +166 -0
  31. twopy/database/catalog.py +90 -0
  32. twopy/database/connection.py +27 -0
  33. twopy/database/generic.py +254 -0
  34. twopy/database/modeled.py +419 -0
  35. twopy/database/sql.py +46 -0
  36. twopy/database/types.py +116 -0
  37. twopy/inspection.py +492 -0
  38. twopy/matlab.py +398 -0
  39. twopy/napari/__init__.py +27 -0
  40. twopy/napari/__main__.py +10 -0
  41. twopy/napari/constants.py +13 -0
  42. twopy/napari/controls.py +739 -0
  43. twopy/napari/display.py +181 -0
  44. twopy/napari/display_paths.py +197 -0
  45. twopy/napari/interactive.py +310 -0
  46. twopy/napari/launcher.py +175 -0
  47. twopy/napari/layout.py +147 -0
  48. twopy/napari/loading.py +207 -0
  49. twopy/napari/movie.py +71 -0
  50. twopy/napari/paths.py +181 -0
  51. twopy/napari/plotting/__init__.py +21 -0
  52. twopy/napari/plotting/data.py +266 -0
  53. twopy/napari/plotting/docks.py +1052 -0
  54. twopy/napari/plotting/export.py +803 -0
  55. twopy/napari/plotting/export_controls.py +209 -0
  56. twopy/napari/plotting/label_visibility.py +260 -0
  57. twopy/napari/plotting/options.py +269 -0
  58. twopy/napari/plotting/panels.py +119 -0
  59. twopy/napari/plotting/widgets.py +767 -0
  60. twopy/napari/protocols.py +90 -0
  61. twopy/napari/responses.py +91 -0
  62. twopy/napari/roi.py +238 -0
  63. twopy/napari/session.py +381 -0
  64. twopy/napari/state.py +106 -0
  65. twopy/napari/text.py +26 -0
  66. twopy/napari/types.py +39 -0
  67. twopy/napari/viewer.py +405 -0
  68. twopy/parity/__init__.py +30 -0
  69. twopy/parity/dff.py +399 -0
  70. twopy/parity/roi.py +145 -0
  71. twopy/parity/saved_analysis.py +174 -0
  72. twopy/photodiode.py +375 -0
  73. twopy/photodiode_classification.py +484 -0
  74. twopy/roi.py +462 -0
  75. twopy/session.py +167 -0
  76. twopy/spatial.py +154 -0
  77. twopy/stimulus.py +233 -0
  78. twopy/synchronization.py +235 -0
  79. twopy/typing_guards.py +255 -0
  80. twopy-0.1.0.dist-info/METADATA +311 -0
  81. twopy-0.1.0.dist-info/RECORD +84 -0
  82. twopy-0.1.0.dist-info/WHEEL +5 -0
  83. twopy-0.1.0.dist-info/entry_points.txt +2 -0
  84. twopy-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,419 @@
1
+ """Modeled Clark lab recording queries.
2
+
3
+ Inputs: lab database folder and researcher-facing filters.
4
+ Outputs: typed ``DatabaseExperiment`` rows for recording discovery.
5
+
6
+ This module owns SQL that depends on the observed ``stimulusPresentation`` and
7
+ ``fly`` tables.
8
+ """
9
+
10
+ import sqlite3
11
+ from collections.abc import Sequence
12
+ from contextlib import closing
13
+ from pathlib import Path
14
+
15
+ from twopy.database.catalog import read_database_catalog
16
+ from twopy.database.connection import connect_read_only
17
+ from twopy.database.types import DatabaseAccess, DatabaseExperiment, DatabaseValue
18
+
19
+ __all__ = ["find_recordings", "find_stimulus_presentations"]
20
+
21
+
22
+ def find_stimulus_presentations(
23
+ database_dir: Path,
24
+ *,
25
+ date_year: int | None = None,
26
+ date_month: int | None = None,
27
+ date_day: int | None = None,
28
+ path_contains: str | None = None,
29
+ stimulus_contains: str | None = None,
30
+ genotype_contains: str | None = None,
31
+ sensor_contains: str | None = None,
32
+ cell_type_contains: str | None = None,
33
+ hemisphere: str | None = None,
34
+ person_contains: str | None = None,
35
+ limit: int = 100,
36
+ database_access: DatabaseAccess = "direct",
37
+ cache_dir: Path | None = None,
38
+ ) -> tuple[DatabaseExperiment, ...]:
39
+ """Find recording experiments with joined fly metadata.
40
+
41
+ Args:
42
+ database_dir: Folder containing lab SQLite DB files.
43
+ date_year: Optional four-digit year matched against experiment date.
44
+ date_month: Optional month number matched against experiment date.
45
+ date_day: Optional day-of-month matched against experiment date.
46
+ path_contains: Optional substring matched against ``relativeDataPath``.
47
+ stimulus_contains: Optional substring matched against ``stimulusFunction``.
48
+ genotype_contains: Optional substring matched against fly ``genotype``.
49
+ sensor_contains: Optional substring matched against fly
50
+ ``fluorescentProtein``.
51
+ cell_type_contains: Optional substring matched against fly ``cellType``.
52
+ hemisphere: Optional exact match against fly ``eye``.
53
+ person_contains: Optional substring matched against fly ``surgeon``.
54
+ limit: Maximum number of experiments to return.
55
+ database_access: ``direct`` queries DB files in place. ``copy`` queries
56
+ local cached copies because network DB queries can be slow while
57
+ file transfer is fast.
58
+ cache_dir: Optional cache directory used when ``database_access`` is
59
+ ``copy``.
60
+
61
+ Returns:
62
+ Typed experiment rows sorted by database file order and DB row order.
63
+
64
+ Raises:
65
+ FileNotFoundError: If no database files can be found.
66
+ ValueError: If ``limit`` is less than one.
67
+ """
68
+ if limit < 1:
69
+ msg = "Database query limit must be at least 1"
70
+ raise ValueError(msg)
71
+
72
+ catalog = read_database_catalog(
73
+ database_dir,
74
+ database_access=database_access,
75
+ cache_dir=cache_dir,
76
+ )
77
+ remaining = limit
78
+ experiments: list[DatabaseExperiment] = []
79
+
80
+ for database_path in catalog.sqlite_files:
81
+ with closing(connect_read_only(database_path)) as connection:
82
+ if not _has_tables(connection, ("stimulusPresentation", "fly")):
83
+ continue
84
+
85
+ rows = _query_stimulus_presentations(
86
+ connection=connection,
87
+ date_year=date_year,
88
+ date_month=date_month,
89
+ date_day=date_day,
90
+ path_contains=path_contains,
91
+ stimulus_contains=stimulus_contains,
92
+ genotype_contains=genotype_contains,
93
+ sensor_contains=sensor_contains,
94
+ cell_type_contains=cell_type_contains,
95
+ hemisphere=hemisphere,
96
+ person_contains=person_contains,
97
+ limit=remaining,
98
+ )
99
+ experiments.extend(
100
+ _experiment_from_row(database_path=database_path, row=row)
101
+ for row in rows
102
+ )
103
+ remaining = limit - len(experiments)
104
+ if remaining == 0:
105
+ return tuple(experiments)
106
+
107
+ return tuple(experiments)
108
+
109
+
110
+ def find_recordings(
111
+ database_dir: Path,
112
+ *,
113
+ year: int | None = None,
114
+ month: int | None = None,
115
+ day: int | None = None,
116
+ genotype: str | None = None,
117
+ stimulus: str | None = None,
118
+ sensor: str | None = None,
119
+ cell_type: str | None = None,
120
+ hemisphere: str | None = None,
121
+ person: str | None = None,
122
+ limit: int = 100,
123
+ database_access: DatabaseAccess = "direct",
124
+ cache_dir: Path | None = None,
125
+ ) -> tuple[DatabaseExperiment, ...]:
126
+ """Script-friendly entrance function for finding recording experiments.
127
+
128
+ Args:
129
+ database_dir: Folder containing lab SQLite DB files.
130
+ year: Optional experiment year.
131
+ month: Optional experiment month.
132
+ day: Optional experiment day-of-month.
133
+ genotype: Optional genotype substring.
134
+ stimulus: Optional stimulus-function substring.
135
+ sensor: Optional sensor substring from fly ``fluorescentProtein``.
136
+ cell_type: Optional cell-type substring from fly ``cellType``.
137
+ hemisphere: Optional exact fly-eye value, such as ``left`` or ``right``.
138
+ person: Optional experimenter substring from fly ``surgeon``.
139
+ limit: Maximum number of recordings to return.
140
+ database_access: ``direct`` queries DB files in place. ``copy`` queries
141
+ local cached copies because network DB queries can be slow while
142
+ file transfer is fast.
143
+ cache_dir: Optional cache directory used when ``database_access`` is
144
+ ``copy``.
145
+
146
+ Returns:
147
+ Typed recording experiment rows.
148
+
149
+ This wrapper uses researcher-facing names and delegates to the database
150
+ schema-specific query below.
151
+ """
152
+ return find_stimulus_presentations(
153
+ database_dir,
154
+ date_year=year,
155
+ date_month=month,
156
+ date_day=day,
157
+ stimulus_contains=stimulus,
158
+ genotype_contains=genotype,
159
+ sensor_contains=sensor,
160
+ cell_type_contains=cell_type,
161
+ hemisphere=hemisphere,
162
+ person_contains=person,
163
+ limit=limit,
164
+ database_access=database_access,
165
+ cache_dir=cache_dir,
166
+ )
167
+
168
+
169
+ def _has_tables(connection: sqlite3.Connection, table_names: tuple[str, ...]) -> bool:
170
+ """Check whether all named tables exist.
171
+
172
+ Args:
173
+ connection: Open SQLite connection.
174
+ table_names: Required table names.
175
+
176
+ Returns:
177
+ ``True`` when every table exists, otherwise ``False``.
178
+
179
+ This lets modeled queries skip unrelated SQLite files instead of failing.
180
+ """
181
+ rows = connection.execute(
182
+ """
183
+ SELECT name
184
+ FROM sqlite_master
185
+ WHERE type = 'table'
186
+ """,
187
+ ).fetchall()
188
+ available = {str(row["name"]) for row in rows}
189
+ return all(name in available for name in table_names)
190
+
191
+
192
+ def _query_stimulus_presentations(
193
+ *,
194
+ connection: sqlite3.Connection,
195
+ date_year: int | None,
196
+ date_month: int | None,
197
+ date_day: int | None,
198
+ path_contains: str | None,
199
+ stimulus_contains: str | None,
200
+ genotype_contains: str | None,
201
+ sensor_contains: str | None,
202
+ cell_type_contains: str | None,
203
+ hemisphere: str | None,
204
+ person_contains: str | None,
205
+ limit: int,
206
+ ) -> Sequence[sqlite3.Row]:
207
+ """Run the modeled stimulus-presentation query.
208
+
209
+ Args:
210
+ connection: Open SQLite connection.
211
+ date_year: Optional experiment year.
212
+ date_month: Optional experiment month.
213
+ date_day: Optional experiment day-of-month.
214
+ path_contains: Optional relative path substring.
215
+ stimulus_contains: Optional stimulus-function substring.
216
+ genotype_contains: Optional genotype substring.
217
+ sensor_contains: Optional fluorescent-protein substring.
218
+ cell_type_contains: Optional cell-type substring.
219
+ hemisphere: Optional exact fly-eye value.
220
+ person_contains: Optional surgeon substring.
221
+ limit: Maximum rows to return.
222
+
223
+ Returns:
224
+ SQLite rows with stimulus and fly metadata.
225
+
226
+ The query uses left join because a stimulus row is still useful even if fly
227
+ metadata is incomplete.
228
+ """
229
+ where_clauses: list[str] = []
230
+ parameters: list[DatabaseValue] = []
231
+
232
+ _append_date_filters(
233
+ where_clauses=where_clauses,
234
+ parameters=parameters,
235
+ year=date_year,
236
+ month=date_month,
237
+ day=date_day,
238
+ )
239
+ _append_optional_text_filters(
240
+ where_clauses=where_clauses,
241
+ parameters=parameters,
242
+ filters={
243
+ 'CAST(sp."relativeDataPath" AS TEXT) LIKE ?': path_contains,
244
+ 'CAST(sp."stimulusFunction" AS TEXT) LIKE ?': stimulus_contains,
245
+ 'CAST(f."genotype" AS TEXT) LIKE ?': genotype_contains,
246
+ 'CAST(f."fluorescentProtein" AS TEXT) LIKE ?': sensor_contains,
247
+ 'CAST(f."cellType" AS TEXT) LIKE ?': cell_type_contains,
248
+ 'CAST(f."surgeon" AS TEXT) LIKE ?': person_contains,
249
+ },
250
+ )
251
+
252
+ if hemisphere:
253
+ where_clauses.append('f."eye" = ?')
254
+ parameters.append(hemisphere)
255
+
256
+ where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
257
+ parameters.append(limit)
258
+
259
+ return connection.execute(
260
+ f"""
261
+ SELECT
262
+ sp."stimulusPresentationId",
263
+ sp."fly",
264
+ sp."relativeDataPath",
265
+ sp."stimulusFunction",
266
+ sp."date",
267
+ sp."dataQuality",
268
+ f."genotype",
269
+ f."cellType",
270
+ f."fluorescentProtein",
271
+ f."eye",
272
+ f."surgeon"
273
+ FROM "stimulusPresentation" AS sp
274
+ LEFT JOIN "fly" AS f ON f."flyId" = sp."fly"
275
+ {where_sql}
276
+ ORDER BY sp."stimulusPresentationId"
277
+ LIMIT ?
278
+ """,
279
+ parameters,
280
+ ).fetchall()
281
+
282
+
283
+ def _append_optional_text_filters(
284
+ *,
285
+ where_clauses: list[str],
286
+ parameters: list[DatabaseValue],
287
+ filters: dict[str, str | None],
288
+ ) -> None:
289
+ """Append optional substring filters to a modeled query.
290
+
291
+ Args:
292
+ where_clauses: SQL where clauses being built.
293
+ parameters: SQL parameter values being built.
294
+ filters: Mapping of SQL clauses to optional text values.
295
+
296
+ Returns:
297
+ None. The function appends one clause and parameter for each value.
298
+ """
299
+ for clause, value in filters.items():
300
+ if value:
301
+ where_clauses.append(clause)
302
+ parameters.append(f"%{value}%")
303
+
304
+
305
+ def _experiment_from_row(database_path: Path, row: sqlite3.Row) -> DatabaseExperiment:
306
+ """Convert one joined SQLite row into a typed experiment.
307
+
308
+ Args:
309
+ database_path: Source SQLite file.
310
+ row: SQLite row from the modeled query.
311
+
312
+ Returns:
313
+ A typed experiment search result.
314
+ """
315
+ return DatabaseExperiment(
316
+ database_path=database_path,
317
+ stimulus_presentation_id=int(row["stimulusPresentationId"]),
318
+ fly_id=int(row["fly"]),
319
+ relative_data_path=str(row["relativeDataPath"]),
320
+ stimulus_function=_optional_string(row["stimulusFunction"]),
321
+ date=_optional_string(row["date"]),
322
+ genotype=_optional_string(row["genotype"]),
323
+ cell_type=_optional_string(row["cellType"]),
324
+ fluorescent_protein=_optional_string(row["fluorescentProtein"]),
325
+ hemisphere=_optional_string(row["eye"]),
326
+ person=_optional_string(row["surgeon"]),
327
+ data_quality=_optional_int(row["dataQuality"]),
328
+ )
329
+
330
+
331
+ def _append_date_filters(
332
+ *,
333
+ where_clauses: list[str],
334
+ parameters: list[DatabaseValue],
335
+ year: int | None,
336
+ month: int | None,
337
+ day: int | None,
338
+ ) -> None:
339
+ """Add flexible year, month, and day filters for experiment dates.
340
+
341
+ Args:
342
+ where_clauses: SQL where clauses being built.
343
+ parameters: SQL parameter values being built.
344
+ year: Optional experiment year.
345
+ month: Optional experiment month.
346
+ day: Optional experiment day-of-month.
347
+
348
+ Returns:
349
+ None. The function appends clauses and parameters in place.
350
+
351
+ The lab DB stores dates as ``YYYY-MM-DD HH:MM:SS`` text. ``substr`` keeps the
352
+ query simple and allows year-only, month-only, day-only, or combined filters.
353
+ """
354
+ if year is not None:
355
+ _validate_range("year", year, 1, 9999)
356
+ where_clauses.append('substr(CAST(sp."date" AS TEXT), 1, 4) = ?')
357
+ parameters.append(f"{year:04d}")
358
+
359
+ if month is not None:
360
+ _validate_range("month", month, 1, 12)
361
+ where_clauses.append('substr(CAST(sp."date" AS TEXT), 6, 2) = ?')
362
+ parameters.append(f"{month:02d}")
363
+
364
+ if day is not None:
365
+ _validate_range("day", day, 1, 31)
366
+ where_clauses.append('substr(CAST(sp."date" AS TEXT), 9, 2) = ?')
367
+ parameters.append(f"{day:02d}")
368
+
369
+
370
+ def _validate_range(name: str, value: int, minimum: int, maximum: int) -> None:
371
+ """Validate one integer filter range.
372
+
373
+ Args:
374
+ name: Filter name for error messages.
375
+ value: Integer value to check.
376
+ minimum: Smallest accepted value.
377
+ maximum: Largest accepted value.
378
+
379
+ Returns:
380
+ None when the value is valid.
381
+
382
+ Raises:
383
+ ValueError: If the value falls outside the accepted range.
384
+ """
385
+ if not minimum <= value <= maximum:
386
+ msg = f"{name} must be between {minimum} and {maximum}; got {value}"
387
+ raise ValueError(msg)
388
+
389
+
390
+ def _optional_string(value: object) -> str | None:
391
+ """Convert nullable SQLite values into optional strings.
392
+
393
+ Args:
394
+ value: SQLite value.
395
+
396
+ Returns:
397
+ ``None`` for database nulls, otherwise ``str(value)``.
398
+ """
399
+ return None if value is None else str(value)
400
+
401
+
402
+ def _optional_int(value: object) -> int | None:
403
+ """Convert nullable SQLite values into optional integers.
404
+
405
+ Args:
406
+ value: SQLite value.
407
+
408
+ Returns:
409
+ ``None`` for database nulls, otherwise ``int(value)``.
410
+ """
411
+ if value is None:
412
+ return None
413
+
414
+ if isinstance(value, str | bytes | int | float):
415
+ return int(value)
416
+
417
+ value_type = type(value).__name__
418
+ msg = f"Expected nullable integer-compatible SQLite value, got {value_type}"
419
+ raise TypeError(msg)
twopy/database/sql.py ADDED
@@ -0,0 +1,46 @@
1
+ """Small SQL helpers shared by database query functions.
2
+
3
+ Inputs: discovered SQLite table metadata and identifier names.
4
+ Outputs: validated filters and quoted identifiers for SQL strings.
5
+
6
+ Values still use SQLite parameters. These helpers only handle names, because
7
+ table and column identifiers cannot be passed as SQL parameters.
8
+ """
9
+
10
+ from collections.abc import Sequence
11
+
12
+ from twopy.database.types import DatabaseTable
13
+
14
+ __all__ = ["quote_identifier", "validate_filter_columns"]
15
+
16
+
17
+ def quote_identifier(identifier: str) -> str:
18
+ """Quote a SQLite identifier.
19
+
20
+ Args:
21
+ identifier: Table or column name from SQLite metadata.
22
+
23
+ Returns:
24
+ Identifier wrapped in double quotes with embedded quotes escaped.
25
+ """
26
+ return '"' + identifier.replace('"', '""') + '"'
27
+
28
+
29
+ def validate_filter_columns(table: DatabaseTable, names: Sequence[str]) -> None:
30
+ """Ensure filter names are columns in the selected table.
31
+
32
+ Args:
33
+ table: Table being queried.
34
+ names: Filter column names requested by the caller.
35
+
36
+ Returns:
37
+ None when every name exists.
38
+
39
+ Raises:
40
+ ValueError: If any filter name is not a table column.
41
+ """
42
+ available = {column.name for column in table.columns}
43
+ missing = tuple(name for name in names if name not in available)
44
+ if missing:
45
+ msg = f"Missing column(s) in {table.name}: {', '.join(missing)}"
46
+ raise ValueError(msg)
@@ -0,0 +1,116 @@
1
+ """Typed values and records used by the database query layer.
2
+
3
+ Inputs: SQLite database metadata and rows.
4
+ Outputs: small immutable Python objects that describe database files, tables,
5
+ columns, raw records, and modeled recording experiments.
6
+
7
+ These definitions are split out so query code can stay short and auditable.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ __all__ = [
15
+ "DEFAULT_DATABASE_FILES",
16
+ "DatabaseAccess",
17
+ "DatabaseCatalog",
18
+ "DatabaseColumn",
19
+ "DatabaseExperiment",
20
+ "DatabaseRecord",
21
+ "DatabaseTable",
22
+ "DatabaseValue",
23
+ ]
24
+
25
+ DatabaseValue = str | int | float | bytes | None
26
+ DatabaseAccess = Literal["direct", "copy"]
27
+
28
+ DEFAULT_DATABASE_FILES = ("experimentLog.db", "experimentInitLog.db")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class DatabaseCatalog:
33
+ """Known SQLite database files in the lab database folder.
34
+
35
+ Inputs: database root folder and discovered SQLite file paths.
36
+ Outputs: immutable paths used by query functions.
37
+
38
+ This object exists so GUI code can show which DB files are available before
39
+ users try to search experiments.
40
+ """
41
+
42
+ database_dir: Path
43
+ sqlite_files: tuple[Path, ...]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class DatabaseColumn:
48
+ """Description of one SQLite table column.
49
+
50
+ Inputs: SQLite ``PRAGMA table_info`` output.
51
+ Outputs: column name, SQLite type text, and whether SQLite marks it as a
52
+ primary-key column.
53
+
54
+ Column metadata lets filtering code validate user-provided filter names
55
+ before building SQL.
56
+ """
57
+
58
+ name: str
59
+ sqlite_type: str
60
+ is_primary_key: bool
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class DatabaseTable:
65
+ """Description of one SQLite table.
66
+
67
+ Inputs: SQLite database path, table name, and discovered columns.
68
+ Outputs: immutable table metadata for query planning and GUI display.
69
+
70
+ This is a small schema snapshot; it does not include table rows.
71
+ """
72
+
73
+ database_path: Path
74
+ name: str
75
+ columns: tuple[DatabaseColumn, ...]
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class DatabaseRecord:
80
+ """One experiment-like row returned from a database query.
81
+
82
+ Inputs: SQLite source path, table name, and row values.
83
+ Outputs: a typed record that preserves source context with row data.
84
+
85
+ The row values remain a dictionary because the real DB schema may evolve and
86
+ has not yet been normalized into twopy-specific objects.
87
+ """
88
+
89
+ database_path: Path
90
+ table_name: str
91
+ values: dict[str, DatabaseValue]
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class DatabaseExperiment:
96
+ """Joined stimulus-presentation and fly metadata for one experiment.
97
+
98
+ Inputs: rows from ``stimulusPresentation`` and ``fly`` tables.
99
+ Outputs: the fields most useful for finding a recording before loading data.
100
+
101
+ This object is the first twopy-specific database view. It keeps the raw DB
102
+ flexible while giving the GUI a stable, typed search result.
103
+ """
104
+
105
+ database_path: Path
106
+ stimulus_presentation_id: int
107
+ fly_id: int
108
+ relative_data_path: str
109
+ stimulus_function: str | None
110
+ date: str | None
111
+ genotype: str | None
112
+ cell_type: str | None
113
+ fluorescent_protein: str | None
114
+ hemisphere: str | None
115
+ person: str | None
116
+ data_quality: int | None