Repository: sunpy/sunpy

Issue:
Creating a TimeSeries from a url for GOES/XRS raises a NotImplementedType error
<!-- 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 bug. -->
Creating a TimeSeries object by passing in the URL to a GOES/XRS fits file raises an error, tested on master and 0.9.6.
### Expected behavior
<!-- What did you expect to happen. -->
A XRSTimeSeries is created
### Actual behavior
<!-- What actually happened. -->
<!-- Was the output confusing or poorly described? -->
TypeError raised
```
In [3]: goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-611d12dec7b6> in <module>
----> 1 goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')

/usr/local/lib/python3.6/site-packages/sunpy/timeseries/timeseries_factory.py in __call__(self, *args, **kwargs)
    410         for filepath in filepaths:
    411             try:
--> 412                 new_ts = self._check_registered_widgets(filepath=filepath, **kwargs)
    413             except (NoMatchError, MultipleMatchError, ValidationFunctionError):
    414                 if not silence_errors:

/usr/local/lib/python3.6/site-packages/sunpy/timeseries/timeseries_factory.py in _check_registered_widgets(self, **kwargs)
    534         units = kwargs.pop('units', None)
    535         if filepath:
--> 536             data, meta, units = WidgetType._parse_file(filepath)
    537
    538         # Now return a TimeSeries from the given file.

TypeError: 'NotImplementedType' object is not iterable
```
### Steps to Reproduce
<!-- Ideally a code example could be provided so we can run it ourselves. -->
<!-- If you are pasting code, use tripe backticks (```) around your code snippet. -->

```
from sunpy.timeseries import TimeSeries
g1 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits')
# or
# g2 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')
```
### System Details
<!-- We at least need to know the SunPy version you are using. -->
<!-- We provide a short function in SunPy that will provide some of the below information. -->
<!-- It is sunpy.util.system_info(), this is optional but strongly recommended. -->

  - SunPy Version: master or 0.9.6
  - Python Version: python3.6
  - OS information: macOS 10.14.4 / Ubuntu strech
Creating a TimeSeries from a url for GOES/XRS raises a NotImplementedType error
<!-- 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 bug. -->
Creating a TimeSeries object by passing in the URL to a GOES/XRS fits file raises an error, tested on master and 0.9.6.
### Expected behavior
<!-- What did you expect to happen. -->
A XRSTimeSeries is created
### Actual behavior
<!-- What actually happened. -->
<!-- Was the output confusing or poorly described? -->
TypeError raised
```
In [3]: goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-611d12dec7b6> in <module>
----> 1 goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')

/usr/local/lib/python3.6/site-packages/sunpy/timeseries/timeseries_factory.py in __call__(self, *args, **kwargs)
    410         for filepath in filepaths:
    411             try:
--> 412                 new_ts = self._check_registered_widgets(filepath=filepath, **kwargs)
    413             except (NoMatchError, MultipleMatchError, ValidationFunctionError):
    414                 if not silence_errors:

/usr/local/lib/python3.6/site-packages/sunpy/timeseries/timeseries_factory.py in _check_registered_widgets(self, **kwargs)
    534         units = kwargs.pop('units', None)
    535         if filepath:
--> 536             data, meta, units = WidgetType._parse_file(filepath)
    537
    538         # Now return a TimeSeries from the given file.

TypeError: 'NotImplementedType' object is not iterable
```
### Steps to Reproduce
<!-- Ideally a code example could be provided so we can run it ourselves. -->
<!-- If you are pasting code, use tripe backticks (```) around your code snippet. -->

```
from sunpy.timeseries import TimeSeries
g1 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits')
# or
# g2 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/2017/go1320170103.fits')
```
### System Details
<!-- We at least need to know the SunPy version you are using. -->
<!-- We provide a short function in SunPy that will provide some of the below information. -->
<!-- It is sunpy.util.system_info(), this is optional but strongly recommended. -->

  - SunPy Version: master or 0.9.6
  - Python Version: python3.6
  - OS information: macOS 10.14.4 / Ubuntu strech


Discussion:
I think you always need to specify source but doing so leads to a different error which I think is tied to your other issue: 

```python
g1 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits',source="XRS")
```

```python
/opt/miniconda/envs/sunpy/lib/python3.7/site-packages/astropy/time/core.py in _get_time_fmt(self, val, val2, format, scale, precision, in_subfmt, out_subfmt)
    485                   format.lower() in self.FORMATS):
    486             if format is None:
--> 487                 raise ValueError("No time format was given, and the input is "
    488                                  "not unique")
    489             else:

ValueError: No time format was given, and the input is not unique
```
Ah ok I don’t think the docs refelect that at the moment, on mobile so hard to be sure.  Aslo it is a bit odd that depending on the source (local, or url) different errors are raised surely should be source independent?
If you have the file downloaded and open it without the source keyword, it gives you a different error?

The current error is a really bad error and it really does need to be replaced with a better message. 
If I have the file locally I only get the error I mentioned in #3078 with or without the source keyword. 

But if I use a URL I do get different errors with and without the keyword
`goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits')` raises `TypeError: 'NotImplementedType' object is not iterable`
while
`g = TimeSeries('../flarenet/data/goes/go06860129.fits')` raises `ValueError: No time format was given, and the input is not unique`
I think you always need to specify source but doing so leads to a different error which I think is tied to your other issue: 

```python
g1 = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits',source="XRS")
```

```python
/opt/miniconda/envs/sunpy/lib/python3.7/site-packages/astropy/time/core.py in _get_time_fmt(self, val, val2, format, scale, precision, in_subfmt, out_subfmt)
    485                   format.lower() in self.FORMATS):
    486             if format is None:
--> 487                 raise ValueError("No time format was given, and the input is "
    488                                  "not unique")
    489             else:

ValueError: No time format was given, and the input is not unique
```
Ah ok I don’t think the docs refelect that at the moment, on mobile so hard to be sure.  Aslo it is a bit odd that depending on the source (local, or url) different errors are raised surely should be source independent?
If you have the file downloaded and open it without the source keyword, it gives you a different error?

The current error is a really bad error and it really does need to be replaced with a better message. 
If I have the file locally I only get the error I mentioned in #3078 with or without the source keyword. 

But if I use a URL I do get different errors with and without the keyword
`goes = TimeSeries('https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits')` raises `TypeError: 'NotImplementedType' object is not iterable`
while
`g = TimeSeries('../flarenet/data/goes/go06860129.fits')` raises `ValueError: No time format was given, and the input is not unique`

Resolving patch:
diff --git a/sunpy/timeseries/sources/goes.py b/sunpy/timeseries/sources/goes.py
--- a/sunpy/timeseries/sources/goes.py
+++ b/sunpy/timeseries/sources/goes.py
@@ -112,8 +112,8 @@ def peek(self, title="GOES Xray Flux"):
         figure.show()
 
     # ToDo: is this part of the DL pipeline? If so delete.
-    @classmethod
-    def _get_goes_sat_num(self, start, end):
+    @staticmethod
+    def _get_goes_sat_num(start, end):
         """Parses the query time to determine which GOES satellite to use."""
 
         goes_operational = {
@@ -133,10 +133,8 @@ def _get_goes_sat_num(self, start, end):
 
         sat_list = []
         for sat_num in goes_operational:
-            if ((start >= goes_operational[sat_num].start and
-                 start <= goes_operational[sat_num].end and
-                (end >= goes_operational[sat_num].start and
-                 end <= goes_operational[sat_num].end))):
+            if (goes_operational[sat_num].start <= start <= goes_operational[sat_num].end and
+                    goes_operational[sat_num].start <= end <= goes_operational[sat_num].end):
                 # if true then the satellite with sat_num is available
                 sat_list.append(sat_num)
 
@@ -168,7 +166,7 @@ def _parse_hdus(cls, hdulist):
             xrsa = hdulist[2].data['FLUX'][0][:, 1]
             seconds_from_start = hdulist[2].data['TIME'][0]
         elif 1 <= len(hdulist) <= 3:
-            start_time = parse_time(header['TIMEZERO'])
+            start_time = parse_time(header['TIMEZERO'], format='utime')
             seconds_from_start = hdulist[0].data[0]
             xrsb = hdulist[0].data[1]
             xrsa = hdulist[0].data[2]
diff --git a/sunpy/timeseries/timeseries_factory.py b/sunpy/timeseries/timeseries_factory.py
--- a/sunpy/timeseries/timeseries_factory.py
+++ b/sunpy/timeseries/timeseries_factory.py
@@ -103,7 +103,8 @@ class TimeSeriesFactory(BasicRegistrationFactory):
     ...                                             'file1.fits', url, 'eit_*.fits')  # doctest: +SKIP
     """
 
-    def _read_file(self, fname, **kwargs):
+    @staticmethod
+    def _read_file(fname, **kwargs):
         """
         Test reading a file with sunpy.io for automatic source detection.
 
@@ -141,7 +142,8 @@ def _read_file(self, fname, **kwargs):
         else:
             return False, fname
 
-    def _validate_meta(self, meta):
+    @staticmethod
+    def _validate_meta(meta):
         """
         Validate a meta argument for use as metadata.
         Currently only validates by class.
@@ -155,7 +157,8 @@ def _validate_meta(self, meta):
         else:
             return False
 
-    def _validate_units(self, units):
+    @staticmethod
+    def _validate_units(units):
         """
         Validates the astropy unit-information associated with a TimeSeries.
         Should be a dictionary of some form (but not MetaDict) with only
@@ -177,7 +180,8 @@ def _validate_units(self, units):
         # Passed all the tests
         return result
 
-    def _from_table(self, t):
+    @staticmethod
+    def _from_table(t):
         """
         Extract the data, metadata and units from an astropy table for use in
         constructing a TimeSeries.
@@ -200,9 +204,7 @@ def _from_table(self, t):
         # Check if another column is defined as the index/primary_key
         if table.primary_key:
             # Check there is only one primary_key/index column
-            if len(table.primary_key) == 1:
-                table.primary_key[0]
-            else:
+            if len(table.primary_key) != 1:
                 raise ValueError("Invalid input Table, TimeSeries doesn't support conversion"
                                  " of tables with more then one index column.")
 
@@ -256,9 +258,6 @@ def _parse_args(self, *args, **kwargs):
         already_timeseries = list()
         filepaths = list()
 
-        # Take source kwarg if defined
-        source = kwargs.get('source', None)
-
         # Account for nested lists of items. Simply outputs a single list of
         # items, nested lists are expanded to element level.
         args = expand_list(args)
@@ -269,8 +268,7 @@ def _parse_args(self, *args, **kwargs):
             arg = args[i]
 
             # Data-header pair in a tuple
-            if (isinstance(arg, (np.ndarray, Table, pd.DataFrame))):
-                # and self._validate_meta(args[i+1])):
+            if isinstance(arg, (np.ndarray, Table, pd.DataFrame)):
                 # Assume a Pandas Dataframe is given
                 data = arg
                 units = OrderedDict()
@@ -286,7 +284,7 @@ def _parse_args(self, *args, **kwargs):
 
                 # If there are 1 or 2 more arguments:
                 for _ in range(2):
-                    if (len(args) > i+1):
+                    if len(args) > i+1:
                         # If that next argument isn't data but is metaddata or units:
                         if not isinstance(args[i+1], (np.ndarray, Table, pd.DataFrame)):
                             if self._validate_units(args[i+1]):
@@ -308,13 +306,8 @@ def _parse_args(self, *args, **kwargs):
                   os.path.isfile(os.path.expanduser(arg))):
 
                 path = os.path.expanduser(arg)
-
-                read, result = self._read_file(path, **kwargs)
-
-                if read:
-                    data_header_pairs.append(result)
-                else:
-                    filepaths.append(result)
+                result = self._read_file(path, **kwargs)
+                data_header_pairs, filepaths = _apply_result(data_header_pairs, filepaths, result)
 
             # Directory
             elif (isinstance(arg, str) and
@@ -325,26 +318,20 @@ def _parse_args(self, *args, **kwargs):
                 for afile in files:
                     # returns a boolean telling us if it were read and either a
                     # tuple or the original filepath for reading by a source
-                    read, result = self._read_file(afile, **kwargs)
-                    if read:
-                        data_header_pairs.append(result)
-                    else:
-                        filepaths.append(result)
+                    result = self._read_file(afile, **kwargs)
+                    data_header_pairs, filepaths = _apply_result(data_header_pairs, filepaths,
+                                                                 result)
 
             # Glob
-            elif (isinstance(arg, str) and '*' in arg):
+            elif isinstance(arg, str) and '*' in arg:
 
                 files = glob.glob(os.path.expanduser(arg))
-
                 for afile in files:
-                    # data_header_unit_tuples += self._read_file(afile, **kwargs)
                     # returns a boolean telling us if it were read and either a
                     # tuple or the original filepath for reading by a source
-                    read, result = self._read_file(afile, **kwargs)
-                    if read:
-                        data_header_pairs.append(result)
-                    else:
-                        filepaths.append(result)
+                    result = self._read_file(afile, **kwargs)
+                    data_header_pairs, filepaths = _apply_result(data_header_pairs, filepaths,
+                                                                 result)
 
             # Already a TimeSeries
             elif isinstance(arg, GenericTimeSeries):
@@ -355,12 +342,9 @@ def _parse_args(self, *args, **kwargs):
                   _is_url(arg)):
                 url = arg
                 path = download_file(url, get_and_create_download_dir())
-                pairs = self._read_file(path, **kwargs)
-                # data_header_pairs += pairs
-                filepaths.append(pairs[1])
-
+                result = self._read_file(path, **kwargs)
+                data_header_pairs, filepaths = _apply_result(data_header_pairs, filepaths, result)
             else:
-                # raise ValueError("File not found or invalid input")
                 raise NoMatchError("File not found or invalid input")
             i += 1
 
@@ -402,14 +386,13 @@ def __call__(self, *args, **kwargs):
         for filepath in filepaths:
             try:
                 new_ts = self._check_registered_widgets(filepath=filepath, **kwargs)
+                new_timeseries.append(new_ts)
             except (NoMatchError, MultipleMatchError, ValidationFunctionError):
                 if not silence_errors:
                     raise
             except Exception:
                 raise
 
-            new_timeseries.append(new_ts)
-
         # data_header_pairs is a list of HDUs as read by sunpy.io
         # For each set of HDus find the matching class and read the
         # data_header_unit_tuples by calling the _parse_hdus method
@@ -456,14 +439,13 @@ def __call__(self, *args, **kwargs):
             try:
                 new_ts = self._check_registered_widgets(data=data, meta=meta,
                                                         units=units, **kwargs)
+                new_timeseries.append(new_ts)
             except (NoMatchError, MultipleMatchError, ValidationFunctionError):
                 if not silence_errors:
                     raise
             except Exception:
                 raise
 
-            new_timeseries.append(new_ts)
-
         new_timeseries += already_timeseries
 
         # Concatenate the timeseries into one if specified.
@@ -531,6 +513,16 @@ def _check_registered_widgets(self, **kwargs):
         return WidgetType(data, meta, units, **kwargs)
 
 
+def _apply_result(data_header_pairs, filepaths, result):
+    read, result = result
+    if read:
+        data_header_pairs.append(result)
+    else:
+        filepaths.append(result)
+
+    return data_header_pairs, filepaths
+
+
 def _is_url(arg):
     try:
         urlopen(arg)


Test patch:
diff --git a/sunpy/timeseries/tests/test_timeseries_factory.py b/sunpy/timeseries/tests/test_timeseries_factory.py
--- a/sunpy/timeseries/tests/test_timeseries_factory.py
+++ b/sunpy/timeseries/tests/test_timeseries_factory.py
@@ -178,6 +178,21 @@ def test_noaa_pre(self):
         ts_noaa_pre = sunpy.timeseries.TimeSeries(noaa_pre_filepath, source='NOAAPredictIndices')
         assert isinstance(ts_noaa_pre, sunpy.timeseries.sources.noaa.NOAAPredictIndicesTimeSeries)
 
+# ==============================================================================
+# Remote Sources Tests
+# ==============================================================================
+
+    @pytest.mark.remote_data
+    def test_goes_remote(self):
+        # Older format file
+        goes = sunpy.timeseries.TimeSeries(
+            'https://umbra.nascom.nasa.gov/goes/fits/1986/go06860129.fits')
+        assert isinstance(goes, sunpy.timeseries.sources.goes.XRSTimeSeries)
+        # Newer format
+        goes = sunpy.timeseries.TimeSeries(
+            'https://umbra.nascom.nasa.gov/goes/fits/2018/go1520180626.fits')
+        assert isinstance(goes, sunpy.timeseries.sources.goes.XRSTimeSeries)
+
 #==============================================================================
 # Manual TimeSeries Tests
 #==============================================================================