Skip to content

Commit 66be1b2

Browse files
wtbarnesnabobalisCadair
authored
Add support for processing keywords (#43)
* add support for processing keywords * additional supported processing keywords * additional allowed processing keywords * add support for processing keywords * changelog * test cutout export * example showing how to request a cutout * Update CHANGELOG.rst Co-authored-by: Nabil Freij <nabil.freij@gmail.com> * Update CHANGELOG.rst Co-authored-by: Nabil Freij <nabil.freij@gmail.com> * Update CHANGELOG.rst Co-authored-by: Nabil Freij <nabil.freij@gmail.com> * Update drms/json.py Co-authored-by: Nabil Freij <nabil.freij@gmail.com> * Update drms/json.py * clarify no validation * test rebin * link to im_patch docs * fix rebin test * Apply suggestions from code review * Update CHANGELOG.rst Co-authored-by: Stuart Mumford <stuart@cadair.com> Co-authored-by: Nabil Freij <nabil.freij@gmail.com> Co-authored-by: Stuart Mumford <stuart@cadair.com>
1 parent c3578c1 commit 66be1b2

File tree

5 files changed

+220
-18
lines changed

5 files changed

+220
-18
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ Backwards Incompatible Changes
1414
Deprecations and Removals
1515
-------------------------
1616

17-
- ``Client.get()`` has been removed, use ``Client.query()`` instead.
17+
- ``Client.get()`` has been removed, use :meth:`drms.client.Client.query()` instead.
18+
19+
Support for Processing Keywords
20+
--------------------------------
21+
22+
- :meth:`drms.client.Client.export` now accepts a ``process`` keyword argument
23+
- This allows users to specify additional server-side processing options such as image cutouts
24+
- See the "Processing" section of the `JSOC Data Export page <http://jsoc.stanford.edu/ajax/exportdata.html>`__ for more information.

drms/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,7 @@ def export(
11381138
n=None,
11391139
email=None,
11401140
requestor=None,
1141+
process=None,
11411142
):
11421143
"""
11431144
Submit a data export request.
@@ -1188,6 +1189,13 @@ def export(
11881189
current default email address is used, which in this case
11891190
has to be set before calling export() by using the
11901191
:attr:`Client.email` attribute.
1192+
process : `dict`, None
1193+
Dictionary of processing commands. Each entry is also a `dict`
1194+
containing all of the applicable options for that processing
1195+
command. Note that only the name of the process, and not the
1196+
arguments, are validated by the `~drms.client.Client`. In the case of invalid
1197+
or malformed processing arguments, JSOC may still return
1198+
an unprocessed image without the export request failing.
11911199
requestor : str, None or bool
11921200
Export user ID. Default is None, in which case the user
11931201
name is determined from the email address. If set to False,
@@ -1223,6 +1231,7 @@ def export(
12231231
filenamefmt=filenamefmt,
12241232
n=n,
12251233
requestor=requestor,
1234+
process=process,
12261235
)
12271236
return ExportRequest(d, client=self)
12281237

drms/json.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -284,17 +284,7 @@ def check_address(self, email):
284284
req = self._json_request(self._server.url_check_address + query)
285285
return req.data
286286

287-
def exp_request(
288-
self,
289-
ds,
290-
notify,
291-
method='url_quick',
292-
protocol='as-is',
293-
protocol_args=None,
294-
filenamefmt=None,
295-
n=None,
296-
requestor=None,
297-
):
287+
def exp_request(self, *args, **kwargs):
298288
"""
299289
Request data export.
300290
@@ -317,6 +307,10 @@ def exp_request(
317307
filenamefmt : str, None
318308
Custom filename format string for exported files. This is
319309
ignored for 'url_quick'/'as-is' data exports.
310+
process : `dict`, None
311+
Dictionary of processing commands. Each entry is also a `dict`
312+
containing all of the applicable options for that processing
313+
command.
320314
n : int or None
321315
Limits the number of records requested. For positive
322316
values, the first n records of the record set are returned,
@@ -334,6 +328,21 @@ def exp_request(
334328
Dictionary containing the server response to the export
335329
request.
336330
"""
331+
req = self._json_request(self._exp_request_url(*args, **kwargs))
332+
return req.data
333+
334+
def _exp_request_url(
335+
self,
336+
ds,
337+
notify,
338+
method='url_quick',
339+
protocol='as-is',
340+
protocol_args=None,
341+
filenamefmt=None,
342+
n=None,
343+
process=None,
344+
requestor=None,
345+
):
337346
method = method.lower()
338347
method_list = ['url_quick', 'url', 'url-tar', 'ftp', 'ftp-tar']
339348
if method not in method_list:
@@ -393,18 +402,34 @@ def exp_request(
393402
if filenamefmt is not None:
394403
d['filenamefmt'] = filenamefmt
395404

396-
if n is not None:
397-
n = int(n)
398-
d['process=n'] = f'{int(n)}'
405+
n = int(n) if n is not None else 0
406+
d['process=n'] = f'{n}'
407+
if process is not None:
408+
allowed_processes = [
409+
'im_patch',
410+
'resize',
411+
'rebin',
412+
'aia_scale_aialev1',
413+
'aia_scale_orig',
414+
'aia_scale_other',
415+
'Maproj',
416+
'HmiB2ptr',
417+
]
418+
process_strings = {}
419+
for p, opts in process.items():
420+
if p not in allowed_processes:
421+
raise ValueError(f'{p} is not one of the allowed processing options: {allowed_processes}')
422+
process_strings[p] = ','.join([f'{k}={v}' for k, v in opts.items()])
423+
processes = '|'.join([f'{k},{v}' for k, v in process_strings.items()])
424+
d['process=n'] = f'{d["process=n"]}|{processes}'
399425

400426
if requestor is None:
401427
d['requestor'] = notify.split('@')[0]
402428
elif requestor is not False:
403429
d['requestor'] = requestor
404430

405-
query = f'?{urlencode(d)}'
406-
req = self._json_request(self._server.url_jsoc_fetch + query)
407-
return req.data
431+
query = '?' + urlencode(d)
432+
return self._server.url_jsoc_fetch + query
408433

409434
def exp_status(self, requestid):
410435
"""

drms/tests/test_jsoc_export.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,93 @@ def test_export_fits_basic(jsoc_client_export):
5959
assert url.endswith('continuum.fits') or url.endswith('magnetogram.fits')
6060

6161

62+
@pytest.mark.jsoc
63+
@pytest.mark.export
64+
@pytest.mark.remote_data
65+
def test_export_im_patch(jsoc_client_export):
66+
# TODO: check that this has actually done the export/processing properly?
67+
# NOTE: processing exports seem to fail silently on the server side if
68+
# the correct names/arguments are not passed. Not clear how to check
69+
# that this has not happened.
70+
process = {'im_patch': {
71+
't_ref': '2015-10-17T04:33:30.000',
72+
't': 0,
73+
'r': 0,
74+
'c': 0,
75+
'locunits': 'arcsec',
76+
'boxunits': 'arcsec',
77+
'x': -517.2,
78+
'y': -246,
79+
'width': 345.6,
80+
'height': 345.6,
81+
}}
82+
req = jsoc_client_export.export(
83+
'aia.lev1_euv_12s[2015-10-17T04:33:30.000/1m@12s][171]{image}',
84+
method='url',
85+
protocol='fits',
86+
process=process,
87+
requestor=False,
88+
)
89+
90+
assert isinstance(req, drms.ExportRequest)
91+
assert req.wait(timeout=60)
92+
assert req.has_succeeded()
93+
assert req.protocol == 'fits'
94+
95+
for record in req.urls.record:
96+
record = record.lower()
97+
assert record.startswith('aia.lev1_euv_12s_mod')
98+
99+
for filename in req.urls.filename:
100+
assert filename.endswith('image.fits')
101+
102+
for url in req.urls.url:
103+
assert url.endswith('image.fits')
104+
105+
106+
@pytest.mark.jsoc
107+
@pytest.mark.export
108+
@pytest.mark.remote_data
109+
def test_export_rebin(jsoc_client_export):
110+
# TODO: check that this has actually done the export/processing properly?
111+
# NOTE: processing exports seem to fail silently on the server side if
112+
# the correct names/arguments are not passed. Not clear how to check
113+
# that this has not happened.
114+
req = jsoc_client_export.export(
115+
'hmi.M_720s[2020-10-17_22:12:00_TAI/24m]{magnetogram}',
116+
method='url',
117+
protocol='fits',
118+
process={'rebin': {'method': 'boxcar', 'scale': 0.25}},
119+
requestor=False,
120+
)
121+
122+
assert isinstance(req, drms.ExportRequest)
123+
assert req.wait(timeout=60)
124+
assert req.has_succeeded()
125+
assert req.protocol == 'fits'
126+
127+
for record in req.urls.record:
128+
record = record.lower()
129+
assert record.startswith('hmi.m_720s_mod')
130+
131+
for filename in req.urls.filename:
132+
assert filename.endswith('magnetogram.fits')
133+
134+
for url in req.urls.url:
135+
assert url.endswith('magnetogram.fits')
136+
137+
138+
@pytest.mark.jsoc
139+
@pytest.mark.export
140+
@pytest.mark.remote_data
141+
def test_export_invalid_process(jsoc_client_export):
142+
with pytest.raises(ValueError, match='foobar is not one of the allowed processing options'):
143+
jsoc_client_export.export(
144+
'aia.lev1_euv_12s[2015-10-17T04:33:30.000/1m@12s][171]{image}',
145+
process={'foobar': {}}
146+
)
147+
148+
62149
@pytest.mark.jsoc
63150
@pytest.mark.export
64151
@pytest.mark.remote_data

examples/cutout_export_request.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
===================================
3+
Exporting data with cutout requests
4+
===================================
5+
6+
This example shows how to submit a data export request with a cutout request using the ``im_patch`` command.
7+
"""
8+
9+
import os
10+
11+
import drms
12+
13+
###############################################################################
14+
# Create DRMS client, uses the JSOC baseurl by default, set debug=True to see the DRMS query URLs.
15+
16+
client = drms.Client(verbose=True)
17+
18+
# This example requires a registered export email address. You can register
19+
# JSOC exports at: http://jsoc.stanford.edu/ajax/register_email.html
20+
email = 'nabil.freij@gmail.com'
21+
22+
# Download directory
23+
out_dir = os.path.join('downloads')
24+
25+
# Create download directory if it does not exist yet.
26+
if not os.path.exists(out_dir):
27+
os.makedirs(out_dir)
28+
29+
###############################################################################
30+
# Construct the DRMS query string: "Series[harpnum][timespan]{data segments}"
31+
32+
qstr = 'aia.lev1_euv_12s[2015-10-17T04:33:30.000/1m@12s][171]{image}'
33+
print(f'Data export query:\n {qstr}\n')
34+
35+
###############################################################################
36+
# Construct the dictionary specifying that we want to request a cutout.
37+
# This is done via the ``im_patch`` command.
38+
# We request a 345.6 arcsecond cutout (on both sides) centered on the coordinate (-517.2, -246) arcseconds as defined in the helioprojective frame of SDO at time ``t_ref``.
39+
# The ``t`` controls whether tracking is disabled (``1``) or enabled (``0``).
40+
# ``r`` controls the use of sub-pixel registration.
41+
# ``c`` controls whether off-limb pixels are filled with NaNs.
42+
# For additional details about ``im_patch``, see the `documentation <http://jsoc.stanford.edu/doxygen_html/group__im__patch.html>`_.
43+
process = {'im_patch': {
44+
't_ref': '2015-10-17T04:33:30.000',
45+
't': 0,
46+
'r': 0,
47+
'c': 0,
48+
'locunits': 'arcsec',
49+
'boxunits': 'arcsec',
50+
'x': -517.2,
51+
'y': -246,
52+
'width': 345.6,
53+
'height': 345.6,
54+
}}
55+
56+
# Submit export request using the 'fits' protocol
57+
print('Submitting export request...')
58+
result = client.export(
59+
qstr,
60+
method='url',
61+
protocol='fits',
62+
email=email,
63+
process=process,
64+
)
65+
66+
# Print request URL.
67+
print(f'\nRequest URL: {result.request_url}')
68+
print(f'{int(len(result.urls))} file(s) available for download.\n')
69+
70+
# Download selected files.
71+
result.wait()
72+
result.download(out_dir)
73+
print('Download finished.')
74+
print(f'\nDownload directory:\n "{os.path.abspath(out_dir)}"\n')

0 commit comments

Comments
 (0)