Repository: obspy/obspy

Issue:
can't read Double Difference catalog
I got errors trying to read QuakeML file generated by web-request

cat_dd = obspy.core.event.read_events('./foo.xml')

Single origin is generated by the following web request:
http://service.ncedc.org/fdsnws/event/1/query?start=2018-01-04T00:00:00.0&end=2018-01-05T00:00:00.0&minmag=3&catalog=DD&includeallmagnitudes=false&includearrivals=false&includemechanisms=false&orderby=time&format=xml

Reports error:
ValueError: Setting attribute "origin_type" failed. Value "Double Difference" could not be converted to type "Enum(["hypocenter", "centroid", "amplitude", "macroseismic", "rupture start", "rupture end"])"

This breaks some code that used to work

Note: first time bug report


Discussion:
Note that the origin type from QuakeML file is:
<type>Double Difference</type>


This may be a problem with QuakeMl written by NCEDC. Can this be checked?
It seems that the QuakeML schema does not allow for that value as the `origin type`. IMHO it would be more appropriate for the source to adapt in this case.
Yea that is most definitely an invalid QuakeML file. The allowed origin types are documented here: http://docs.obspy.org/packages/obspy.core.event.header.html#obspy.core.event.header.OriginType

They coincide with what QuakeML allows: https://github.com/obspy/obspy/blob/cd581f0e27d34987681ecb0ea624cb2b0e3f4197/obspy/io/quakeml/data/QuakeML-BED-1.2.xsd#L45-L53

I think we should still change ObsPy so it would throw a warning in these cases but at least it would read the rest of the file.
Thanks Lion. I’ll forward to NCEDC



Resolving patch:
diff --git a/obspy/io/quakeml/core.py b/obspy/io/quakeml/core.py
--- a/obspy/io/quakeml/core.py
+++ b/obspy/io/quakeml/core.py
@@ -43,7 +43,7 @@
                               StationMagnitudeContribution, Tensor, TimeWindow,
                               WaveformStreamID)
 from obspy.core.utcdatetime import UTCDateTime
-from obspy.core.util import AttribDict
+from obspy.core.util import AttribDict, Enum
 
 
 NSMAP_QUAKEML = {None: "http://quakeml.org/xmlns/bed/1.2",
@@ -181,6 +181,18 @@ def _xpath2obj(self, xpath, element=None, convert_to=str, namespace=None):
             warnings.warn(msg % (text, convert_to))
         return None
 
+    def _set_enum(self, xpath, element, obj, key):
+        obj_type = obj._property_dict[key]
+        if not isinstance(obj_type, Enum):  # pragma: no cover
+            raise ValueError
+        value = self._xpath2obj(xpath, element)
+        try:
+            setattr(obj, key, value)
+        except ValueError as e:
+            msg = ('%s. The attribute "%s" will not be set and will be missing'
+                   ' in the resulting object.' % (e.args[0], key))
+            warnings.warn(msg)
+
     def _xpath(self, xpath, element=None, namespace=None):
         if element is None:
             element = self.xml_root
@@ -278,9 +290,10 @@ def _origin_quality(self, parent):
     def _event_description(self, parent):
         out = []
         for el in self._xpath('description', parent):
-            text = self._xpath2obj('text', el)
-            type = self._xpath2obj('type', el)
-            out.append(EventDescription(text=text, type=type))
+            desc = EventDescription()
+            desc.text = self._xpath2obj('text', el)
+            self._set_enum('type', el, desc, 'type')
+            out.append(desc)
             self._extra(el, out[-1])
         return out
 
@@ -368,8 +381,8 @@ def _origin_uncertainty(self, parent):
             return None
         element = elements[0]
         obj = OriginUncertainty()
-        obj.preferred_description = self._xpath2obj(
-            'preferredDescription', element)
+        self._set_enum('preferredDescription', element,
+                       obj, 'preferred_description')
         obj.horizontal_uncertainty = self._xpath2obj(
             'horizontalUncertainty', element, float)
         obj.min_horizontal_uncertainty = self._xpath2obj(
@@ -461,11 +474,11 @@ def _pick(self, element):
         obj.backazimuth, obj.backazimuth_errors = \
             self._float_value(element, 'backazimuth')
         obj.slowness_method_id = self._xpath2obj('slownessMethodID', element)
-        obj.onset = self._xpath2obj('onset', element)
+        self._set_enum('onset', element, obj, 'onset')
         obj.phase_hint = self._xpath2obj('phaseHint', element)
-        obj.polarity = self._xpath2obj('polarity', element)
-        obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
-        obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
+        self._set_enum('polarity', element, obj, 'polarity')
+        self._set_enum('evaluationMode', element, obj, 'evaluation_mode')
+        self._set_enum('evaluationStatus', element, obj, 'evaluation_status')
         obj.comments = self._comments(element)
         obj.creation_info = self._creation_info(element)
         obj.resource_id = element.get('publicID')
@@ -501,8 +514,8 @@ def _amplitude(self, element):
             self._float_value(element, 'genericAmplitude')
         # optional parameter
         obj.type = self._xpath2obj('type', element)
-        obj.category = self._xpath2obj('category', element)
-        obj.unit = self._xpath2obj('unit', element)
+        self._set_enum('category', element, obj, 'category')
+        self._set_enum('unit', element, obj, 'unit')
         obj.method_id = self._xpath2obj('methodID', element)
         obj.period, obj.period_errors = self._float_value(element, 'period')
         obj.snr = self._xpath2obj('snr', element)
@@ -515,8 +528,8 @@ def _amplitude(self, element):
         obj.scaling_time, obj.scaling_time_errors = \
             self._time_value(element, 'scalingTime')
         obj.magnitude_hint = self._xpath2obj('magnitudeHint', element)
-        obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
-        obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
+        self._set_enum('evaluationMode', element, obj, 'evaluation_mode')
+        self._set_enum('evaluationStatus', element, obj, 'evaluation_status')
         obj.comments = self._comments(element)
         obj.creation_info = self._creation_info(element)
         obj.resource_id = element.get('publicID')
@@ -552,7 +565,7 @@ def _origin(self, element, arrivals):
             self._float_value(element, 'longitude')
         # optional parameter
         obj.depth, obj.depth_errors = self._float_value(element, 'depth')
-        obj.depth_type = self._xpath2obj('depthType', element)
+        self._set_enum('depthType', element, obj, 'depth_type')
         obj.time_fixed = self._xpath2obj('timeFixed', element, bool)
         obj.epicenter_fixed = self._xpath2obj('epicenterFixed', element, bool)
         obj.reference_system_id = self._xpath2obj('referenceSystemID', element)
@@ -560,10 +573,10 @@ def _origin(self, element, arrivals):
         obj.earth_model_id = self._xpath2obj('earthModelID', element)
         obj.composite_times = self._composite_times(element)
         obj.quality = self._origin_quality(element)
-        obj.origin_type = self._xpath2obj('type', element)
+        self._set_enum('type', element, obj, 'origin_type')
         obj.region = self._xpath2obj('region', element)
-        obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
-        obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
+        self._set_enum('evaluationMode', element, obj, 'evaluation_mode')
+        self._set_enum('evaluationStatus', element, obj, 'evaluation_status')
         obj.creation_info = self._creation_info(element)
         obj.comments = self._comments(element)
         obj.origin_uncertainty = self._origin_uncertainty(element)
@@ -601,8 +614,8 @@ def _magnitude(self, element):
         obj.method_id = self._xpath2obj('methodID', element)
         obj.station_count = self._xpath2obj('stationCount', element, int)
         obj.azimuthal_gap = self._xpath2obj('azimuthalGap', element, float)
-        obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
-        obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
+        self._set_enum('evaluationMode', element, obj, 'evaluation_mode')
+        self._set_enum('evaluationStatus', element, obj, 'evaluation_status')
         obj.creation_info = self._creation_info(element)
         obj.station_magnitude_contributions = \
             self._station_magnitude_contributions(element)
@@ -744,7 +757,7 @@ def _source_time_function(self, parent):
             return None
         obj = SourceTimeFunction()
         # required parameters
-        obj.type = self._xpath2obj('type', sub_el)
+        self._set_enum('type', sub_el, obj, 'type')
         obj.duration = self._xpath2obj('duration', sub_el, float)
         # optional parameter
         obj.rise_time = self._xpath2obj('riseTime', sub_el, float)
@@ -785,7 +798,7 @@ def _data_used(self, parent):
         for el in self._xpath('dataUsed', parent):
             data_used = DataUsed()
             # required parameters
-            data_used.wave_type = self._xpath2obj('waveType', el)
+            self._set_enum('waveType', el, data_used, 'wave_type')
             # optional parameter
             data_used.station_count = \
                 self._xpath2obj('stationCount', el, int)
@@ -830,8 +843,8 @@ def _moment_tensor(self, parent):
         obj.source_time_function = self._source_time_function(mt_el)
         obj.data_used = self._data_used(mt_el)
         obj.method_id = self._xpath2obj('methodID', mt_el)
-        obj.category = self._xpath2obj('category', mt_el)
-        obj.inversion_type = self._xpath2obj('inversionType', mt_el)
+        self._set_enum('category', mt_el, obj, 'category')
+        self._set_enum('inversionType', mt_el, obj, 'inversion_type')
         obj.creation_info = self._creation_info(mt_el)
         obj.comments = self._comments(mt_el)
         obj.resource_id = mt_el.get('publicID')
@@ -874,8 +887,8 @@ def _focal_mechanism(self, element):
         obj.moment_tensor = self._moment_tensor(element)
         obj.nodal_planes = self._nodal_planes(element)
         obj.principal_axes = self._principal_axes(element)
-        obj.evaluation_mode = self._xpath2obj('evaluationMode', element)
-        obj.evaluation_status = self._xpath2obj('evaluationStatus', element)
+        self._set_enum('evaluationMode', element, obj, 'evaluation_mode')
+        self._set_enum('evaluationStatus', element, obj, 'evaluation_status')
         obj.creation_info = self._creation_info(element)
         obj.comments = self._comments(element)
         obj.resource_id = element.get('publicID')
@@ -927,8 +940,8 @@ def _deserialize(self):
                 msg += "with QuakeML standard -- event will be ignored."
                 warnings.warn(msg, UserWarning)
                 continue
-            event.event_type_certainty = self._xpath2obj(
-                'typeCertainty', event_el)
+            self._set_enum('typeCertainty', event_el,
+                           event, 'event_type_certainty')
             event.creation_info = self._creation_info(event_el)
             event.event_descriptions = self._event_description(event_el)
             event.comments = self._comments(event_el)


Test patch:
diff --git a/obspy/io/quakeml/tests/data/invalid_enum.xml b/obspy/io/quakeml/tests/data/invalid_enum.xml
new file mode 100644
--- /dev/null
+++ b/obspy/io/quakeml/tests/data/invalid_enum.xml
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<q:quakeml xmlns="http://quakeml.org/xmlns/bed/1.2"
+  xmlns:q="http://quakeml.org/xmlns/quakeml/1.2">
+  <eventParameters publicID="smi:eu.emsc/unid">
+    <event publicID="smi:ch.ethz.sed/event/historical/1165">
+      <origin publicID="smi:www.iris.edu/ws/event/query?originId=7680412">
+        <time>
+          <value>2011-03-11T05:46:24.120000Z</value>
+        </time>
+        <latitude>
+          <value>38.297</value>
+        </latitude>
+        <longitude>
+          <value>142.373</value>
+        </longitude>
+        <depth>
+          <value>29.0</value>
+          <confidenceLevel>50.0</confidenceLevel>
+        </depth>
+        <depthType>randomized</depthType>
+      </origin>
+    </event>
+  </eventParameters>
+</q:quakeml>
diff --git a/obspy/io/quakeml/tests/test_quakeml.py b/obspy/io/quakeml/tests/test_quakeml.py
--- a/obspy/io/quakeml/tests/test_quakeml.py
+++ b/obspy/io/quakeml/tests/test_quakeml.py
@@ -1088,6 +1088,25 @@ def test_writing_invalid_quakeml_id(self):
             cat2[0].resource_id.id,
             "smi:org.gfz-potsdam.de/geofon/RMHP(60)>>ITAPER(3)>>BW(4,5,15)")
 
+    def test_reading_invalid_enums(self):
+        """
+        Raise a warning when an invalid enum value is attempted to be read.
+        """
+        filename = os.path.join(self.path, "invalid_enum.xml")
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            cat = read_events(filename)
+        self.assertEqual(len(w), 1)
+        self.assertEqual(
+            w[0].message.args[0],
+            'Setting attribute "depth_type" failed. Value "randomized" could '
+            'not be converted to type "Enum(["from location", "from moment '
+            'tensor inversion", ..., "operator assigned", "other"])". The '
+            'attribute "depth_type" will not be set and will be missing in '
+            'the resulting object.')
+        # It should of course not be set.
+        self.assertIsNone(cat[0].origins[0].depth_type)
+
 
 def suite():
     return unittest.makeSuite(QuakeMLTestCase, 'test')