Repository: obspy/obspy

Issue:
get_datalogger_resp() and get_sensor_resp()
I appreciate the new NRL class in the Obspy master.. great job!

It would be valuable to be able to pull separate sensor and datalogger response stages into a Obspy Response object, as get_response() does.

In some of my use cases, my sensor is home-made not part of the NRL library while the datalogger is. After this, I could append the stages using response.response_stages.append()


Discussion:
@dsentinel, @nick-iris Would it be fine for you to change `get_datalogger_resp()` and `get_sensor_resp()` to return and ObsPy `Response` object? Both are only used once in the code base and they are converted to `Response` objects immediately.

Resolving patch:
diff --git a/obspy/clients/nrl/client.py b/obspy/clients/nrl/client.py
--- a/obspy/clients/nrl/client.py
+++ b/obspy/clients/nrl/client.py
@@ -138,29 +138,37 @@ def _parse_ini(self, path):
     def _clean_str(self, string):
         return string.strip('\'"')
 
-    def get_datalogger_resp(self, datalogger_keys):
+    def get_datalogger_response(self, datalogger_keys):
         """
-        Get the RESP string of a datalogger by keys.
+        Get the datalogger response.
 
         :type datalogger_keys: list of str
-        :rtype: str
+        :rtype: :class:`~obspy.core.inventory.response.Response`
         """
         datalogger = self.dataloggers
         for key in datalogger_keys:
             datalogger = datalogger[key]
-        return self._read_resp(datalogger[1])
 
-    def get_sensor_resp(self, sensor_keys):
+        # Parse to an inventory object and return a response object.
+        with io.BytesIO(self._read_resp(datalogger[1]).encode()) as buf:
+            buf.seek(0, 0)
+            return obspy.read_inventory(buf, format="RESP")[0][0][0].response
+
+    def get_sensor_response(self, sensor_keys):
         """
-        Get the RESP string of a sensor by keys.
+        Get the sensor response.
 
         :type sensor_keys: list of str
-        :rtype: str
+        :rtype: :class:`~obspy.core.inventory.response.Response`
         """
         sensor = self.sensors
         for key in sensor_keys:
             sensor = sensor[key]
-        return self._read_resp(sensor[1])
+
+        # Parse to an inventory object and return a response object.
+        with io.BytesIO(self._read_resp(sensor[1]).encode()) as buf:
+            buf.seek(0, 0)
+            return obspy.read_inventory(buf, format="RESP")[0][0][0].response
 
     def get_response(self, datalogger_keys, sensor_keys):
         """
@@ -192,20 +200,8 @@ def get_response(self, datalogger_keys, sensor_keys):
             Stage 9: Coefficients... from COUNTS to COUNTS, gain: 1
             Stage 10: Coefficients... from COUNTS to COUNTS, gain: 1
         """
-        # Parse both to inventory objects.
-        with io.BytesIO(
-                self.get_datalogger_resp(datalogger_keys).encode()) as buf:
-            buf.seek(0, 0)
-            dl_resp = obspy.read_inventory(buf, format="RESP")
-        with io.BytesIO(
-                self.get_sensor_resp(sensor_keys).encode()) as buf:
-            buf.seek(0, 0)
-            sensor_resp = obspy.read_inventory(buf, format="RESP")
-
-        # Both can by construction only contain a single channel with a
-        # response object.
-        dl_resp = dl_resp[0][0][0].response
-        sensor_resp = sensor_resp[0][0][0].response
+        dl_resp = self.get_datalogger_response(datalogger_keys)
+        sensor_resp = self.get_sensor_response(sensor_keys)
 
         # Combine both by replace stage one in the data logger with stage
         # one of the sensor.
@@ -266,7 +262,7 @@ def _get_cp_from_ini(self, path):
         """
         Returns a configparser from a path to an index.txt
         """
-        cp = configparser.SafeConfigParser()
+        cp = configparser.ConfigParser()
         with codecs.open(path, mode='r', encoding='UTF-8') as f:
             if sys.version_info.major == 2:  # pragma: no cover
                 cp.readfp(f)


Test patch:
diff --git a/obspy/clients/nrl/tests/test_nrl.py b/obspy/clients/nrl/tests/test_nrl.py
--- a/obspy/clients/nrl/tests/test_nrl.py
+++ b/obspy/clients/nrl/tests/test_nrl.py
@@ -6,8 +6,10 @@
 import os
 import unittest
 
+import numpy as np
 
-from obspy.core.inventory import Response
+from obspy.core.inventory import (Response, PolesZerosResponseStage,
+                                  ResponseStage, CoefficientsTypeResponseStage)
 from obspy.clients.nrl.client import NRL, LocalNRL, RemoteNRL
 
 
@@ -38,10 +40,45 @@ def test_nrl_types(self):
         self.assertIsInstance(self.nrl_online, RemoteNRL)
 
     def test_get_response(self):
+        # Get only the sensor response.
+        sensor_resp = self.nrl_local.get_sensor_response(self.local_sensor_key)
+
+        # Get only the datalogger response.
+        dl_resp = self.nrl_local.get_datalogger_response(self.local_dl_key)
+
+        # Get full response.
         resp = self.nrl_local.get_response(
             datalogger_keys=self.local_dl_key,
             sensor_keys=self.local_sensor_key)
+
+        # All of them should be Response objects.
         self.assertIsInstance(resp, Response)
+        self.assertIsInstance(dl_resp, Response)
+        self.assertIsInstance(sensor_resp, Response)
+
+        # The full response is the first stage from the sensor and all
+        # following from the datalogger.
+        self.assertEqual(resp.response_stages[0],
+                         sensor_resp.response_stages[0])
+        self.assertEqual(resp.response_stages[1:],
+                         dl_resp.response_stages[1:])
+
+        # Test the actual responses. Testing the parsing of the exact values
+        # and what not is done in obspy.io.xseed.
+        paz = sensor_resp.response_stages[0]
+        self.assertIsInstance(paz, PolesZerosResponseStage)
+        np.testing.assert_allclose(
+            paz.poles, [(-0.037008 + 0.037008j), (-0.037008 - 0.037008j),
+                        (-502.65 + 0j), (-1005 + 0j), (-1131 + 0j)])
+        np.testing.assert_allclose(paz.zeros, [0j, 0j])
+
+        self.assertEqual(len(dl_resp.response_stages), 15)
+        self.assertEqual(len(resp.response_stages), 15)
+
+        self.assertIsInstance(resp.response_stages[1], ResponseStage)
+        for _i in range(2, 15):
+            self.assertIsInstance(resp.response_stages[_i],
+                                  CoefficientsTypeResponseStage)
 
     def test_nrl_class_str_method(self):
         out = str(self.nrl_local)