Repository: obspy/obspy

Issue:
Fix Seedlink basic_client get_info failing when location and channel not in Seedlink reply
<!--

Thank your for contributing to ObsPy!

!! Please check that you select the **correct base branch** (details see below link) !!

Before submitting a PR, please review the pull request guidelines:
https://github.com/obspy/obspy/blob/master/CONTRIBUTING.md#submitting-a-pull-request

Also, please make sure you are following the ObsPy branching model:
https://github.com/obspy/obspy/wiki/ObsPy-Git-Branching-Model

-->

### What does this PR do?

In `seedlink.basic_client.get_info`, sets `loc` and `cha` as empty strings if they are not included in the Seedlink reply.

### Why was it initiated?  Any relevant Issues?

Current behaviour is to set `loc` and `cha` in `station_cache` as `None` if they aren't in the Seedlink reply. This causes the function to fail, as `station_cache` is then searched using `fname`, with `loc` and `cha` provided as `name` argument. A value of `None` for `name` will cause `fname` to raise, as it calls `os.path.normpath(name)`, which expects a string. Empty strings get around this issue. This does mean missing codes will be returned as '' and it's up to the user to understand this means Seedlink didn't reply with the code, and that the stream may not be available.

### PR Checklist
- [x] Correct base branch selected? `master` for new features, `maintenance_...` for bug fixes
- [x] This PR is not directly related to an existing issue (which has no PR yet).
- [ ] If the PR is making changes to documentation, docs pages can be built automatically.
      Just remove the space in the following string after the + sign: "+ DOCS"
- [x] If any network modules should be tested for the PR, add them as a comma separated list
      (e.g. `clients.fdsn,clients.arclink`) after the colon in the following magic string: "+TESTS:clients.seedlink"
      (you can also add "ALL" to just simply run all tests across all modules)
- [ ] All tests still pass.
- [ ] Any new features or fixed regressions are be covered via new tests.
- [ ] Any new or changed features have are fully documented.
- [ ] Significant changes have been added to `CHANGELOG.txt` .
- [x] First time contributors have added your name to `CONTRIBUTORS.txt` .



Discussion:
@brenmous this looks good. could you 
- rebase this on current master
- what do you think about adding a warning.warn in the ``if not subtags:`` condition?
Hmm.. we could alternatively change this part of the code to let items with `None` values get through (or not) instead:

```python
264             info = [(net, sta, loc, cha) for net, sta, loc, cha in                             
265                     self._station_cache if                                                     
266                     fnmatch.fnmatch(net, network or '*') and                                   
267                     fnmatch.fnmatch(sta, station or '*') and                                   
268                     fnmatch.fnmatch(loc, location or '*') and                                  
269                     fnmatch.fnmatch(cha, channel or '*')]                                      
270             return sorted(info)                                                                
```

What do you think @brenmous? That way we could still allow `None` values to indicate something for that network+station code is advertized but there is no data currently.
Somehow this didn't get listed in the `1.4.1` milestone by github like it should have and got left out..

Resolving patch:
diff --git a/obspy/clients/seedlink/basic_client.py b/obspy/clients/seedlink/basic_client.py
--- a/obspy/clients/seedlink/basic_client.py
+++ b/obspy/clients/seedlink/basic_client.py
@@ -193,7 +193,7 @@ def _multiselect_request(self, multiselect, starttime, endtime):
         return stream
 
     def get_info(self, network=None, station=None, location=None, channel=None,
-                 level='station', cache=True):
+                 level='station', cache=True, warn_on_excluded_stations=False):
         """
         Request available stations information from the seedlink server.
 
@@ -204,8 +204,8 @@ def get_info(self, network=None, station=None, location=None, channel=None,
         >>> info = client.get_info(station="FDFM")
         >>> print(info)
         [('G', 'FDFM')]
-        >>> info = client.get_info(station="FD?M", channel='*Z',
-        ...                        level='channel')
+        >>> info = client.get_info(
+        ...     station="FD?M", channel='*Z', level='channel')
         >>> print(info)  # doctest: +NORMALIZE_WHITESPACE
         [('G', 'FDFM', '00', 'BHZ'), ('G', 'FDFM', '00', 'HHZ'),
          ('G', 'FDFM', '00', 'HNZ'), ('G', 'FDFM', '00', 'LHZ'),
@@ -217,6 +217,17 @@ def get_info(self, network=None, station=None, location=None, channel=None,
         need to force fetching new information from the server (should only
         concern programs running in background for a very long time).
 
+        .. note::
+            Stations/channels are excluded from the results for which the
+            server indicates it is serving them in general but it also states
+            no data are in ring buffer currently.
+            If interested in these "no data" stations/channels, either set
+            ``warn_on_excluded_stations=True`` which will show a warning
+            message with excluded stations or use ``debug=True`` when
+            initializing the client which will print the raw server ``seedlink
+            INFO`` xml response which will show these stations listed with
+            ``begin_seq`` and ``end_seq`` both with value ``'000000'``.
+
         :type network: str
         :param network: Network code. Supports ``fnmatch`` wildcards, e.g.
             ``*`` and ``?``.
@@ -232,6 +243,10 @@ def get_info(self, network=None, station=None, location=None, channel=None,
         :type cache: bool
         :param cache: Subsequent function calls are cached, use ``cache=False``
             to force fetching station metadata again from the server.
+        :type warn_on_excluded_stations: bool
+        :param warn_on_excluded_stations: Whether to show a warning for
+            stations that are excluded from the results because the server
+            indicates there is no data currently available.
         :rtype: list
         :returns: list of 2-tuples (or 4-tuples with ``level='channel'``) with
             network/station (network/station/location/channel, respectively)
@@ -289,26 +304,43 @@ def get_info(self, network=None, station=None, location=None, channel=None,
             parser = etree.XMLParser(encoding='utf-8')
             xml = etree.fromstring(info.encode('utf-8'), parser=parser)
         station_cache = set()
+        excluded_stations = set()
         for tag in xml.xpath('./station'):
             net = tag.attrib['network']
             sta = tag.attrib['name']
             item = (net, sta)
             if level == 'channel':
                 subtags = tag.xpath('./stream')
-                for subtag in subtags:
-                    loc = subtag.attrib['location']
-                    cha = subtag.attrib['seedname']
-                    station_cache.add(item + (loc, cha))
                 # If no data is in ring buffer (e.g. station outage?) then it
                 # seems the seedlink server replies with no subtags for the
                 # channels
                 if not subtags:
-                    station_cache.add(item + (None, None))
-            else:
+                    excluded_stations.add(item)
+                    continue
+                for subtag in subtags:
+                    loc = subtag.attrib['location']
+                    cha = subtag.attrib['seedname']
+                    station_cache.add(item + (loc, cha))
+            elif level == 'station':
+                # remove stations that seem to have no data
+                if all(tag.attrib[key] == '000000'
+                       for key in ('begin_seq', 'end_seq')):
+                    excluded_stations.add(item)
+                    continue
                 station_cache.add(item)
+            else:
+                raise NotImplementedError()
         # change results to an Inventory object
         self._station_cache = station_cache
         self._station_cache_level = level
+        if warn_on_excluded_stations and excluded_stations:
+            msg = ('Some stations were excluded from results because server '
+                   'indicates no data available (use debug=True in Client '
+                   'initialization for details, suppress warning with '
+                   'warn_on_excluded_stations=False): ')
+            msg += ', '.join('.'.join(item)
+                             for item in sorted(excluded_stations))
+            warnings.warn(msg)
         return self.get_info(
             network=network, station=station, location=location,
             channel=channel, cache=True, level=level)


Test patch: