starbash 0.1.9__py3-none-any.whl → 0.1.15__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 (44) hide show
  1. repo/__init__.py +1 -1
  2. repo/manager.py +14 -23
  3. repo/repo.py +52 -10
  4. starbash/__init__.py +10 -3
  5. starbash/aliases.py +145 -0
  6. starbash/analytics.py +3 -2
  7. starbash/app.py +512 -473
  8. starbash/check_version.py +18 -0
  9. starbash/commands/__init__.py +2 -1
  10. starbash/commands/info.py +88 -14
  11. starbash/commands/process.py +76 -24
  12. starbash/commands/repo.py +41 -68
  13. starbash/commands/select.py +141 -142
  14. starbash/commands/user.py +88 -23
  15. starbash/database.py +219 -112
  16. starbash/defaults/starbash.toml +24 -3
  17. starbash/exception.py +21 -0
  18. starbash/main.py +29 -7
  19. starbash/paths.py +35 -5
  20. starbash/processing.py +724 -0
  21. starbash/recipes/README.md +3 -0
  22. starbash/recipes/master_bias/starbash.toml +16 -19
  23. starbash/recipes/master_dark/starbash.toml +33 -0
  24. starbash/recipes/master_flat/starbash.toml +26 -18
  25. starbash/recipes/osc.py +190 -0
  26. starbash/recipes/osc_dual_duo/starbash.toml +54 -44
  27. starbash/recipes/osc_simple/starbash.toml +82 -0
  28. starbash/recipes/osc_single_duo/starbash.toml +51 -32
  29. starbash/recipes/seestar/starbash.toml +82 -0
  30. starbash/recipes/starbash.toml +30 -9
  31. starbash/selection.py +32 -36
  32. starbash/templates/repo/master.toml +7 -3
  33. starbash/templates/repo/processed.toml +15 -0
  34. starbash/templates/userconfig.toml +9 -0
  35. starbash/toml.py +13 -13
  36. starbash/tool.py +230 -96
  37. starbash-0.1.15.dist-info/METADATA +216 -0
  38. starbash-0.1.15.dist-info/RECORD +45 -0
  39. starbash/recipes/osc_dual_duo/starbash.py +0 -151
  40. starbash-0.1.9.dist-info/METADATA +0 -145
  41. starbash-0.1.9.dist-info/RECORD +0 -37
  42. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
  43. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
  44. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/licenses/LICENSE +0 -0
starbash/database.py CHANGED
@@ -1,16 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import sqlite3
4
- from pathlib import Path
5
- from typing import Any, Optional
5
+ from dataclasses import dataclass
6
6
  from datetime import datetime, timedelta
7
- import json
8
- from typing import TypeAlias
7
+ from pathlib import Path
8
+ from typing import Any
9
9
 
10
+ from .aliases import normalize_target_name
10
11
  from .paths import get_user_data_dir
11
12
 
12
- SessionRow: TypeAlias = dict[str, Any]
13
- ImageRow: TypeAlias = dict[str, Any]
13
+ type SessionRow = dict[str, Any]
14
+ type ImageRow = dict[str, Any]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class SearchCondition:
19
+ """A search condition for database queries.
20
+
21
+ Args:
22
+ column_name: The column name to filter on (e.g., 'i.date_obs', 'r.url')
23
+ comparison_op: The comparison operator (e.g., '=', '>=', '<=', 'LIKE')
24
+ value: The value to compare against
25
+ """
26
+
27
+ column_name: str
28
+ comparison_op: str
29
+ value: Any
14
30
 
15
31
 
16
32
  def get_column_name(k: str) -> str:
@@ -34,8 +50,8 @@ class Database:
34
50
  #2: images
35
51
  Provides an `images` table for FITS metadata and basic helpers.
36
52
 
37
- The images table stores DATE-OBS and DATE as indexed SQL columns for
38
- efficient date-based queries, while other FITS metadata is stored in JSON.
53
+ The images table stores DATE-OBS, DATE, and IMAGETYP as indexed SQL columns for
54
+ efficient date-based and type-based queries, while other FITS metadata is stored in JSON.
39
55
 
40
56
  The 'path' column contains a path **relative** to the repository root.
41
57
  Each image belongs to exactly one repo, linked via the repo_id foreign key.
@@ -68,7 +84,11 @@ class Database:
68
84
  IMAGETYP_KEY = "IMAGETYP"
69
85
  OBJECT_KEY = "OBJECT"
70
86
  TELESCOP_KEY = "TELESCOP"
87
+ EXPTIME_KEY = "EXPTIME" # in all image files
88
+ TOTALEXP_KEY = "TOTALEXP" # in stacked ASI files
89
+
71
90
  ID_KEY = "id" # for finding any row by its ID
91
+ REPO_URL_KEY = "repo_url"
72
92
 
73
93
  SESSIONS_TABLE = "sessions"
74
94
  IMAGES_TABLE = "images"
@@ -76,7 +96,7 @@ class Database:
76
96
 
77
97
  def __init__(
78
98
  self,
79
- base_dir: Optional[Path] = None,
99
+ base_dir: Path | None = None,
80
100
  ) -> None:
81
101
  # Resolve base data directory (allow override for tests)
82
102
  if base_dir is None:
@@ -115,7 +135,7 @@ class Database:
115
135
  """
116
136
  )
117
137
 
118
- # Create images table with DATE-OBS and DATE as indexed columns
138
+ # Create images table with DATE-OBS, DATE, and IMAGETYP as indexed columns
119
139
  cursor.execute(
120
140
  f"""
121
141
  CREATE TABLE IF NOT EXISTS {self.IMAGES_TABLE} (
@@ -124,6 +144,7 @@ class Database:
124
144
  path TEXT NOT NULL,
125
145
  date_obs TEXT,
126
146
  date TEXT,
147
+ imagetyp TEXT COLLATE NOCASE,
127
148
  metadata TEXT NOT NULL,
128
149
  FOREIGN KEY (repo_id) REFERENCES {self.REPOS_TABLE}(id),
129
150
  UNIQUE(repo_id, path)
@@ -152,6 +173,13 @@ class Database:
152
173
  """
153
174
  )
154
175
 
176
+ # Create index on imagetyp for efficient image type filtering
177
+ cursor.execute(
178
+ f"""
179
+ CREATE INDEX IF NOT EXISTS idx_images_imagetyp ON {self.IMAGES_TABLE}(imagetyp)
180
+ """
181
+ )
182
+
155
183
  # Create sessions table
156
184
  cursor.execute(
157
185
  f"""
@@ -159,12 +187,13 @@ class Database:
159
187
  id INTEGER PRIMARY KEY AUTOINCREMENT,
160
188
  start TEXT NOT NULL,
161
189
  end TEXT NOT NULL,
162
- filter TEXT NOT NULL,
163
- imagetyp TEXT NOT NULL,
164
- object TEXT NOT NULL,
165
- telescop TEXT NOT NULL,
190
+ filter TEXT COLLATE NOCASE,
191
+ imagetyp TEXT COLLATE NOCASE NOT NULL,
192
+ object TEXT,
193
+ telescop TEXT COLLATENOCASE NOT NULL,
166
194
  num_images INTEGER NOT NULL,
167
195
  exptime_total REAL NOT NULL,
196
+ exptime REAL NOT NULL,
168
197
  image_doc_id INTEGER,
169
198
  FOREIGN KEY (image_doc_id) REFERENCES {self.IMAGES_TABLE}(id)
170
199
  )
@@ -175,7 +204,7 @@ class Database:
175
204
  cursor.execute(
176
205
  f"""
177
206
  CREATE INDEX IF NOT EXISTS idx_sessions_lookup
178
- ON {self.SESSIONS_TABLE}(filter, imagetyp, object, telescop, start, end)
207
+ ON {self.SESSIONS_TABLE}(filter, imagetyp, object, telescop, exptime, start, end)
179
208
  """
180
209
  )
181
210
 
@@ -186,38 +215,44 @@ class Database:
186
215
  """Remove a repo record by URL.
187
216
 
188
217
  This will cascade delete all images belonging to this repo, and all sessions
189
- that reference those images.
218
+ that reference those images via image_doc_id.
219
+
220
+ The relationship is: repos -> images (via repo_id) -> sessions (via image_doc_id).
221
+ Sessions have an image_doc_id field that points to a representative image.
222
+ We delete sessions whose representative image belongs to the repo being deleted.
190
223
 
191
224
  Args:
192
225
  url: The repository URL (e.g., 'file:///path/to/repo')
193
226
  """
194
227
  cursor = self._db.cursor()
195
228
 
196
- # First get the repo_id
197
- repo_id = self.get_repo_id(url)
198
- if repo_id is None:
199
- return # Repo doesn't exist, nothing to delete
200
-
201
- # Delete sessions that reference images from this repo
202
- # This deletes sessions where image_doc_id points to any image in this repo
229
+ # Use a 3-way join to find and delete sessions that reference images from this repo
230
+ # repo_url -> repo_id -> images.id -> sessions.image_doc_id
203
231
  cursor.execute(
204
232
  f"""
205
233
  DELETE FROM {self.SESSIONS_TABLE}
206
- WHERE image_doc_id IN (
207
- SELECT id FROM {self.IMAGES_TABLE} WHERE repo_id = ?
234
+ WHERE id IN (
235
+ SELECT s.id
236
+ FROM {self.SESSIONS_TABLE} s
237
+ INNER JOIN {self.IMAGES_TABLE} i ON s.image_doc_id = i.id
238
+ INNER JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
239
+ WHERE r.url = ?
208
240
  )
209
241
  """,
210
- (repo_id,),
242
+ (url,),
211
243
  )
212
244
 
213
- # Delete all images from this repo
245
+ # Delete all images from this repo (using repo_id from URL)
214
246
  cursor.execute(
215
- f"DELETE FROM {self.IMAGES_TABLE} WHERE repo_id = ?",
216
- (repo_id,),
247
+ f"""
248
+ DELETE FROM {self.IMAGES_TABLE}
249
+ WHERE repo_id = (SELECT id FROM {self.REPOS_TABLE} WHERE url = ?)
250
+ """,
251
+ (url,),
217
252
  )
218
253
 
219
254
  # Finally delete the repo itself
220
- cursor.execute(f"DELETE FROM {self.REPOS_TABLE} WHERE id = ?", (repo_id,))
255
+ cursor.execute(f"DELETE FROM {self.REPOS_TABLE} WHERE url = ?", (url,))
221
256
 
222
257
  self._db.commit()
223
258
 
@@ -282,7 +317,7 @@ class Database:
282
317
 
283
318
  The record must include a 'path' key (relative to repo); other keys are arbitrary FITS metadata.
284
319
  The path is stored as-is - caller is responsible for making it relative to the repo.
285
- DATE-OBS and DATE are extracted and stored as indexed columns for efficient queries.
320
+ DATE-OBS, DATE, and IMAGETYP are extracted and stored as indexed columns for efficient queries.
286
321
 
287
322
  Args:
288
323
  record: Dictionary containing image metadata including 'path' (relative to repo)
@@ -300,24 +335,26 @@ class Database:
300
335
  if repo_id is None:
301
336
  repo_id = self.upsert_repo(repo_url)
302
337
 
303
- # Extract date fields for column storage
338
+ # Extract special fields for column storage
304
339
  date_obs = record.get(self.DATE_OBS_KEY)
305
340
  date = record.get(self.DATE_KEY)
341
+ imagetyp = record.get(self.IMAGETYP_KEY)
306
342
 
307
- # Separate path and date fields from metadata
343
+ # Separate path and special fields from metadata
308
344
  metadata = {k: v for k, v in record.items() if k != "path"}
309
345
  metadata_json = json.dumps(metadata)
310
346
 
311
347
  cursor = self._db.cursor()
312
348
  cursor.execute(
313
349
  f"""
314
- INSERT INTO {self.IMAGES_TABLE} (repo_id, path, date_obs, date, metadata) VALUES (?, ?, ?, ?, ?)
350
+ INSERT INTO {self.IMAGES_TABLE} (repo_id, path, date_obs, date, imagetyp, metadata) VALUES (?, ?, ?, ?, ?, ?)
315
351
  ON CONFLICT(repo_id, path) DO UPDATE SET
316
352
  date_obs = excluded.date_obs,
317
353
  date = excluded.date,
354
+ imagetyp = excluded.imagetyp,
318
355
  metadata = excluded.metadata
319
356
  """,
320
- (repo_id, str(path), date_obs, date, metadata_json),
357
+ (repo_id, str(path), date_obs, date, imagetyp, metadata_json),
321
358
  )
322
359
 
323
360
  self._db.commit()
@@ -332,38 +369,36 @@ class Database:
332
369
  return result[0]
333
370
  return cursor.lastrowid if cursor.lastrowid is not None else 0
334
371
 
335
- def search_image(self, conditions: dict[str, Any]) -> list[SessionRow]:
372
+ def search_image(self, conditions: list[SearchCondition]) -> list[ImageRow]:
336
373
  """Search for images matching the given conditions.
337
374
 
338
375
  Args:
339
- conditions: Dictionary of metadata key-value pairs to match.
340
- Special keys:
341
- - 'date_start': Filter images with DATE-OBS >= this date
342
- - 'date_end': Filter images with DATE-OBS <= this date
376
+ conditions: List of SearchCondition tuples, each containing:
377
+ - column_name: The column to filter on (e.g., 'i.date_obs', 'r.url', 'i.imagetyp')
378
+ - comparison_op: The comparison operator (e.g., '=', '>=', '<=')
379
+ - value: The value to compare against
343
380
 
344
381
  Returns:
345
382
  List of matching image records with relative path, repo_id, and repo_url
346
- """
347
- # Extract special date filter keys (make a copy to avoid modifying caller's dict)
348
- conditions_copy = dict(conditions)
349
- date_start = conditions_copy.pop("date_start", None)
350
- date_end = conditions_copy.pop("date_end", None)
351
383
 
352
- # Build SQL query with WHERE clauses for date filtering
384
+ Example:
385
+ conditions = [
386
+ SearchCondition('r.url', '=', 'file:///path/to/repo'),
387
+ SearchCondition('i.imagetyp', '=', 'BIAS'),
388
+ SearchCondition('i.date_obs', '>=', '2025-01-01'),
389
+ ]
390
+ """
391
+ # Build SQL query with WHERE clauses from conditions
353
392
  where_clauses = []
354
393
  params = []
355
394
 
356
- if date_start:
357
- where_clauses.append("i.date_obs >= ?")
358
- params.append(date_start)
359
-
360
- if date_end:
361
- where_clauses.append("i.date_obs <= ?")
362
- params.append(date_end)
395
+ for condition in conditions:
396
+ where_clauses.append(f"{condition.column_name} {condition.comparison_op} ?")
397
+ params.append(condition.value)
363
398
 
364
399
  # Build the query with JOIN to repos table
365
400
  query = f"""
366
- SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
401
+ SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.imagetyp, i.metadata, r.url as repo_url
367
402
  FROM {self.IMAGES_TABLE} i
368
403
  JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
369
404
  """
@@ -379,46 +414,56 @@ class Database:
379
414
  # Store the relative path, repo_id, and repo_url for caller
380
415
  metadata["path"] = row["path"]
381
416
  metadata["repo_id"] = row["repo_id"]
382
- metadata["repo_url"] = row["repo_url"]
417
+ metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
383
418
  metadata["id"] = row["id"]
384
419
 
385
- # Add date fields back to metadata for compatibility
420
+ # Add special fields back to metadata for compatibility
386
421
  if row["date_obs"]:
387
422
  metadata[self.DATE_OBS_KEY] = row["date_obs"]
388
423
  if row["date"]:
389
424
  metadata[self.DATE_KEY] = row["date"]
425
+ if row["imagetyp"]:
426
+ metadata[self.IMAGETYP_KEY] = row["imagetyp"]
390
427
 
391
- # Check if remaining conditions match (those stored in JSON metadata)
392
- match = all(metadata.get(k) == v for k, v in conditions_copy.items())
393
-
394
- if match:
395
- results.append(metadata)
428
+ results.append(metadata)
396
429
 
397
430
  return results
398
431
 
399
- def search_session(
400
- self, where_tuple: tuple[str, list[Any]] = ("", [])
401
- ) -> list[SessionRow]:
432
+ def search_session(self, conditions: list[SearchCondition] = []) -> list[SessionRow]:
402
433
  """Search for sessions matching the given conditions.
403
434
 
404
435
  Args:
405
- conditions: Dictionary of session key-value pairs to match, or None for all.
406
- Special keys:
407
- - 'date_start': Filter sessions starting on or after this date
408
- - 'date_end': Filter sessions starting on or before this date
436
+ conditions: List of SearchCondition tuples for filtering sessions.
437
+ Column names should be from the sessions table. If no table prefix
438
+ is given (e.g., "OBJECT"), it will be prefixed with "s." automatically.
409
439
 
410
440
  Returns:
411
- List of matching session records with metadata from the reference image
441
+ List of matching session records with metadata from the reference image and repo_url
412
442
  """
413
- # Build WHERE clause dynamically based on conditions
414
- where_clause, params = where_tuple
443
+ # Build WHERE clause from SearchCondition list
444
+ where_clauses = []
445
+ params = []
446
+
447
+ for condition in conditions:
448
+ # Add table prefix 's.' if not already present to avoid ambiguous column names
449
+ column_name = condition.column_name
450
+ if "." not in column_name:
451
+ # Session table columns that might be ambiguous with images table
452
+ column_name = f"s.{column_name.lower()}"
453
+ where_clauses.append(f"{column_name} {condition.comparison_op} ?")
454
+ params.append(condition.value)
455
+
456
+ # Build the query with JOIN to images and repos tables to get reference image metadata and repo_url
457
+ where_clause = ""
458
+ if where_clauses:
459
+ where_clause = " WHERE " + " AND ".join(where_clauses)
415
460
 
416
- # Build the query with JOIN to images table to get reference image metadata
417
461
  query = f"""
418
462
  SELECT s.id, s.start, s.end, s.filter, s.imagetyp, s.object, s.telescop,
419
- s.num_images, s.exptime_total, s.image_doc_id, i.metadata
463
+ s.num_images, s.exptime_total, s.exptime, s.image_doc_id, i.metadata, r.url as repo_url
420
464
  FROM {self.SESSIONS_TABLE} s
421
465
  LEFT JOIN {self.IMAGES_TABLE} i ON s.image_doc_id = i.id
466
+ LEFT JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
422
467
  {where_clause}
423
468
  """
424
469
 
@@ -473,7 +518,7 @@ class Database:
473
518
  cursor = self._db.cursor()
474
519
  cursor.execute(
475
520
  f"""
476
- SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
521
+ SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.imagetyp, i.metadata, r.url as repo_url
477
522
  FROM {self.IMAGES_TABLE} i
478
523
  JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
479
524
  WHERE r.url = ? AND i.path = ?
@@ -488,14 +533,16 @@ class Database:
488
533
  metadata = json.loads(row["metadata"])
489
534
  metadata["path"] = row["path"]
490
535
  metadata["repo_id"] = row["repo_id"]
491
- metadata["repo_url"] = row["repo_url"]
536
+ metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
492
537
  metadata["id"] = row["id"]
493
538
 
494
- # Add date fields back to metadata for compatibility
539
+ # Add special fields back to metadata for compatibility
495
540
  if row["date_obs"]:
496
541
  metadata[self.DATE_OBS_KEY] = row["date_obs"]
497
542
  if row["date"]:
498
543
  metadata[self.DATE_KEY] = row["date"]
544
+ if row["imagetyp"]:
545
+ metadata[self.IMAGETYP_KEY] = row["imagetyp"]
499
546
 
500
547
  return metadata
501
548
 
@@ -504,7 +551,7 @@ class Database:
504
551
  cursor = self._db.cursor()
505
552
  cursor.execute(
506
553
  f"""
507
- SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
554
+ SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.imagetyp, i.metadata, r.url as repo_url
508
555
  FROM {self.IMAGES_TABLE} i
509
556
  JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
510
557
  """
@@ -516,14 +563,16 @@ class Database:
516
563
  # Return relative path, repo_id, and repo_url for caller
517
564
  metadata["path"] = row["path"]
518
565
  metadata["repo_id"] = row["repo_id"]
519
- metadata["repo_url"] = row["repo_url"]
566
+ metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
520
567
  metadata["id"] = row["id"]
521
568
 
522
- # Add date fields back to metadata for compatibility
569
+ # Add special fields back to metadata for compatibility
523
570
  if row["date_obs"]:
524
571
  metadata[self.DATE_OBS_KEY] = row["date_obs"]
525
572
  if row["date"]:
526
573
  metadata[self.DATE_KEY] = row["date"]
574
+ if row["imagetyp"]:
575
+ metadata[self.IMAGETYP_KEY] = row["imagetyp"]
527
576
 
528
577
  results.append(metadata)
529
578
 
@@ -542,7 +591,7 @@ class Database:
542
591
  cursor.execute(
543
592
  f"""
544
593
  SELECT id, start, end, filter, imagetyp, object, telescop,
545
- num_images, exptime_total, image_doc_id
594
+ num_images, exptime_total, exptime, image_doc_id
546
595
  FROM {self.SESSIONS_TABLE}
547
596
  WHERE id = ?
548
597
  """,
@@ -561,15 +610,10 @@ class Database:
561
610
  Searches for sessions with the same filter, image type, target, and telescope
562
611
  whose start time is within +/- 8 hours of the provided date.
563
612
  """
564
- date = to_find.get(Database.START_KEY)
613
+ date = to_find.get(get_column_name(Database.START_KEY))
565
614
  assert date
566
- image_type = to_find.get(Database.IMAGETYP_KEY)
615
+ image_type = to_find.get(get_column_name(Database.IMAGETYP_KEY))
567
616
  assert image_type
568
- filter = to_find.get(Database.FILTER_KEY)
569
- assert filter
570
- target = to_find.get(Database.OBJECT_KEY)
571
- assert target
572
- telescop = to_find.get(Database.TELESCOP_KEY, "unspecified")
573
617
 
574
618
  # Convert the provided ISO8601 date string to a datetime, then
575
619
  # search for sessions with the same filter whose start time is
@@ -581,17 +625,75 @@ class Database:
581
625
 
582
626
  # Since session 'start' is stored as ISO8601 strings, lexicographic
583
627
  # comparison aligns with chronological ordering for a uniform format.
628
+
629
+ # Build WHERE clause handling NULL values properly
630
+ # In SQL, you cannot use = with NULL, must use IS NULL
631
+ # If a field is not in to_find, we don't filter on it at all
632
+ where_clauses = []
633
+ params = []
634
+
635
+ # Handle imagetyp (required)
636
+ where_clauses.append("imagetyp = ?")
637
+ params.append(image_type)
638
+
639
+ # Handle filter (optional - only filter if present in to_find)
640
+ filter_key = get_column_name(Database.FILTER_KEY)
641
+ filter = to_find.get(filter_key) # filter can be the string "None"
642
+ if filter:
643
+ if filter is None:
644
+ where_clauses.append("filter IS NULL")
645
+ else:
646
+ where_clauses.append("filter = ?")
647
+ params.append(filter)
648
+
649
+ # Handle object/target (optional - only filter if present in to_find)
650
+ object_key = get_column_name(Database.OBJECT_KEY)
651
+ target = to_find.get(object_key)
652
+ if target:
653
+ target = normalize_target_name(target)
654
+ if target is None:
655
+ where_clauses.append("object IS NULL")
656
+ else:
657
+ where_clauses.append("object = ?")
658
+ params.append(target)
659
+
660
+ # Handle telescop (optional - only filter if present in to_find)
661
+ telescop_key = get_column_name(Database.TELESCOP_KEY)
662
+ telescop = to_find.get(telescop_key)
663
+ if telescop:
664
+ if telescop is None:
665
+ where_clauses.append("telescop IS NULL")
666
+ else:
667
+ where_clauses.append("telescop = ?")
668
+ params.append(telescop)
669
+
670
+ # Handle exptime (optional - only filter if present in to_find)
671
+ exptime_key = get_column_name(Database.EXPTIME_KEY)
672
+ if exptime_key in to_find:
673
+ exptime = to_find.get(exptime_key)
674
+ if exptime is None:
675
+ where_clauses.append("exptime IS NULL")
676
+ else:
677
+ where_clauses.append("exptime = ?")
678
+ params.append(exptime)
679
+
680
+ # Time window
681
+ where_clauses.append("start >= ?")
682
+ where_clauses.append("start <= ?")
683
+ params.extend([start_min, start_max])
684
+
685
+ where_clause = " AND ".join(where_clauses)
686
+
584
687
  cursor = self._db.cursor()
585
688
  cursor.execute(
586
689
  f"""
587
690
  SELECT id, start, end, filter, imagetyp, object, telescop,
588
- num_images, exptime_total, image_doc_id
691
+ num_images, exptime_total, exptime, image_doc_id
589
692
  FROM {self.SESSIONS_TABLE}
590
- WHERE filter = ? AND imagetyp = ? AND object = ? AND telescop = ?
591
- AND start >= ? AND start <= ?
693
+ WHERE {where_clause}
592
694
  LIMIT 1
593
695
  """,
594
- (filter, image_type, target, telescop, start_min, start_max),
696
+ params,
595
697
  )
596
698
 
597
699
  row = cursor.fetchone()
@@ -600,22 +702,26 @@ class Database:
600
702
 
601
703
  return dict(row)
602
704
 
603
- def upsert_session(
604
- self, new: SessionRow, existing: SessionRow | None = None
605
- ) -> None:
705
+ def upsert_session(self, new: SessionRow, existing: SessionRow | None = None) -> None:
606
706
  """Insert or update a session record."""
607
707
  cursor = self._db.cursor()
608
708
 
609
709
  if existing:
610
710
  # Update existing session with new data
611
- updated_start = min(new[Database.START_KEY], existing[Database.START_KEY])
612
- updated_end = max(new[Database.END_KEY], existing[Database.END_KEY])
613
- updated_num_images = existing.get(Database.NUM_IMAGES_KEY, 0) + new.get(
614
- Database.NUM_IMAGES_KEY, 0
711
+ updated_start = min(
712
+ new[get_column_name(Database.START_KEY)],
713
+ existing[get_column_name(Database.START_KEY)],
714
+ )
715
+ updated_end = max(
716
+ new[get_column_name(Database.END_KEY)],
717
+ existing[get_column_name(Database.END_KEY)],
615
718
  )
719
+ updated_num_images = existing.get(
720
+ get_column_name(Database.NUM_IMAGES_KEY), 0
721
+ ) + new.get(get_column_name(Database.NUM_IMAGES_KEY), 0)
616
722
  updated_exptime_total = existing.get(
617
- Database.EXPTIME_TOTAL_KEY, 0
618
- ) + new.get(Database.EXPTIME_TOTAL_KEY, 0)
723
+ get_column_name(Database.EXPTIME_TOTAL_KEY), 0
724
+ ) + new.get(get_column_name(Database.EXPTIME_TOTAL_KEY), 0)
619
725
 
620
726
  cursor.execute(
621
727
  f"""
@@ -636,19 +742,20 @@ class Database:
636
742
  cursor.execute(
637
743
  f"""
638
744
  INSERT INTO {self.SESSIONS_TABLE}
639
- (start, end, filter, imagetyp, object, telescop, num_images, exptime_total, image_doc_id)
640
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
745
+ (start, end, filter, imagetyp, object, telescop, num_images, exptime_total, exptime, image_doc_id)
746
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
641
747
  """,
642
748
  (
643
- new[Database.START_KEY],
644
- new[Database.END_KEY],
645
- new[Database.FILTER_KEY],
646
- new[Database.IMAGETYP_KEY],
647
- new[Database.OBJECT_KEY],
648
- new.get(Database.TELESCOP_KEY, "unspecified"),
649
- new[Database.NUM_IMAGES_KEY],
650
- new[Database.EXPTIME_TOTAL_KEY],
651
- new.get(Database.IMAGE_DOC_KEY),
749
+ new[get_column_name(Database.START_KEY)],
750
+ new[get_column_name(Database.END_KEY)],
751
+ new.get(get_column_name(Database.FILTER_KEY)),
752
+ new[get_column_name(Database.IMAGETYP_KEY)],
753
+ normalize_target_name(new.get(get_column_name(Database.OBJECT_KEY))),
754
+ new.get(get_column_name(Database.TELESCOP_KEY)),
755
+ new[get_column_name(Database.NUM_IMAGES_KEY)],
756
+ new[get_column_name(Database.EXPTIME_TOTAL_KEY)],
757
+ new[get_column_name(Database.EXPTIME_KEY)],
758
+ new[get_column_name(Database.IMAGE_DOC_KEY)],
652
759
  ),
653
760
  )
654
761
 
@@ -659,7 +766,7 @@ class Database:
659
766
  self._db.close()
660
767
 
661
768
  # Context manager support
662
- def __enter__(self) -> "Database":
769
+ def __enter__(self) -> Database:
663
770
  return self
664
771
 
665
772
  def __exit__(self, exc_type, exc, tb) -> None:
@@ -12,13 +12,34 @@ kind = "preferences"
12
12
  dark = ["dark", "darks"]
13
13
  flat = ["flat", "flats"]
14
14
  bias = ["bias", "biases"]
15
+ light = ["light", "lights"]
15
16
 
16
17
  # file suffixes
17
- fit = ["fits", "fit"]
18
+ fits = ["fits", "fit"]
18
19
 
19
20
  # filter names
20
- SiiOiii = ["SiiOiii", "SII-OIII", "S2-O3"]
21
- HaOiii = ["HaOiii", "HA-OIII", "Halpha-O3"]
21
+ SiiOiii = ["SiiOiii", "S2O3"]
22
+ HaOiii = ["HaOiii", "HaO3"]
23
+
24
+ None = ["None"]
25
+
26
+ camera_osc = ["OSC", "ZWO ASI2600MC Duo"]
27
+ camera_seestar = ["Seestar", "Seestar S50", "Seestar S30", "Seestar S30 Pro"]
28
+
29
+ # Passes SII 672.4nm and H-Beta 486.1nm lines
30
+ # Capturing of the two main emission wavebands in the deep red and blue at the same time
31
+ #
32
+ # The ALP-T dual band 3.5nm SII&Hb filter is a dual narrowband filter, which lets the deep
33
+ # red Sulfur-II 672.4nm and the blue Hydrogen-Beta 486.1nm lines through and is primarily
34
+ # engineered for color cameras to assist astrophotographers taking deep sky images with
35
+ # superior SNR(Signal to Noise Ratio). With an FWHM halfbandwidth designed at 3.5nm and
36
+ # achieving an optical density (OD) of 4.5 on unwanted wavelengths, it works strongly in
37
+ # blocking light pollution, moonlight, and airglow, leding to enhanced contrast in nebulae
38
+ # images by effectively passing the SII and H-beta emission lines signal only.
39
+ #
40
+ # http://www.antliafilter.com/pd.jsp?fromColId=2&id=160#_pp=2_671
41
+ SiiHb = ["SiiHb", "S2Hb"]
42
+
22
43
 
23
44
  # FIXME, somewhere here list default patterns which can be used to identify NINA, ASIAIR, SEESTAR
24
45
  # raw repo layouts
starbash/exception.py ADDED
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+
4
+ class UserHandledError(ValueError):
5
+ """An exception that terminates processing of the current file, but we want to help the user fix the problem."""
6
+
7
+ def ask_user_handled(self) -> bool:
8
+ """Prompt the user with a friendly message about the error.
9
+ Returns:
10
+ True if the error was handled, False otherwise.
11
+ """
12
+ from starbash import console # Lazy import to avoid circular dependency
13
+
14
+ console.print(f"Error: {self}")
15
+ return False
16
+
17
+ def __rich__(self) -> Any:
18
+ return self.__str__() # At least this is something readable...
19
+
20
+
21
+ __all__ = ["UserHandledError"]