Repository: obspy/obspy

Issue:
read_inventory fails on bad station metadata
For some reason which I have not figured out yet, station IV.LATE (INGV, Latera) caused the ``read_inventory`` function to fail with an ``AttributeError: 'NoneType' object has no attribute 'strip'`` 

Traceback:

```python
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/core/inventory/inventory.py", line 72, in read_inventory
    format=format)[0]
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/core/util/base.py", line 462, in _read_from_plugin
    list_obj = read_format(filename, **kwargs)
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/io/stationxml/core.py", line 155, in _read_stationxml
    networks.append(_read_network(network, _ns))
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/io/stationxml/core.py", line 201, in _read_network
    stations.append(_read_station(station, _ns))
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/io/stationxml/core.py", line 242, in _read_station
    channels.append(_read_channel(channel, _ns))
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/io/stationxml/core.py", line 312, in _read_channel
    longitude=longitude, elevation=elevation, depth=depth)
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/core/inventory/channel.py", line 129, in __init__
    self.location_code = location_code
  File "/Users/shahar/anaconda2/lib/python2.7/site-packages/obspy/core/inventory/channel.py", line 207, in location_code
    self._location_code = value.strip()
AttributeError: 'NoneType' object has no attribute 'strip'
```

After some debugging, I realised that in the ``_read_channel`` function, all values (longitude, latitude, elevation, depth, code, and location_code) are ``None`` and that causes the ``Channel.__init__`` to fail.

Currently, my workaround is to wrap line 237 in ``obspy/io/stationxml/core.py`` in a ``try/except`` statement:

```python
    channels = []
    for channel in sta_element.findall(_ns("Channel")):
        try:
            channels.append(_read_channel(channel, _ns))
        except AttributeError:
            warnings.warn('No channels available for station:\n'
                          '{}'.format(str(station)))
        # channels.append(_read_channel(channel, _ns))
    station.channels = channels
```

This results in a station entry in the inventory object with 0 channels.

I don't know if this is the preferred behavior but for now it works for me.


Discussion:
Hi Shahar,

can you upload the file? If it has a channel and all the coordinates are not set it is not a valid StationXML file - but I agree that ObsPy should be forgiving here and read it nonetheless.

Cheers!
Here you go:

[IV.LATE.xml.zip](https://github.com/obspy/obspy/files/1141939/IV.LATE.xml.zip)

We did it for SC3ML in https://github.com/obspy/obspy/pull/1817 so might as well do it for StationXML too.

Resolving patch:
diff --git a/obspy/io/stationxml/core.py b/obspy/io/stationxml/core.py
--- a/obspy/io/stationxml/core.py
+++ b/obspy/io/stationxml/core.py
@@ -235,7 +235,22 @@ def _read_station(sta_element, _ns):
         station.external_references.append(_read_external_reference(ref, _ns))
     channels = []
     for channel in sta_element.findall(_ns("Channel")):
-        channels.append(_read_channel(channel, _ns))
+        # Skip empty channels.
+        if not channel.items() and not channel.attrib:
+            continue
+        cha = _read_channel(channel, _ns)
+        # Might be None in case the channel could not be parsed.
+        if cha is None:
+            # This is None if, and only if, one of the coordinates could not
+            # be set.
+            msg = ("Channel %s.%s of station %s does not have a complete set "
+                   "of coordinates and thus it cannot be read. It will not be "
+                   "part of the final inventory object." % (
+                    channel.get("locationCode"), channel.get("code"),
+                    sta_element.get("code")))
+            warnings.warn(msg, UserWarning)
+        else:
+            channels.append(cha)
     station.channels = channels
     return station
 
@@ -293,6 +308,20 @@ def _read_floattype_list(parent, tag, cls, unit=False, datum=False,
 
 
 def _read_channel(cha_element, _ns):
+    """
+    Returns either a :class:`~obspy.core.inventory.channel.Channel` object or
+    ``None``.
+
+    It should return ``None`` if and only if it did not manage to
+    successfully create a :class:`~obspy.core.inventory.channel.Channel`
+    object which can only happen if one of the coordinates is not set. All the
+    others are optional. If either the location or channel code is not set it
+    raises but that is fine as that would deviate too much from the StationXML
+    standard to be worthwhile to recover from.
+    """
+    code = cha_element.get("code")
+    location_code = cha_element.get("locationCode")
+
     longitude = _read_floattype(cha_element, _ns("Longitude"), Longitude,
                                 datum=True)
     latitude = _read_floattype(cha_element, _ns("Latitude"), Latitude,
@@ -300,8 +329,11 @@ def _read_channel(cha_element, _ns):
     elevation = _read_floattype(cha_element, _ns("Elevation"), Distance,
                                 unit=True)
     depth = _read_floattype(cha_element, _ns("Depth"), Distance, unit=True)
-    code = cha_element.get("code")
-    location_code = cha_element.get("locationCode")
+
+    # All of these must be given, otherwise it is an invalid station.
+    if None in [longitude, latitude, elevation, depth]:
+        return None
+
     channel = obspy.core.inventory.Channel(
         code=code, location_code=location_code, latitude=latitude,
         longitude=longitude, elevation=elevation, depth=depth)


Test patch:
diff --git a/obspy/io/stationxml/tests/data/channel_without_coordinates.xml b/obspy/io/stationxml/tests/data/channel_without_coordinates.xml
new file mode 100644
--- /dev/null
+++ b/obspy/io/stationxml/tests/data/channel_without_coordinates.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="1.0" xsi:schemaLocation="http://www.fdsn.org/xml/station/1 http://www.fdsn.org/xml/station/fdsn-station-1.0.xsd">
+  <Source>SeisNet-mysql</Source>
+  <Sender>INGV-CNT</Sender>
+  <Module>INGV-CNT WEB SERVICE: fdsnws-station | version: 1.1.34</Module>
+  <ModuleURI>http://webservices.rm.ingv.it/fdsnws/station/1/query?endtime=2017-03-01T00%3A00%3A00.000000&amp;format=xml&amp;includerestricted=false&amp;level=response&amp;network=IV&amp;starttime=2016-08-24T00%3A00%3A00.000000&amp;station=LATE</ModuleURI>
+  <Created>2017-07-12T12:18:17</Created>
+  <Network code="IV" startDate="1988-01-01T00:00:00" restrictedStatus="open">
+    <Description>Italian Seismic Network</Description>
+    <TotalNumberStations>540</TotalNumberStations>
+    <SelectedNumberStations>1</SelectedNumberStations>
+    <Station code="LATE" startDate="2006-05-04T00:00:00" restrictedStatus="open">
+      <Latitude>42.6137</Latitude>
+      <Longitude>11.804</Longitude>
+      <Elevation>610</Elevation>
+      <Site>
+        <Name>Latera</Name>
+      </Site>
+      <CreationDate>2017-07-12T12:18:15</CreationDate>
+      <TotalNumberChannels>63</TotalNumberChannels>
+      <SelectedNumberChannels>0</SelectedNumberChannels>
+      <Channel code="BHZ" locationCode="00">
+      </Channel>
+    </Station>
+  </Network>
+</FDSNStationXML>
diff --git a/obspy/io/stationxml/tests/data/empty_channel.xml b/obspy/io/stationxml/tests/data/empty_channel.xml
new file mode 100644
--- /dev/null
+++ b/obspy/io/stationxml/tests/data/empty_channel.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="1.0" xsi:schemaLocation="http://www.fdsn.org/xml/station/1 http://www.fdsn.org/xml/station/fdsn-station-1.0.xsd">
+  <Source>SeisNet-mysql</Source>
+  <Sender>INGV-CNT</Sender>
+  <Module>INGV-CNT WEB SERVICE: fdsnws-station | version: 1.1.34</Module>
+  <ModuleURI>http://webservices.rm.ingv.it/fdsnws/station/1/query?endtime=2017-03-01T00%3A00%3A00.000000&amp;format=xml&amp;includerestricted=false&amp;level=response&amp;network=IV&amp;starttime=2016-08-24T00%3A00%3A00.000000&amp;station=LATE</ModuleURI>
+  <Created>2017-07-12T12:18:17</Created>
+  <Network code="IV" startDate="1988-01-01T00:00:00" restrictedStatus="open">
+    <Description>Italian Seismic Network</Description>
+    <TotalNumberStations>540</TotalNumberStations>
+    <SelectedNumberStations>1</SelectedNumberStations>
+    <Station code="LATE" startDate="2006-05-04T00:00:00" restrictedStatus="open">
+      <Latitude>42.6137</Latitude>
+      <Longitude>11.804</Longitude>
+      <Elevation>610</Elevation>
+      <Site>
+        <Name>Latera</Name>
+      </Site>
+      <CreationDate>2017-07-12T12:18:15</CreationDate>
+      <TotalNumberChannels>63</TotalNumberChannels>
+      <SelectedNumberChannels>0</SelectedNumberChannels>
+      <Channel/>
+    </Station>
+  </Network>
+</FDSNStationXML>
diff --git a/obspy/io/stationxml/tests/test_stationxml.py b/obspy/io/stationxml/tests/test_stationxml.py
--- a/obspy/io/stationxml/tests/test_stationxml.py
+++ b/obspy/io/stationxml/tests/test_stationxml.py
@@ -21,6 +21,7 @@
 import unittest
 import warnings
 
+import obspy
 from obspy.core.util import AttribDict
 from obspy.core.inventory import Inventory, Network
 from obspy.core.util.base import NamedTemporaryFile
@@ -1055,6 +1056,43 @@ def test_write_with_extra_tags_and_read(self):
             # now, read again to test if it's parsed correctly..
             inv = obspy.read_inventory(tmpfile)
 
+    def test_reading_file_with_empty_channel_object(self):
+        """
+        Tests reading a file with an empty channel object. This is strictly
+        speaking not valid but we are forgiving.
+        """
+        filename = os.path.join(self.data_dir, "empty_channel.xml")
+        inv = obspy.read_inventory(filename)
+        self.assertEqual(
+            inv.get_contents(),
+            {'networks': ['IV'], 'stations': ['IV.LATE (Latera)'],
+             'channels': []})
+
+    def test_reading_channel_without_coordinates(self):
+        """
+        Tests reading a file with an empty channel object. This is strictly
+        speaking not valid but we are forgiving.
+        """
+        filename = os.path.join(self.data_dir,
+                                "channel_without_coordinates.xml")
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            inv = obspy.read_inventory(filename)
+
+        # Should raise a warning that it could not read the channel without
+        # coordinates.
+        self.assertEqual(len(w), 1)
+        self.assertEqual(
+            w[0].message.args[0],
+            "Channel 00.BHZ of station LATE does not have a complete set of "
+            "coordinates and thus it cannot be read. It will not be part of "
+            "the final inventory object.")
+
+        self.assertEqual(
+            inv.get_contents(),
+            {'networks': ['IV'], 'stations': ['IV.LATE (Latera)'],
+             'channels': []})
+
 
 def suite():
     return unittest.makeSuite(StationXMLTestCase, "test")