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.
@@ -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}")