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.
- repo/__init__.py +1 -1
- repo/manager.py +14 -23
- repo/repo.py +52 -10
- starbash/__init__.py +10 -3
- starbash/aliases.py +145 -0
- starbash/analytics.py +3 -2
- starbash/app.py +512 -473
- starbash/check_version.py +18 -0
- starbash/commands/__init__.py +2 -1
- starbash/commands/info.py +88 -14
- starbash/commands/process.py +76 -24
- starbash/commands/repo.py +41 -68
- starbash/commands/select.py +141 -142
- starbash/commands/user.py +88 -23
- starbash/database.py +219 -112
- starbash/defaults/starbash.toml +24 -3
- starbash/exception.py +21 -0
- starbash/main.py +29 -7
- starbash/paths.py +35 -5
- starbash/processing.py +724 -0
- starbash/recipes/README.md +3 -0
- starbash/recipes/master_bias/starbash.toml +16 -19
- starbash/recipes/master_dark/starbash.toml +33 -0
- starbash/recipes/master_flat/starbash.toml +26 -18
- starbash/recipes/osc.py +190 -0
- starbash/recipes/osc_dual_duo/starbash.toml +54 -44
- starbash/recipes/osc_simple/starbash.toml +82 -0
- starbash/recipes/osc_single_duo/starbash.toml +51 -32
- starbash/recipes/seestar/starbash.toml +82 -0
- starbash/recipes/starbash.toml +30 -9
- starbash/selection.py +32 -36
- starbash/templates/repo/master.toml +7 -3
- starbash/templates/repo/processed.toml +15 -0
- starbash/templates/userconfig.toml +9 -0
- starbash/toml.py +13 -13
- starbash/tool.py +230 -96
- starbash-0.1.15.dist-info/METADATA +216 -0
- starbash-0.1.15.dist-info/RECORD +45 -0
- starbash/recipes/osc_dual_duo/starbash.py +0 -151
- starbash-0.1.9.dist-info/METADATA +0 -145
- starbash-0.1.9.dist-info/RECORD +0 -37
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
- {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
|
|
5
|
-
from typing import Any, Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime, timedelta
|
|
7
|
-
import
|
|
8
|
-
from typing import
|
|
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
|
|
13
|
-
ImageRow
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
163
|
-
imagetyp TEXT NOT NULL,
|
|
164
|
-
object TEXT
|
|
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
|
-
#
|
|
197
|
-
repo_id
|
|
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
|
|
207
|
-
SELECT 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
|
-
(
|
|
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"
|
|
216
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
340
|
-
|
|
341
|
-
-
|
|
342
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
where_clauses.append("
|
|
358
|
-
params.append(
|
|
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[
|
|
417
|
+
metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
|
|
383
418
|
metadata["id"] = row["id"]
|
|
384
419
|
|
|
385
|
-
# Add
|
|
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
|
-
|
|
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:
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
414
|
-
|
|
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[
|
|
536
|
+
metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
|
|
492
537
|
metadata["id"] = row["id"]
|
|
493
538
|
|
|
494
|
-
# Add
|
|
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[
|
|
566
|
+
metadata[Database.REPO_URL_KEY] = row[Database.REPO_URL_KEY]
|
|
520
567
|
metadata["id"] = row["id"]
|
|
521
568
|
|
|
522
|
-
# Add
|
|
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
|
|
591
|
-
AND start >= ? AND start <= ?
|
|
693
|
+
WHERE {where_clause}
|
|
592
694
|
LIMIT 1
|
|
593
695
|
""",
|
|
594
|
-
|
|
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(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
646
|
-
new[Database.IMAGETYP_KEY],
|
|
647
|
-
new
|
|
648
|
-
new.get(Database.TELESCOP_KEY
|
|
649
|
-
new[Database.NUM_IMAGES_KEY],
|
|
650
|
-
new[Database.EXPTIME_TOTAL_KEY],
|
|
651
|
-
new
|
|
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) ->
|
|
769
|
+
def __enter__(self) -> Database:
|
|
663
770
|
return self
|
|
664
771
|
|
|
665
772
|
def __exit__(self, exc_type, exc, tb) -> None:
|
starbash/defaults/starbash.toml
CHANGED
|
@@ -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
|
-
|
|
18
|
+
fits = ["fits", "fit"]
|
|
18
19
|
|
|
19
20
|
# filter names
|
|
20
|
-
SiiOiii = ["SiiOiii", "
|
|
21
|
-
HaOiii = ["HaOiii", "
|
|
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"]
|