adss 1.0__py3-none-any.whl → 1.2__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.
- adss/__init__.py +24 -0
- adss/auth.py +121 -0
- adss/client.py +671 -0
- adss/endpoints/__init__.py +14 -0
- adss/endpoints/admin.py +433 -0
- adss/endpoints/images.py +898 -0
- adss/endpoints/metadata.py +216 -0
- adss/endpoints/queries.py +498 -0
- adss/endpoints/users.py +311 -0
- adss/exceptions.py +57 -0
- adss/models/__init__.py +13 -0
- adss/models/metadata.py +138 -0
- adss/models/query.py +134 -0
- adss/models/user.py +123 -0
- adss/utils.py +107 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/METADATA +1 -1
- adss-1.2.dist-info/RECORD +30 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/WHEEL +1 -1
- adss-1.0.dist-info/RECORD +0 -16
- {adss-1.0.dist-info → adss-1.2.dist-info}/LICENSE +0 -0
- {adss-1.0.dist-info → adss-1.2.dist-info}/top_level.txt +0 -0
adss/endpoints/images.py
ADDED
@@ -0,0 +1,898 @@
|
|
1
|
+
"""
|
2
|
+
Image operations and management functionality for the Astronomy TAP Client.
|
3
|
+
"""
|
4
|
+
from typing import Dict, List, Optional, Union, Any
|
5
|
+
import os
|
6
|
+
|
7
|
+
from adss.exceptions import ResourceNotFoundError
|
8
|
+
from adss.utils import handle_response_errors
|
9
|
+
|
10
|
+
|
11
|
+
class ImagesEndpoint:
|
12
|
+
"""
|
13
|
+
Handles image-related operations and management.
|
14
|
+
"""
|
15
|
+
def __init__(self, base_url: str, auth_manager):
|
16
|
+
self.base_url = base_url.rstrip('/')
|
17
|
+
self.auth_manager = auth_manager
|
18
|
+
|
19
|
+
def get_collections(self, skip: int = 0, limit: int = 100, **kwargs) -> List[Dict[str, Any]]:
|
20
|
+
url = f"{self.base_url}/adss/v1/images/collections/"
|
21
|
+
try:
|
22
|
+
headers = self.auth_manager._get_auth_headers()
|
23
|
+
except:
|
24
|
+
headers = {"Accept": "application/json"}
|
25
|
+
params = {"skip": skip, "limit": limit}
|
26
|
+
|
27
|
+
try:
|
28
|
+
resp = self.auth_manager.request(
|
29
|
+
method="GET",
|
30
|
+
url=url,
|
31
|
+
headers=headers,
|
32
|
+
params=params,
|
33
|
+
auth_required=False,
|
34
|
+
**kwargs
|
35
|
+
)
|
36
|
+
handle_response_errors(resp)
|
37
|
+
return resp.json()
|
38
|
+
except Exception as e:
|
39
|
+
raise ResourceNotFoundError(f"Failed to get image collections: {e}")
|
40
|
+
|
41
|
+
def get_collection(self, collection_id: int, **kwargs) -> Dict[str, Any]:
|
42
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}"
|
43
|
+
try:
|
44
|
+
headers = self.auth_manager._get_auth_headers()
|
45
|
+
except:
|
46
|
+
headers = {"Accept": "application/json"}
|
47
|
+
|
48
|
+
try:
|
49
|
+
resp = self.auth_manager.request(
|
50
|
+
method="GET",
|
51
|
+
url=url,
|
52
|
+
headers=headers,
|
53
|
+
auth_required=False,
|
54
|
+
**kwargs
|
55
|
+
)
|
56
|
+
handle_response_errors(resp)
|
57
|
+
return resp.json()
|
58
|
+
except Exception as e:
|
59
|
+
raise ResourceNotFoundError(f"Failed to get image collection {collection_id}: {e}")
|
60
|
+
|
61
|
+
def create_collection(self, name: str, base_path: str, description: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
62
|
+
url = f"{self.base_url}/adss/v1/images/collections/"
|
63
|
+
headers = self.auth_manager._get_auth_headers()
|
64
|
+
payload = {"name": name, "base_path": base_path}
|
65
|
+
if description:
|
66
|
+
payload["description"] = description
|
67
|
+
|
68
|
+
try:
|
69
|
+
resp = self.auth_manager.request(
|
70
|
+
method="POST",
|
71
|
+
url=url,
|
72
|
+
headers=headers,
|
73
|
+
json=payload,
|
74
|
+
auth_required=True,
|
75
|
+
**kwargs
|
76
|
+
)
|
77
|
+
handle_response_errors(resp)
|
78
|
+
return resp.json()
|
79
|
+
except Exception as e:
|
80
|
+
raise ResourceNotFoundError(f"Failed to create image collection: {e}")
|
81
|
+
|
82
|
+
def update_collection(self, collection_id: int, name: Optional[str] = None,
|
83
|
+
description: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
84
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}"
|
85
|
+
headers = self.auth_manager._get_auth_headers()
|
86
|
+
payload: Dict[str, Any] = {}
|
87
|
+
if name:
|
88
|
+
payload["name"] = name
|
89
|
+
if description is not None:
|
90
|
+
payload["description"] = description
|
91
|
+
|
92
|
+
try:
|
93
|
+
resp = self.auth_manager.request(
|
94
|
+
method="PUT",
|
95
|
+
url=url,
|
96
|
+
headers=headers,
|
97
|
+
json=payload,
|
98
|
+
auth_required=True,
|
99
|
+
**kwargs
|
100
|
+
)
|
101
|
+
handle_response_errors(resp)
|
102
|
+
return resp.json()
|
103
|
+
except Exception as e:
|
104
|
+
raise ResourceNotFoundError(f"Failed to update collection {collection_id}: {e}")
|
105
|
+
|
106
|
+
def delete_collection(self, collection_id: int, **kwargs) -> bool:
|
107
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}"
|
108
|
+
headers = self.auth_manager._get_auth_headers()
|
109
|
+
|
110
|
+
try:
|
111
|
+
resp = self.auth_manager.request(
|
112
|
+
method="DELETE",
|
113
|
+
url=url,
|
114
|
+
headers=headers,
|
115
|
+
auth_required=True,
|
116
|
+
**kwargs
|
117
|
+
)
|
118
|
+
handle_response_errors(resp)
|
119
|
+
return True
|
120
|
+
except Exception as e:
|
121
|
+
raise ResourceNotFoundError(f"Failed to delete collection {collection_id}: {e}")
|
122
|
+
|
123
|
+
def list_files(self,
|
124
|
+
collection_id: int,
|
125
|
+
skip: int = 0,
|
126
|
+
limit: int = 100,
|
127
|
+
filter_name: Optional[str] = None,
|
128
|
+
filter_str: Optional[str] = None,
|
129
|
+
object_name: Optional[str] = None,
|
130
|
+
ra: Optional[float] = None,
|
131
|
+
dec: Optional[float] = None,
|
132
|
+
radius: Optional[float] = None,
|
133
|
+
ra_min: Optional[float] = None,
|
134
|
+
ra_max: Optional[float] = None,
|
135
|
+
dec_min: Optional[float] = None,
|
136
|
+
dec_max: Optional[float] = None,
|
137
|
+
**kwargs) -> List[Dict[str, Any]]:
|
138
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/files"
|
139
|
+
try:
|
140
|
+
headers = self.auth_manager._get_auth_headers()
|
141
|
+
except:
|
142
|
+
headers = {"Accept": "application/json"}
|
143
|
+
|
144
|
+
params: Dict[str, Union[int, float, str]] = {"skip": skip, "limit": limit}
|
145
|
+
if filter_name:
|
146
|
+
params["filter_name"] = filter_name
|
147
|
+
if filter_str:
|
148
|
+
params["filter_str"] = filter_str
|
149
|
+
if object_name:
|
150
|
+
params["object_name"] = object_name
|
151
|
+
if ra is not None and dec is not None and radius is not None:
|
152
|
+
params.update({"ra": ra, "dec": dec, "radius": radius})
|
153
|
+
if ra_min is not None and ra_max is not None and dec_min is not None and dec_max is not None:
|
154
|
+
params.update({"ra_min": ra_min, "ra_max": ra_max, "dec_min": dec_min, "dec_max": dec_max})
|
155
|
+
|
156
|
+
try:
|
157
|
+
resp = self.auth_manager.request(
|
158
|
+
method="GET",
|
159
|
+
url=url,
|
160
|
+
headers=headers,
|
161
|
+
params=params,
|
162
|
+
auth_required=False,
|
163
|
+
**kwargs
|
164
|
+
)
|
165
|
+
handle_response_errors(resp)
|
166
|
+
return resp.json()
|
167
|
+
except Exception as e:
|
168
|
+
raise ResourceNotFoundError(f"Failed to list files in collection {collection_id}: {e}")
|
169
|
+
|
170
|
+
def cone_search(self,
|
171
|
+
collection_id: int,
|
172
|
+
ra: float,
|
173
|
+
dec: float,
|
174
|
+
radius: float,
|
175
|
+
filter_name: Optional[str] = None,
|
176
|
+
limit: int = 100,
|
177
|
+
**kwargs) -> List[Dict[str, Any]]:
|
178
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/cone_search"
|
179
|
+
try:
|
180
|
+
headers = self.auth_manager._get_auth_headers()
|
181
|
+
except:
|
182
|
+
headers = {"Accept": "application/json"}
|
183
|
+
|
184
|
+
params = {"ra": ra, "dec": dec, "radius": radius, "limit": limit}
|
185
|
+
if filter_name:
|
186
|
+
params["filter_name"] = filter_name
|
187
|
+
|
188
|
+
try:
|
189
|
+
resp = self.auth_manager.request(
|
190
|
+
method="GET",
|
191
|
+
url=url,
|
192
|
+
headers=headers,
|
193
|
+
params=params,
|
194
|
+
auth_required=False,
|
195
|
+
**kwargs
|
196
|
+
)
|
197
|
+
handle_response_errors(resp)
|
198
|
+
return resp.json()
|
199
|
+
except Exception as e:
|
200
|
+
raise ResourceNotFoundError(f"Failed to perform cone search: {e}")
|
201
|
+
|
202
|
+
def download_file(self, file_id: int, output_path: Optional[str] = None, **kwargs) -> Union[bytes, str]:
|
203
|
+
url = f"{self.base_url}/adss/v1/images/files/{file_id}/download"
|
204
|
+
try:
|
205
|
+
headers = self.auth_manager._get_auth_headers()
|
206
|
+
except:
|
207
|
+
headers = {"Accept": "application/octet-stream"}
|
208
|
+
|
209
|
+
try:
|
210
|
+
resp = self.auth_manager.request(
|
211
|
+
method="GET",
|
212
|
+
url=url,
|
213
|
+
headers=headers,
|
214
|
+
stream=True,
|
215
|
+
auth_required=False,
|
216
|
+
**kwargs
|
217
|
+
)
|
218
|
+
handle_response_errors(resp)
|
219
|
+
|
220
|
+
cd = resp.headers.get('Content-Disposition', '')
|
221
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else ''
|
222
|
+
|
223
|
+
if output_path and os.path.isdir(output_path):
|
224
|
+
output_path = os.path.join(output_path, filename)
|
225
|
+
if output_path:
|
226
|
+
with open(output_path, 'wb') as f:
|
227
|
+
for chunk in resp.iter_content(8192):
|
228
|
+
f.write(chunk)
|
229
|
+
return output_path
|
230
|
+
return resp.content
|
231
|
+
except Exception as e:
|
232
|
+
raise ResourceNotFoundError(f"Failed to download image file {file_id}: {e}")
|
233
|
+
|
234
|
+
def scan_directory(self, collection_id: int, rescan_existing: bool = False, **kwargs) -> Dict[str, Any]:
|
235
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/scan"
|
236
|
+
headers = self.auth_manager._get_auth_headers()
|
237
|
+
payload = {"rescan_existing": rescan_existing}
|
238
|
+
|
239
|
+
try:
|
240
|
+
resp = self.auth_manager.request(
|
241
|
+
method="POST",
|
242
|
+
url=url,
|
243
|
+
headers=headers,
|
244
|
+
json=payload,
|
245
|
+
auth_required=True,
|
246
|
+
**kwargs
|
247
|
+
)
|
248
|
+
handle_response_errors(resp)
|
249
|
+
return resp.json()
|
250
|
+
except Exception as e:
|
251
|
+
raise ResourceNotFoundError(f"Failed to scan directory: {e}")
|
252
|
+
|
253
|
+
def get_scan_status(self, job_id: str, **kwargs) -> Dict[str, Any]:
|
254
|
+
url = f"{self.base_url}/adss/v1/images/scan-jobs/{job_id}"
|
255
|
+
headers = self.auth_manager._get_auth_headers()
|
256
|
+
|
257
|
+
try:
|
258
|
+
resp = self.auth_manager.request(
|
259
|
+
method="GET",
|
260
|
+
url=url,
|
261
|
+
headers=headers,
|
262
|
+
auth_required=False,
|
263
|
+
**kwargs
|
264
|
+
)
|
265
|
+
handle_response_errors(resp)
|
266
|
+
return resp.json()
|
267
|
+
except Exception as e:
|
268
|
+
raise ResourceNotFoundError(f"Failed to get scan job status: {e}")
|
269
|
+
|
270
|
+
|
271
|
+
class LuptonImagesEndpoint:
|
272
|
+
"""
|
273
|
+
Handles Lupton RGB image operations.
|
274
|
+
"""
|
275
|
+
def __init__(self, base_url: str, auth_manager):
|
276
|
+
self.base_url = base_url.rstrip('/')
|
277
|
+
self.auth_manager = auth_manager
|
278
|
+
|
279
|
+
def create_rgb(self,
|
280
|
+
r_file_id: int, g_file_id: int, b_file_id: int,
|
281
|
+
ra: Optional[float] = None, dec: Optional[float] = None,
|
282
|
+
size: Optional[float] = None, size_unit: str = "arcmin",
|
283
|
+
stretch: float = 3.0, Q: float = 8.0,
|
284
|
+
output_path: Optional[str] = None,
|
285
|
+
**kwargs) -> Union[bytes, str]:
|
286
|
+
url = f"{self.base_url}/adss/v1/images/lupton_images/rgb"
|
287
|
+
try:
|
288
|
+
headers = self.auth_manager._get_auth_headers()
|
289
|
+
except:
|
290
|
+
headers = {"Accept": "image/png"}
|
291
|
+
|
292
|
+
payload: Dict[str, Any] = {
|
293
|
+
"r_file_id": r_file_id,
|
294
|
+
"g_file_id": g_file_id,
|
295
|
+
"b_file_id": b_file_id,
|
296
|
+
"stretch": stretch,
|
297
|
+
"Q": Q,
|
298
|
+
"size_unit": size_unit,
|
299
|
+
"format": "png"
|
300
|
+
}
|
301
|
+
if ra is not None:
|
302
|
+
payload["ra"] = ra
|
303
|
+
if dec is not None:
|
304
|
+
payload["dec"] = dec
|
305
|
+
if size is not None:
|
306
|
+
payload["size"] = size
|
307
|
+
|
308
|
+
try:
|
309
|
+
resp = self.auth_manager.request(
|
310
|
+
method="POST",
|
311
|
+
url=url,
|
312
|
+
headers=headers,
|
313
|
+
json=payload,
|
314
|
+
auth_required=False,
|
315
|
+
**kwargs
|
316
|
+
)
|
317
|
+
handle_response_errors(resp)
|
318
|
+
|
319
|
+
cd = resp.headers.get('Content-Disposition', '')
|
320
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'rgb_image.png'
|
321
|
+
|
322
|
+
if output_path and os.path.isdir(output_path):
|
323
|
+
output_path = os.path.join(output_path, filename)
|
324
|
+
if output_path:
|
325
|
+
with open(output_path, 'wb') as f:
|
326
|
+
f.write(resp.content)
|
327
|
+
return output_path
|
328
|
+
return resp.content
|
329
|
+
|
330
|
+
except Exception as e:
|
331
|
+
raise ResourceNotFoundError(f"Failed to create RGB image: {e}")
|
332
|
+
|
333
|
+
def create_rgb_by_filenames(self,
|
334
|
+
r_filename: str, g_filename: str, b_filename: str,
|
335
|
+
ra: Optional[float] = None, dec: Optional[float] = None,
|
336
|
+
size: Optional[float] = None, size_unit: str = "arcmin",
|
337
|
+
stretch: float = 3.0, Q: float = 8.0,
|
338
|
+
output_path: Optional[str] = None,
|
339
|
+
**kwargs) -> Union[bytes, str]:
|
340
|
+
"""Create an RGB composite from three images using their filenames.
|
341
|
+
|
342
|
+
Args:
|
343
|
+
r_filename: Filename of the red channel image
|
344
|
+
g_filename: Filename of the green channel image
|
345
|
+
b_filename: Filename of the blue channel image
|
346
|
+
ra: Optional right ascension in degrees (for cutout)
|
347
|
+
dec: Optional declination in degrees (for cutout)
|
348
|
+
size: Optional size in arcminutes by default
|
349
|
+
size_unit: Units for size ("arcmin", "arcsec", or "pixels")
|
350
|
+
stretch: Stretch parameter for Lupton algorithm
|
351
|
+
Q: Q parameter for Lupton algorithm
|
352
|
+
output_path: Optional path to save the image to. If not provided, the image data is returned as bytes.
|
353
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
354
|
+
|
355
|
+
Returns:
|
356
|
+
If output_path is provided, returns the path to the saved file.
|
357
|
+
Otherwise, returns the image data as bytes.
|
358
|
+
"""
|
359
|
+
url = f"{self.base_url}/adss/v1/images/lupton_images/rgb/by-name"
|
360
|
+
try:
|
361
|
+
headers = self.auth_manager._get_auth_headers()
|
362
|
+
except:
|
363
|
+
headers = {"Accept": "image/png"}
|
364
|
+
|
365
|
+
payload: Dict[str, Any] = {
|
366
|
+
"r_filename": r_filename,
|
367
|
+
"g_filename": g_filename,
|
368
|
+
"b_filename": b_filename,
|
369
|
+
"stretch": stretch,
|
370
|
+
"Q": Q,
|
371
|
+
"size_unit": size_unit,
|
372
|
+
"format": "png"
|
373
|
+
}
|
374
|
+
if ra is not None:
|
375
|
+
payload["ra"] = ra
|
376
|
+
if dec is not None:
|
377
|
+
payload["dec"] = dec
|
378
|
+
if size is not None:
|
379
|
+
payload["size"] = size
|
380
|
+
|
381
|
+
try:
|
382
|
+
resp = self.auth_manager.request(
|
383
|
+
method="POST",
|
384
|
+
url=url,
|
385
|
+
headers=headers,
|
386
|
+
json=payload,
|
387
|
+
auth_required=False,
|
388
|
+
**kwargs
|
389
|
+
)
|
390
|
+
handle_response_errors(resp)
|
391
|
+
|
392
|
+
cd = resp.headers.get('Content-Disposition', '')
|
393
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'rgb_image.png'
|
394
|
+
|
395
|
+
if output_path and os.path.isdir(output_path):
|
396
|
+
output_path = os.path.join(output_path, filename)
|
397
|
+
if output_path:
|
398
|
+
with open(output_path, 'wb') as f:
|
399
|
+
f.write(resp.content)
|
400
|
+
return output_path
|
401
|
+
return resp.content
|
402
|
+
|
403
|
+
except Exception as e:
|
404
|
+
raise ResourceNotFoundError(f"Failed to create RGB image by filenames: {e}")
|
405
|
+
|
406
|
+
def create_rgb_by_coordinates(self,
|
407
|
+
collection_id: int, ra: float, dec: float, size: float,
|
408
|
+
r_filter: str, g_filter: str, b_filter: str,
|
409
|
+
size_unit: str = "arcmin", stretch: float = 3.0, Q: float = 8.0,
|
410
|
+
pattern: Optional[str] = None,
|
411
|
+
output_path: Optional[str] = None,
|
412
|
+
**kwargs) -> Union[bytes, str]:
|
413
|
+
url = f"{self.base_url}/adss/v1/images/lupton_images/collections/{collection_id}/rgb_by_coordinates"
|
414
|
+
try:
|
415
|
+
headers = self.auth_manager._get_auth_headers()
|
416
|
+
except:
|
417
|
+
headers = {"Accept": "image/png"}
|
418
|
+
|
419
|
+
payload: Dict[str, Any] = {
|
420
|
+
"ra": ra, "dec": dec, "size": size,
|
421
|
+
"r_filter": r_filter, "g_filter": g_filter, "b_filter": b_filter,
|
422
|
+
"size_unit": size_unit, "stretch": stretch, "Q": Q,
|
423
|
+
"format": "png"
|
424
|
+
}
|
425
|
+
if pattern:
|
426
|
+
payload["pattern"] = pattern
|
427
|
+
|
428
|
+
try:
|
429
|
+
resp = self.auth_manager.request(
|
430
|
+
method="POST",
|
431
|
+
url=url,
|
432
|
+
headers=headers,
|
433
|
+
json=payload,
|
434
|
+
auth_required=False,
|
435
|
+
**kwargs
|
436
|
+
)
|
437
|
+
handle_response_errors(resp)
|
438
|
+
|
439
|
+
cd = resp.headers.get('Content-Disposition', '')
|
440
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'rgb_image.png'
|
441
|
+
|
442
|
+
if output_path and os.path.isdir(output_path):
|
443
|
+
output_path = os.path.join(output_path, filename)
|
444
|
+
if output_path:
|
445
|
+
with open(output_path, 'wb') as f:
|
446
|
+
f.write(resp.content)
|
447
|
+
return output_path
|
448
|
+
return resp.content
|
449
|
+
|
450
|
+
except Exception as e:
|
451
|
+
raise ResourceNotFoundError(f"Failed to create RGB image by coordinates: {e}")
|
452
|
+
|
453
|
+
def create_rgb_by_object(self,
|
454
|
+
collection_id: int, object_name: str,
|
455
|
+
r_filter: str, g_filter: str, b_filter: str,
|
456
|
+
ra: Optional[float] = None, dec: Optional[float] = None,
|
457
|
+
size: Optional[float] = None, size_unit: str = "arcmin",
|
458
|
+
stretch: float = 3.0, Q: float = 8.0,
|
459
|
+
pattern: Optional[str] = None,
|
460
|
+
output_path: Optional[str] = None,
|
461
|
+
**kwargs) -> Union[bytes, str]:
|
462
|
+
url = f"{self.base_url}/adss/v1/images/lupton_images/collections/{collection_id}/rgb_by_object"
|
463
|
+
try:
|
464
|
+
headers = self.auth_manager._get_auth_headers()
|
465
|
+
except:
|
466
|
+
headers = {"Accept": "image/png"}
|
467
|
+
|
468
|
+
payload: Dict[str, Any] = {
|
469
|
+
"object_name": object_name,
|
470
|
+
"r_filter": r_filter, "g_filter": g_filter, "b_filter": b_filter,
|
471
|
+
"size_unit": size_unit, "stretch": stretch, "Q": Q,
|
472
|
+
"format": "png"
|
473
|
+
}
|
474
|
+
if ra is not None:
|
475
|
+
payload["ra"] = ra
|
476
|
+
if dec is not None:
|
477
|
+
payload["dec"] = dec
|
478
|
+
if size is not None:
|
479
|
+
payload["size"] = size
|
480
|
+
if pattern:
|
481
|
+
payload["pattern"] = pattern
|
482
|
+
|
483
|
+
try:
|
484
|
+
resp = self.auth_manager.request(
|
485
|
+
method="POST",
|
486
|
+
url=url,
|
487
|
+
headers=headers,
|
488
|
+
json=payload,
|
489
|
+
auth_required=False,
|
490
|
+
**kwargs
|
491
|
+
)
|
492
|
+
handle_response_errors(resp)
|
493
|
+
|
494
|
+
cd = resp.headers.get('Content-Disposition', '')
|
495
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'rgb_image.png'
|
496
|
+
|
497
|
+
if output_path and os.path.isdir(output_path):
|
498
|
+
output_path = os.path.join(output_path, filename)
|
499
|
+
if output_path:
|
500
|
+
with open(output_path, 'wb') as f:
|
501
|
+
f.write(resp.content)
|
502
|
+
return output_path
|
503
|
+
return resp.content
|
504
|
+
|
505
|
+
except Exception as e:
|
506
|
+
raise ResourceNotFoundError(f"Failed to create RGB image by object: {e}")
|
507
|
+
|
508
|
+
|
509
|
+
class StampImagesEndpoint:
|
510
|
+
"""
|
511
|
+
Handles stamp image operations.
|
512
|
+
"""
|
513
|
+
def __init__(self, base_url: str, auth_manager):
|
514
|
+
self.base_url = base_url.rstrip('/')
|
515
|
+
self.auth_manager = auth_manager
|
516
|
+
|
517
|
+
def create_stamp(self,
|
518
|
+
file_id: int, ra: float, dec: float, size: float,
|
519
|
+
size_unit: str = "arcmin", format: str = "fits",
|
520
|
+
zmin: Optional[float] = None, zmax: Optional[float] = None,
|
521
|
+
output_path: Optional[str] = None,
|
522
|
+
**kwargs) -> Union[bytes, str]:
|
523
|
+
url = f"{self.base_url}/adss/v1/images/stamp_images/files/{file_id}/stamp"
|
524
|
+
try:
|
525
|
+
headers = self.auth_manager._get_auth_headers()
|
526
|
+
except:
|
527
|
+
headers = {"Accept": "image/png" if format == "png" else "application/fits"}
|
528
|
+
|
529
|
+
payload: Dict[str, Any] = {
|
530
|
+
"ra": ra, "dec": dec, "size": size,
|
531
|
+
"size_unit": size_unit, "format": format
|
532
|
+
}
|
533
|
+
if zmin is not None:
|
534
|
+
payload["zmin"] = zmin
|
535
|
+
if zmax is not None:
|
536
|
+
payload["zmax"] = zmax
|
537
|
+
|
538
|
+
try:
|
539
|
+
resp = self.auth_manager.request(
|
540
|
+
method="POST",
|
541
|
+
url=url,
|
542
|
+
headers=headers,
|
543
|
+
json=payload,
|
544
|
+
auth_required=False,
|
545
|
+
**kwargs
|
546
|
+
)
|
547
|
+
handle_response_errors(resp)
|
548
|
+
|
549
|
+
cd = resp.headers.get('Content-Disposition', '')
|
550
|
+
ext = "fits" if format == "fits" else "png"
|
551
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else f"stamp.{ext}"
|
552
|
+
|
553
|
+
if output_path and os.path.isdir(output_path):
|
554
|
+
output_path = os.path.join(output_path, filename)
|
555
|
+
if output_path:
|
556
|
+
with open(output_path, 'wb') as f:
|
557
|
+
f.write(resp.content)
|
558
|
+
return output_path
|
559
|
+
return resp.content
|
560
|
+
|
561
|
+
except Exception as e:
|
562
|
+
raise ResourceNotFoundError(f"Failed to create stamp from file {file_id}: {e}")
|
563
|
+
|
564
|
+
def create_stamp_by_filename(self,
|
565
|
+
filename: str, ra: float, dec: float, size: float,
|
566
|
+
size_unit: str = "arcmin", format: str = "fits",
|
567
|
+
zmin: Optional[float] = None, zmax: Optional[float] = None,
|
568
|
+
output_path: Optional[str] = None,
|
569
|
+
**kwargs) -> Union[bytes, str]:
|
570
|
+
"""Create a postage stamp cutout from an image identified by its filename.
|
571
|
+
|
572
|
+
Args:
|
573
|
+
filename: Filename of the image file to use
|
574
|
+
ra: Right ascension in degrees
|
575
|
+
dec: Declination in degrees
|
576
|
+
size: Size of the cutout
|
577
|
+
size_unit: Units for size ("arcmin", "arcsec", or "pixels")
|
578
|
+
format: Output format ("fits" or "png")
|
579
|
+
zmin: Optional minimum intensity percentile for PNG output
|
580
|
+
zmax: Optional maximum intensity percentile for PNG output
|
581
|
+
output_path: Optional path to save the stamp to. If not provided, the image data is returned as bytes.
|
582
|
+
**kwargs: Additional keyword arguments to pass to the request (e.g., verify=False)
|
583
|
+
|
584
|
+
Returns:
|
585
|
+
If output_path is provided, returns the path to the saved file.
|
586
|
+
Otherwise, returns the image data as bytes.
|
587
|
+
"""
|
588
|
+
url = f"{self.base_url}/adss/v1/images/stamp_images/files/by-name/{filename}/stamp"
|
589
|
+
try:
|
590
|
+
headers = self.auth_manager._get_auth_headers()
|
591
|
+
except:
|
592
|
+
headers = {"Accept": "image/png" if format == "png" else "application/fits"}
|
593
|
+
|
594
|
+
payload: Dict[str, Any] = {
|
595
|
+
"ra": ra, "dec": dec, "size": size,
|
596
|
+
"size_unit": size_unit, "format": format
|
597
|
+
}
|
598
|
+
if zmin is not None:
|
599
|
+
payload["zmin"] = zmin
|
600
|
+
if zmax is not None:
|
601
|
+
payload["zmax"] = zmax
|
602
|
+
|
603
|
+
try:
|
604
|
+
resp = self.auth_manager.request(
|
605
|
+
method="POST",
|
606
|
+
url=url,
|
607
|
+
headers=headers,
|
608
|
+
json=payload,
|
609
|
+
auth_required=False,
|
610
|
+
**kwargs
|
611
|
+
)
|
612
|
+
handle_response_errors(resp)
|
613
|
+
|
614
|
+
cd = resp.headers.get('Content-Disposition', '')
|
615
|
+
ext = "fits" if format == "fits" else "png"
|
616
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else f"stamp.{ext}"
|
617
|
+
|
618
|
+
if output_path and os.path.isdir(output_path):
|
619
|
+
output_path = os.path.join(output_path, filename)
|
620
|
+
if output_path:
|
621
|
+
with open(output_path, 'wb') as f:
|
622
|
+
f.write(resp.content)
|
623
|
+
return output_path
|
624
|
+
return resp.content
|
625
|
+
|
626
|
+
except Exception as e:
|
627
|
+
raise ResourceNotFoundError(f"Failed to create stamp from file {filename}: {e}")
|
628
|
+
|
629
|
+
def create_stamp_by_coordinates(self,
|
630
|
+
collection_id: int, ra: float, dec: float,
|
631
|
+
size: float, filter: str, size_unit: str = "arcmin",
|
632
|
+
format: str = "fits", zmin: Optional[float] = None,
|
633
|
+
zmax: Optional[float] = None, pattern: Optional[str] = None,
|
634
|
+
output_path: Optional[str] = None,
|
635
|
+
**kwargs) -> Union[bytes, str]:
|
636
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/stamp_by_coordinates"
|
637
|
+
try:
|
638
|
+
headers = self.auth_manager._get_auth_headers()
|
639
|
+
except:
|
640
|
+
headers = {"Accept": "image/png" if format == "png" else "application/fits"}
|
641
|
+
|
642
|
+
payload: Dict[str, Any] = {
|
643
|
+
"ra": ra, "dec": dec, "size": size,
|
644
|
+
"filter": filter, "size_unit": size_unit, "format": format
|
645
|
+
}
|
646
|
+
if zmin is not None:
|
647
|
+
payload["zmin"] = zmin
|
648
|
+
if zmax is not None:
|
649
|
+
payload["zmax"] = zmax
|
650
|
+
if pattern:
|
651
|
+
payload["pattern"] = pattern
|
652
|
+
|
653
|
+
try:
|
654
|
+
resp = self.auth_manager.request(
|
655
|
+
method="POST",
|
656
|
+
url=url,
|
657
|
+
headers=headers,
|
658
|
+
json=payload,
|
659
|
+
auth_required=False,
|
660
|
+
**kwargs
|
661
|
+
)
|
662
|
+
handle_response_errors(resp)
|
663
|
+
|
664
|
+
cd = resp.headers.get('Content-Disposition', '')
|
665
|
+
ext = "fits" if format == "fits" else "png"
|
666
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else f"stamp.{ext}"
|
667
|
+
|
668
|
+
if output_path and os.path.isdir(output_path):
|
669
|
+
output_path = os.path.join(output_path, filename)
|
670
|
+
if output_path:
|
671
|
+
with open(output_path, 'wb') as f:
|
672
|
+
f.write(resp.content)
|
673
|
+
return output_path
|
674
|
+
return resp.content
|
675
|
+
|
676
|
+
except Exception as e:
|
677
|
+
raise ResourceNotFoundError(f"Failed to create stamp by coordinates: {e}")
|
678
|
+
|
679
|
+
def create_stamp_by_object(self,
|
680
|
+
collection_id: int, object_name: str,
|
681
|
+
filter_name: str, ra: float, dec: float, size: float,
|
682
|
+
size_unit: str = "arcmin", format: str = "fits",
|
683
|
+
zmin: Optional[float] = None, zmax: Optional[float] = None,
|
684
|
+
pattern: Optional[str] = None,
|
685
|
+
output_path: Optional[str] = None,
|
686
|
+
**kwargs) -> Union[bytes, str]:
|
687
|
+
url = f"{self.base_url}/adss/v1/images/stamp_images/collections/{collection_id}/stamp_by_object"
|
688
|
+
try:
|
689
|
+
headers = self.auth_manager._get_auth_headers()
|
690
|
+
except:
|
691
|
+
headers = {"Accept": "image/png" if format == "png" else "application/fits"}
|
692
|
+
|
693
|
+
payload: Dict[str, Any] = {
|
694
|
+
"object_name": object_name, "filter_name": filter_name,
|
695
|
+
"ra": ra, "dec": dec, "size": size,
|
696
|
+
"size_unit": size_unit, "format": format
|
697
|
+
}
|
698
|
+
if zmin is not None:
|
699
|
+
payload["zmin"] = zmin
|
700
|
+
if zmax is not None:
|
701
|
+
payload["zmax"] = zmax
|
702
|
+
if pattern:
|
703
|
+
payload["pattern"] = pattern
|
704
|
+
|
705
|
+
try:
|
706
|
+
resp = self.auth_manager.request(
|
707
|
+
method="POST",
|
708
|
+
url=url,
|
709
|
+
headers=headers,
|
710
|
+
json=payload,
|
711
|
+
auth_required=False,
|
712
|
+
**kwargs
|
713
|
+
)
|
714
|
+
handle_response_errors(resp)
|
715
|
+
|
716
|
+
cd = resp.headers.get('Content-Disposition', '')
|
717
|
+
ext = "fits" if format == "fits" else "png"
|
718
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else f"stamp.{ext}"
|
719
|
+
|
720
|
+
if output_path and os.path.isdir(output_path):
|
721
|
+
output_path = os.path.join(output_path, filename)
|
722
|
+
if output_path:
|
723
|
+
with open(output_path, 'wb') as f:
|
724
|
+
f.write(resp.content)
|
725
|
+
return output_path
|
726
|
+
return resp.content
|
727
|
+
|
728
|
+
except Exception as e:
|
729
|
+
raise ResourceNotFoundError(f"Failed to create stamp by object: {e}")
|
730
|
+
|
731
|
+
|
732
|
+
class TrilogyImagesEndpoint:
|
733
|
+
"""
|
734
|
+
Handles Trilogy RGB image operations.
|
735
|
+
"""
|
736
|
+
def __init__(self, base_url: str, auth_manager):
|
737
|
+
self.base_url = base_url.rstrip('/')
|
738
|
+
self.auth_manager = auth_manager
|
739
|
+
|
740
|
+
def create_trilogy_rgb(self,
|
741
|
+
r_file_ids: List[int], g_file_ids: List[int], b_file_ids: List[int],
|
742
|
+
ra: Optional[float] = None, dec: Optional[float] = None,
|
743
|
+
size: Optional[float] = None, size_unit: str = "arcmin",
|
744
|
+
noiselum: float = 0.15, satpercent: float = 15.0, colorsatfac: float = 2.0,
|
745
|
+
output_path: Optional[str] = None,
|
746
|
+
**kwargs) -> Union[bytes, str]:
|
747
|
+
url = f"{self.base_url}/adss/v1/images/trilogy-rgb"
|
748
|
+
try:
|
749
|
+
headers = self.auth_manager._get_auth_headers()
|
750
|
+
except:
|
751
|
+
headers = {"Accept": "image/png"}
|
752
|
+
|
753
|
+
payload: Dict[str, Any] = {
|
754
|
+
"r_file_ids": r_file_ids,
|
755
|
+
"g_file_ids": g_file_ids,
|
756
|
+
"b_file_ids": b_file_ids,
|
757
|
+
"noiselum": noiselum,
|
758
|
+
"satpercent": satpercent,
|
759
|
+
"colorsatfac": colorsatfac,
|
760
|
+
"size_unit": size_unit,
|
761
|
+
"format": "png"
|
762
|
+
}
|
763
|
+
if ra is not None:
|
764
|
+
payload["ra"] = ra
|
765
|
+
if dec is not None:
|
766
|
+
payload["dec"] = dec
|
767
|
+
if size is not None:
|
768
|
+
payload["size"] = size
|
769
|
+
|
770
|
+
try:
|
771
|
+
resp = self.auth_manager.request(
|
772
|
+
method="POST",
|
773
|
+
url=url,
|
774
|
+
headers=headers,
|
775
|
+
json=payload,
|
776
|
+
auth_required=False,
|
777
|
+
**kwargs
|
778
|
+
)
|
779
|
+
handle_response_errors(resp)
|
780
|
+
|
781
|
+
cd = resp.headers.get('Content-Disposition', '')
|
782
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'trilogy_rgb.png'
|
783
|
+
|
784
|
+
if output_path and os.path.isdir(output_path):
|
785
|
+
output_path = os.path.join(output_path, filename)
|
786
|
+
if output_path:
|
787
|
+
with open(output_path, 'wb') as f:
|
788
|
+
f.write(resp.content)
|
789
|
+
return output_path
|
790
|
+
return resp.content
|
791
|
+
|
792
|
+
except Exception as e:
|
793
|
+
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image: {e}")
|
794
|
+
|
795
|
+
def create_trilogy_rgb_by_coordinates(self,
|
796
|
+
collection_id: int, ra: float, dec: float, size: float,
|
797
|
+
r_filters: List[str], g_filters: List[str], b_filters: List[str],
|
798
|
+
size_unit: str = "arcmin",
|
799
|
+
noiselum: float = 0.15, satpercent: float = 15.0,
|
800
|
+
colorsatfac: float = 2.0, pattern: Optional[str] = None,
|
801
|
+
output_path: Optional[str] = None,
|
802
|
+
**kwargs) -> Union[bytes, str]:
|
803
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/trilogy-rgb_by_coordinates"
|
804
|
+
try:
|
805
|
+
headers = self.auth_manager._get_auth_headers()
|
806
|
+
except:
|
807
|
+
headers = {"Accept": "image/png"}
|
808
|
+
|
809
|
+
payload: Dict[str, Any] = {
|
810
|
+
"ra": ra, "dec": dec, "size": size,
|
811
|
+
"r_filters": r_filters, "g_filters": g_filters, "b_filters": b_filters,
|
812
|
+
"size_unit": size_unit, "noiselum": noiselum,
|
813
|
+
"satpercent": satpercent, "colorsatfac": colorsatfac,
|
814
|
+
"format": "png"
|
815
|
+
}
|
816
|
+
if pattern:
|
817
|
+
payload["pattern"] = pattern
|
818
|
+
|
819
|
+
try:
|
820
|
+
resp = self.auth_manager.request(
|
821
|
+
method="POST",
|
822
|
+
url=url,
|
823
|
+
headers=headers,
|
824
|
+
json=payload,
|
825
|
+
auth_required=False,
|
826
|
+
**kwargs
|
827
|
+
)
|
828
|
+
handle_response_errors(resp)
|
829
|
+
|
830
|
+
cd = resp.headers.get('Content-Disposition', '')
|
831
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'trilogy_rgb.png'
|
832
|
+
|
833
|
+
if output_path and os.path.isdir(output_path):
|
834
|
+
output_path = os.path.join(output_path, filename)
|
835
|
+
if output_path:
|
836
|
+
with open(output_path, 'wb') as f:
|
837
|
+
f.write(resp.content)
|
838
|
+
return output_path
|
839
|
+
return resp.content
|
840
|
+
|
841
|
+
except Exception as e:
|
842
|
+
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image by coordinates: {e}")
|
843
|
+
|
844
|
+
def create_trilogy_rgb_by_object(self,
|
845
|
+
collection_id: int, object_name: str,
|
846
|
+
r_filters: List[str], g_filters: List[str], b_filters: List[str],
|
847
|
+
ra: Optional[float] = None, dec: Optional[float] = None,
|
848
|
+
size: Optional[float] = None, size_unit: str = "arcmin",
|
849
|
+
noiselum: float = 0.15, satpercent: float = 15.0,
|
850
|
+
colorsatfac: float = 2.0, pattern: Optional[str] = None,
|
851
|
+
output_path: Optional[str] = None,
|
852
|
+
**kwargs) -> Union[bytes, str]:
|
853
|
+
url = f"{self.base_url}/adss/v1/images/collections/{collection_id}/trilogy-rgb_by_object"
|
854
|
+
try:
|
855
|
+
headers = self.auth_manager._get_auth_headers()
|
856
|
+
except:
|
857
|
+
headers = {"Accept": "image/png"}
|
858
|
+
|
859
|
+
payload: Dict[str, Any] = {
|
860
|
+
"object_name": object_name,
|
861
|
+
"r_filters": r_filters, "g_filters": g_filters, "b_filters": b_filters,
|
862
|
+
"size_unit": size_unit, "noiselum": noiselum,
|
863
|
+
"satpercent": satpercent, "colorsatfac": colorsatfac,
|
864
|
+
"format": "png"
|
865
|
+
}
|
866
|
+
if ra is not None:
|
867
|
+
payload["ra"] = ra
|
868
|
+
if dec is not None:
|
869
|
+
payload["dec"] = dec
|
870
|
+
if size is not None:
|
871
|
+
payload["size"] = size
|
872
|
+
if pattern:
|
873
|
+
payload["pattern"] = pattern
|
874
|
+
|
875
|
+
try:
|
876
|
+
resp = self.auth_manager.request(
|
877
|
+
method="POST",
|
878
|
+
url=url,
|
879
|
+
headers=headers,
|
880
|
+
json=payload,
|
881
|
+
auth_required=False,
|
882
|
+
**kwargs
|
883
|
+
)
|
884
|
+
handle_response_errors(resp)
|
885
|
+
|
886
|
+
cd = resp.headers.get('Content-Disposition', '')
|
887
|
+
filename = cd.split('filename=')[1].strip('"') if 'filename=' in cd else 'trilogy_rgb.png'
|
888
|
+
|
889
|
+
if output_path and os.path.isdir(output_path):
|
890
|
+
output_path = os.path.join(output_path, filename)
|
891
|
+
if output_path:
|
892
|
+
with open(output_path, 'wb') as f:
|
893
|
+
f.write(resp.content)
|
894
|
+
return output_path
|
895
|
+
return resp.content
|
896
|
+
|
897
|
+
except Exception as e:
|
898
|
+
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image by object: {e}")
|