diff --git a/ale/__init__.py b/ale/__init__.py
index f2d46ca2575f049a9d174327036200f3c0d83b73..b649ee82414e20bfe6ebad469da86f654dae8f38 100644
--- a/ale/__init__.py
+++ b/ale/__init__.py
@@ -1,2 +1,2 @@
 from . import drivers
-from .drivers import load
+from .drivers import load, loads
diff --git a/ale/drivers/__init__.py b/ale/drivers/__init__.py
index d5f94b12912d999f4424cf0bce68f301f138cad9..9b816352ce5ea5f71720c5159906d73329501834 100644
--- a/ale/drivers/__init__.py
+++ b/ale/drivers/__init__.py
@@ -13,12 +13,15 @@ from datetime import datetime, date
 
 from abc import ABC
 
+import datetime
+
 # dynamically load drivers
 __all__ = [os.path.splitext(os.path.basename(d))[0] for d in glob(os.path.join(os.path.dirname(__file__), '*_driver.py'))]
 __driver_modules__ = [importlib.import_module('.'+m, package='ale.drivers') for m in __all__]
 
 drivers = dict(chain.from_iterable(inspect.getmembers(dmod, lambda x: inspect.isclass(x) and "_driver" in x.__module__) for dmod in __driver_modules__))
 
+
 def load(label):
     """
     Attempt to load a given label from all possible drivers
@@ -29,14 +32,29 @@ def load(label):
                String path to the given label file
     """
     for name, driver in drivers.items():
-        try:
-            print("TRYING:", driver)
+            print("Trying:", name)
             res = driver(label)
-            if res.is_valid():
-                with res as r:
-                    return res
-
-        except Exception as e:
-            import traceback
-            traceback.print_exc()
+            try:
+                if res.is_valid():
+                    with res as r:
+                            return res.to_dict()
+            except Exception as e:
+                import traceback
+                print("Driver Failed:", e)
+                traceback.print_exc()
     raise Exception('No Such Driver for Label')
+
+
+def loads(label):
+    class JsonEncoder(json.JSONEncoder):
+        def default(self, obj):
+            if isinstance(obj, np.ndarray):
+                return obj.tolist()
+            if isinstance(obj, np.int64):
+                return int(obj)
+            if isinstance(obj, datetime.datetime):
+                return obj.__str__()
+            return json.JSONEncoder.default(self, obj)
+
+    res = load(label)
+    return json.dumps(res, cls=JsonEncoder)
diff --git a/ale/drivers/base.py b/ale/drivers/base.py
index 743e782c6531bf4da23af7d39be7925fa7f6c568..e7fbca6fd2b5fd151b3ef2d5b4386fc234729a57 100644
--- a/ale/drivers/base.py
+++ b/ale/drivers/base.py
@@ -1,7 +1,211 @@
-from dateutil import parser
+import os
+import re
+import struct
+from glob import glob
+
 import numpy as np
+from dateutil import parser
+
 import pvl
 import spiceypy as spice
+from ale import config
+
+
+def read_table_data(table_label, cube):
+    """
+    Helper function to read all of the binary table data
+
+    Parameters
+    ----------
+    table_label : PVLModule
+                  The ISIS table label
+    cube : file
+           The ISIS cube file
+
+    Returns
+    -------
+    bytes :
+        The binary portion of the table data
+    """
+    cubehandle = open(cube, "rb")
+    cubehandle.seek(table_label['StartByte'])
+    return cubehandle.read(table_label['Bytes'])
+
+def field_size(field_label):
+    """
+    Helper function to determine the size of a binary
+    table field
+
+    Parameters
+    ----------
+    field_label : PVLModule
+                  The field label
+
+    Returns
+    -------
+    int :
+        The size of the one entry in bytes
+    """
+    data_sizes = {
+        'Integer' : 4,
+        'Double'  : 8,
+        'Real'    : 4,
+        'Text'    : 1
+    }
+    return data_sizes[field_label['Type']] * field_label['Size']
+
+def field_format(field_label):
+    """
+    Helper function to get the format string for a
+    single entry in a table field
+
+    Parameters
+    ----------
+    field_label : PVLModule
+                  The field label
+
+    Returns
+    -------
+    str :
+        The format string for the entry binary
+    """
+    data_formats = {
+        'Integer' : 'i',
+        'Double'  : 'd',
+        'Real'    : 'f'
+    }
+    return data_formats[field_label['Type']] * field_label['Size']
+
+def parse_field(field_label, data, encoding='latin_1'):
+    """
+    Parses a binary table field entry and converts it into
+    an in memory data type
+
+    Parameters
+    ----------
+    field_label : PVLModule
+                  The field label
+
+    data : bytes
+           The binary data for the field entry
+
+    Returns
+    -------
+    Union[int, float, str, list] :
+        The table field entry converted to a native python
+        type
+    """
+    if field_label['Type'] == 'Text':
+        field_data = data[:field_label['Size']].decode(encoding=encoding)
+    else:
+        data_format = field_format(field_label)
+        field_data = struct.unpack_from(data_format, data)
+        if len(field_data) == 1:
+            field_data = field_data[0]
+    return field_data
+
+def parse_table_data(table_label, data):
+    """
+    Parses an ISIS table into a dict where the keys are the
+    field names and the values are lists of entries.
+
+    Parameters
+    ----------
+    table_label : PVLModule
+                  The table label
+
+    data : bytes
+           The binary data for the entire table
+
+    Returns
+    -------
+    dict :
+        The table as a dict
+    """
+    fields = table_label.getlist('Field')
+    results = {field['Name']:[] for field in fields}
+    offset = 0
+    for record in range(table_label['Records']):
+        for field in fields:
+            field_data = parse_field(field, data[offset:])
+            results[field['Name']].append(field_data)
+            offset += field_size(field)
+    return results
+
+def parse_rotation_table(label, field_data):
+    """
+    Parses ISIS rotation table data.
+
+    Parameters
+    ----------
+    table_label : PVLModule
+                  The table label
+
+    field_data : dict
+                 The table data as a dict with field names
+                 as keys and lists of entries as values
+
+    Returns
+    -------
+    dict :
+        The rotation data
+    """
+    results = {}
+    if all (key in field_data for key in ('J2000Q0','J2000Q1','J2000Q2','J2000Q3')):
+        results['Rotations'] = [ [q0, q1, q2, q3] for q0, q1, q2, q3 in zip(field_data['J2000Q0'],field_data['J2000Q1'],field_data['J2000Q2'],field_data['J2000Q3']) ]
+    if all (key in field_data for key in ('AV1','AV2','AV3')):
+        results['AngularVelocities'] = np.array( [ [av1, av2, av3] for av1, av2, av3 in zip(field_data['AV1'],field_data['AV2'],field_data['AV3']) ] )
+    if 'ET' in field_data:
+        results['Times'] = np.array(field_data['ET'])
+    if all (key in field_data for key in ('J2000Ang1','J2000Ang2','J2000Ang3')):
+        results['EulerCoefficients'] = np.array([field_data['J2000Ang1'],field_data['J2000Ang2'],field_data['J2000Ang3']])
+        results['BaseTime'] = field_data['J2000Ang1'][-1]
+        results['TimeScale'] = field_data['J2000Ang2'][-1]
+
+    if 'TimeDependentFrames' in label:
+        results['TimeDependentFrames'] = np.array(label['TimeDependentFrames'])
+    if all (key in label for key in ('ConstantRotation','ConstantFrames')):
+        const_rotation_mat = np.array(label['ConstantRotation'])
+        results['ConstantRotation'] = np.reshape(const_rotation_mat, (3, 3))
+        results['ConstantFrames'] = np.array(label['ConstantFrames'])
+    if all (key in label for key in ('PoleRa','PoleDec','PrimeMeridian')):
+        results['BodyRotationCoefficients'] = np.array( [label['PoleRa'],label['PoleDec'],label['PrimeMeridian']] )
+    if all (key in label for key in ('PoleRaNutPrec','PoleDecNutPrec','PmNutPrec','SysNutPrec0','SysNutPrec1')):
+        results['SatelliteNutationPrecessionCoefficients'] = np.array( [label['PoleRaNutPrec'],label['PoleDecNutPrec'],label['PmNutPrec']] )
+        results['PlanetNutationPrecessionAngleCoefficients'] = np.array( [label['SysNutPrec0'],label['SysNutPrec1']] )
+    return results
+
+def parse_position_table(field_data):
+    """
+    Parses ISIS position table data.
+
+    Parameters
+    ----------
+    table_label : PVLModule
+        The table label
+
+    field_data : dict
+        The table data as a dict with field names as keys
+        and lists of entries as values
+
+    Returns
+    -------
+    dict :
+      The position data
+    """
+    results = {}
+    if all (key in field_data for key in ('J2000X','J2000Y','J2000Z')):
+        results['Positions'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000X'],field_data['J2000Y'],field_data['J2000Z']) ] )
+    if 'ET' in field_data:
+        results['Times'] = np.array(field_data['ET'])
+    if all (key in field_data for key in ('J2000XV','J2000YV','J2000ZV')):
+        results['Velocities'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000XV'],field_data['J2000YV'],field_data['J2000ZV']) ] )
+    if all (key in field_data for key in ('J2000SVX','J2000SVY','J2000SVZ')):
+        results['PositionCoefficients'] = np.array( [field_data['J2000SVX'][:-1],field_data['J2000SVY'][:-1],field_data['J2000SVZ'][:-1]] )
+        results['BaseTime'] = field_data['J2000SVX'][-1]
+        results['TimeScale'] = field_data['J2000SVY'][-1]
+    return results
+
 
 class Driver():
     """
@@ -12,13 +216,15 @@ class Driver():
     _file : str
             Reference to file path to be used by mixins for opening.
     """
-    def __init__(self, file):
+    def __init__(self, file, num_ephem=909, num_quats=909):
         """
         Parameters
         ----------
         file : str
                path to file to be parsed
         """
+        self._num_quaternions = num_quats
+        self._num_ephem = num_ephem
         self._file = file
 
     def __str__(self):
@@ -45,6 +251,7 @@ class Driver():
             iid = self.instrument_id
             return True
         except Exception as e:
+            print(e)
             return False
 
     def to_dict(self):
@@ -57,10 +264,111 @@ class Driver():
         dict
             Dictionary of key, attribute pairs
         """
-        keys = set(dir(self)) & self.required_keys
-        return {p:getattr(self, p) for p in keys}
+        keys = set()
+        return {p:getattr(self, p) for p in dir(self) if p[0] != "_" and isinstance(getattr(type(self), p), property)}
+
+
+    @property
+    def label(self):
+        if not hasattr(self, "_label"):
+            if isinstance(self._file, pvl.PVLModule):
+                self._label = self._file
+            try:
+                self._label = pvl.loads(self._file)
+            except Exception:
+                self._label = pvl.load(self._file)
+            except:
+                raise ValueError("{} is not a valid label".format(self._file))
+        return self._label
+
+    @property
+    def file(self):
+        return self._file
+
+    @property
+    def interpolation_method(self):
+        return "lagrange"
+
+    @property
+    def starting_detector_line(self):
+        return 1
+
+    @property
+    def starting_detector_sample(self):
+        return 1
+
+    @property
+    def detector_sample_summing(self):
+        return 1
+
+    @property
+    def detector_line_summing(self):
+        return 1
+
+    @property
+    def name_platform(self):
+        return "Generic Platform"
+
+    @property
+    def name_sensor(self):
+        return "Generic Sensor"
+
+    @property
+    def radii(self):
+        return {
+            "semimajor" : self._semimajor,
+            "semiminor" : self._semiminor,
+            "unit" : "m" # default to meters
+        }
+
+    @property
+    def reference_height(self):
+        # TODO: This should be a reasonable #
+        return {
+            "minheight" : 0,
+            "maxheight": 1000,
+            "unit": "m"
+        }
+
+    @property
+    def focal_length_model(self):
+        return {
+            "focal_length" : self._focal_length
+        }
+
+    @property
+    def detector_center(self):
+        if not hasattr(self, '_detector_center'):
+            self._detector_center = {
+                "line" : self._detector_center_line,
+                "sample" : self._detector_center_sample
+            }
+        return self._detector_center
+
+    @property
+    def sensor_position(self):
+        return {
+            "positions" : self._sensor_position,
+            "velocities" : self._sensor_velocity,
+            "unit" : "m"
+        }
+
+    @property
+    def sensor_orientation(self):
+        return {
+            "quaternions" : self._sensor_orientation
+        }
+
+    @property
+    def sun_position(self):
+        return {
+            "positions" : self._sun_position,
+            "velocities" : self._sun_velocity,
+            "unit" : "m"
+        }
+
 
-class LineScanner(Driver):
+class LineScanner():
     @property
     def name_model(self):
         """
@@ -88,7 +396,7 @@ class LineScanner(Driver):
 
     @property
     def dt_quaternion(self):
-        return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_ephemerides
+        return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_quaternions
 
     @property
     def line_scan_rate(self):
@@ -102,16 +410,27 @@ class LineScanner(Driver):
 
     @property
     def number_of_ephemerides(self):
-        #TODO: Not make this hardcoded
-        return 909
+        return self._num_ephem
 
     @property
     def number_of_quaternions(self):
         #TODO: Not make this hardcoded
-        return 909
+        return self._num_quaternions
+
+    @property
+    def ending_ephemeris_time(self):
+        return (self.image_lines * self.line_exposure_duration) + self.starting_ephemeris_time
+
+    @property
+    def center_ephemeris_time(self):
+        return (self.starting_ephemeris_time + self.ending_ephemeris_time)/2
+
 
+class Framer():
+    @property
+    def name_sensor(self):
+        return "Generic Framer"
 
-class Framer(Driver):
     @property
     def name_model(self):
         """
@@ -150,54 +469,10 @@ class PDS3():
              Dict-like object with PVL keys
 
     """
-    def _compute_ephemerides(self):
-        """
-        Helper function to pull position and velocity in one pass
-        so that the results can then be cached in the associated
-        properties.
-        """
-        eph = np.empty((self.number_of_ephemerides, 3))
-        eph_rates = np.empty(eph.shape)
-        current_et = self.starting_ephemeris_time
-        for i in range(self.number_of_ephemerides):
-            state, _ = spice.spkezr(self.spacecraft_name,
-                                    current_et,
-                                    self.reference_frame,
-                                    'NONE',
-                                    self.target_name,) # If this is the sensor, insufficient, if this is the spacecraft, it works? Huh?
-            eph[i] = state[:3]
-            eph_rates[i] = state[3:]
-            current_et += getattr(self, 'dt_ephemeris', 0)
-        # By default, spice works in km
-        eph *= 1000
-        eph_rates *= 1000
-        self._sensor_velocity = eph_rates
-        self._sensor_position = eph
-
-    @property
-    def focal_plane_tempature(self):
-        return self.label['FOCAL_PLANE_TEMPERATURE'].value
 
     @property
-    def label(self):
-        """
-        Loads a PVL from from the _file attribute.
-
-        Returns
-        -------
-        : PVLModule
-          Dict-like object with PVL keys
-        """
-        if not hasattr(self, "_label"):
-            if isinstance(self._file, pvl.PVLModule):
-                self._label = label
-            try:
-                self._label = pvl.loads(self._file, strict=False)
-            except AttributeError:
-                self._label = pvl.load(self._file, strict=False)
-            except:
-                raise Exception("{} is not a valid label".format(label))
-        return self._label
+    def _focal_plane_tempature(self):
+        return self.label['FOCAL_PLANE_TEMPERATURE'].value
 
     @property
     def line_exposure_duration(self):
@@ -271,17 +546,12 @@ class PDS3():
         return sc
 
     @property
-    def ending_ephemeris_time(self):
-        return (self.image_lines * self.line_exposure_duration) + self.starting_ephemeris_time
-
-    @property
-    def center_ephemeris_time(self):
-        return (self.starting_ephemeris_time + self.ending_ephemeris_time)/2
-
+    def _detector_center_line(self):
+        return spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2)[0]
 
     @property
-    def detector_center(self):
-        return list(spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2))
+    def _detector_center_sample(self):
+        return spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2)[1]
 
     @property
     def spacecraft_name(self):
@@ -296,18 +566,6 @@ class PDS3():
         """
         return self.label['MISSION_NAME']
 
-    @property
-    def starting_detector_line(self):
-        return 1
-
-    @property
-    def starting_detector_sample(self):
-        return 1
-
-    @property
-    def detector_sample_summing(self):
-        return 1
-
     @property
     def detector_line_summing(self):
         return self.label.get('SAMPLING_FACTOR', 1)
@@ -337,7 +595,7 @@ class Spice():
         spice.unload(self.metakernel)
 
     @property
-    def odtx(self):
+    def _odtx(self):
         """
         Returns
         -------
@@ -347,7 +605,7 @@ class Spice():
         return spice.gdpool('INS{}_OD_T_X'.format(self.ikid),0, 10).tolist()
 
     @property
-    def odty(self):
+    def _odty(self):
         """
         Returns
         -------
@@ -357,7 +615,7 @@ class Spice():
         return spice.gdpool('INS{}_OD_T_Y'.format(self.ikid), 0, 10).tolist()
 
     @property
-    def odtk(self):
+    def _odtk(self):
         """
         Returns
         -------
@@ -389,11 +647,11 @@ class Spice():
         return list(spice.gdpool('INS{}_ITRANSS'.format(self.fikid), 0, 3))
 
     @property
-    def focal_length(self):
+    def _focal_length(self):
         return float(spice.gdpool('INS{}_FOCAL_LENGTH'.format(self.ikid), 0, 1)[0])
 
     @property
-    def semimajor(self):
+    def _semimajor(self):
         """
         Returns
         -------
@@ -404,7 +662,7 @@ class Spice():
         return rad[1][1]
 
     @property
-    def semiminor(self):
+    def _semiminor(self):
         """
         Returns
         -------
@@ -419,7 +677,7 @@ class Spice():
         return 'IAU_{}'.format(self.target_name)
 
     @property
-    def sun_position(self):
+    def _sun_position(self):
         sun_state, _ = spice.spkezr("SUN",
                                      self.center_ephemeris_time,
                                      self.reference_frame,
@@ -429,7 +687,7 @@ class Spice():
         return [sun_state[:4].tolist()]
 
     @property
-    def sun_velocity(self):
+    def _sun_velocity(self):
         sun_state, lt = spice.spkezr("SUN",
                                      self.center_ephemeris_time,
                                      self.reference_frame,
@@ -439,21 +697,42 @@ class Spice():
         return [sun_state[3:6].tolist()]
 
     @property
-    def sensor_position(self):
-
-        if not hasattr(self, '_sensor_position'):
-            self._compute_ephemerides()
-        return self._sensor_position.tolist()
+    def _sensor_position(self):
+        if not hasattr(self, '_position'):
+            eph = []
+            current_et = self.starting_ephemeris_time
+            for i in range(self.number_of_ephemerides):
+                state, _ = spice.spkezr(self.spacecraft_name,
+                                        current_et,
+                                        self.reference_frame,
+                                        'NONE',
+                                        self.target_name,)
+                eph.append(state[:3])
+                current_et += getattr(self, "dt_ephemeris", 0)
+            # By default, spice works in km
+            self._position = [e * 1000 for e in eph]
+        return self._position
 
     @property
-    def sensor_velocity(self):
-        if not hasattr(self, '_sensor_velocity'):
-            self._compute_ephemerides()
-        return self._sensor_velocity.tolist()
+    def _sensor_velocity(self):
+        if not hasattr(self, '_velocity'):
+            eph_rates = []
+            current_et = self.starting_ephemeris_time
+            for i in range(self.number_of_ephemerides):
+                state, _ = spice.spkezr(self.spacecraft_name,
+                                        current_et,
+                                        self.reference_frame,
+                                        'NONE',
+                                        self.target_name,)
+                eph_rates.append(state[3:])
+                current_et += getattr(self, "dt_ephemeris", 0)
+            # By default, spice works in km
+            self._velocity = [e*1000 for e  in eph_rates]
+        return self._velocity
 
     @property
-    def sensor_orientation(self):
-        if not hasattr(self, '_sensor_orientation'):
+    def _sensor_orientation(self):
+        if not hasattr(self, '_orientation'):
             current_et = self.starting_ephemeris_time
             qua = np.empty((self.number_of_ephemerides, 4))
             for i in range(self.number_of_quaternions):
@@ -465,21 +744,18 @@ class Spice():
                 qua[i,:3] = q[1:]
                 qua[i,3] = q[0]
                 current_et += getattr(self, 'dt_quaternion', 0)
-            self._sensor_orientation = qua
-        return self._sensor_orientation.tolist()
+            self._orientation = qua
+        print(len(self._orientation))
+        return self._orientation.tolist()
 
     @property
-    def reference_height(self):
-        # TODO: This should be a reasonable #
-        return 0, 100
+    def _detector_center_sample(self):
+        return float(spice.gdpool('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), 0, 1)[0])
+
 
     @property
-    def detector_center(self):
-        if not hasattr(self, '_detector_center'):
-            center_line = float(spice.gdpool('INS{}_BORESIGHT_LINE'.format(self.ikid), 0, 1)[0])
-            center_sample = float(spice.gdpool('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), 0, 1)[0])
-            self._detector_center = [center_line, center_sample]
-        return self._detector_center
+    def _detector_center_line(self):
+        return float(spice.gdpool('INS{}_BORESIGHT_LINE'.format(self.ikid), 0, 1)[0])
 
     @property
     def center_ephemeris_time(self):
@@ -505,18 +781,6 @@ class Spice():
 
 
 class Isis3():
-    @property
-    def label(self):
-        if not hasattr(self, "_label"):
-            if isinstance(self._file, pvl.PVLModule):
-                self._label = self._file
-            try:
-                self._label = pvl.loads(self._file, strict=False)
-            except AttributeError:
-                self._label = pvl.load(self._file, strict=False)
-            except:
-                raise ValueError("{} is not a valid label".format(self._file))
-        return self._label
 
     @property
     def start_time(self):
@@ -576,5 +840,447 @@ class Isis3():
     def starting_ephemeris_time(self):
         if not hasattr(self, '_starting_ephemeris_time'):
             sclock = self.label['IsisCube']['Archive']['SpacecraftClockStartCount']
-            self._starting_ephemeris_time = spice.scs2e(self.spacecraft_id, sclock)
+            self._starting_ephemeris_time = spice.scs2e(self.spacecraft_id, sclock).value
         return self._starting_ephemeris_time
+
+
+class IsisSpice(Isis3):
+    """Mixin class for reading from an ISIS cube that has been spiceinit'd
+
+    Attributes
+    ----------
+    _label : PVLModule
+             Dict-like object with PVL keys
+
+    _inst_pointing_table : dict
+                           Dictionary that contains information about the
+                           rotation from J2000 to the sensor reference frame.
+                           All of the values for each property, such as angular
+                           velocity, are stored in a list or numpy array where
+                           each entry is the property at a different time.
+
+    _body_orientation_table : dict
+                              Dictionary that contains information about the
+                              rotation from J2000 to the body fixed reference
+                              frame. All of the  values for each property, such
+                              as angular velocity, are stored in a list or
+                              numpy array where each entry is the property at a
+                              different time.
+
+    _inst_position_table : dict
+                           Dictionary that contains information about the
+                           location of the sensor relative to the center of the
+                           target body. All of the  values for each property,
+                           such as velocity, are stored in a list or numpy
+                           array where each entry is the property at a
+                           different time.
+
+    _sun_position_table : dict
+                          Dictionary that contains information about the
+                          location of the sun relative to the center of the
+                          target body. All of the  values for each property,
+                          such as velocity, are stored in a list or numpy
+                          array where each entry is the property at a
+                          different time.
+
+    """
+
+    def _init_tables(self):
+        # init tables
+        for table in self.label.getlist('Table'):
+            binary_data = read_table_data(table, self._file)
+            field_data = parse_table_data(table, binary_data)
+            if table['Name'] == 'InstrumentPointing':
+                self._inst_pointing_table = parse_rotation_table(table, field_data)
+            elif table['Name'] == 'BodyRotation':
+                self._body_orientation_table = parse_rotation_table(table, field_data)
+            elif table['Name'] == 'InstrumentPosition':
+                self._inst_position_table = parse_position_table(field_data)
+            elif table['Name'] == 'SunPosition':
+                self._sun_position_table = parse_position_table(field_data)
+
+    @property
+    def label(self):
+        """
+        Loads a PVL from from the _file attribute and
+        parses the binary table data.
+
+        Returns
+        -------
+        PVLModule :
+            Dict-like object with PVL keys
+        """
+        if not hasattr(self, "_label"):
+            try:
+                self._label = pvl.load(self.file)
+            except:
+                raise ValueError("{} is not a valid label".format(self.file))
+        return self._label
+
+    def __enter__(self):
+        """
+        Stub method to conform with how other driver mixins
+        are used.
+        """
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        Stub method to conform with how other driver mixins
+        are used.
+        """
+        pass
+
+    @property
+    def number_of_quaternions(self):
+        """
+        The number of instrument rotation quaternions
+
+        Returns
+        -------
+        int :
+            The number of quaternions
+        """
+        return len(self._sensor_orientation)
+
+    @property
+    def number_of_ephemerides(self):
+        """
+        The number of instrument position states. These may
+        be just positions or positions and vbelocities.
+
+        Returns
+        -------
+        int :
+            The number of states
+        """
+        return len(self._sensor_position)
+
+    @property
+    def _sclock_hex_string(self):
+        """
+        The hex encoded image start time computed from the
+        spacecraft clock count
+
+        Returns
+        -------
+        str :
+            The hex string representation of the image
+            start time as a double
+        """
+        for key in self.naif_keywords:
+            if re.match('CLOCK_ET_.*_COMPUTED', key[0]):
+                # If the hex string is only numbers and contains leading 0s,
+                # the PVL library strips them off (ie. 0000000000002040 becomes
+                # 2040). Pad to 16 in case this happens.
+                return str(key[1]).zfill(16)
+        raise ValueError("No computed spacecraft clock time found in NaifKeywords.")
+
+    @property
+    def starting_ephemeris_time(self):
+        """
+        The image start time in ephemeris time
+
+        Returns
+        -------
+        float :
+            The image start ephemeris time
+        """
+        return struct.unpack('d', bytes.fromhex(self._sclock_hex_string))[0]
+
+    @property
+    def _detector_center_sample(self):
+        """
+        The center of the CCD in detector pixels
+
+        Returns
+        -------
+        list :
+            The center of the CCD formatted as line, sample
+        """
+        return self.naif_keywords.get('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), None)
+
+    @property
+    def _detector_center_line(self):
+        """
+        The center of the CCD in detector pixels
+
+        Returns
+        -------
+        list :
+            The center of the CCD formatted as line, sample
+        """
+        return self.naif_keywords.get('INS{}_BORESIGHT_LINE'.format(self.ikid), None)
+
+    @property
+    def _cube_label(self):
+        """
+        The ISIS cube label portion of the file label
+
+        Returns
+        -------
+        PVLModule :
+            The ISIS cube label
+
+        """
+        if 'IsisCube' not in self.label:
+            raise ValueError("Could not find ISIS cube label.")
+        return self.label['IsisCube']
+
+    @property
+    def _kernels_group(self):
+        """
+        The Kernels group from the ISIS cube label.
+        This is where the original SPICE kernels are listed.
+
+        Returns
+        -------
+        PVLModule :
+            The kernels group
+        """
+        if 'Kernels' not in self._cube_label:
+            raise ValueError("Could not find Kernels group in ISIS cube label.")
+        return self._cube_label['Kernels']
+
+    @property
+    def ikid(self):
+        """
+        The NAIF id for the instrument
+
+        Returns
+        -------
+        int :
+            The instrument id
+        """
+        if 'NaifIkCode' not in self._kernels_group:
+            raise ValueError("Could not find Instrument NAIF ID in Kernels group.")
+        return self._kernels_group['NaifIkCode']
+
+    @property
+    def focal2pixel_lines(self):
+        """
+        The line component of the affine transformation
+        from focal plane coordinates to centered ccd pixels
+
+        Returns
+        -------
+        list :
+            The coefficients of the affine transformation
+            formatted as constant, x, y
+        """
+        return self.naif_keywords.get('INS{}_ITRANSL'.format(self.ikid), None)
+
+    @property
+    def focal2pixel_samples(self):
+        """
+        The sample component of the affine transformation
+        from focal plane coordinates to centered ccd pixels
+
+        Returns
+        -------
+        list :
+            The coefficients of the affine transformation
+            formatted as constant, x, y
+        """
+        return self.naif_keywords.get('INS{}_ITRANSS'.format(self.ikid), None)
+
+    @property
+    def _focal_length(self):
+        """
+        The focal length of the instrument
+
+        Returns
+        -------
+        float :
+            The focal length in millimeters
+        """
+        return self.naif_keywords.get('INS{}_FOCAL_LENGTH'.format(self.ikid), None)
+
+    @property
+    def _body_radii(self):
+        """
+        The triaxial radii of the target body
+
+        Returns
+        -------
+        list :
+            The body radii in kilometers. For most bodies,
+            this is formatted as semimajor, semimajor,
+            semiminor
+        """
+        for key in self.naif_keywords:
+            if re.match('BODY-?\d*_RADII', key[0]):
+                return self.naif_keywords[key[0]]
+
+    @property
+    def _semimajor(self):
+        """
+        The radius of the target body at its widest
+        diameter
+
+        Returns
+        -------
+        float :
+            The radius in kilometers
+        """
+        return self._body_radii[0]
+
+    @property
+    def _semiminor(self):
+        """
+        The radius of the target body perpendicular to its
+        widest diameter
+
+        Returns
+        -------
+        float :
+            The radius in kilometers
+        """
+        return self._body_radii[2]
+
+    @property
+    def _body_time_dependent_frames(self):
+        """
+        List of time dependent reference frames between the
+        target body reference frame and the J2000 frame.
+
+        Returns
+        -------
+        list :
+            The list of frames starting with the body
+            reference frame and ending with the final time
+            dependent frame.
+        """
+        if not hasattr(self, "_body_orientation_table"):
+            self._init_tables()
+        if 'TimeDependentFrames' not in self._body_orientation_table:
+            raise ValueError("Could not find body time dependent frames.")
+        return self._body_orientation_table['TimeDependentFrames']
+
+    @property
+    def reference_frame(self):
+        """
+        The NAIF ID for the target body reference frame
+
+        Returns
+        -------
+        int :
+            The frame ID
+        """
+        return self._body_time_dependent_frames[0]
+
+    @property
+    def _sun_position(self):
+        """
+        The sun position
+
+        Returns
+        -------
+        array :
+            The sun position vectors relative to the center
+            of the target body in the J2000 reference frame
+            as a 2d numpy array
+        """
+        if not hasattr(self, "_sun_position_table"):
+            self._init_tables()
+        return self._sun_position_table.get('Positions', 'None')
+
+    @property
+    def _sun_velocity(self):
+        """
+        The sun velocity
+
+        Returns
+        -------
+        array :
+            The sun velocity vectors in the J2000 reference
+            frame as a 2d numpy array
+        """
+        if not hasattr(self, "_sun_position_table"):
+            self._init_tables()
+        return self._sun_position_table.get('Velocities', None)
+
+    @property
+    def _sensor_position(self):
+        """
+        """
+        if not hasattr(self, "_inst_position_table"):
+            self._init_tables()
+        return self._inst_position_table.get('Positions', None)
+
+    @property
+    def _sensor_velocity(self):
+        """
+        The sensor velocity
+
+        Returns
+        -------
+        array :
+            The sensor velocity vectors in the J2000
+              reference frame as a 2d numpy array
+        """
+        if not hasattr(self, "_inst_position_table"):
+            self._init_tables()
+        return self._inst_position_table.get('Velocities', None)
+
+    @property
+    def _sensor_orientation(self):
+        """
+        The rotation from J2000 to the sensor reference
+        frame
+
+        Returns
+        -------
+        array :
+            The sensor rotation quaternions as a numpy
+            quaternion array
+        """
+        if not hasattr(self, "_inst_pointing_table"):
+            self._init_tables()
+        return self._inst_pointing_table.get('Rotations', None)
+
+    @property
+    def body_orientation(self):
+        """
+        The rotation from J2000 to the target body
+        reference frame
+
+        Returns
+        -------
+        array :
+            The body rotation quaternions as a numpy
+            quaternion array
+        """
+        if not hasattr(self, "_body_orientation_table"):
+            self._init_tables()
+        return self._body_orientation_table.get('Rotations', None)
+
+    @property
+    def naif_keywords(self):
+        """
+        The NaifKeywords group from the file label that
+        contains stored values from the original SPICE
+        kernels
+
+        Returns
+        -------
+        PVLModule :
+            The stored NAIF keyword values
+        """
+        if 'NaifKeywords' not in self.label:
+            raise ValueError("Could not find NaifKeywords in label.")
+        return self.label['NaifKeywords']
+
+    @property
+    def _odtk(self):
+        return self.label["NaifKeywords"]["INS{}_OD_K".format(self.ikid)]
+
+
+class RadialDistortion():
+    @property
+    def optical_distortion(self):
+        return {
+            "Radial": {
+                "coefficients" : self._odtk
+            }
+        }
diff --git a/ale/drivers/cassini_driver.py b/ale/drivers/cassini_driver.py
index c584bd197756f91b387ecb4d8e248f7007ab6c29..408beebacc6b6d2627b7ca9f4a4b7a6b4cc11538 100644
--- a/ale/drivers/cassini_driver.py
+++ b/ale/drivers/cassini_driver.py
@@ -6,21 +6,18 @@ import spiceypy as spice
 import numpy as np
 
 from ale import config
-from ale.drivers.base import Framer
-from ale.drivers import keys
+from ale.drivers.base import Framer, RadialDistortion, Driver
 
 
-class CassiniISS(Framer):
+class CassiniISS(Driver, Framer, RadialDistortion):
     """
-    Cassini mixin class for defining snowflake Spice calls. 
+    Cassini mixin class for defining snowflake Spice calls.
     """
     id_lookup = {
         "ISSNA" : "CASSINI_ISS_NAC",
         "ISSWA" : "CASSINI_ISS_WAC"
     }
 
-    required_keys = keys.base | keys.framer | keys.radial_distortion
-
     @property
     def metakernel(self):
         """
diff --git a/ale/drivers/isis_spice_driver.py b/ale/drivers/isis_spice_driver.py
deleted file mode 100644
index a8c5f2312977c97d3ae0f13c5fcb115cb6643f2b..0000000000000000000000000000000000000000
--- a/ale/drivers/isis_spice_driver.py
+++ /dev/null
@@ -1,627 +0,0 @@
-from glob import glob
-import os
-import struct
-import re
-
-import pvl
-import numpy as np
-import quaternion
-
-from ale import config
-from ale.drivers.base import Driver, Isis3
-
-def read_table_data(table_label, cube):
-    """
-    Helper function to read all of the binary table data
-
-    Parameters
-    ----------
-    table_label : PVLModule
-                  The ISIS table label
-    cube : file
-           The ISIS cube file
-
-    Returns
-    -------
-    bytes :
-        The binary portion of the table data
-    """
-    cube.seek(label['StartByte']-1) # This -1 is straight out of ISIS
-    return cube.read(label['Bytes'])
-
-def field_size(field_label):
-    """
-    Helper function to determine the size of a binary
-    table field
-
-    Parameters
-    ----------
-    field_label : PVLModule
-                  The field label
-
-    Returns
-    -------
-    int :
-        The size of the one entry in bytes
-    """
-    data_sizes = {
-        'Integer' : 4,
-        'Double'  : 8,
-        'Real'    : 4,
-        'Text'    : 1
-    }
-    return data_sizes[field_label['Type']] * field_label['Size']
-
-def field_format(field_label):
-    """
-    Helper function to get the format string for a
-    single entry in a table field
-
-    Parameters
-    ----------
-    field_label : PVLModule
-                  The field label
-
-    Returns
-    -------
-    str :
-        The format string for the entry binary
-    """
-    data_formats = {
-        'Integer' : 'i',
-        'Double'  : 'd',
-        'Real'    : 'f'
-    }
-    return data_formats[field_label['Type']] * field_label['Size']
-
-def parse_field(field_label, data, encoding='latin_1'):
-    """
-    Parses a binary table field entry and converts it into
-    an in memory data type
-
-    Parameters
-    ----------
-    field_label : PVLModule
-                  The field label
-
-    data : bytes
-           The binary data for the field entry
-
-    Returns
-    -------
-    Union[int, float, str, list] :
-        The table field entry converted to a native python
-        type
-    """
-    if field_label['Type'] == 'Text':
-        field_data = data[:field_label['Size']].decode(encoding=encoding)
-    else:
-        data_format = field_format(field_label)
-        field_data = struct.unpack_from(data_format, data)
-        if len(field_data) == 1:
-            field_data = field_data[0]
-    return field_data
-
-def parse_table_data(table_label, data):
-    """
-    Parses an ISIS table into a dict where the keys are the
-    field names and the values are lists of entries.
-
-    Parameters
-    ----------
-    table_label : PVLModule
-                  The table label
-
-    data : bytes
-           The binary data for the entire table
-
-    Returns
-    -------
-    dict :
-        The table as a dict
-    """
-    fields = table_label.getlist('Field')
-    results = {field['Name']:[] for field in fields}
-    offset = 0
-    for record in range(table_label['Records']):
-        for field in fields:
-            field_data = parse_field(field, data[offset:])
-            results[field['Name']].append(field_data)
-            offset += field_size(field)
-    return results
-
-def parse_rotation_table(label, field_data):
-    """
-    Parses ISIS rotation table data.
-
-    Parameters
-    ----------
-    table_label : PVLModule
-                  The table label
-
-    field_data : dict
-                 The table data as a dict with field names
-                 as keys and lists of entries as values
-
-    Returns
-    -------
-    dict :
-        The rotation data
-    """
-    results = {}
-    if all (key in field_data for key in ('J2000Q0','J2000Q1','J2000Q2','J2000Q3')):
-        results['Rotations'] = quaternion.as_quat_array( [ [q0, q1, q2, q3] for q0, q1, q2, q3 in zip(field_data['J2000Q0'],field_data['J2000Q1'],field_data['J2000Q2'],field_data['J2000Q3']) ] )
-    if all (key in field_data for key in ('AV1','AV2','AV3')):
-        results['AngularVelocities'] = np.array( [ [av1, av2, av3] for av1, av2, av3 in zip(field_data['AV1'],field_data['AV2'],field_data['AV3']) ] )
-    if 'ET' in field_data:
-        results['Times'] = np.array(field_data['ET'])
-    if all (key in field_data for key in ('J2000Ang1','J2000Ang2','J2000Ang3')):
-        results['EulerCoefficients'] = np.array([field_data['J2000Ang1'],field_data['J2000Ang2'],field_data['J2000Ang3']])
-        results['BaseTime'] = field_data['J2000Ang1'][-1]
-        results['TimeScale'] = field_data['J2000Ang2'][-1]
-
-    if 'TimeDependentFrames' in label:
-        results['TimeDependentFrames'] = np.array(label['TimeDependentFrames'])
-    if all (key in label for key in ('ConstantRotation','ConstantFrames')):
-        const_rotation_mat = np.array(label['ConstantRotation'])
-        results['ConstantRotation'] = quaternion.from_rotation_matrix(np.reshape(const_rotation_mat, (3, 3)))
-        results['ConstantFrames'] = np.array(label['ConstantFrames'])
-    if all (key in label for key in ('PoleRa','PoleDec','PrimeMeridian')):
-        results['BodyRotationCoefficients'] = np.array( [label['PoleRa'],label['PoleDec'],label['PrimeMeridian']] )
-    if all (key in label for key in ('PoleRaNutPrec','PoleDecNutPrec','PmNutPrec','SysNutPrec0','SysNutPrec1')):
-        results['SatelliteNutationPrecessionCoefficients'] = np.array( [label['PoleRaNutPrec'],label['PoleDecNutPrec'],label['PmNutPrec']] )
-        results['PlanetNutationPrecessionAngleCoefficients'] = np.array( [label['SysNutPrec0'],label['SysNutPrec1']] )
-    return results
-
-def parse_position_table(field_data):
-    """
-    Parses ISIS position table data.
-
-    Parameters
-    ----------
-    table_label : PVLModule
-        The table label
-
-    field_data : dict
-        The table data as a dict with field names as keys
-        and lists of entries as values
-
-    Returns
-    -------
-    dict :
-      The position data
-    """
-    results = {}
-    if all (key in field_data for key in ('J2000X','J2000Y','J2000Z')):
-        results['Positions'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000X'],field_data['J2000Y'],field_data['J2000Z']) ] )
-    if 'ET' in field_data:
-        results['Times'] = np.array(field_data['ET'])
-    if all (key in field_data for key in ('J2000XV','J2000YV','J2000ZV')):
-        results['Velocities'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000XV'],field_data['J2000YV'],field_data['J2000ZV']) ] )
-    if all (key in field_data for key in ('J2000SVX','J2000SVY','J2000SVZ')):
-        results['PositionCoefficients'] = np.array( [field_data['J2000SVX'][:-1],field_data['J2000SVY'][:-1],field_data['J2000SVZ'][:-1]] )
-        results['BaseTime'] = field_data['J2000SVX'][-1]
-        results['TimeScale'] = field_data['J2000SVY'][-1]
-    return results
-
-class IsisSpice(Isis3):
-    """Mixin class for reading from an ISIS cube that has been spiceinit'd
-
-    Attributes
-    ----------
-    _label : PVLModule
-             Dict-like object with PVL keys
-
-    _inst_pointing_table : dict
-                           Dictionary that contains information about the
-                           rotation from J2000 to the sensor reference frame.
-                           All of the values for each property, such as angular
-                           velocity, are stored in a list or numpy array where
-                           each entry is the property at a different time.
-
-    _body_orientation_table : dict
-                              Dictionary that contains information about the
-                              rotation from J2000 to the body fixed reference
-                              frame. All of the  values for each property, such
-                              as angular velocity, are stored in a list or
-                              numpy array where each entry is the property at a
-                              different time.
-
-    _inst_position_table : dict
-                           Dictionary that contains information about the
-                           location of the sensor relative to the center of the
-                           target body. All of the  values for each property,
-                           such as velocity, are stored in a list or numpy
-                           array where each entry is the property at a
-                           different time.
-
-    _sun_position_table : dict
-                          Dictionary that contains information about the
-                          location of the sun relative to the center of the
-                          target body. All of the  values for each property,
-                          such as velocity, are stored in a list or numpy
-                          array where each entry is the property at a
-                          different time.
-
-    """
-
-    @property
-    def label(self):
-        """
-        Loads a PVL from from the _file attribute and
-        parses the binary table data.
-
-        Returns
-        -------
-        PVLModule :
-            Dict-like object with PVL keys
-        """
-        if not hasattr(self, "_label"):
-            try:
-                self._label = pvl.load(self._file)
-            except:
-                raise ValueError("{} is not a valid label".format(self._file))
-            for table in self._label.getlist('Table'):
-                binary_data = read_table_data(table, self._file)
-                field_data = parse_table_data(table, binary_data)
-                if table['Name'] == 'InstrumentPointing':
-                    self._inst_pointing_table = parse_rotation_table(table, field_data)
-                elif table['Name'] == 'BodyRotation':
-                    self._body_orientation_table = parse_rotation_table(table, field_data)
-                elif table['Name'] == 'InstrumentPosition':
-                    self._inst_position_table = parse_position_table(field_data)
-                elif table['Name'] == 'SunPosition':
-                    self._sun_position_table = parse_position_table(field_data)
-        return self._label
-
-    def __enter__(self):
-        """
-        Stub method to conform with how other driver mixins
-        are used.
-        """
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        """
-        Stub method to conform with how other driver mixins
-        are used.
-        """
-        pass
-
-    @property
-    def number_of_quaternions(self):
-        """
-        The number of instrument rotation quaternions
-
-        Returns
-        -------
-        int :
-            The number of quaternions
-        """
-        return len(self.sensor_orientation)
-
-    @property
-    def number_of_ephemerides(self):
-        """
-        The number of instrument position states. These may
-        be just positions or positions and vbelocities.
-
-        Returns
-        -------
-        int :
-            The number of states
-        """
-        return len(self.sensor_position)
-
-    @property
-    def _sclock_hex_string(self):
-        """
-        The hex encoded image start time computed from the
-        spacecraft clock count
-
-        Returns
-        -------
-        str :
-            The hex string representation of the image
-            start time as a double
-        """
-        for key in self.naif_keywords:
-            if re.match('CLOCK_ET_.*_COMPUTED', key[0]):
-                # If the hex string is only numbers and contains leading 0s,
-                # the PVL library strips them off (ie. 0000000000002040 becomes
-                # 2040). Pad to 16 in case this happens.
-                return str(key[1]).zfill(16)
-        raise ValueError("No computed spacecraft clock time found in NaifKeywords.")
-
-    @property
-    def starting_ephemeris_time(self):
-        """
-        The image start time in ephemeris time
-
-        Returns
-        -------
-        float :
-            The image start ephemeris time
-        """
-        return struct.unpack('d', bytes.fromhex(self._sclock_hex_string))[0]
-
-    @property
-    def detector_center(self):
-        """
-        The center of the CCD in detector pixels
-
-        Returns
-        -------
-        list :
-            The center of the CCD formatted as line, sample
-        """
-        return [
-            self.naif_keywords.get('INS{}_BORESIGHT_LINE'.format(self.ikid), None),
-            self.naif_keywords.get('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), None)
-        ]
-
-    @property
-    def _cube_label(self):
-        """
-        The ISIS cube label portion of the file label
-
-        Returns
-        -------
-        PVLModule :
-            The ISIS cube label
-        """
-        if 'IsisCube' not in self.label:
-            raise ValueError("Could not find ISIS cube label.")
-        return self.label['IsisCube']
-
-    @property
-    def _kernels_group(self):
-        """
-        The Kernels group from the ISIS cube label.
-        This is where the original SPICE kernels are listed.
-
-        Returns
-        -------
-        PVLModule :
-            The kernels group
-        """
-        if 'Kernels' not in self._cube_label:
-            raise ValueError("Could not find Kernels group in ISIS cube label.")
-        return self._cube_label['Kernels']
-
-    @property
-    def ikid(self):
-        """
-        The NAIF id for the instrument
-
-        Returns
-        -------
-        int :
-            The instrument id
-        """
-        if 'NaifIkCode' not in self._kernels_group:
-            raise ValueError("Could not find Instrument NAIF ID in Kernels group.")
-        return self._kernels_group['NaifIkCode']
-
-    @property
-    def focal2pixel_lines(self):
-        """
-        The line component of the affine transformation
-        from focal plane coordinates to centered ccd pixels
-
-        Returns
-        -------
-        list :
-            The coefficients of the affine transformation
-            formatted as constant, x, y
-        """
-        return self.naif_keywords.get('INS{}_ITRANSL'.format(self.ikid), None)
-
-    @property
-    def focal2pixel_samples(self):
-        """
-        The sample component of the affine transformation
-        from focal plane coordinates to centered ccd pixels
-
-        Returns
-        -------
-        list :
-            The coefficients of the affine transformation
-            formatted as constant, x, y
-        """
-        return self.naif_keywords.get('INS{}_ITRANSS'.format(self.ikid), None)
-
-    @property
-    def focal_length(self):
-        """
-        The focal length of the instrument
-
-        Returns
-        -------
-        float :
-            The focal length in millimeters
-        """
-        return self.naif_keywords.get('INS{}_FOCAL_LENGTH'.format(self.ikid), None)
-
-    @property
-    def body_radii(self):
-        """
-        The triaxial radii of the target body
-
-        Returns
-        -------
-        list :
-            The body radii in kilometers. For most bodies,
-            this is formatted as semimajor, semimajor,
-            semiminor
-        """
-        for key in self.naif_keywords:
-            if re.match('BODY-?\d*_RADII', key[0]):
-                return self.naif_keywords[key[0]]
-
-    @property
-    def semimajor(self):
-        """
-        The radius of the target body at its widest
-        diameter
-
-        Returns
-        -------
-        float :
-            The radius in kilometers
-        """
-        return self.body_radii[0]
-
-    @property
-    def semiminor(self):
-        """
-        The radius of the target body perpendicular to its
-        widest diameter
-
-        Returns
-        -------
-        float :
-            The radius in kilometers
-        """
-        return self.body_radii[2]
-
-    @property
-    def _body_time_dependent_frames(self):
-        """
-        List of time dependent reference frames between the
-        target body reference frame and the J2000 frame.
-
-        Returns
-        -------
-        list :
-            The list of frames starting with the body
-            reference frame and ending with the final time
-            dependent frame.
-        """
-        if not hasattr(self, "_body_orientation_table"):
-            self.label
-        if 'TimeDependentFrames' not in self._body_orientation_table:
-            raise ValueError("Could not find body time dependent frames.")
-        return self._body_orientation_table['TimeDependentFrames']
-
-    @property
-    def reference_frame(self):
-        """
-        The NAIF ID for the target body reference frame
-
-        Returns
-        -------
-        int :
-            The frame ID
-        """
-        return self._body_time_dependent_frames[0]
-
-    @property
-    def sun_position(self):
-        """
-        The sun position
-
-        Returns
-        -------
-        array :
-            The sun position vectors relative to the center
-            of the target body in the J2000 reference frame
-            as a 2d numpy array
-        """
-        if not hasattr(self, "_sun_position_table"):
-            self.label
-        return self._sun_position_table.get('Positions', 'None')
-
-    @property
-    def sun_velocity(self):
-        """
-        The sun velocity
-
-        Returns
-        -------
-        array :
-            The sun velocity vectors in the J2000 reference
-            frame as a 2d numpy array
-        """
-        if not hasattr(self, "_sun_position_table"):
-            self.label
-        return self._sun_position_table.get('Velocities', None)
-
-    @property
-    def sensor_position(self):
-        """
-        The sensor position
-
-        Returns
-        -------
-        array :
-            The sensor position vectors relative to the
-            center of the target body in the J2000
-            reference frame as a 2d numpy array
-        """
-        if not hasattr(self, "_inst_position_table"):
-            self.label
-        return self._inst_position_table.get('Positions', None)
-
-    @property
-    def sensor_velocity(self):
-        """
-        The sensor velocity
-
-        Returns
-        -------
-        array :
-            The sensor velocity vectors in the J2000
-              reference frame as a 2d numpy array
-        """
-        if not hasattr(self, "_inst_position_table"):
-            self.label
-        return self._inst_position_table.get('Velocities', None)
-
-    @property
-    def sensor_orientation(self):
-        """
-        The rotation from J2000 to the sensor reference
-        frame
-
-        Returns
-        -------
-        array :
-            The sensor rotation quaternions as a numpy
-            quaternion array
-        """
-        if not hasattr(self, "_inst_pointing_table"):
-            self.label
-        return self._inst_pointing_table.get('Rotations', None)
-
-    @property
-    def body_orientation(self):
-        """
-        The rotation from J2000 to the target body
-        reference frame
-
-        Returns
-        -------
-        array :
-            The body rotation quaternions as a numpy
-            quaternion array
-        """
-        if not hasattr(self, "_body_orientation_table"):
-            self.label
-        return self._body_orientation_table.get('Rotations', None)
-
-    @property
-    def naif_keywords(self):
-        """
-        The NaifKeywords group from the file label that
-        contains stored values from the original SPICE
-        kernels
-
-        Returns
-        -------
-        PVLModule :
-            The stored NAIF keyword values
-        """
-        if 'NaifKeywords' not in self.label:
-            raise ValueError("Could not find NaifKeywords in label.")
-        return self.label['NaifKeywords']
diff --git a/ale/drivers/keys.py b/ale/drivers/keys.py
deleted file mode 100644
index 94cf987c7e744dcb3362fc59015155a94f8a0165..0000000000000000000000000000000000000000
--- a/ale/drivers/keys.py
+++ /dev/null
@@ -1,67 +0,0 @@
-base = {
-    'name_model',
-    'center_ephemeris_time',
-    'detector_center',
-    'detector_line_summing',
-    'detector_sample_summing',
-    'ending_ephemeris_time',
-    'exposure_duration',
-    'focal2pixel_lines',
-    'focal2pixel_samples',
-    'focal_epsilon',
-    'focal_length',
-    'ikid',
-    'image_lines',
-    'image_samples',
-    'instrument_id',
-    'interpolation_method',
-    'reference_frame',
-    'reference_height',
-    'semimajor',
-    'semiminor',
-    'sensor_orientation',
-    'sensor_position',
-    'sensor_velocity',
-    'spacecraft_clock_stop_count',
-    'spacecraft_id',
-    'spacecraft_name',
-    'start_time',
-    'starting_detector_line',
-    'starting_detector_sample',
-    'starting_ephemeris_time',
-    'sun_position',
-    'sun_velocity',
-    'target_name',
-    'number_of_ephemerides',
-    'number_of_ephemerides'
-}
-
-filter = {
-    'fikid'
-}
-
-transverse_distortion = {
-    'odtk',
-    'odtx'
-}
-
-radial_distortion = {
-    'odty'
-}
-
-framer = {
-    'filter_number',
-}
-
-temp_dep_focal_legth = {
-    'focal_plane_tempature'
-}
-
-linescanner = {
-    'line_exposure_duration',
-    'line_scan_rate',
-    't0_ephemeris',
-    't0_quaternion',
-    'dt_ephemeris',
-    'dt_quaternion',
-}
diff --git a/ale/drivers/lro_driver.py b/ale/drivers/lro_driver.py
index a97cab4507a1188c918ea3783ed8bd4851c73320..78c752dae6ce35d956f78b03723d36bca19adedf 100644
--- a/ale/drivers/lro_driver.py
+++ b/ale/drivers/lro_driver.py
@@ -6,15 +6,13 @@ import pvl
 import spiceypy as spice
 
 from ale.util import get_metakernels
-from ale.drivers.base import LineScanner, Spice, PDS3, Isis3
-from ale.drivers import keys
+from ale.drivers.base import LineScanner, Spice, PDS3, Isis3, Driver
 
 
-class LrocSpice(Spice, LineScanner):
+class LrocSpice(Driver, Spice, LineScanner):
     """
     Lroc mixin class for defining snowflake Spice calls.
     """
-    required_keys = keys.base | keys.linescanner
 
     @property
     def metakernel(self):
diff --git a/ale/drivers/mdis_driver.py b/ale/drivers/mdis_driver.py
index 1091f8a6e01e2b9e040109387dbd29127b69a10d..4453980a4306352184801deef3abe72a3e43a202 100644
--- a/ale/drivers/mdis_driver.py
+++ b/ale/drivers/mdis_driver.py
@@ -6,11 +6,10 @@ import spiceypy as spice
 import numpy as np
 
 from ale import config
-from ale.drivers.base import Framer, Spice, PDS3, Isis3
-from ale.drivers import keys
+from ale.drivers.base import Framer, Spice, PDS3, Isis3, Driver
 
 
-class MdisSpice(Spice, Framer):
+class MdisSpice(Driver, Spice, Framer):
     """
     MDIS mixin class for defining snowflake Spice calls. Since MDIS has unique
     Spice keys, those are defined here as an intermediate mixin for MDIS drivers
@@ -24,7 +23,6 @@ class MdisSpice(Spice, Framer):
         'MERCURY DUAL IMAGING SYSTEM WIDE ANGLE CAMERA':'MSGR_MDIS_WAC'
     }
 
-    required_keys = keys.base | keys.framer | keys.filter | keys.transverse_distortion | keys.temp_dep_focal_legth
 
     @property
     def metakernel(self):
@@ -45,7 +43,7 @@ class MdisSpice(Spice, Framer):
         return self._metakernel
 
     @property
-    def focal_length(self):
+    def _focal_length(self):
         """
         Computes Focal Length from Kernels
 
@@ -65,7 +63,7 @@ class MdisSpice(Spice, Framer):
         f_t = np.poly1d(coeffs[::-1])
 
         # eval at the focal_plane_tempature
-        return f_t(self.focal_plane_tempature)
+        return f_t(self._focal_plane_tempature)
 
     @property
     def starting_detector_sample(self):
diff --git a/ale/drivers/mro_driver.py b/ale/drivers/mro_driver.py
index 2cf539b564460845b2db631472e99c521975286e..78d759ce6216af1d10f59e3d80c511e5fc3ec6f3 100644
--- a/ale/drivers/mro_driver.py
+++ b/ale/drivers/mro_driver.py
@@ -6,11 +6,38 @@ import pvl
 import spiceypy as spice
 from ale import config
 
-from ale.drivers.base import LineScanner, Spice, PDS3, Isis3
-from ale.drivers import keys
+from ale.drivers.base import LineScanner, Spice, PDS3, Isis3, IsisSpice, Driver, RadialDistortion
 
+class CtxIsisSpice(Driver, IsisSpice, LineScanner, RadialDistortion):
 
-class CtxSpice(Spice, LineScanner):
+    @property
+    def instrument_id(self):
+        """
+        Returns an instrument id for uniquely identifying the instrument, but often
+        also used to be piped into Spice Kernels to acquire IKIDs. Therefore they
+        the same ID the Spice expects in bods2c calls.
+
+        Returns
+        -------
+        : str
+          instrument id
+        """
+        return "N/A"
+
+    @property
+    def spacecraft_id(self):
+        return "N/A"
+
+    @property
+    def ikid(self):
+        return int(self.label["IsisCube"]["Kernels"]["NaifFrameCode"])
+
+    @property
+    def line_exposure_duration(self):
+        return self.label["IsisCube"]["Instrument"]["LineExposureDuration"].value * 0.001 # Scale to seconds
+
+
+class CtxSpice(Driver, Spice, LineScanner, RadialDistortion):
     """
     Spice mixins that defines MRO CTX specific snowflake Spice calls.
     """
@@ -18,8 +45,6 @@ class CtxSpice(Spice, LineScanner):
             'CONTEXT CAMERA':'MRO_CTX'
     }
 
-    required_keys = keys.base | keys.linescanner | keys.radial_distortion
-
     @property
     def metakernel(self):
         """
diff --git a/src/ale.cpp b/src/ale.cpp
index 5cf73cd190d8562f841b7500e94614449be3e2c0..e63db09fe624f128a1c32bac7d854a8b0cd0723c 100644
--- a/src/ale.cpp
+++ b/src/ale.cpp
@@ -355,7 +355,7 @@ namespace ale {
      PyObject *pDict = PyModule_GetDict(pModule);
 
      // Get the add method from the dictionary.
-     PyObject *pFunc = PyDict_GetItemString(pDict, "load");
+     PyObject *pFunc = PyDict_GetItemString(pDict, "loads");
      if(!pFunc) {
        // import errors do not set a PyError flag, need to use a custom
        // error message instead.
@@ -382,15 +382,13 @@ namespace ale {
         throw invalid_argument(getPyTraceback());
      }
 
-     std::string cResult;
-
-     // use PyObject_Str to ensure return is always a string
      PyObject *pResultStr = PyObject_Str(pResult);
      PyObject *temp_bytes = PyUnicode_AsUTF8String(pResultStr); // Owned reference
+
      if(!temp_bytes){
        throw invalid_argument(getPyTraceback());
      }
-
+     std::string cResult;
      char *temp_str = PyBytes_AS_STRING(temp_bytes); // Borrowed pointer
      cResult = temp_str; // copy into std::string
 
diff --git a/tests/pytests/test_isis_spice_drivers.py b/tests/pytests/test_isis_spice_drivers.py
index 551e0d6bed269a700b5d5ab12ef21ef58c38310b..481f508de8506c88caab5302e1980f165e8527ba 100644
--- a/tests/pytests/test_isis_spice_drivers.py
+++ b/tests/pytests/test_isis_spice_drivers.py
@@ -3,8 +3,7 @@ from collections import namedtuple
 import pytest
 
 import ale
-from ale.drivers import isis_spice_driver
-from ale.drivers.isis_spice_driver import IsisSpice
+from ale.drivers import base
 from ale import util
 
 import pvl
@@ -431,14 +430,14 @@ End
         count = table_label['Records'] * len(table_label.getlist('Field'))
         doubles = list(range(count))
         return struct.pack('d' * count, *doubles)
-    monkeypatch.setattr(isis_spice_driver, 'read_table_data', test_table_data)
+    monkeypatch.setattr(base, 'read_table_data', test_table_data)
 
     def test_label(file):
         return pvl.loads(label)
     monkeypatch.setattr(pvl, 'load', test_label)
 
-    test_image = IsisSpice()
-    test_image._file = 'testfile.cub'
+    test_image = type('TestCubeDriver', (base.Driver, base.IsisSpice), {})(label)
+    # test_image._file = 'testfile.cub'
     return test_image
 
 def test_read(test_cube):
@@ -454,7 +453,7 @@ def test_starting_ephemeris_time(test_cube):
     assert test_cube.starting_ephemeris_time == 8.0
 
 def test_detector_center(test_cube):
-    assert test_cube.detector_center == [512.5, 512.5]
+    assert [test_cube._detector_center_line, test_cube._detector_center_sample]  == [512.5, 512.5]
 
 def test_ikid(test_cube):
     assert test_cube.ikid == -236820
@@ -466,37 +465,37 @@ def test_focal2pixel_samples(test_cube):
     assert test_cube.focal2pixel_samples == [0.0, 71.42857143, 0.0]
 
 def test_focal_length(test_cube):
-    assert test_cube.focal_length == 549.11781953727
+    assert test_cube._focal_length == 549.11781953727
 
 def test_body_radii(test_cube):
-    assert test_cube.body_radii == [6051.8, 6051.8, 6051.8]
+    assert test_cube._body_radii == [6051.8, 6051.8, 6051.8]
 
 def test_semimajor(test_cube):
-    assert test_cube.semimajor == 6051.8
+    assert test_cube._semimajor == 6051.8
 
 def test_semiminor(test_cube):
-    assert test_cube.semiminor == 6051.8
+    assert test_cube._semiminor == 6051.8
 
 def test_reference_frame(test_cube):
     assert test_cube.reference_frame == 10012
 
 def test_sun_position(test_cube):
-    assert np.array_equal(test_cube.sun_position, np.array([[0, 1, 2]]))
+    assert np.array_equal(test_cube._sun_position, np.array([[0, 1, 2]]))
 
 def test_sun_velocity(test_cube):
-    assert np.array_equal(test_cube.sun_velocity, np.array([[3, 4, 5]]))
+    assert np.array_equal(test_cube._sun_velocity, np.array([[3, 4, 5]]))
 
 def test_sensor_position(test_cube):
-    assert np.array_equal(test_cube.sensor_position, np.array([[0, 1, 2]]))
+    assert np.array_equal(test_cube._sensor_position, np.array([[0, 1, 2]]))
 
 def test_sensor_velocity(test_cube):
-    assert np.array_equal(test_cube.sensor_velocity, np.array([[3, 4, 5]]))
+    assert np.array_equal(test_cube._sensor_velocity, np.array([[3, 4, 5]]))
 
 def test_sensor_orientation(test_cube):
-    assert np.array_equal(test_cube.sensor_orientation, quaternion.as_quat_array([[0, 1, 2, 3]]))
+    assert np.array_equal(test_cube._sensor_orientation, np.asarray([[0, 1, 2, 3]]))
 
 def test_body_orientation(test_cube):
-    assert np.array_equal(test_cube.body_orientation, quaternion.as_quat_array([[0, 1, 2, 3]]))
+    assert np.array_equal(test_cube.body_orientation, np.asarray([[0, 1, 2, 3]]))
 
 def test_naif_keywords(test_cube):
     assert isinstance(test_cube.naif_keywords, pvl.PVLObject)
diff --git a/tests/pytests/test_lro_drivers.py b/tests/pytests/test_lro_drivers.py
index 56c81a783290db69d1d90f934a5ab1fe5d1afe85..4930672c8f51ffeeb43dedd252bde2ad03d5081d 100644
--- a/tests/pytests/test_lro_drivers.py
+++ b/tests/pytests/test_lro_drivers.py
@@ -115,4 +115,3 @@ def test_lro_creation(lro_lroclabel):
     with LrocPds3Driver(lro_lroclabel) as m:
         d = m.to_dict()
         assert isinstance(d, dict)
-        assert(set(d.keys()) == m.required_keys)
diff --git a/tests/pytests/test_mdis_driver.py b/tests/pytests/test_mdis_driver.py
index a290173041896bc4e9902652d102f8c5bb14ecc0..efcbd00a3425eeec72f5280a70e34fe900d14ad9 100644
--- a/tests/pytests/test_mdis_driver.py
+++ b/tests/pytests/test_mdis_driver.py
@@ -247,4 +247,3 @@ def test_mdis_creation(mdislabel):
     with MdisPDS3Driver(mdislabel) as m:
         d = m.to_dict()
         assert isinstance(d, dict)
-        assert(set(d.keys()) == m.required_keys)
diff --git a/tests/pytests/test_mro_drivers.py b/tests/pytests/test_mro_drivers.py
index a18b153593d260e6faa40471bea43b5c19892106..543d9cce77e0cd05517d4702d73969c381872068 100644
--- a/tests/pytests/test_mro_drivers.py
+++ b/tests/pytests/test_mro_drivers.py
@@ -66,4 +66,3 @@ def test_ctx_creation(mroctx_label):
     with CtxPds3Driver(mroctx_label) as m:
         d = m.to_dict()
     assert isinstance(d, dict)
-    assert(set(d.keys()) == m.required_keys)