Skip to content

Commit 1d06067

Browse files
committed
Extend Gemini to include getting file content and logging out of the Archive.
1 parent 524c2f4 commit 1d06067

File tree

4 files changed

+172
-12
lines changed

4 files changed

+172
-12
lines changed

astroquery/gemini/core.py

+98-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1+
# Licensed under a 3-clause BSD style license - see LICENSE.rst
12
"""
2-
Search functionality for the Gemini archive of observations.
3+
==================================================
4+
Gemini Observatory Archive (GOA) Astroquery Module
5+
==================================================
36
4-
For questions, contact [email protected]
7+
Query public and proprietary data from GOA.
58
"""
6-
79
import os
8-
910
from datetime import date
1011

1112
from astroquery import log
13+
import astropy
1214
from astropy import units
1315
from astropy.table import Table, MaskedColumn
14-
15-
from astroquery.gemini.urlhelper import URLHelper
1616
import numpy as np
1717

18+
from .urlhelper import URLHelper
1819
from ..query import QueryWithLogin
1920
from ..utils.class_or_instance import class_or_instance
2021
from . import conf
@@ -433,6 +434,97 @@ def get_file(self, filename, *, download_dir='.', timeout=None):
433434
local_filepath = os.path.join(download_dir, filename)
434435
self._download_file(url=url, local_filepath=local_filepath, timeout=timeout)
435436

437+
def _download_file_content(self, url, timeout=None, auth=None, method="GET", **kwargs):
438+
"""Download content from a URL and return it. Resembles
439+
`_download_file` but returns the content instead of saving it to a
440+
local file.
441+
442+
Parameters
443+
----------
444+
url : str
445+
The URL from where to download the file.
446+
timeout : int, optional
447+
Time in seconds to wait for the server response, by default
448+
`None`.
449+
auth : dict[str, Any], optional
450+
Authentication details, by default `None`.
451+
method : str, optional
452+
The HTTP method to use, by default "GET".
453+
454+
Returns
455+
-------
456+
bytes
457+
The downloaded content.
458+
"""
459+
460+
response = self._session.request(method, url, timeout=timeout, auth=auth, **kwargs)
461+
response.raise_for_status()
462+
463+
if 'content-length' in response.headers:
464+
length = int(response.headers['content-length'])
465+
if length == 0:
466+
log.warn(f'URL {url} has length=0')
467+
468+
blocksize = astropy.utils.data.conf.download_block_size
469+
content = b""
470+
471+
for block in response.iter_content(blocksize):
472+
content += block
473+
474+
response.close()
475+
476+
return content
477+
478+
def logout(self):
479+
"""Logout from the GOA service by deleting the specific session cookie
480+
and updating the authentication state.
481+
"""
482+
# Delete specific cookie.
483+
cookie_name = "gemini_archive_session"
484+
if cookie_name in self._session.cookies:
485+
del self._session.cookies[cookie_name]
486+
487+
# Update authentication state.
488+
self._authenticated = False
489+
490+
def get_file_content(self, filename, timeout=None, auth=None, method="GET", **kwargs):
491+
"""Wrapper around `_download_file_content`.
492+
493+
Parameters
494+
----------
495+
filename : str
496+
Name of the file to download content.
497+
timeout : int, optional
498+
Time in seconds to wait for the server response, by default
499+
`None`.
500+
auth : dict[str, Any], optional
501+
Authentication details, by default `None`.
502+
method : str, optional
503+
The HTTP method to use, by default "GET".
504+
505+
Returns
506+
-------
507+
bytes
508+
The downloaded content.
509+
"""
510+
url = self.get_file_url(filename)
511+
return self._download_file_content(url, timeout=timeout, auth=auth, method=method, **kwargs)
512+
513+
def get_file_url(self, filename):
514+
"""Generate the file URL based on the filename.
515+
516+
Parameters
517+
----------
518+
filename : str
519+
The name of the file.
520+
521+
Returns
522+
-------
523+
str
524+
The URL where the file can be downloaded.
525+
"""
526+
return f"https://archive.gemini.edu/file/{filename}"
527+
436528

437529
def _gemini_json_to_table(json):
438530
"""

astroquery/gemini/tests/test_gemini.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,37 @@ class MockResponse:
2424

2525
def __init__(self, text):
2626
self.text = text
27+
self.headers = {'content-length': str(len(text))}
28+
self.status_code = 200
2729

2830
def json(self):
2931
return json.loads(self.text)
3032

33+
def raise_for_status(self):
34+
pass
35+
36+
def iter_content(self, blocksize):
37+
yield self.text
38+
39+
def close(self):
40+
pass
41+
3142

3243
@pytest.fixture
3344
def patch_get(request):
3445
""" mock get requests so they return our canned JSON to mimic Gemini's archive website """
3546
mp = request.getfixturevalue("monkeypatch")
3647

3748
mp.setattr(requests.Session, 'request', get_mockreturn)
38-
return mp
49+
50+
51+
@pytest.fixture
52+
def patch_content(monkeypatch):
53+
"""Mock requests with encoded content."""
54+
def mock_request(*args, **kwargs):
55+
return MockResponse(b"mock_content")
56+
57+
monkeypatch.setattr(requests.Session, 'request', mock_request)
3958

4059

4160
# to inspect behavior, updated when the mock get call is made
@@ -171,3 +190,25 @@ def test_url_helper_eng_fail(test_arg):
171190
urlsplit = url.split('/')
172191
assert (('notengineering' in urlsplit) == should_have_noteng)
173192
assert (('NotFail' in urlsplit) == should_have_notfail)
193+
194+
195+
def test_logout():
196+
"""Test logout functionality."""
197+
gemini.Observations._session.cookies = {"gemini_archive_session": "some_value"}
198+
gemini.Observations._authenticated = True
199+
gemini.Observations.logout()
200+
assert "gemini_archive_session" not in gemini.Observations._session.cookies
201+
assert gemini.Observations._authenticated is False
202+
203+
204+
def test_get_file_content(patch_content):
205+
"""Test wrapper around _download_file_content."""
206+
content = gemini.Observations.get_file_content("filename", timeout=5)
207+
assert content == b"mock_content"
208+
209+
210+
def test_get_file_url():
211+
"""Test generating file URL based on filename."""
212+
url = gemini.Observations.get_file_url("filename")
213+
assert url == "https://archive.gemini.edu/file/filename"
214+

astroquery/gemini/tests/test_remote.py

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
https://astroquery.readthedocs.io/en/latest/testing.html
99
"""
1010
import pytest
11+
import requests
1112

1213
import os
1314
import shutil
@@ -23,6 +24,10 @@
2324
""" Coordinates to use for testing """
2425
coords = SkyCoord(210.80242917, 54.34875, unit="deg")
2526

27+
# Filename and url
28+
filename = "S20231016S0018.fits.bz2" # Small file
29+
file_url = f"https://archive.gemini.edu/file{filename}"
30+
2631

2732
@pytest.mark.remote_data
2833
class TestGemini:
@@ -78,3 +83,14 @@ def test_get_file(self):
7883
os.unlink(filepath)
7984
if os.path.exists(tempdir):
8085
shutil.rmtree(tempdir)
86+
87+
def test_get_file_content(self):
88+
"""Test the `get_file_content` function."""
89+
content = gemini.Observations.get_file_content(filename)
90+
assert isinstance(content, bytes)
91+
assert len(content) > 0
92+
93+
def test_get_file_content_with_timeout(self):
94+
"""Test `get_file_content` with a timeout."""
95+
with pytest.raises(requests.exceptions.Timeout):
96+
gemini.Observations.get_file_content(filename, timeout=0.001)

docs/gemini/gemini.rst

+16-5
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,21 @@ the *NotFail* or *notengineering* terms respectively.
132132
Authenticated Sessions
133133
----------------------
134134

135-
The Gemini module allows for authenticated sessions using your GOA account. This is the same account you login
136-
with on the GOA homepage at `<https://archive.gemini.edu/>`__. The `astroquery.gemini.ObservationsClass.login`
137-
method returns `True` if successful.
135+
The Gemini module allows for authenticated sessions using your GOA account. This is the same account you
136+
login with on the GOA homepage at `<https://archive.gemini.edu/>`__. The
137+
`astroquery.gemini.ObservationsClass.login` method returns `True` if successful. To logout, use the
138+
`astroquery.gemini.ObservationsClass.logout` method to remove the Gemini Observatory Archive session cookie.
138139

139140
.. doctest-skip::
140141

141142
>>> from astroquery.gemini import Observations
142143
>>> Observations.login(username, password)
143144
>>> # do something with your elevated access
145+
>>> Observations.logout()
144146

145147

146-
File Downloading
147-
----------------
148+
File Downloading and File Content Getting
149+
-----------------------------------------
148150

149151
As a convenience, you can request file downloads directly from the Gemini module. This constructs the appropriate
150152
URL and fetches the file. It will use any authenticated session you may have, so it will retrieve any
@@ -156,6 +158,15 @@ proprietary data you may be permissioned for.
156158
>>> Observations.get_file("GS2020AQ319-10.fits", download_dir="/tmp") # doctest: +IGNORE_OUTPUT
157159

158160

161+
To get the file content without writing to disk, you can use the method
162+
`astroquery.gemini.ObservationsClass.get_file_content`. This constructs the appropriate url and fetches the
163+
file contents. This will use any authenticated session you have for proprietary data.
164+
165+
.. doctest-remote-data::
166+
>>> from astroquery.gemini import Observations
167+
>>> Observations.get_file_content("GS2020AQ319-10.fits") # doctest: +IGNORE_OUTPUT
168+
169+
159170
Reference/API
160171
=============
161172

0 commit comments

Comments
 (0)