starbash 0.1.6__py3-none-any.whl → 0.1.9__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.
- {starbash/repo → repo}/__init__.py +2 -1
- repo/manager.py +144 -0
- starbash/repo/manager.py → repo/repo.py +29 -116
- starbash/__init__.py +28 -1
- starbash/analytics.py +6 -7
- starbash/app.py +449 -86
- starbash/commands/__init__.py +7 -0
- starbash/commands/info.py +53 -25
- starbash/commands/process.py +154 -0
- starbash/commands/repo.py +168 -77
- starbash/commands/select.py +157 -68
- starbash/database.py +252 -135
- starbash/defaults/starbash.toml +17 -0
- starbash/main.py +4 -1
- starbash/recipes/master_bias/starbash.toml +25 -8
- starbash/recipes/starbash.toml +5 -0
- starbash/selection.py +109 -45
- starbash/templates/repo/master.toml +13 -0
- starbash/templates/userconfig.toml +1 -1
- starbash/toml.py +29 -0
- starbash/tool.py +84 -12
- {starbash-0.1.6.dist-info → starbash-0.1.9.dist-info}/METADATA +37 -16
- starbash-0.1.9.dist-info/RECORD +37 -0
- starbash-0.1.6.dist-info/RECORD +0 -33
- {starbash-0.1.6.dist-info → starbash-0.1.9.dist-info}/WHEEL +0 -0
- {starbash-0.1.6.dist-info → starbash-0.1.9.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.6.dist-info → starbash-0.1.9.dist-info}/licenses/LICENSE +0 -0
starbash/database.py
CHANGED
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import sqlite3
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from typing import Any, Optional
|
|
7
6
|
from datetime import datetime, timedelta
|
|
8
7
|
import json
|
|
8
|
+
from typing import TypeAlias
|
|
9
9
|
|
|
10
10
|
from .paths import get_user_data_dir
|
|
11
11
|
|
|
12
|
+
SessionRow: TypeAlias = dict[str, Any]
|
|
13
|
+
ImageRow: TypeAlias = dict[str, Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_column_name(k: str) -> str:
|
|
17
|
+
"""Convert keynames to SQL legal column names"""
|
|
18
|
+
k = k.lower()
|
|
19
|
+
k = k.replace(" ", "_")
|
|
20
|
+
k = k.replace("-", "_")
|
|
21
|
+
return k
|
|
22
|
+
|
|
12
23
|
|
|
13
24
|
class Database:
|
|
14
25
|
"""SQLite-backed application database.
|
|
15
26
|
|
|
16
27
|
Stores data under the OS-specific user data directory using platformdirs.
|
|
28
|
+
|
|
29
|
+
Tables:
|
|
30
|
+
#1: repos
|
|
31
|
+
A table with one row per repository. Contains only 'id' (primary key) and 'url' (unique).
|
|
32
|
+
The URL identifies the repository root (e.g., 'file:///path/to/repo').
|
|
33
|
+
|
|
34
|
+
#2: images
|
|
17
35
|
Provides an `images` table for FITS metadata and basic helpers.
|
|
18
36
|
|
|
19
37
|
The images table stores DATE-OBS and DATE as indexed SQL columns for
|
|
20
38
|
efficient date-based queries, while other FITS metadata is stored in JSON.
|
|
39
|
+
|
|
40
|
+
The 'path' column contains a path **relative** to the repository root.
|
|
41
|
+
Each image belongs to exactly one repo, linked via the repo_id foreign key.
|
|
42
|
+
The combination of (repo_id, path) is unique.
|
|
43
|
+
|
|
44
|
+
Image retrieval methods (get_image, search_image, all_images) join with the repos
|
|
45
|
+
table to include repo_url in results, allowing callers to reconstruct absolute paths.
|
|
46
|
+
|
|
47
|
+
#3: sessions
|
|
48
|
+
The sessions table has one row per observing session, summarizing key info.
|
|
49
|
+
Sessions are identified by filter, image type, target, telescope, etc, and start/end times.
|
|
50
|
+
They correspond to groups of images taken together during an observing run (e.g.
|
|
51
|
+
session start/end describes the range of images DATE-OBS).
|
|
52
|
+
|
|
53
|
+
Each session also has an image_doc_id field which will point to a representative
|
|
54
|
+
image in the images table. Eventually we'll use joins to add extra info from images to
|
|
55
|
+
the exposed 'session' row.
|
|
56
|
+
|
|
21
57
|
"""
|
|
22
58
|
|
|
23
59
|
EXPTIME_KEY = "EXPTIME"
|
|
@@ -28,13 +64,15 @@ class Database:
|
|
|
28
64
|
EXPTIME_TOTAL_KEY = "exptime-total"
|
|
29
65
|
DATE_OBS_KEY = "DATE-OBS"
|
|
30
66
|
DATE_KEY = "DATE"
|
|
31
|
-
IMAGE_DOC_KEY = "image-doc"
|
|
67
|
+
IMAGE_DOC_KEY = "image-doc-id"
|
|
32
68
|
IMAGETYP_KEY = "IMAGETYP"
|
|
33
69
|
OBJECT_KEY = "OBJECT"
|
|
34
70
|
TELESCOP_KEY = "TELESCOP"
|
|
71
|
+
ID_KEY = "id" # for finding any row by its ID
|
|
35
72
|
|
|
36
73
|
SESSIONS_TABLE = "sessions"
|
|
37
74
|
IMAGES_TABLE = "images"
|
|
75
|
+
REPOS_TABLE = "repos"
|
|
38
76
|
|
|
39
77
|
def __init__(
|
|
40
78
|
self,
|
|
@@ -57,18 +95,38 @@ class Database:
|
|
|
57
95
|
self._init_tables()
|
|
58
96
|
|
|
59
97
|
def _init_tables(self) -> None:
|
|
60
|
-
"""Create the images and sessions tables if they don't exist."""
|
|
98
|
+
"""Create the repos, images and sessions tables if they don't exist."""
|
|
61
99
|
cursor = self._db.cursor()
|
|
62
100
|
|
|
101
|
+
# Create repos table
|
|
102
|
+
cursor.execute(
|
|
103
|
+
f"""
|
|
104
|
+
CREATE TABLE IF NOT EXISTS {self.REPOS_TABLE} (
|
|
105
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
106
|
+
url TEXT UNIQUE NOT NULL
|
|
107
|
+
)
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Create index on url for faster lookups
|
|
112
|
+
cursor.execute(
|
|
113
|
+
f"""
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_repos_url ON {self.REPOS_TABLE}(url)
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
|
|
63
118
|
# Create images table with DATE-OBS and DATE as indexed columns
|
|
64
119
|
cursor.execute(
|
|
65
120
|
f"""
|
|
66
121
|
CREATE TABLE IF NOT EXISTS {self.IMAGES_TABLE} (
|
|
67
122
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
-
|
|
123
|
+
repo_id INTEGER NOT NULL,
|
|
124
|
+
path TEXT NOT NULL,
|
|
69
125
|
date_obs TEXT,
|
|
70
126
|
date TEXT,
|
|
71
|
-
metadata TEXT NOT NULL
|
|
127
|
+
metadata TEXT NOT NULL,
|
|
128
|
+
FOREIGN KEY (repo_id) REFERENCES {self.REPOS_TABLE}(id),
|
|
129
|
+
UNIQUE(repo_id, path)
|
|
72
130
|
)
|
|
73
131
|
"""
|
|
74
132
|
)
|
|
@@ -107,7 +165,8 @@ class Database:
|
|
|
107
165
|
telescop TEXT NOT NULL,
|
|
108
166
|
num_images INTEGER NOT NULL,
|
|
109
167
|
exptime_total REAL NOT NULL,
|
|
110
|
-
image_doc_id INTEGER
|
|
168
|
+
image_doc_id INTEGER,
|
|
169
|
+
FOREIGN KEY (image_doc_id) REFERENCES {self.IMAGES_TABLE}(id)
|
|
111
170
|
)
|
|
112
171
|
"""
|
|
113
172
|
)
|
|
@@ -122,18 +181,125 @@ class Database:
|
|
|
122
181
|
|
|
123
182
|
self._db.commit()
|
|
124
183
|
|
|
184
|
+
# --- Convenience helpers for common repo operations ---
|
|
185
|
+
def remove_repo(self, url: str) -> None:
|
|
186
|
+
"""Remove a repo record by URL.
|
|
187
|
+
|
|
188
|
+
This will cascade delete all images belonging to this repo, and all sessions
|
|
189
|
+
that reference those images.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
url: The repository URL (e.g., 'file:///path/to/repo')
|
|
193
|
+
"""
|
|
194
|
+
cursor = self._db.cursor()
|
|
195
|
+
|
|
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
|
|
203
|
+
cursor.execute(
|
|
204
|
+
f"""
|
|
205
|
+
DELETE FROM {self.SESSIONS_TABLE}
|
|
206
|
+
WHERE image_doc_id IN (
|
|
207
|
+
SELECT id FROM {self.IMAGES_TABLE} WHERE repo_id = ?
|
|
208
|
+
)
|
|
209
|
+
""",
|
|
210
|
+
(repo_id,),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Delete all images from this repo
|
|
214
|
+
cursor.execute(
|
|
215
|
+
f"DELETE FROM {self.IMAGES_TABLE} WHERE repo_id = ?",
|
|
216
|
+
(repo_id,),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Finally delete the repo itself
|
|
220
|
+
cursor.execute(f"DELETE FROM {self.REPOS_TABLE} WHERE id = ?", (repo_id,))
|
|
221
|
+
|
|
222
|
+
self._db.commit()
|
|
223
|
+
|
|
224
|
+
def upsert_repo(self, url: str) -> int:
|
|
225
|
+
"""Insert or update a repo record by unique URL.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
url: The repository URL (e.g., 'file:///path/to/repo')
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
The rowid of the inserted/updated record.
|
|
232
|
+
"""
|
|
233
|
+
cursor = self._db.cursor()
|
|
234
|
+
cursor.execute(
|
|
235
|
+
f"""
|
|
236
|
+
INSERT INTO {self.REPOS_TABLE} (url) VALUES (?)
|
|
237
|
+
ON CONFLICT(url) DO NOTHING
|
|
238
|
+
""",
|
|
239
|
+
(url,),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
self._db.commit()
|
|
243
|
+
|
|
244
|
+
# Get the rowid of the inserted/existing record
|
|
245
|
+
cursor.execute(f"SELECT id FROM {self.REPOS_TABLE} WHERE url = ?", (url,))
|
|
246
|
+
result = cursor.fetchone()
|
|
247
|
+
if result:
|
|
248
|
+
return result[0]
|
|
249
|
+
return cursor.lastrowid if cursor.lastrowid is not None else 0
|
|
250
|
+
|
|
251
|
+
def get_repo_id(self, url: str) -> int | None:
|
|
252
|
+
"""Get the repo_id for a given URL.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
url: The repository URL
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The repo_id if found, None otherwise
|
|
259
|
+
"""
|
|
260
|
+
cursor = self._db.cursor()
|
|
261
|
+
cursor.execute(f"SELECT id FROM {self.REPOS_TABLE} WHERE url = ?", (url,))
|
|
262
|
+
result = cursor.fetchone()
|
|
263
|
+
return result[0] if result else None
|
|
264
|
+
|
|
265
|
+
def get_repo_url(self, repo_id: int) -> str | None:
|
|
266
|
+
"""Get the URL for a given repo_id.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
repo_id: The repository ID
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The URL if found, None otherwise
|
|
273
|
+
"""
|
|
274
|
+
cursor = self._db.cursor()
|
|
275
|
+
cursor.execute(f"SELECT url FROM {self.REPOS_TABLE} WHERE id = ?", (repo_id,))
|
|
276
|
+
result = cursor.fetchone()
|
|
277
|
+
return result[0] if result else None
|
|
278
|
+
|
|
125
279
|
# --- Convenience helpers for common image operations ---
|
|
126
|
-
def upsert_image(self, record: dict[str, Any]) -> int:
|
|
280
|
+
def upsert_image(self, record: dict[str, Any], repo_url: str) -> int:
|
|
127
281
|
"""Insert or update an image record by unique path.
|
|
128
282
|
|
|
129
|
-
The record must include a 'path' key; other keys are arbitrary FITS metadata.
|
|
283
|
+
The record must include a 'path' key (relative to repo); other keys are arbitrary FITS metadata.
|
|
284
|
+
The path is stored as-is - caller is responsible for making it relative to the repo.
|
|
130
285
|
DATE-OBS and DATE are extracted and stored as indexed columns for efficient queries.
|
|
131
|
-
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
record: Dictionary containing image metadata including 'path' (relative to repo)
|
|
289
|
+
repo_url: The repository URL this image belongs to
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The rowid of the inserted/updated record.
|
|
132
293
|
"""
|
|
133
294
|
path = record.get("path")
|
|
134
295
|
if not path:
|
|
135
296
|
raise ValueError("record must include 'path'")
|
|
136
297
|
|
|
298
|
+
# Get or create the repo_id for this URL
|
|
299
|
+
repo_id = self.get_repo_id(repo_url)
|
|
300
|
+
if repo_id is None:
|
|
301
|
+
repo_id = self.upsert_repo(repo_url)
|
|
302
|
+
|
|
137
303
|
# Extract date fields for column storage
|
|
138
304
|
date_obs = record.get(self.DATE_OBS_KEY)
|
|
139
305
|
date = record.get(self.DATE_KEY)
|
|
@@ -145,25 +311,28 @@ class Database:
|
|
|
145
311
|
cursor = self._db.cursor()
|
|
146
312
|
cursor.execute(
|
|
147
313
|
f"""
|
|
148
|
-
INSERT INTO {self.IMAGES_TABLE} (path, date_obs, date, metadata) VALUES (?, ?, ?, ?)
|
|
149
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
314
|
+
INSERT INTO {self.IMAGES_TABLE} (repo_id, path, date_obs, date, metadata) VALUES (?, ?, ?, ?, ?)
|
|
315
|
+
ON CONFLICT(repo_id, path) DO UPDATE SET
|
|
150
316
|
date_obs = excluded.date_obs,
|
|
151
317
|
date = excluded.date,
|
|
152
318
|
metadata = excluded.metadata
|
|
153
319
|
""",
|
|
154
|
-
(path, date_obs, date, metadata_json),
|
|
320
|
+
(repo_id, str(path), date_obs, date, metadata_json),
|
|
155
321
|
)
|
|
156
322
|
|
|
157
323
|
self._db.commit()
|
|
158
324
|
|
|
159
325
|
# Get the rowid of the inserted/updated record
|
|
160
|
-
cursor.execute(
|
|
326
|
+
cursor.execute(
|
|
327
|
+
f"SELECT id FROM {self.IMAGES_TABLE} WHERE repo_id = ? AND path = ?",
|
|
328
|
+
(repo_id, str(path)),
|
|
329
|
+
)
|
|
161
330
|
result = cursor.fetchone()
|
|
162
331
|
if result:
|
|
163
332
|
return result[0]
|
|
164
333
|
return cursor.lastrowid if cursor.lastrowid is not None else 0
|
|
165
334
|
|
|
166
|
-
def search_image(self, conditions: dict[str, Any]) -> list[
|
|
335
|
+
def search_image(self, conditions: dict[str, Any]) -> list[SessionRow]:
|
|
167
336
|
"""Search for images matching the given conditions.
|
|
168
337
|
|
|
169
338
|
Args:
|
|
@@ -173,7 +342,7 @@ class Database:
|
|
|
173
342
|
- 'date_end': Filter images with DATE-OBS <= this date
|
|
174
343
|
|
|
175
344
|
Returns:
|
|
176
|
-
List of matching image records
|
|
345
|
+
List of matching image records with relative path, repo_id, and repo_url
|
|
177
346
|
"""
|
|
178
347
|
# Extract special date filter keys (make a copy to avoid modifying caller's dict)
|
|
179
348
|
conditions_copy = dict(conditions)
|
|
@@ -185,15 +354,19 @@ class Database:
|
|
|
185
354
|
params = []
|
|
186
355
|
|
|
187
356
|
if date_start:
|
|
188
|
-
where_clauses.append("date_obs >= ?")
|
|
357
|
+
where_clauses.append("i.date_obs >= ?")
|
|
189
358
|
params.append(date_start)
|
|
190
359
|
|
|
191
360
|
if date_end:
|
|
192
|
-
where_clauses.append("date_obs <= ?")
|
|
361
|
+
where_clauses.append("i.date_obs <= ?")
|
|
193
362
|
params.append(date_end)
|
|
194
363
|
|
|
195
|
-
# Build the query
|
|
196
|
-
query = f"
|
|
364
|
+
# Build the query with JOIN to repos table
|
|
365
|
+
query = f"""
|
|
366
|
+
SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
|
|
367
|
+
FROM {self.IMAGES_TABLE} i
|
|
368
|
+
JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
|
|
369
|
+
"""
|
|
197
370
|
if where_clauses:
|
|
198
371
|
query += " WHERE " + " AND ".join(where_clauses)
|
|
199
372
|
|
|
@@ -203,7 +376,10 @@ class Database:
|
|
|
203
376
|
results = []
|
|
204
377
|
for row in cursor.fetchall():
|
|
205
378
|
metadata = json.loads(row["metadata"])
|
|
379
|
+
# Store the relative path, repo_id, and repo_url for caller
|
|
206
380
|
metadata["path"] = row["path"]
|
|
381
|
+
metadata["repo_id"] = row["repo_id"]
|
|
382
|
+
metadata["repo_url"] = row["repo_url"]
|
|
207
383
|
metadata["id"] = row["id"]
|
|
208
384
|
|
|
209
385
|
# Add date fields back to metadata for compatibility
|
|
@@ -218,11 +394,11 @@ class Database:
|
|
|
218
394
|
if match:
|
|
219
395
|
results.append(metadata)
|
|
220
396
|
|
|
221
|
-
return results
|
|
397
|
+
return results
|
|
222
398
|
|
|
223
399
|
def search_session(
|
|
224
|
-
self,
|
|
225
|
-
) -> list[
|
|
400
|
+
self, where_tuple: tuple[str, list[Any]] = ("", [])
|
|
401
|
+
) -> list[SessionRow]:
|
|
226
402
|
"""Search for sessions matching the given conditions.
|
|
227
403
|
|
|
228
404
|
Args:
|
|
@@ -232,62 +408,32 @@ class Database:
|
|
|
232
408
|
- 'date_end': Filter sessions starting on or before this date
|
|
233
409
|
|
|
234
410
|
Returns:
|
|
235
|
-
List of matching session records
|
|
411
|
+
List of matching session records with metadata from the reference image
|
|
236
412
|
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
413
|
+
# Build WHERE clause dynamically based on conditions
|
|
414
|
+
where_clause, params = where_tuple
|
|
415
|
+
|
|
416
|
+
# Build the query with JOIN to images table to get reference image metadata
|
|
417
|
+
query = f"""
|
|
418
|
+
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
|
|
420
|
+
FROM {self.SESSIONS_TABLE} s
|
|
421
|
+
LEFT JOIN {self.IMAGES_TABLE} i ON s.image_doc_id = i.id
|
|
422
|
+
{where_clause}
|
|
246
423
|
"""
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
# Extract date range conditions if present
|
|
250
|
-
date_start = conditions.get("date_start")
|
|
251
|
-
date_end = conditions.get("date_end")
|
|
252
424
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
k: v
|
|
256
|
-
for k, v in conditions.items()
|
|
257
|
-
if k not in ("date_start", "date_end") and v is not None
|
|
258
|
-
}
|
|
425
|
+
cursor = self._db.cursor()
|
|
426
|
+
cursor.execute(query, params)
|
|
259
427
|
|
|
260
428
|
results = []
|
|
261
429
|
for row in cursor.fetchall():
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self.IMAGETYP_KEY: row["imagetyp"],
|
|
268
|
-
self.OBJECT_KEY: row["object"],
|
|
269
|
-
self.TELESCOP_KEY: row["telescop"],
|
|
270
|
-
self.NUM_IMAGES_KEY: row["num_images"],
|
|
271
|
-
self.EXPTIME_TOTAL_KEY: row["exptime_total"],
|
|
272
|
-
self.IMAGE_DOC_KEY: row["image_doc_id"],
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
# Check if all standard conditions match
|
|
276
|
-
match = all(session.get(k) == v for k, v in standard_conditions.items())
|
|
277
|
-
|
|
278
|
-
# Apply date range filtering
|
|
279
|
-
if match and date_start:
|
|
280
|
-
session_start = session.get(self.START_KEY, "")
|
|
281
|
-
match = match and session_start >= date_start
|
|
282
|
-
|
|
283
|
-
if match and date_end:
|
|
284
|
-
session_start = session.get(self.START_KEY, "")
|
|
285
|
-
match = match and session_start <= date_end
|
|
286
|
-
|
|
287
|
-
if match:
|
|
288
|
-
results.append(session)
|
|
430
|
+
session_dict = dict(row)
|
|
431
|
+
# Parse the metadata JSON if it exists
|
|
432
|
+
if session_dict.get("metadata"):
|
|
433
|
+
session_dict["metadata"] = json.loads(session_dict["metadata"])
|
|
434
|
+
results.append(session_dict)
|
|
289
435
|
|
|
290
|
-
return results
|
|
436
|
+
return results
|
|
291
437
|
|
|
292
438
|
def len_table(self, table_name: str) -> int:
|
|
293
439
|
"""Return the total number of rows in the specified table."""
|
|
@@ -314,20 +460,35 @@ class Database:
|
|
|
314
460
|
result = cursor.fetchone()
|
|
315
461
|
return result[0] if result and result[0] is not None else 0
|
|
316
462
|
|
|
317
|
-
def get_image(self, path: str) ->
|
|
318
|
-
"""Get an image record by path.
|
|
463
|
+
def get_image(self, repo_url: str, path: str) -> ImageRow | None:
|
|
464
|
+
"""Get an image record by repo_url and relative path.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
repo_url: The repository URL
|
|
468
|
+
path: Path relative to the repository root
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Image record with relative path, repo_id, and repo_url, or None if not found
|
|
472
|
+
"""
|
|
319
473
|
cursor = self._db.cursor()
|
|
320
474
|
cursor.execute(
|
|
321
|
-
f"
|
|
322
|
-
|
|
475
|
+
f"""
|
|
476
|
+
SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
|
|
477
|
+
FROM {self.IMAGES_TABLE} i
|
|
478
|
+
JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
|
|
479
|
+
WHERE r.url = ? AND i.path = ?
|
|
480
|
+
""",
|
|
481
|
+
(repo_url, path),
|
|
323
482
|
)
|
|
324
|
-
row = cursor.fetchone()
|
|
325
483
|
|
|
484
|
+
row = cursor.fetchone()
|
|
326
485
|
if row is None:
|
|
327
486
|
return None
|
|
328
487
|
|
|
329
488
|
metadata = json.loads(row["metadata"])
|
|
330
489
|
metadata["path"] = row["path"]
|
|
490
|
+
metadata["repo_id"] = row["repo_id"]
|
|
491
|
+
metadata["repo_url"] = row["repo_url"]
|
|
331
492
|
metadata["id"] = row["id"]
|
|
332
493
|
|
|
333
494
|
# Add date fields back to metadata for compatibility
|
|
@@ -338,17 +499,24 @@ class Database:
|
|
|
338
499
|
|
|
339
500
|
return metadata
|
|
340
501
|
|
|
341
|
-
def all_images(self) -> list[
|
|
342
|
-
"""Return all image records."""
|
|
502
|
+
def all_images(self) -> list[ImageRow]:
|
|
503
|
+
"""Return all image records with relative paths, repo_id, and repo_url."""
|
|
343
504
|
cursor = self._db.cursor()
|
|
344
505
|
cursor.execute(
|
|
345
|
-
f"
|
|
506
|
+
f"""
|
|
507
|
+
SELECT i.id, i.repo_id, i.path, i.date_obs, i.date, i.metadata, r.url as repo_url
|
|
508
|
+
FROM {self.IMAGES_TABLE} i
|
|
509
|
+
JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
|
|
510
|
+
"""
|
|
346
511
|
)
|
|
347
512
|
|
|
348
513
|
results = []
|
|
349
514
|
for row in cursor.fetchall():
|
|
350
515
|
metadata = json.loads(row["metadata"])
|
|
516
|
+
# Return relative path, repo_id, and repo_url for caller
|
|
351
517
|
metadata["path"] = row["path"]
|
|
518
|
+
metadata["repo_id"] = row["repo_id"]
|
|
519
|
+
metadata["repo_url"] = row["repo_url"]
|
|
352
520
|
metadata["id"] = row["id"]
|
|
353
521
|
|
|
354
522
|
# Add date fields back to metadata for compatibility
|
|
@@ -361,35 +529,6 @@ class Database:
|
|
|
361
529
|
|
|
362
530
|
return results
|
|
363
531
|
|
|
364
|
-
def all_sessions(self) -> list[dict[str, Any]]:
|
|
365
|
-
"""Return all session records."""
|
|
366
|
-
cursor = self._db.cursor()
|
|
367
|
-
cursor.execute(
|
|
368
|
-
f"""
|
|
369
|
-
SELECT id, start, end, filter, imagetyp, object, telescop,
|
|
370
|
-
num_images, exptime_total, image_doc_id
|
|
371
|
-
FROM {self.SESSIONS_TABLE}
|
|
372
|
-
"""
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
results = []
|
|
376
|
-
for row in cursor.fetchall():
|
|
377
|
-
session = {
|
|
378
|
-
"id": row["id"],
|
|
379
|
-
self.START_KEY: row["start"],
|
|
380
|
-
self.END_KEY: row["end"],
|
|
381
|
-
self.FILTER_KEY: row["filter"],
|
|
382
|
-
self.IMAGETYP_KEY: row["imagetyp"],
|
|
383
|
-
self.OBJECT_KEY: row["object"],
|
|
384
|
-
self.TELESCOP_KEY: row["telescop"],
|
|
385
|
-
self.NUM_IMAGES_KEY: row["num_images"],
|
|
386
|
-
self.EXPTIME_TOTAL_KEY: row["exptime_total"],
|
|
387
|
-
self.IMAGE_DOC_KEY: row["image_doc_id"],
|
|
388
|
-
}
|
|
389
|
-
results.append(session)
|
|
390
|
-
|
|
391
|
-
return results
|
|
392
|
-
|
|
393
532
|
def get_session_by_id(self, session_id: int) -> dict[str, Any] | None:
|
|
394
533
|
"""Get a session record by its ID.
|
|
395
534
|
|
|
@@ -414,20 +553,9 @@ class Database:
|
|
|
414
553
|
if row is None:
|
|
415
554
|
return None
|
|
416
555
|
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
self.END_KEY: row["end"],
|
|
421
|
-
self.FILTER_KEY: row["filter"],
|
|
422
|
-
self.IMAGETYP_KEY: row["imagetyp"],
|
|
423
|
-
self.OBJECT_KEY: row["object"],
|
|
424
|
-
self.TELESCOP_KEY: row["telescop"],
|
|
425
|
-
self.NUM_IMAGES_KEY: row["num_images"],
|
|
426
|
-
self.EXPTIME_TOTAL_KEY: row["exptime_total"],
|
|
427
|
-
self.IMAGE_DOC_KEY: row["image_doc_id"],
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
def get_session(self, to_find: dict[str, str]) -> dict[str, Any] | None:
|
|
556
|
+
return dict(row)
|
|
557
|
+
|
|
558
|
+
def get_session(self, to_find: dict[str, str]) -> SessionRow | None:
|
|
431
559
|
"""Find a session matching the given criteria.
|
|
432
560
|
|
|
433
561
|
Searches for sessions with the same filter, image type, target, and telescope
|
|
@@ -470,21 +598,10 @@ class Database:
|
|
|
470
598
|
if row is None:
|
|
471
599
|
return None
|
|
472
600
|
|
|
473
|
-
return
|
|
474
|
-
"id": row["id"],
|
|
475
|
-
self.START_KEY: row["start"],
|
|
476
|
-
self.END_KEY: row["end"],
|
|
477
|
-
self.FILTER_KEY: row["filter"],
|
|
478
|
-
self.IMAGETYP_KEY: row["imagetyp"],
|
|
479
|
-
self.OBJECT_KEY: row["object"],
|
|
480
|
-
self.TELESCOP_KEY: row["telescop"],
|
|
481
|
-
self.NUM_IMAGES_KEY: row["num_images"],
|
|
482
|
-
self.EXPTIME_TOTAL_KEY: row["exptime_total"],
|
|
483
|
-
self.IMAGE_DOC_KEY: row["image_doc_id"],
|
|
484
|
-
}
|
|
601
|
+
return dict(row)
|
|
485
602
|
|
|
486
603
|
def upsert_session(
|
|
487
|
-
self, new:
|
|
604
|
+
self, new: SessionRow, existing: SessionRow | None = None
|
|
488
605
|
) -> None:
|
|
489
606
|
"""Insert or update a session record."""
|
|
490
607
|
cursor = self._db.cursor()
|
starbash/defaults/starbash.toml
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
[repo]
|
|
4
4
|
kind = "preferences"
|
|
5
5
|
|
|
6
|
+
[aliases]
|
|
7
|
+
# aliases can be used to map non standard (or non english) frame names to standard terms
|
|
8
|
+
# This is also used to map filters based on common misspellings or variations.
|
|
9
|
+
# We assume the first listed option in the list is the 'canonical' name used for printing etc...
|
|
10
|
+
|
|
11
|
+
# frame types
|
|
12
|
+
dark = ["dark", "darks"]
|
|
13
|
+
flat = ["flat", "flats"]
|
|
14
|
+
bias = ["bias", "biases"]
|
|
15
|
+
|
|
16
|
+
# file suffixes
|
|
17
|
+
fit = ["fits", "fit"]
|
|
18
|
+
|
|
19
|
+
# filter names
|
|
20
|
+
SiiOiii = ["SiiOiii", "SII-OIII", "S2-O3"]
|
|
21
|
+
HaOiii = ["HaOiii", "HA-OIII", "Halpha-O3"]
|
|
22
|
+
|
|
6
23
|
# FIXME, somewhere here list default patterns which can be used to identify NINA, ASIAIR, SEESTAR
|
|
7
24
|
# raw repo layouts
|
|
8
25
|
|
starbash/main.py
CHANGED
|
@@ -6,7 +6,7 @@ import starbash.url as url
|
|
|
6
6
|
import starbash
|
|
7
7
|
|
|
8
8
|
from .app import Starbash, get_user_config_path, setup_logging
|
|
9
|
-
from .commands import info, repo, select, user
|
|
9
|
+
from .commands import info, process, repo, select, user
|
|
10
10
|
from . import console
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(
|
|
@@ -17,6 +17,9 @@ app.add_typer(user.app, name="user", help="Manage user settings.")
|
|
|
17
17
|
app.add_typer(repo.app, name="repo", help="Manage Starbash repositories.")
|
|
18
18
|
app.add_typer(select.app, name="select", help="Manage session and target selection.")
|
|
19
19
|
app.add_typer(info.app, name="info", help="Display system and data information.")
|
|
20
|
+
app.add_typer(
|
|
21
|
+
process.app, name="process", help="Process images using automated workflows."
|
|
22
|
+
)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
@app.callback(invoke_without_command=True)
|