Repository: sunpy/sunpy

Issue:
No Fermi GBM Client for data download
<!-- This comments are hidden when you submit the issue so you do not need to remove them!
Please be sure to check out our contributing guidelines: https://github.com/sunpy/sunpy/blob/master/CONTRIBUTING.rst
Please be sure to check out our code of conduct:
https://github.com/sunpy/sunpy/blob/master/CODE_OF_CONDUCT.rst -->

<!-- Please have a search on our GitHub repository to see if a similar issue has already been posted.
If a similar issue is closed, have a quick look to see if you are satisfied by the resolution.
If not please go ahead and open an issue! -->

### Description
<!-- Provide a general description of the feature you would like. -->
<!-- "If you want to, you can suggest a draft design or API. -->
<!-- This way we have a deeper discussion on the feature. -->
In regards to using Fido for downloading data, there doesn't seem to be a FERMI GBM client for downloading data 

Is there a specific reason for this or is there scope to implement one?



Discussion:
Well, this seems to be a glaring bug. We used to have the ability to download Fermi data back in 0.9 land through lightcurve. (https://github.com/sunpy/sunpy/blob/0.9/sunpy/lightcurve/sources/fermi_gbm.py#L97) it seems that it was never ported to Fido?
I'll give this a shot 

Resolving patch:
diff --git a/sunpy/net/attrs.py b/sunpy/net/attrs.py
--- a/sunpy/net/attrs.py
+++ b/sunpy/net/attrs.py
@@ -4,6 +4,7 @@
 from .jsoc import attrs as jsoc
 from .dataretriever.attrs import goes
 
-from .vso.attrs import Time, Instrument, Wavelength, Level, Sample
+from .vso.attrs import Time, Instrument, Wavelength, Level, Sample, Detector, Resolution
 
-__all__ = ['Time', 'Instrument', 'Wavelength', 'Level', 'Sample', 'vso', 'jsoc', 'goes']
+__all__ = ['Time', 'Instrument', 'Wavelength', 'Level', 'Sample', 'Detector', 'Resolution' 'vso',
+           'jsoc', 'goes']
diff --git a/sunpy/net/dataretriever/__init__.py b/sunpy/net/dataretriever/__init__.py
--- a/sunpy/net/dataretriever/__init__.py
+++ b/sunpy/net/dataretriever/__init__.py
@@ -15,8 +15,8 @@
 from .sources.norh import NoRHClient
 from .sources.rhessi import RHESSIClient
 from .sources.noaa import NOAAIndicesClient, NOAAPredictClient, SRSClient
-
+from .sources.fermi_gbm import GBMClient
 
 __all__ = ['QueryResponseBlock', 'QueryResponse', 'GenericClient',
            'EVEClient', 'XRSClient', 'LYRAClient', 'NOAAIndicesClient',
-           'NOAAPredictClient', 'NoRHClient', 'RHESSIClient', 'SRSClient']
+           'NOAAPredictClient', 'NoRHClient', 'RHESSIClient', 'SRSClient', 'GBMClient']
diff --git a/sunpy/net/dataretriever/sources/fermi_gbm.py b/sunpy/net/dataretriever/sources/fermi_gbm.py
new file mode 100644
--- /dev/null
+++ b/sunpy/net/dataretriever/sources/fermi_gbm.py
@@ -0,0 +1,141 @@
+from sunpy.util.scraper import Scraper
+from ..client import GenericClient
+
+__all__ = ['GBMClient']
+
+
+class GBMClient(GenericClient):
+    """
+    This GBMClient is for the Gamma-Ray Burst Monitor (GBM) instrument
+    aboard the Fermi satellite. Although GBMs primary objective is to
+    detect gamma-ray bursts, it provides high quailty high energy solar
+    flare observations.
+
+    The instrument consists of 12 Sodium Iodide (NaI) scintillation
+    detectors, which are sensitive to an energy range of 4keV to 1MeV.
+    At any one time, 6 of the NaI detectors are Sunward facing.
+    The detectors are numbered 'n1' to 'n11'. This client supports the user
+    to choose which detector to use through the `a.Detector <sunpy.net.attrs.Detector>` attribute.
+    The default detector is 'n5'.
+
+    The GBM data comes in daily version files in two formats:
+
+        * CSPEC - counts accumulated every  4.096 seconds in 128 energy channels for each detector.
+        * CTIME - counts accumulated every 0.256 seconds in 8 energy channels
+
+    Both of which can be accessed through the attrs `a.Resolution <sunpy.net.attrs.Resolution>`.
+    The default data type is CSPEC unless the user defines.
+
+    Examples
+    --------
+    >>> from sunpy.net import Fido, attrs as a
+    >>> res = Fido.search(a.Time('2015-06-21 00:00', '2015-06-23 23:59'),
+    ...                   a.Instrument('gbm'), a.Detector('n3'),
+    ...                   a.Resolution('ctime')) #doctest: +REMOTE_DATA
+    >>> print(res) #doctest: +REMOTE_DATA
+    Results from 1 Provider:
+    <BLANKLINE>
+    3 Results from the GBMClient:
+         Start Time           End Time      Source Instrument Wavelength
+           str19               str19         str5     str3       str3
+    ------------------- ------------------- ------ ---------- ----------
+    2015-06-21 00:00:00 2015-06-23 23:59:00  FERMI        GBM        nan
+    2015-06-21 00:00:00 2015-06-23 23:59:00  FERMI        GBM        nan
+    2015-06-21 00:00:00 2015-06-23 23:59:00  FERMI        GBM        nan
+    <BLANKLINE>
+    <BLANKLINE>
+    <BLANKLINE>
+    """
+    def _get_url_for_timerange(self, timerange, **kwargs):
+        """
+        Returns the url for Fermi/GBM data for the given date.
+
+        Parameters
+        ----------
+        timerange : `sunpy.time.TimeRange`
+            The time range for which to download the data.
+
+        Returns
+        -------
+        `str`:
+            The url(s) for time of interest.
+        """
+        # Checks if detector keyword
+        # If not defaults to detector 5
+        if 'detector' in kwargs:
+            det = _check_detector(kwargs['detector'])
+        else:
+            det = 'n5'
+
+        # Check for resolution keyword - either CSPEC or CTIME
+        # Default type is CSPEC
+        if 'resolution' in kwargs:
+            data_type = _check_type(kwargs['resolution'])
+        else:
+            data_type = 'cspec'
+
+        gbm_pattern = ('https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/daily/'
+                       '%Y/%m/%d/current/glg_{data_type}_{det}_%y%m%d_v00.pha')
+        gbm_files = Scraper(gbm_pattern, data_type=data_type, det=det)
+        urls = gbm_files.filelist(timerange)
+
+        return urls
+
+    def _makeimap(self):
+        """
+        Helper function used to hold information about source.
+        """
+        self.map_['source'] = 'FERMI'
+        self.map_['instrument'] = 'GBM'
+        self.map_['physobs'] = 'flux'
+        self.map_['provider'] = 'NASA'
+
+    @classmethod
+    def _can_handle_query(cls, *query):
+        """
+        Answers whether a client can service the query.
+
+        Parameters
+        ----------
+        query : `list`
+            A list of of query objects.
+
+        Returns
+        -------
+        `bool`
+            `True` if this client can service the query, otherwise `False`.
+        """
+        chkattr = ['Time', 'Instrument', 'Detector', 'Resolution']
+        chklist = [x.__class__.__name__ in chkattr for x in query]
+        for x in query:
+            if x.__class__.__name__ == 'Instrument' and x.value.lower() == 'gbm':
+                return all(chklist)
+        return False
+
+
+def _check_detector(detector, **kwargs):
+    """
+    checks to see if detector is in right format.
+    """
+    detector_numbers = [str(i) for i in range(12)]
+    detector_list = ['n' + i for i in detector_numbers]
+
+    if detector in detector_list:
+        return detector
+    elif detector in detector_numbers:
+        return 'n' + detector
+    else:
+        raise ValueError('Detector number needs to be a string. Available detectors are n0-n11')
+
+
+def _check_type(datatype, **kwargs):
+    """
+    checks is datatype is either "CSPEC" or "CTIME".
+    """
+    if not isinstance(datatype, str):
+        raise ValueError('{} is not str - either cspec or ctime'.format(datatype))
+
+    if datatype.lower() != 'cspec' and datatype.lower() != 'ctime':
+        raise ValueError('{} not value datatype - either cspec or ctime'.format(datatype))
+    else:
+        return datatype.lower()


Test patch:
diff --git a/sunpy/net/dataretriever/tests/test_fermi_gbm.py b/sunpy/net/dataretriever/tests/test_fermi_gbm.py
new file mode 100644
--- /dev/null
+++ b/sunpy/net/dataretriever/tests/test_fermi_gbm.py
@@ -0,0 +1,81 @@
+import pytest
+
+from sunpy.time import parse_time
+from sunpy.time.timerange import TimeRange
+from sunpy.net.dataretriever.client import QueryResponse
+import sunpy.net.dataretriever.sources.fermi_gbm as fermi_gbm
+from sunpy.net.fido_factory import UnifiedResponse
+from sunpy.net import Fido
+from sunpy.net import attrs as a
+from sunpy.net.tests.strategies import time_attr
+from hypothesis import given
+
+LCClient = fermi_gbm.GBMClient()
+
+
+@pytest.mark.remote_data
+@pytest.mark.parametrize("timerange,url_start,url_end",
+                         [(TimeRange('2011/06/07', '2011/06/09'),
+                          'https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/daily/2011/06/07/'
+                          'current/glg_cspec_n5_110607_v00.pha',
+                          'https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/daily/2011/06/09/'
+                          'current/glg_cspec_n5_110609_v00.pha'),
+                          (TimeRange('2016/09/09', '2016/09/11'),
+                          'https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/daily/2016/09/09/'
+                          'current/glg_cspec_n5_160909_v00.pha',
+                          'https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/daily/'
+                          '2016/09/11/current/glg_cspec_n5_160911_v00.pha')])
+def test_get_url_for_time_range(timerange, url_start, url_end):
+    urls = LCClient._get_url_for_timerange(timerange, detector='n5', resolution='cspec')
+    assert isinstance(urls, list)
+    assert urls[0] == url_start
+    assert urls[-1] == url_end
+
+
+@given(time_attr())
+def test_can_handle_query(time):
+    ans1 = LCClient._can_handle_query(time, a.Instrument('gbm'))
+    assert ans1 is True
+    ans2 = LCClient._can_handle_query(time, a.Instrument('gbm'),
+                                      a.Detector('n5'))
+    assert ans2 is True
+    ans3 = LCClient._can_handle_query(time, a.Instrument('gbm'),
+                                      a.Detector('n5'), a.Resolution('ctime'))
+    assert ans3 is True
+    ans4 = LCClient._can_handle_query(time)
+    assert ans4 is False
+
+
+@pytest.mark.remote_data
+@pytest.mark.parametrize("time,instrument", [
+    (a.Time('2012/8/9', '2012/8/10'), a.Instrument('gbm')),
+])
+def test_query(time, instrument):
+    qr1 = LCClient.search(time, instrument)
+    assert isinstance(qr1, QueryResponse)
+    assert len(qr1) == 2
+    assert qr1.time_range().start == time.start
+    assert qr1.time_range().end == time.end
+
+
+@pytest.mark.remote_data
+@pytest.mark.parametrize("time,instrument", [
+    (a.Time('2012/11/27', '2012/11/27'), a.Instrument('gbm')),
+])
+def test_get(time, instrument):
+    qr1 = LCClient.search(time, instrument)
+    download_list = LCClient.fetch(qr1)
+    assert len(download_list) == len(qr1)
+
+
+@pytest.mark.remote_data
+@pytest.mark.parametrize(
+    'query',
+    [(a.Time('2012/10/4', '2012/10/6') & a.Instrument('gbm') & a.Detector('n5'))])
+def test_fido(query):
+    qr = Fido.search(query)
+    client = qr.get_response(0).client
+    assert isinstance(qr, UnifiedResponse)
+    assert type(client) == type(LCClient)
+    response = Fido.fetch(qr)
+    assert len(response) == qr._numfile