def read(self, fromTime, untilTime):
    """Read data from underlying slices and return as a single time-series

    :param fromTime: Beginning of interval in unix epoch seconds
    :param untilTime: End of interval in unix epoch seconds
    :returns: :class:`TimeSeriesData`
    """
    if self.timeStep is None:
      self.readMetadata()

    # Normalize the timestamps to fit proper intervals
    fromTime = int(fromTime - (fromTime % self.timeStep))
    untilTime = int(untilTime - (untilTime % self.timeStep))

    sliceBoundary = None  # to know when to split up queries across slices
    resultValues = []
    earliestData = None
    timeStep = self.timeStep
    method = self.aggregationMethod

    for slice in self.slices:
      # If there was a prior slice covering the requested interval, dont ask for that data again
      if (sliceBoundary is not None) and untilTime > sliceBoundary:
        requestUntilTime = sliceBoundary
      else:
        requestUntilTime = untilTime

      # if the requested interval starts after the start of this slice
      if fromTime >= slice.startTime:
        try:
          series = slice.read(fromTime, requestUntilTime)
        except NoData:
          break

        if series.timeStep != timeStep:
          if len(resultValues) == 0:
            # First slice holding series data, this becomes the default timeStep.
            timeStep = series.timeStep
          elif series.timeStep < timeStep:
            # Series is at a different precision, aggregate to fit our current set.
            series.values = aggregateSeries(method, series.timeStep, timeStep, series.values)
          else:
            # Normalize current set to fit new series data.
            resultValues = aggregateSeries(method, timeStep, series.timeStep, resultValues)
            timeStep = series.timeStep

        earliestData = series.startTime

        rightMissing = (requestUntilTime - series.endTime) // timeStep
        rightNulls = [None for i in range(rightMissing)]
        resultValues = series.values + rightNulls + resultValues
        break

      # or if slice contains data for part of the requested interval
      elif untilTime >= slice.startTime:
        try:
          series = slice.read(slice.startTime, requestUntilTime)
        except NoData:
          continue

        if series.timeStep != timeStep:
          if len(resultValues) == 0:
            # First slice holding series data, this becomes the default timeStep.
            timeStep = series.timeStep
          elif series.timeStep < timeStep:
            # Series is at a different precision, aggregate to fit our current set.
            series.values = aggregateSeries(method, series.timeStep, timeStep, series.values)
          else:
            # Normalize current set to fit new series data.
            resultValues = aggregateSeries(method, timeStep, series.timeStep, resultValues)
            timeStep = series.timeStep

        earliestData = series.startTime

        rightMissing = (requestUntilTime - series.endTime) // timeStep
        rightNulls = [None for i in range(rightMissing)]
        resultValues = series.values + rightNulls + resultValues

      # this is the right-side boundary on the next iteration
      sliceBoundary = slice.startTime

    # The end of the requested interval predates all slices
    if earliestData is None:
      missing = int(untilTime - fromTime) // timeStep
      resultValues = [None for i in range(missing)]

    # Left pad nulls if the start of the requested interval predates all slices
    else:
      leftMissing = (earliestData - fromTime) // timeStep
      leftNulls = [None for i in range(leftMissing)]
      resultValues = leftNulls + resultValues

    return TimeSeriesData(fromTime, untilTime, timeStep, resultValues)