Repository: sunpy/sunpy

Issue:
Add support for the 1m average GOES-XRS data
### Describe the feature

The GOES/XRS 1 minute data can be read in with the XRSTimeSeries parser, but we currently don't have the functionality to search/download this data (simple update to the client and make use the a.Resolution attribute to query?).

This relates to #6419 but given that the reader already exists within sunpy core, it probably makes sense to update goes client to also query this data product. 

### Proposed solution

Update the pattern to use a.Resolution https://github.com/sunpy/sunpy/blob/main/sunpy/net/dataretriever/sources/goes.py#L78

the only difference between the paths is `../xrsf-l2-avg1m_science/...` vs `/xrsf-l2-flx1s_science/` which is currently there


Discussion:

Resolving patch:
diff --git a/examples/time_series/goes_hek_m25.py b/examples/time_series/goes_hek_m25.py
--- a/examples/time_series/goes_hek_m25.py
+++ b/examples/time_series/goes_hek_m25.py
@@ -15,9 +15,10 @@
 ###############################################################################
 # Let's grab GOES XRS data for a particular time of interest and the HEK flare
 # data for this time from the NOAA Space Weather Prediction Center (SWPC).
+# Here we are searching for data from the GOES-15 satellite and for the 1-min average time-sampled data.
 
 tr = a.Time('2011-06-07 04:00', '2011-06-07 12:00')
-results = Fido.search(tr, a.Instrument.xrs & a.goes.SatelliteNumber(15) | a.hek.FL & (a.hek.FRM.Name == 'SWPC'))  # NOQA
+results = Fido.search(tr, a.Instrument.xrs & a.goes.SatelliteNumber(15) & a.Resolution("avg1m") | a.hek.FL & (a.hek.FRM.Name == 'SWPC'))  # NOQA
 
 ###############################################################################
 # Then download the XRS data and load it into a TimeSeries.
diff --git a/examples/time_series/goes_xrs_example.py b/examples/time_series/goes_xrs_example.py
--- a/examples/time_series/goes_xrs_example.py
+++ b/examples/time_series/goes_xrs_example.py
@@ -46,14 +46,16 @@
 print(result)
 
 #############################################################
-# As we can see this now returns three results, one file for GOES
+# As we can see this now returns six results, two files for each GOES
 # 13, one for GOES 14 and one for GOES 15, which can be identified
-# by the ``SatelliteNumber`` column. However, we probably will only want
-# one of these files for our analysis, so we can query by the `sunpy.net.attrs`:
+# by the ``SatelliteNumber`` column.
+# The GOES data provided can either be high-cadence (1s/2s/3s based on GOES satellite) or avergaed over 1 minute.
+# This can be noted in the ``Resolution`` column, where the ``avg1m`` and ``flx1s`` attributes are the 1 minute average and the high-cadence data, respectively.
+# However, we probably will only want one of these files for our analysis, so we can query by the `sunpy.net.attrs`:
 # `sunpy.net.dataretriever.attrs.goes.SatelliteNumber` to specify what GOES satellite number we want
-# to use.
+# to use, and `sunpy.net.attrs.Resolution` for the resolution. Here we will use the high-cadence observations.
 
-result_goes15 = Fido.search(a.Time(tstart, tend), a.Instrument("XRS"), a.goes.SatelliteNumber(15))
+result_goes15 = Fido.search(a.Time(tstart, tend), a.Instrument("XRS"), a.goes.SatelliteNumber(15), a.Resolution("flx1s"))
 print(result_goes15)
 
 #############################################################
@@ -113,9 +115,9 @@
 # which are now and its now available through sunpy.net.Fido.
 
 ###############################################################
-# Lets query for some recent data over two days.
+# Lets query for some data over two days.
 
-results = Fido.search(a.Time("2020-11-20 00:00", "2020-11-21 23:00"), a.Instrument("XRS"))
+results = Fido.search(a.Time("2020-11-20 00:00", "2020-11-21 23:00"), a.Instrument("XRS"), a.Resolution("flx1s"))
 print(results)
 
 ###############################################################
diff --git a/sunpy/net/dataretriever/sources/goes.py b/sunpy/net/dataretriever/sources/goes.py
--- a/sunpy/net/dataretriever/sources/goes.py
+++ b/sunpy/net/dataretriever/sources/goes.py
@@ -52,17 +52,21 @@ class XRSClient(GenericClient):
     <sunpy.net.fido_factory.UnifiedResponse object at ...>
     Results from 1 Provider:
     <BLANKLINE>
-    4 Results from the XRSClient:
+    8 Results from the XRSClient:
     Source: <8: https://umbra.nascom.nasa.gov/goes/fits
     8-15: https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/
     16-17: https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/
     <BLANKLINE>
-           Start Time               End Time        Instrument ... Source Provider
-    ----------------------- ----------------------- ---------- ... ------ --------
-    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999        XRS ...   GOES     NOAA
-    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999        XRS ...   GOES     NOAA
-    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999        XRS ...   GOES     NOAA
-    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999        XRS ...   GOES     NOAA
+           Start Time               End Time        ... Provider Resolution
+    ----------------------- ----------------------- ... -------- ----------
+    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999 ...     NOAA      flx1s
+    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999 ...     NOAA      flx1s
+    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999 ...     NOAA      avg1m
+    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999 ...     NOAA      avg1m
+    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999 ...     NOAA      flx1s
+    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999 ...     NOAA      flx1s
+    2016-01-01 00:00:00.000 2016-01-01 23:59:59.999 ...     NOAA      avg1m
+    2016-01-02 00:00:00.000 2016-01-02 23:59:59.999 ...     NOAA      avg1m
     <BLANKLINE>
     <BLANKLINE>
     """
@@ -71,14 +75,14 @@ class XRSClient(GenericClient):
     pattern_old = '{}/fits/{year:4d}/go{SatelliteNumber:02d}{}{month:2d}{day:2d}.fits'
     # The reprocessed 8-15 data should be taken from NOAA.
     baseurl_new = (r"https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/"
-                   r"goes{SatelliteNumber:02d}/gxrs-l2-irrad_science/%Y/%m/sci_gxrs-l2-irrad_g{SatelliteNumber:02d}_d%Y%m%d_.*\.nc")
-    pattern_new = ("{}/goes{SatelliteNumber:02d}/gxrs-l2-irrad_science/{year:4d}/"
-                   "{month:2d}/sci_gxrs-l2-irrad_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
+                   r"goes{SatelliteNumber:02d}/{filename_res}-l2-{resolution}_science/%Y/%m/sci_{filename_res}-l2-{resolution}_g{SatelliteNumber:02d}_d%Y%m%d_.*\.nc")
+    pattern_new = ("{}/goes{SatelliteNumber:02d}/{filename_res}-l2-{resolution}_science/{year:4d}/"
+                   "{month:2d}/sci_{filename_res}-l2-{resolution}_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
     # GOES-R Series 16-17 XRS data from NOAA.
     baseurl_r = (r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes{SatelliteNumber}"
-                 r"/l2/data/xrsf-l2-flx1s_science/%Y/%m/sci_xrsf-l2-flx1s_g{SatelliteNumber}_d%Y%m%d_.*\.nc")
-    pattern_r = ("{}/goes/goes{SatelliteNumber:02d}/l2/data/xrsf-l2-flx1s_science/{year:4d}/"
-                 "{month:2d}/sci_xrsf-l2-flx1s_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
+                 r"/l2/data/xrsf-l2-{Resolution}_science/%Y/%m/sci_xrsf-l2-{Resolution}_g{SatelliteNumber}_d%Y%m%d_.*\.nc")
+    pattern_r = ("{}/goes/goes{SatelliteNumber:02d}/l2/data/xrsf-l2-{Resolution}_science/{year:4d}/"
+                 "{month:2d}/sci_xrsf-l2-{Resolution}_g{SatelliteNumber:02d}_d{year:4d}{month:2d}{day:2d}_{}.nc")
 
     @property
     def info_url(self):
@@ -98,10 +102,16 @@ def post_search_hook(self, i, matchdict):
         rowdict["Physobs"] = matchdict["Physobs"][0]
         rowdict["url"] = i["url"]
         rowdict["Source"] = matchdict["Source"][0]
-        if i["url"].endswith(".fits"):
+        if i["url"].endswith(".fits"):  # for older FITS files
             rowdict["Provider"] = matchdict["Provider"][0]
-        else:
+        else:  # only Resolution attrs for the netcdf files
             rowdict["Provider"] = matchdict["Provider"][1]
+            if "avg1m" in i["url"]:
+                rowdict["Resolution"] = "avg1m"
+            elif ("flx1s" in i["url"]) or ("irrad" in i["url"]):
+                rowdict["Resolution"] = "flx1s"
+            else:
+                raise RuntimeError("Could not parse resolution from URL")
         return rowdict
 
     def search(self, *args, **kwargs):
@@ -141,16 +151,32 @@ def _get_metalist(self, matchdict):
         # The data before the re-processed GOES 8-15 data.
         if (matchdict["End Time"] < "2001-03-01") or (matchdict["End Time"] >= "2001-03-01" and matchdict["Provider"] == ["sdac"]):
             metalist += self._get_metalist_fn(matchdict, self.baseurl_old, self.pattern_old)
-        # New data from NOAA.
+        # New data from NOAA. It searches for both the high cadence and 1 minute average data.
         else:
             if matchdict["End Time"] >= "2017-02-07":
-                for sat in [16, 17]:
-                    metalist += self._get_metalist_fn(matchdict,
-                                                      self.baseurl_r.format(SatelliteNumber=sat), self.pattern_r)
+                for sat in matchdict["SatelliteNumber"]:
+                    if int(sat) >= 16:  # here check for GOES 16 and 17
+                        for res in matchdict["Resolution"]:
+                            metalist += self._get_metalist_fn(matchdict,
+                                                              self.baseurl_r.format(SatelliteNumber=int(sat), Resolution=res), self.pattern_r)
+
             if matchdict["End Time"] <= "2020-03-04":
-                for sat in [8, 9, 10, 11, 12, 13, 14, 15]:
-                    metalist += self._get_metalist_fn(matchdict,
-                                                      self.baseurl_new.format(SatelliteNumber=sat), self.pattern_new)
+                for sat in matchdict["SatelliteNumber"]:
+                    if (int(sat) >= 8) & (int(sat) <= 15):  # here check for GOES 8-15
+                        # The 1 minute average data is at a different base URL to that of the high cadence data which is why things are done this way.
+                        for res in matchdict["Resolution"]:
+                            if res == "avg1m":
+                                filename_res = "xrsf"
+                                resolution = "avg1m"
+                                metalist += self._get_metalist_fn(matchdict,
+                                                                  self.baseurl_new.format(SatelliteNumber=int(sat), filename_res=filename_res, resolution=resolution), self.pattern_new)
+                            elif res == "flx1s":
+                                filename_res = "gxrs"
+                                resolution = "irrad"
+                                metalist += self._get_metalist_fn(matchdict,
+                                                                  self.baseurl_new.format(SatelliteNumber=int(sat), filename_res=filename_res, resolution=resolution), self.pattern_new)
+                            else:
+                                raise RuntimeError(f"{res}` is not an accepted resolution attrs for the XRSClient")
         return metalist
 
     @classmethod
@@ -168,7 +194,9 @@ def register_values(cls):
             attrs.Source: [("GOES", "The Geostationary Operational Environmental Satellite Program.")],
             attrs.Provider: [('SDAC', 'The Solar Data Analysis Center.'),
                              ('NOAA', 'The National Oceanic and Atmospheric Administration.')],
-            attrs.goes.SatelliteNumber: [(str(x), f"GOES Satellite Number {x}") for x in goes_number]}
+            attrs.goes.SatelliteNumber: [(str(x), f"GOES Satellite Number {x}") for x in goes_number],
+            attrs.Resolution: [('flx1s', 'High-cadence measurements XRS observation, 1s for GOES-R, 2s for GOES 13-15, 3s for GOES<13'),
+                               ('avg1m', '1-minute averages of XRS measurements')]}
         return adict
 
 


Test patch:
diff --git a/sunpy/database/tests/test_tables.py b/sunpy/database/tests/test_tables.py
--- a/sunpy/database/tests/test_tables.py
+++ b/sunpy/database/tests/test_tables.py
@@ -128,7 +128,7 @@ def test_tag_hashability():
 def test_entries_from_fido_search_result(fido_search_result):
     entries = list(entries_from_fido_search_result(fido_search_result))
     # 68 entries for 8 instruments in fido_search_result
-    assert len(entries) == 68
+    assert len(entries) == 70
     # First 2 entries are from lyra
     assert entries[0] == DatabaseEntry(
         source='PROBA2', provider='ESA', physobs='irradiance',
@@ -155,7 +155,7 @@ def test_entries_from_fido_search_result(fido_search_result):
         size=None,
         instrument='EVE',
         wavemin=0.1, wavemax=30.4)
-    # 2 entries from goes
+    # 4 entries from goes
     assert entries[60] == DatabaseEntry(
         source='GOES', provider='NOAA', physobs='irradiance',
         fileid='https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/goes15/'
@@ -165,7 +165,7 @@ def test_entries_from_fido_search_result(fido_search_result):
         wavemin=np.nan, wavemax=np.nan,
         instrument='XRS')
     # 1 entry from noaa-indices
-    assert entries[62] == DatabaseEntry(
+    assert entries[64] == DatabaseEntry(
         source='SIDC', provider='SWPC', physobs='sunspot number',
         fileid='https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json',
         observation_time_start=None,
@@ -173,7 +173,7 @@ def test_entries_from_fido_search_result(fido_search_result):
         wavemin=np.nan, wavemax=np.nan,
         instrument='NOAA-Indices')
     # 1 entry from noaa-predict
-    assert entries[63] == DatabaseEntry(
+    assert entries[65] == DatabaseEntry(
         source='ISES', provider='SWPC', physobs='sunspot number',
         fileid='https://services.swpc.noaa.gov/json/solar-cycle/predicted-solar-cycle.json',
         observation_time_start=None,
@@ -181,7 +181,7 @@ def test_entries_from_fido_search_result(fido_search_result):
         wavemin=np.nan, wavemax=np.nan,
         instrument='NOAA-Predict')
     # 2 entries from norh
-    assert entries[64] == DatabaseEntry(
+    assert entries[66] == DatabaseEntry(
         source='NAOJ', provider='NRO', physobs=None,
         fileid=("ftp://solar-pub.nao.ac.jp/"
                 "pub/nsro/norh/data/tcx/2012/01/tca120101"),
@@ -190,10 +190,10 @@ def test_entries_from_fido_search_result(fido_search_result):
         wavemin=17634850.470588233, wavemax=17634850.470588233,
         instrument='NORH')
     # 2 entries from rhessi
-    assert 'hsi_obssumm_20120101' in entries[66].fileid
-    assert entries[66] == DatabaseEntry(
+    assert 'hsi_obssumm_20120101' in entries[68].fileid
+    assert entries[68] == DatabaseEntry(
         source="RHESSI", provider='NASA', physobs='summary_lightcurve',
-        fileid=entries[66].fileid,
+        fileid=entries[68].fileid,
         observation_time_start=datetime(2012, 1, 1, 0, 0),
         observation_time_end=datetime(2012, 1, 1, 23, 59, 59, 999000),
         wavemin=np.nan, wavemax=np.nan,
diff --git a/sunpy/net/dataretriever/sources/tests/test_goes_ud.py b/sunpy/net/dataretriever/sources/tests/test_goes_ud.py
--- a/sunpy/net/dataretriever/sources/tests/test_goes_ud.py
+++ b/sunpy/net/dataretriever/sources/tests/test_goes_ud.py
@@ -32,7 +32,7 @@ def LCClient():
       'https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/'
       'goes16/l2/data/xrsf-l2-flx1s_science/2020/08/sci_xrsf-l2-flx1s_g16_d20200802_v2-2-0.nc',
       'https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/'
-      'goes17/l2/data/xrsf-l2-flx1s_science/2020/08/sci_xrsf-l2-flx1s_g17_d20200804_v2-2-0.nc')])
+      'goes17/l2/data/xrsf-l2-avg1m_science/2020/08/sci_xrsf-l2-avg1m_g17_d20200804_v2-2-0.nc')])
 def test_get_url_for_time_range(LCClient, timerange, url_start, url_end):
     qresponse = LCClient.search(timerange)
     urls = [i['url'] for i in qresponse]
@@ -58,11 +58,11 @@ def test_get_overlap_urls(LCClient, timerange, url_start, url_end):
 @pytest.mark.parametrize("timerange, url_start, url_end",
                          [(a.Time("2009/08/30 00:10", "2009/09/02"),
                            "https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/goes10/gxrs-l2-irrad_science/2009/08/sci_gxrs-l2-irrad_g10_d20090830_v0-0-0.nc",
-                           "https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/goes14/gxrs-l2-irrad_science/2009/09/sci_gxrs-l2-irrad_g14_d20090902_v0-0-0.nc")])
+                           "https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/goes14/xrsf-l2-avg1m_science/2009/09/sci_xrsf-l2-avg1m_g14_d20090902_v1-0-0.nc")])
 def test_get_overlap_providers(LCClient, timerange, url_start, url_end):
     qresponse = LCClient.search(timerange)
     urls = [i['url'] for i in qresponse]
-    assert len(urls) == 6
+    assert len(urls) == 8
     assert urls[0] == url_start
     assert urls[-1] == url_end
 
@@ -148,11 +148,30 @@ def test_new_logic(LCClient):
     assert len(download_list) == len(qr)
 
 
+@pytest.mark.remote_data
+def test_resolution_attrs(LCClient):
+    qr_high_cadence = LCClient.search(Time('2012/10/4 20:20', '2012/10/4 21:00'), Instrument('XRS'), a.Resolution.flx1s)
+    assert len(qr_high_cadence) == 2
+    assert "irrad" in qr_high_cadence[0]["url"]  # GOES < 16 have irrad in filename rather than flx1s
+
+    qr_high_cadence_GOESR = LCClient.search(Time('2021/10/4 20:20', '2021/10/4 21:00'), Instrument('XRS'), a.Resolution.flx1s)
+    assert len(qr_high_cadence_GOESR) == 2
+    assert "flx1s" in qr_high_cadence_GOESR[0]["url"]  # GOES < 16 have irrad in filename rather than flx1s
+
+    qr_avg = LCClient.search(Time('2012/10/4 20:20', '2012/10/4 21:00'), Instrument('XRS'), a.Resolution.avg1m)
+    assert len(qr_avg) == 2
+    assert "avg1m" in qr_avg[0]["url"]
+
+    # check is incorrect resolution attrs passed
+    with pytest.raises(RuntimeError):
+        qr_wrong_resolution = LCClient.search(Time('2012/10/4 20:20', '2012/10/4 21:00'), Instrument('XRS'), a.Resolution.ctime)
+
+
 @pytest.mark.remote_data
 @pytest.mark.parametrize(
     "time, instrument, expected_num_files",
-    [(a.Time("2012/10/4", "2012/10/5"), a.Instrument.goes, 4),
-     (a.Time('2013-10-28 01:00', '2013-10-28 03:00'), a.Instrument('XRS'), 2)])
+    [(a.Time("2012/10/4", "2012/10/5"), a.Instrument.goes, 8),
+     (a.Time('2013-10-28 01:00', '2013-10-28 03:00'), a.Instrument('XRS'), 4)])
 def test_fido(time, instrument, expected_num_files):
     qr = Fido.search(time, instrument)
     assert isinstance(qr, UnifiedResponse)