adss 1.31__py3-none-any.whl → 1.33__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/auth.py +212 -28
- adss/client.py +1 -1
- adss/endpoints/images.py +33 -33
- adss/endpoints/queries.py +2 -2
- adss/utils.py +1 -1
- {adss-1.31.dist-info → adss-1.33.dist-info}/METADATA +146 -1
- {adss-1.31.dist-info → adss-1.33.dist-info}/RECORD +10 -10
- {adss-1.31.dist-info → adss-1.33.dist-info}/WHEEL +0 -0
- {adss-1.31.dist-info → adss-1.33.dist-info}/licenses/LICENSE +0 -0
- {adss-1.31.dist-info → adss-1.33.dist-info}/top_level.txt +0 -0
adss/auth.py
CHANGED
@@ -1,11 +1,58 @@
|
|
1
|
-
import
|
1
|
+
import os
|
2
|
+
import time
|
2
3
|
from typing import Dict, Optional, Tuple
|
3
4
|
|
5
|
+
import requests # kept for compatibility (exceptions/type expectations)
|
6
|
+
import httpx
|
7
|
+
|
4
8
|
from adss.exceptions import AuthenticationError
|
5
9
|
from adss.utils import handle_response_errors
|
6
10
|
from adss.models.user import User
|
7
11
|
|
8
|
-
|
12
|
+
|
13
|
+
# --- internal defaults (safe timeouts; env-overridable) ---
|
14
|
+
_CONNECT_TIMEOUT = float(os.getenv("ADSS_CONNECT_TIMEOUT", "5"))
|
15
|
+
_READ_TIMEOUT = float(os.getenv("ADSS_READ_TIMEOUT", "600"))
|
16
|
+
_DEFAULT_TIMEOUT = (_CONNECT_TIMEOUT, _READ_TIMEOUT)
|
17
|
+
|
18
|
+
_TOTAL_RETRIES = int(os.getenv("ADSS_RETRY_TOTAL", "3"))
|
19
|
+
_BACKOFF_FACTOR = float(os.getenv("ADSS_RETRY_BACKOFF", "0.5"))
|
20
|
+
_TRUST_ENV = os.getenv("ADSS_TRUST_ENV", "1").lower() not in ("0", "false", "no")
|
21
|
+
_FORCE_CLOSE_STREAMS = os.getenv("ADSS_FORCE_CLOSE_STREAMS", "0").lower() in ("1", "true", "yes")
|
22
|
+
|
23
|
+
|
24
|
+
def _to_httpx_timeout(t):
|
25
|
+
"""Map (connect, read) tuple or scalar into httpx.Timeout."""
|
26
|
+
if isinstance(t, tuple) and len(t) == 2:
|
27
|
+
connect, read = t
|
28
|
+
return httpx.Timeout(connect=connect, read=read, write=read, pool=connect)
|
29
|
+
if isinstance(t, (int, float)):
|
30
|
+
return httpx.Timeout(t)
|
31
|
+
return httpx.Timeout(connect=_CONNECT_TIMEOUT, read=_READ_TIMEOUT, write=_READ_TIMEOUT, pool=_CONNECT_TIMEOUT)
|
32
|
+
|
33
|
+
|
34
|
+
def _attach_requests_compat(resp: httpx.Response) -> httpx.Response:
|
35
|
+
"""
|
36
|
+
Attach requests-like helpers so existing code doesn't break:
|
37
|
+
- iter_content(chunk_size) -> yields bytes
|
38
|
+
- raw.read([n]) -> bytes (returns remaining body)
|
39
|
+
"""
|
40
|
+
if not hasattr(resp, "iter_content"):
|
41
|
+
def iter_content(chunk_size: int = 1024 * 1024):
|
42
|
+
return resp.iter_bytes(chunk_size=chunk_size)
|
43
|
+
setattr(resp, "iter_content", iter_content)
|
44
|
+
|
45
|
+
if not hasattr(resp, "raw"):
|
46
|
+
class _RawAdapter:
|
47
|
+
def __init__(self, r: httpx.Response):
|
48
|
+
self._r = r
|
49
|
+
def read(self, amt: Optional[int] = None) -> bytes:
|
50
|
+
# httpx sync API doesn't expose partial read; return remaining.
|
51
|
+
return self._r.read()
|
52
|
+
setattr(resp, "raw", _RawAdapter(resp))
|
53
|
+
|
54
|
+
return resp
|
55
|
+
|
9
56
|
|
10
57
|
class Auth:
|
11
58
|
"""
|
@@ -18,6 +65,9 @@ class Auth:
|
|
18
65
|
self.current_user: Optional[User] = None
|
19
66
|
self.verify_ssl = verify_ssl
|
20
67
|
|
68
|
+
# Single keep-alive client; set verify at construction.
|
69
|
+
self._client = httpx.Client(trust_env=_TRUST_ENV, verify=self.verify_ssl)
|
70
|
+
|
21
71
|
def login(self, username: str, password: str, **kwargs) -> Tuple[str, User]:
|
22
72
|
"""
|
23
73
|
Log in with username and password, obtaining an authentication token.
|
@@ -26,7 +76,6 @@ class Auth:
|
|
26
76
|
data = {"username": username, "password": password}
|
27
77
|
|
28
78
|
try:
|
29
|
-
# Use our own request() method here
|
30
79
|
response = self.request(
|
31
80
|
method="POST",
|
32
81
|
url=login_url,
|
@@ -41,12 +90,12 @@ class Auth:
|
|
41
90
|
if not self.token:
|
42
91
|
raise AuthenticationError("Login succeeded but no token returned")
|
43
92
|
|
44
|
-
# Now fetch user info (this will use auth_required=True internally)
|
45
93
|
self.current_user = self._get_current_user(**kwargs)
|
46
94
|
return self.token, self.current_user
|
47
95
|
|
48
|
-
except
|
49
|
-
|
96
|
+
except httpx.RequestError as e:
|
97
|
+
# preserve existing caller except-blocks that catch requests.RequestException
|
98
|
+
raise requests.RequestException(str(e)) # noqa: B904
|
50
99
|
|
51
100
|
def logout(self) -> None:
|
52
101
|
self.token = None
|
@@ -66,7 +115,6 @@ class Auth:
|
|
66
115
|
auth_headers = self._get_auth_headers()
|
67
116
|
|
68
117
|
try:
|
69
|
-
# Again, use request() so SSL and auth headers are applied consistently
|
70
118
|
response = self.request(
|
71
119
|
method="GET",
|
72
120
|
url=me_url,
|
@@ -79,8 +127,8 @@ class Auth:
|
|
79
127
|
user_data = response.json()
|
80
128
|
return User.from_dict(user_data)
|
81
129
|
|
82
|
-
except
|
83
|
-
raise
|
130
|
+
except httpx.RequestError as e:
|
131
|
+
raise requests.RequestException(str(e)) # noqa: B904
|
84
132
|
|
85
133
|
def _get_auth_headers(self) -> Dict[str, str]:
|
86
134
|
headers = {"Accept": "application/json"}
|
@@ -88,6 +136,99 @@ class Auth:
|
|
88
136
|
headers["Authorization"] = f"Bearer {self.token}"
|
89
137
|
return headers
|
90
138
|
|
139
|
+
# ---------------- core helpers ---------------- #
|
140
|
+
|
141
|
+
def _full_url(self, url: str) -> str:
|
142
|
+
return url if url.startswith(('http://', 'https://')) else f"{self.base_url}/{url.lstrip('/')}"
|
143
|
+
|
144
|
+
def _request_with_retries_nonstream(
|
145
|
+
self,
|
146
|
+
*,
|
147
|
+
method: str,
|
148
|
+
url: str,
|
149
|
+
headers: Dict[str, str],
|
150
|
+
params=None,
|
151
|
+
data=None,
|
152
|
+
json=None,
|
153
|
+
files=None,
|
154
|
+
timeout: httpx.Timeout,
|
155
|
+
follow_redirects: bool,
|
156
|
+
) -> httpx.Response:
|
157
|
+
last_exc = None
|
158
|
+
for attempt in range(_TOTAL_RETRIES + 1):
|
159
|
+
try:
|
160
|
+
return self._client.request(
|
161
|
+
method=method.upper(),
|
162
|
+
url=url,
|
163
|
+
headers=headers,
|
164
|
+
params=params,
|
165
|
+
data=data,
|
166
|
+
json=json,
|
167
|
+
files=files,
|
168
|
+
follow_redirects=follow_redirects,
|
169
|
+
timeout=timeout,
|
170
|
+
)
|
171
|
+
except httpx.RequestError as e:
|
172
|
+
last_exc = e
|
173
|
+
if attempt >= _TOTAL_RETRIES:
|
174
|
+
break
|
175
|
+
time.sleep(_BACKOFF_FACTOR * (2 ** attempt))
|
176
|
+
raise requests.RequestException(str(last_exc)) # noqa: B904
|
177
|
+
|
178
|
+
def _request_with_retries_stream(
|
179
|
+
self,
|
180
|
+
*,
|
181
|
+
method: str,
|
182
|
+
url: str,
|
183
|
+
headers: Dict[str, str],
|
184
|
+
params=None,
|
185
|
+
data=None,
|
186
|
+
json=None,
|
187
|
+
files=None,
|
188
|
+
timeout: httpx.Timeout,
|
189
|
+
follow_redirects: bool,
|
190
|
+
) -> httpx.Response:
|
191
|
+
"""
|
192
|
+
Use client.stream(...) but keep the stream open for the caller.
|
193
|
+
We manually __enter__ the context manager and override resp.close()
|
194
|
+
to ensure resources are cleaned when caller closes the response.
|
195
|
+
"""
|
196
|
+
last_exc = None
|
197
|
+
for attempt in range(_TOTAL_RETRIES + 1):
|
198
|
+
try:
|
199
|
+
cm = self._client.stream(
|
200
|
+
method=method.upper(),
|
201
|
+
url=url,
|
202
|
+
headers=headers,
|
203
|
+
params=params,
|
204
|
+
data=data,
|
205
|
+
json=json,
|
206
|
+
files=files,
|
207
|
+
follow_redirects=follow_redirects,
|
208
|
+
timeout=timeout,
|
209
|
+
)
|
210
|
+
resp = cm.__enter__() # don't exit: let caller iterate/close
|
211
|
+
# Make close() also exit the context manager safely
|
212
|
+
_orig_close = resp.close
|
213
|
+
def _close():
|
214
|
+
try:
|
215
|
+
_orig_close()
|
216
|
+
finally:
|
217
|
+
try:
|
218
|
+
cm.__exit__(None, None, None)
|
219
|
+
except Exception:
|
220
|
+
pass
|
221
|
+
resp.close = _close # type: ignore[attr-defined]
|
222
|
+
return resp
|
223
|
+
except httpx.RequestError as e:
|
224
|
+
last_exc = e
|
225
|
+
if attempt >= _TOTAL_RETRIES:
|
226
|
+
break
|
227
|
+
time.sleep(_BACKOFF_FACTOR * (2 ** attempt))
|
228
|
+
raise requests.RequestException(str(last_exc)) # noqa: B904
|
229
|
+
|
230
|
+
# ---------------- public API (unchanged signatures) ------------------- #
|
231
|
+
|
91
232
|
def request(
|
92
233
|
self,
|
93
234
|
method: str,
|
@@ -102,25 +243,57 @@ class Auth:
|
|
102
243
|
if auth_required and not self.is_authenticated():
|
103
244
|
raise AuthenticationError("Authentication required for this request")
|
104
245
|
|
105
|
-
|
106
|
-
if not url.startswith(('http://', 'https://')):
|
107
|
-
url = f"{self.base_url}/{url.lstrip('/')}"
|
246
|
+
url = self._full_url(url)
|
108
247
|
|
109
248
|
# Merge headers
|
110
249
|
final_headers = self._get_auth_headers()
|
111
250
|
if headers:
|
112
251
|
final_headers.update(headers)
|
113
252
|
|
114
|
-
#
|
115
|
-
|
116
|
-
|
253
|
+
# Map requests-style kwargs to httpx
|
254
|
+
timeout = _to_httpx_timeout(kwargs.pop('timeout', _DEFAULT_TIMEOUT))
|
255
|
+
follow_redirects = kwargs.pop('allow_redirects', True)
|
256
|
+
stream_flag = bool(kwargs.pop('stream', False))
|
257
|
+
|
258
|
+
# (verify is fixed per-client at __init__; ignore/strip any incoming 'verify' kw)
|
259
|
+
kwargs.pop('verify', None)
|
117
260
|
|
118
|
-
|
261
|
+
# Build payload pieces compatibly
|
262
|
+
params = kwargs.pop('params', None)
|
263
|
+
data = kwargs.pop('data', None)
|
264
|
+
json_ = kwargs.pop('json', None)
|
265
|
+
files = kwargs.pop('files', None)
|
266
|
+
|
267
|
+
if stream_flag:
|
268
|
+
resp = self._request_with_retries_stream(
|
269
|
+
method=method,
|
270
|
+
url=url,
|
271
|
+
headers=final_headers,
|
272
|
+
params=params,
|
273
|
+
data=data,
|
274
|
+
json=json_,
|
275
|
+
files=files,
|
276
|
+
timeout=timeout,
|
277
|
+
follow_redirects=follow_redirects,
|
278
|
+
)
|
279
|
+
else:
|
280
|
+
resp = self._request_with_retries_nonstream(
|
281
|
+
method=method,
|
282
|
+
url=url,
|
283
|
+
headers=final_headers,
|
284
|
+
params=params,
|
285
|
+
data=data,
|
286
|
+
json=json_,
|
287
|
+
files=files,
|
288
|
+
timeout=timeout,
|
289
|
+
follow_redirects=follow_redirects,
|
290
|
+
)
|
291
|
+
return _attach_requests_compat(resp)
|
119
292
|
|
120
293
|
def refresh_user_info(self, **kwargs) -> User:
|
121
294
|
self.current_user = self._get_current_user(**kwargs)
|
122
295
|
return self.current_user
|
123
|
-
|
296
|
+
|
124
297
|
def download(
|
125
298
|
self,
|
126
299
|
method: str,
|
@@ -140,23 +313,34 @@ class Auth:
|
|
140
313
|
if auth_required and not self.is_authenticated():
|
141
314
|
raise AuthenticationError("Authentication required for this request")
|
142
315
|
|
143
|
-
|
144
|
-
if not url.startswith(('http://', 'https://')):
|
145
|
-
url = f"{self.base_url}/{url.lstrip('/')}"
|
316
|
+
url = self._full_url(url)
|
146
317
|
|
147
318
|
# Merge headers
|
148
319
|
final_headers = self._get_auth_headers()
|
149
320
|
if headers:
|
150
321
|
final_headers.update(headers)
|
322
|
+
if _FORCE_CLOSE_STREAMS:
|
323
|
+
final_headers.setdefault("Connection", "close")
|
151
324
|
|
152
|
-
|
153
|
-
|
154
|
-
|
325
|
+
timeout = _to_httpx_timeout(kwargs.pop('timeout', _DEFAULT_TIMEOUT))
|
326
|
+
follow_redirects = kwargs.pop('allow_redirects', True)
|
327
|
+
kwargs.pop('verify', None) # verify is fixed on client
|
155
328
|
|
156
|
-
|
157
|
-
kwargs
|
329
|
+
params = kwargs.pop('params', None)
|
330
|
+
data = kwargs.pop('data', None)
|
331
|
+
json_ = kwargs.pop('json', None)
|
332
|
+
files = kwargs.pop('files', None)
|
158
333
|
|
159
|
-
resp =
|
334
|
+
resp = self._request_with_retries_stream(
|
335
|
+
method=method,
|
336
|
+
url=url,
|
337
|
+
headers=final_headers,
|
338
|
+
params=params,
|
339
|
+
data=data,
|
340
|
+
json=json_,
|
341
|
+
files=files,
|
342
|
+
timeout=timeout,
|
343
|
+
follow_redirects=follow_redirects,
|
344
|
+
)
|
160
345
|
handle_response_errors(resp) # fail fast on HTTP errors
|
161
|
-
|
162
|
-
return resp
|
346
|
+
return _attach_requests_compat(resp)
|
adss/client.py
CHANGED
@@ -516,7 +516,7 @@ class ADSSClient:
|
|
516
516
|
"""
|
517
517
|
return self.images.cone_search(collection_id, ra, dec, radius, filter_name, limit, **kwargs)
|
518
518
|
|
519
|
-
def
|
519
|
+
def download_file(self, file_id: int, output_path: Optional[str] = None, **kwargs) -> Union[bytes, str]:
|
520
520
|
"""
|
521
521
|
Download an image file.
|
522
522
|
|
adss/endpoints/images.py
CHANGED
@@ -160,7 +160,7 @@ class ImagesEndpoint:
|
|
160
160
|
for chunk in resp.iter_content(8192):
|
161
161
|
f.write(chunk)
|
162
162
|
return output_path
|
163
|
-
return resp.
|
163
|
+
return resp.read()
|
164
164
|
except Exception as e:
|
165
165
|
raise ResourceNotFoundError(f"Failed to download image file {file_id}: {e}")
|
166
166
|
|
@@ -220,9 +220,9 @@ class LuptonImagesEndpoint:
|
|
220
220
|
output_path = os.path.join(output_path, filename)
|
221
221
|
if output_path:
|
222
222
|
with open(output_path, 'wb') as f:
|
223
|
-
f.write(resp.
|
224
|
-
return resp.
|
225
|
-
return resp.
|
223
|
+
f.write(resp.read())
|
224
|
+
return resp.read()
|
225
|
+
return resp.read()
|
226
226
|
|
227
227
|
except Exception as e:
|
228
228
|
raise ResourceNotFoundError(f"Failed to create RGB image: {e}")
|
@@ -293,9 +293,9 @@ class LuptonImagesEndpoint:
|
|
293
293
|
output_path = os.path.join(output_path, filename)
|
294
294
|
if output_path:
|
295
295
|
with open(output_path, 'wb') as f:
|
296
|
-
f.write(resp.
|
297
|
-
return resp.
|
298
|
-
return resp.
|
296
|
+
f.write(resp.read())
|
297
|
+
return resp.read()
|
298
|
+
return resp.read()
|
299
299
|
|
300
300
|
except Exception as e:
|
301
301
|
raise ResourceNotFoundError(f"Failed to create RGB image by filenames: {e}")
|
@@ -340,9 +340,9 @@ class LuptonImagesEndpoint:
|
|
340
340
|
output_path = os.path.join(output_path, filename)
|
341
341
|
if output_path:
|
342
342
|
with open(output_path, 'wb') as f:
|
343
|
-
f.write(resp.
|
344
|
-
return resp.
|
345
|
-
return resp.
|
343
|
+
f.write(resp.read())
|
344
|
+
return resp.read()
|
345
|
+
return resp.read()
|
346
346
|
|
347
347
|
except Exception as e:
|
348
348
|
raise ResourceNotFoundError(f"Failed to create RGB image by coordinates: {e}")
|
@@ -395,9 +395,9 @@ class LuptonImagesEndpoint:
|
|
395
395
|
output_path = os.path.join(output_path, filename)
|
396
396
|
if output_path:
|
397
397
|
with open(output_path, 'wb') as f:
|
398
|
-
f.write(resp.
|
399
|
-
return resp.
|
400
|
-
return resp.
|
398
|
+
f.write(resp.read())
|
399
|
+
return resp.read()
|
400
|
+
return resp.read()
|
401
401
|
|
402
402
|
except Exception as e:
|
403
403
|
raise ResourceNotFoundError(f"Failed to create RGB image by object: {e}")
|
@@ -451,9 +451,9 @@ class StampImagesEndpoint:
|
|
451
451
|
output_path = os.path.join(output_path, filename)
|
452
452
|
if output_path:
|
453
453
|
with open(output_path, 'wb') as f:
|
454
|
-
f.write(resp.
|
455
|
-
return resp.
|
456
|
-
return resp.
|
454
|
+
f.write(resp.read())
|
455
|
+
return resp.read()
|
456
|
+
return resp.read()
|
457
457
|
|
458
458
|
except Exception as e:
|
459
459
|
raise ResourceNotFoundError(f"Failed to create stamp from file {file_id}: {e}")
|
@@ -516,9 +516,9 @@ class StampImagesEndpoint:
|
|
516
516
|
output_path = os.path.join(output_path, filename)
|
517
517
|
if output_path:
|
518
518
|
with open(output_path, 'wb') as f:
|
519
|
-
f.write(resp.
|
520
|
-
return resp.
|
521
|
-
return resp.
|
519
|
+
f.write(resp.read())
|
520
|
+
return resp.read()
|
521
|
+
return resp.read()
|
522
522
|
|
523
523
|
except Exception as e:
|
524
524
|
raise ResourceNotFoundError(f"Failed to create stamp from file {filename}: {e}")
|
@@ -566,9 +566,9 @@ class StampImagesEndpoint:
|
|
566
566
|
output_path = os.path.join(output_path, filename)
|
567
567
|
if output_path:
|
568
568
|
with open(output_path, 'wb') as f:
|
569
|
-
f.write(resp.
|
570
|
-
return resp.
|
571
|
-
return resp.
|
569
|
+
f.write(resp.read())
|
570
|
+
return resp.read()
|
571
|
+
return resp.read()
|
572
572
|
|
573
573
|
except Exception as e:
|
574
574
|
raise ResourceNotFoundError(f"Failed to create stamp by coordinates: {e}")
|
@@ -618,9 +618,9 @@ class StampImagesEndpoint:
|
|
618
618
|
output_path = os.path.join(output_path, filename)
|
619
619
|
if output_path:
|
620
620
|
with open(output_path, 'wb') as f:
|
621
|
-
f.write(resp.
|
622
|
-
return resp.
|
623
|
-
return resp.
|
621
|
+
f.write(resp.read())
|
622
|
+
return resp.read()
|
623
|
+
return resp.read()
|
624
624
|
|
625
625
|
except Exception as e:
|
626
626
|
raise ResourceNotFoundError(f"Failed to create stamp by object: {e}")
|
@@ -682,9 +682,9 @@ class TrilogyImagesEndpoint:
|
|
682
682
|
output_path = os.path.join(output_path, filename)
|
683
683
|
if output_path:
|
684
684
|
with open(output_path, 'wb') as f:
|
685
|
-
f.write(resp.
|
686
|
-
return resp.
|
687
|
-
return resp.
|
685
|
+
f.write(resp.read())
|
686
|
+
return resp.read()
|
687
|
+
return resp.read()
|
688
688
|
|
689
689
|
except Exception as e:
|
690
690
|
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image: {e}")
|
@@ -731,9 +731,9 @@ class TrilogyImagesEndpoint:
|
|
731
731
|
output_path = os.path.join(output_path, filename)
|
732
732
|
if output_path:
|
733
733
|
with open(output_path, 'wb') as f:
|
734
|
-
f.write(resp.
|
734
|
+
f.write(resp.read())
|
735
735
|
return output_path
|
736
|
-
return resp.
|
736
|
+
return resp.read()
|
737
737
|
|
738
738
|
except Exception as e:
|
739
739
|
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image by coordinates: {e}")
|
@@ -787,9 +787,9 @@ class TrilogyImagesEndpoint:
|
|
787
787
|
output_path = os.path.join(output_path, filename)
|
788
788
|
if output_path:
|
789
789
|
with open(output_path, 'wb') as f:
|
790
|
-
f.write(resp.
|
791
|
-
return resp.
|
792
|
-
return resp.
|
790
|
+
f.write(resp.read())
|
791
|
+
return resp.read()
|
792
|
+
return resp.read()
|
793
793
|
|
794
794
|
except Exception as e:
|
795
795
|
raise ResourceNotFoundError(f"Failed to create Trilogy RGB image by object: {e}")
|
adss/endpoints/queries.py
CHANGED
@@ -116,7 +116,7 @@ class QueriesEndpoint:
|
|
116
116
|
)
|
117
117
|
|
118
118
|
# Parse Parquet data
|
119
|
-
df = parquet_to_dataframe(response.
|
119
|
+
df = parquet_to_dataframe(response.read())
|
120
120
|
|
121
121
|
return QueryResult(
|
122
122
|
query=query_obj,
|
@@ -280,7 +280,7 @@ class QueriesEndpoint:
|
|
280
280
|
handle_response_errors(response)
|
281
281
|
|
282
282
|
# Parse Parquet data
|
283
|
-
df = parquet_to_dataframe(response.
|
283
|
+
df = parquet_to_dataframe(response.read())
|
284
284
|
|
285
285
|
# Extract metadata
|
286
286
|
expires_at = response.headers.get('X-Expires-At')
|
adss/utils.py
CHANGED
@@ -19,7 +19,7 @@ def handle_response_errors(response):
|
|
19
19
|
return response
|
20
20
|
|
21
21
|
try:
|
22
|
-
error_data = response.
|
22
|
+
error_data = response.read()
|
23
23
|
error_message = error_data.get('detail', str(error_data))
|
24
24
|
except Exception:
|
25
25
|
error_message = response.text or f"HTTP Error {response.status_code}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: adss
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.33
|
4
4
|
Summary: Astronomical Data Smart System
|
5
5
|
Author-email: Gustavo Schwarz <gustavo.b.schwarz@gmail.com>
|
6
6
|
Project-URL: Homepage, https://github.com/schwarzam/adss
|
@@ -9,6 +9,7 @@ Requires-Python: >=3.8
|
|
9
9
|
Description-Content-Type: text/markdown
|
10
10
|
License-File: LICENSE
|
11
11
|
Requires-Dist: pyarrow
|
12
|
+
Requires-Dist: httpx
|
12
13
|
Requires-Dist: requests
|
13
14
|
Requires-Dist: astropy
|
14
15
|
Dynamic: license-file
|
@@ -178,3 +179,147 @@ cl.get_image_collections()
|
|
178
179
|
]
|
179
180
|
```
|
180
181
|
|
182
|
+
And then to list the files in a collection:
|
183
|
+
|
184
|
+
```python
|
185
|
+
cl.list_files(1) ## pass the collection ID
|
186
|
+
```
|
187
|
+
|
188
|
+
```
|
189
|
+
[
|
190
|
+
{
|
191
|
+
'filename': 'SPLUS-s17s23_F515_swpweight.fz',
|
192
|
+
'full_path': '/dados/splus/SPLUS-s17s23 SPLUS-s17s23_F515_swpweight.fz',
|
193
|
+
'file_type': 'fz',
|
194
|
+
'ra_center': 316.45153076969416,
|
195
|
+
'dec_center': -21.580560694390957,
|
196
|
+
'width': 11000,
|
197
|
+
'height': 11000,
|
198
|
+
'pixel_scale': 0.55000000000008,
|
199
|
+
'hdus': 2,
|
200
|
+
'data_hdu': 1,
|
201
|
+
'object_name': 'SPLUS-s17s23',
|
202
|
+
'filter': 'F515',
|
203
|
+
'instrument': 'T80Cam',
|
204
|
+
'telescope': 'T80',
|
205
|
+
'date_obs': None,
|
206
|
+
'file_size': 51353280,
|
207
|
+
'id': 28,
|
208
|
+
'collection_id': 1,
|
209
|
+
'created_at': '2025-04-22T15:35:05.487208',
|
210
|
+
'updated_at': '2025-05-08T19:53:09.541437'},
|
211
|
+
},...]
|
212
|
+
```
|
213
|
+
|
214
|
+
You can then download a file by its filename:
|
215
|
+
|
216
|
+
```python
|
217
|
+
file_bytes = cl.download_file(
|
218
|
+
file_id=28,
|
219
|
+
output_path=None
|
220
|
+
)
|
221
|
+
```
|
222
|
+
|
223
|
+
Then handle the bytes. Example:
|
224
|
+
|
225
|
+
```python
|
226
|
+
# if a fits you may open like
|
227
|
+
import io
|
228
|
+
from astropy.io import fits
|
229
|
+
|
230
|
+
hdul = fits.open(io.BytesIO(file_bytes))
|
231
|
+
|
232
|
+
# or a image
|
233
|
+
from PIL import Image
|
234
|
+
import matplotlib.pyplot as plt
|
235
|
+
|
236
|
+
image = Image.open(io.BytesIO(file_bytes))
|
237
|
+
plt.imshow(image)
|
238
|
+
```
|
239
|
+
|
240
|
+
### Image Tools
|
241
|
+
|
242
|
+
Now notice that (**if**) the image collection has some wcs parameters as `ra_center`, `dec_center`, `pixel_scale`. This allows us to do some image cutouts and colored images in real time. Example:
|
243
|
+
|
244
|
+
```python
|
245
|
+
cutout_bytes = cl.create_stamp_by_coordinates(
|
246
|
+
collection_id = 1,
|
247
|
+
ra = 0.1,
|
248
|
+
dec = 0.1,
|
249
|
+
size = 300,
|
250
|
+
filter = "R",
|
251
|
+
size_unit="pixels",
|
252
|
+
format = "fits",
|
253
|
+
pattern="swp."
|
254
|
+
)
|
255
|
+
|
256
|
+
hdul = fits.open(BytesIO(cutout_bytes))
|
257
|
+
```
|
258
|
+
|
259
|
+
or if the image collection has object_name info you may filter by it, forcing the cutout from that object:
|
260
|
+
|
261
|
+
```python
|
262
|
+
cutout_bytes = cl.stamp_images.create_stamp_by_object(
|
263
|
+
collection_id=1,
|
264
|
+
object_name="STRIPE82-0002",
|
265
|
+
size=300,
|
266
|
+
ra=0.1,
|
267
|
+
dec=0.1,
|
268
|
+
filter_name="R",
|
269
|
+
size_unit="pixels",
|
270
|
+
format="fits"
|
271
|
+
)
|
272
|
+
cutout = fits.open(BytesIO(cutout_bytes))
|
273
|
+
```
|
274
|
+
|
275
|
+
or just by file_id, this will force the cutout from that specific file:
|
276
|
+
|
277
|
+
```python
|
278
|
+
cl.stamp_images.create_stamp(
|
279
|
+
file_id=28,
|
280
|
+
size=300,
|
281
|
+
ra=0.1,
|
282
|
+
dec=0.1,
|
283
|
+
size_unit="pixels",
|
284
|
+
format="fits"
|
285
|
+
)
|
286
|
+
```
|
287
|
+
|
288
|
+
### Colored images
|
289
|
+
|
290
|
+
Colored images API is very similar to the cutouts. You just need to provide a list of filters and the output format (png or jpg). Example with lupton et al. (2004) algorithm:
|
291
|
+
|
292
|
+
```python
|
293
|
+
im_bytes = cl.create_rgb_image_by_coordinates(
|
294
|
+
collection_id=1,
|
295
|
+
ra=0.1,
|
296
|
+
dec=0.1,
|
297
|
+
size=300,
|
298
|
+
size_unit="pixels",
|
299
|
+
r_filter="I",
|
300
|
+
g_filter="R",
|
301
|
+
b_filter="G",
|
302
|
+
)
|
303
|
+
|
304
|
+
im = Image.open(BytesIO(im_bytes))
|
305
|
+
im.show()
|
306
|
+
```
|
307
|
+
|
308
|
+
Or trilogy algorithm:
|
309
|
+
|
310
|
+
```python
|
311
|
+
im_bytes = cl.trilogy_images.create_trilogy_rgb_by_coordinates(
|
312
|
+
collection_id=1,
|
313
|
+
ra=0.1,
|
314
|
+
dec=0.1,
|
315
|
+
size=300,
|
316
|
+
size_unit="pixels",
|
317
|
+
r_filters=["I", "R", "Z", "F861", "G"],
|
318
|
+
g_filters=["F660"],
|
319
|
+
b_filters=["U", "F378", "F395", "F410", "F430", "F515"],
|
320
|
+
satpercent=0.15,
|
321
|
+
)
|
322
|
+
|
323
|
+
im = Image.open(BytesIO(im_bytes))
|
324
|
+
im.show()
|
325
|
+
```
|
@@ -1,21 +1,21 @@
|
|
1
1
|
adss/__init__.py,sha256=3FpHFL3Pk5BvETwd70P2QqYvDq799Cu2AGxGxudGAAE,1020
|
2
|
-
adss/auth.py,sha256=
|
3
|
-
adss/client.py,sha256=
|
2
|
+
adss/auth.py,sha256=_csq8YJMyNTSgxNjtJW1YxdDeVx0R-Hs9-hcBoExQCs,11977
|
3
|
+
adss/client.py,sha256=oJ0O4ZUdmycprpKXBXEpPWiu4T4Jf79uPsXzOpuEG-w,29895
|
4
4
|
adss/exceptions.py,sha256=YeN-xRHvlSmwyS8ni2jOEhhgZK9J1jsG11pOedy3Gfg,1482
|
5
|
-
adss/utils.py,sha256=
|
5
|
+
adss/utils.py,sha256=hBfE6FJD-R6OTWcIf4ChtHTS07EHFGM6Oh1OE_xOjOE,3557
|
6
6
|
adss/endpoints/__init__.py,sha256=Pr29901fT8ClCS2GasTjTiBNyn7DfVfxILpYDFsMvPA,488
|
7
7
|
adss/endpoints/admin.py,sha256=S6ZrkeA_Lh_LCpF1NHyfMKqjbIiylYXUSV65H_WKg1U,16391
|
8
|
-
adss/endpoints/images.py,sha256=
|
8
|
+
adss/endpoints/images.py,sha256=gsZzh1cfhULz-SUUV4ImiAIS2lv70eJWI1Sk6MJdShw,31876
|
9
9
|
adss/endpoints/metadata.py,sha256=RPrRP6Uz6-uPMIcntMgfss9vAd5iN7JXjZbF8SW0EYg,8238
|
10
|
-
adss/endpoints/queries.py,sha256=
|
10
|
+
adss/endpoints/queries.py,sha256=qpJ0mdJK8DDhznkHX_DOEWkvbWKFyfemewcKyLFLUP4,17631
|
11
11
|
adss/endpoints/users.py,sha256=6Abkl3c3_YKdMYR_JWI-uL9HTHxcjlIOnE29GyN5_QE,10811
|
12
12
|
adss/models/__init__.py,sha256=ADWVaGy4dkpEMH3iS_6EnRSBlEgoM5Vy9zORQr-UG6w,404
|
13
13
|
adss/models/metadata.py,sha256=6fdH_0BenVRmeXkkKbsG2B68O-N2FXTTRgxsEhAHRoU,4058
|
14
14
|
adss/models/query.py,sha256=V1H9UAv9wORAr85aajeY7H1zaxyfNtKuEoBtBU66DbM,4820
|
15
15
|
adss/models/user.py,sha256=5qVT5qOktokmVLkGszPGCTZWv0wC-7aBMvJ8EeBOqdw,3493
|
16
|
-
adss-1.
|
16
|
+
adss-1.33.dist-info/licenses/LICENSE,sha256=yPw116pnd1J4TuMPnvm6I_irZUyC30EoBZ4BtWFAL7I,1557
|
17
17
|
dev/fetch_idr6.py,sha256=b6FrHPr-ZLaDup_wLOaQWP2fK254Sr3YNHbTxuUt088,12788
|
18
|
-
adss-1.
|
19
|
-
adss-1.
|
20
|
-
adss-1.
|
21
|
-
adss-1.
|
18
|
+
adss-1.33.dist-info/METADATA,sha256=KDrZgw4R_Xo9CQyy-i2pZZqfZteiiFf5-aZ_BSV48Is,8759
|
19
|
+
adss-1.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
20
|
+
adss-1.33.dist-info/top_level.txt,sha256=NT2zObOOiTWXc0yowpEjT6BiiI1e7WXlXd0ZoK7T5hk,9
|
21
|
+
adss-1.33.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|