class PandasMixin(object):
    pandas_serializer_class = PandasSerializer

    def with_list_serializer(self, cls):
        meta = getattr(cls, 'Meta', object)
        if getattr(meta, 'list_serializer_class', None):
            return cls

        class SerializerWithListSerializer(cls):
            class Meta(meta):
                list_serializer_class = self.pandas_serializer_class

        return SerializerWithListSerializer

    def get_serializer_class(self):

        # c.f rest_framework.generics.GenericAPIView
        # (not using super() since this is a mixin class)
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__
        )

        renderer = self.request.accepted_renderer
        if hasattr(renderer, 'get_default_renderer'):
            # BrowsableAPIRenderer
            renderer = renderer.get_default_renderer(self)

        if isinstance(renderer, PandasBaseRenderer):
            return self.with_list_serializer(self.serializer_class)
        else:
            return self.serializer_class

    def get_pandas_filename(self, request, format):
        return None

    def get_pandas_headers(self, request):
        format = request.accepted_renderer.format
        filename = self.get_pandas_filename(request, format)
        if not filename:
            return {}

        extension = '.' + format
        if not filename.endswith(extension):
            filename += extension

        return {
            'Content-Disposition': 'attachment; filename="{}"'.format(
                filename
            )
        }

    def update_pandas_headers(self, response):
        headers = self.get_pandas_headers(self.request)
        for key, val in headers.items():
            response[key] = val
        return response class PandasSimpleView(PandasViewBase, APIView):
    """
    Simple (non-model) Pandas API view; override get_data
    with a function that returns a list of dicts.
    """
    serializer_class = SimpleSerializer

    def get_data(self, request, *args, **kwargs):
        return []

    def get(self, request, *args, **kwargs):
        data = self.get_data(request, *args, **kwargs)
        serializer_class = self.get_serializer_class()
        serializer = serializer_class(data, many=True)
        response = Response(serializer.data)
        return self.update_pandas_headers(response) class PandasView(PandasViewBase, ListAPIView):
    """
    Pandas-capable model list view
    """

    def list(self, request, *args, **kwargs):
        response = super(PandasView, self).list(request, *args, **kwargs)
        return self.update_pandas_headers(response) class PandasViewSet(PandasViewBase, ListModelMixin, GenericViewSet):
    """
    Pandas-capable model ViewSet (list only)
    """
    def list(self, request, *args, **kwargs):
        response = super(PandasViewSet, self).list(request, *args, **kwargs)
        return self.update_pandas_headers(response) class PandasUnstackedSerializer(PandasSerializer):
    """
    Pivots dataframe so commonly-repeating values are across the top in a
    multi-row header.  Intended for use with e.g. time series data, where the
    header includes metadata applicable to each time series.
    (Use with wq/chart.js' timeSeries() function)
    """
    index_none_value = '-'
    wq_chart_type = 'timeSeries'

    def get_index(self, dataframe):
        """
        Include header fields in initial index for later unstacking
        """
        return self.get_index_fields() + self.get_header_fields()

    def transform_dataframe(self, dataframe):
        """
        Unstack the dataframe so header fields are across the top.
        """
        dataframe.columns.name = ""

        for i in range(len(self.get_header_fields())):
            dataframe = dataframe.unstack()

        # Remove blank rows / columns
        dataframe = dataframe.dropna(
            axis=0, how='all'
        ).dropna(
            axis=1, how='all'
        )
        return dataframe

    def get_header_fields(self):
        """
        Series metadata fields for header (first few rows)
        """
        return self.get_meta_option('unstacked_header') class PandasScatterSerializer(PandasSerializer):
    """
    Pivots dataframe into a format suitable for plotting two series
    against each other as x vs y on a scatter plot.
    (Use with wq/chart.js' scatter() function)
    """
    index_none_value = '-'
    wq_chart_type = 'scatter'

    def get_index(self, dataframe):
        """
        Include scatter & header fields in initial index for later unstacking
        """
        return (
            self.get_index_fields() +
            self.get_header_fields() +
            self.get_coord_fields()
        )

    def transform_dataframe(self, dataframe):
        """
        Unstack the dataframe so header consists of a composite 'value' header
        plus any other header fields.
        """
        coord_fields = self.get_coord_fields()
        header_fields = self.get_header_fields()

        # Remove any pairs that don't have data for both x & y
        for i in range(len(coord_fields)):
            dataframe = dataframe.unstack()
        dataframe = dataframe.dropna(axis=1, how='all')
        dataframe = dataframe.dropna(axis=0, how='any')

        # Unstack series header
        for i in range(len(header_fields)):
            dataframe = dataframe.unstack()

        # Compute new column headers
        columns = []
        for i in range(len(header_fields) + 1):
            columns.append([])

        for col in dataframe.columns:
            value_name = col[0]
            coord_names = list(col[1:len(coord_fields) + 1])
            header_names = list(col[len(coord_fields) + 1:])
            coord_name = ''
            for name in coord_names:
                if name != self.index_none_value:
                    coord_name += name + '-'
            coord_name += value_name
            columns[0].append(coord_name)
            for i, header_name in enumerate(header_names):
                columns[1 + i].append(header_name)

        dataframe.columns = columns
        dataframe.columns.names = [''] + header_fields

        return dataframe

    def get_coord_fields(self):
        """
        Fields that will be collapsed into a single header with the name of
        each coordinate.
        """
        return self.get_meta_option('scatter_coord')

    def get_header_fields(self):
        """
        Other header fields, if any
        """
        return self.get_meta_option('scatter_header', []) class PandasBoxplotSerializer(PandasSerializer):
    """
    Compute boxplot statistics on dataframe columns, creating a new unstacked
    dataframe where each row describes a boxplot.
    (Use with wq/chart.js' boxplot() function)
    """
    index_none_value = '-'
    wq_chart_type = 'boxplot'

    def get_index(self, dataframe):
        group_field = self.get_group_field()
        date_field = self.get_date_field()
        header_fields = self.get_header_fields()
        extra_index_fields = self.get_extra_index_fields()

        index = []
        if date_field:
            index.append(date_field)
        index += extra_index_fields
        index.append(group_field)
        index += header_fields
        return index

    def transform_dataframe(self, dataframe):
        """
        Use matplotlib to compute boxplot statistics on e.g. timeseries data.
        """
        grouping = self.get_grouping(dataframe)
        group_field = self.get_group_field()
        header_fields = self.get_header_fields()

        if "series" in grouping:
            # Unstack so each series is a column
            for i in range(len(header_fields) + 1):
                dataframe = dataframe.unstack()

        groups = {
            col: dataframe[col]
            for col in dataframe.columns
        }

        if "year" in grouping:
            interval = "year"
        elif "month" in grouping:
            interval = "month"
        else:
            interval = None

        # Compute stats for each column, potentially grouped by year
        series_infos = OrderedDict()
        for header, series in groups.items():
            if interval:
                series_stats = self.boxplots_for_interval(series, interval)
            else:
                series_stats = [self.compute_boxplot(series)]

            for series_stat in series_stats:
                if isinstance(header, tuple):
                    value_name = header[0]
                    col_values = header[1:]
                else:
                    value_name = header
                    col_values = []
                col_names = tuple(zip(dataframe.columns.names[1:], col_values))
                if interval in series_stat:
                    col_names += ((interval, series_stat[interval]),)
                series_infos.setdefault(col_names, dict(col_names))
                series_info = series_infos[col_names]
                for stat_name, val in series_stat.items():
                    if stat_name != interval:
                        series_info[value_name + '-' + stat_name] = val

        dataframe = DataFrame(list(series_infos.values()))
        if 'series' in grouping:
            index = header_fields + [group_field]
            unstack = len(header_fields)
            if interval:
                index = [interval] + index
                unstack += 1
        else:
            index = [interval]
            unstack = 0

        dataframe.set_index(index, inplace=True)
        dataframe.columns.name = ''
        for i in range(unstack):
            dataframe = dataframe.unstack()

        # Remove blank columns
        dataframe = dataframe.dropna(axis=1, how='all')
        return dataframe

    def get_grouping(self, dataframe):
        request = self.context.get('request', None)
        datasets = len(dataframe.columns)
        if request:
            group = request.GET.get('group', None)
            if group:
                return group
        return default_grouping(datasets, self.get_date_field())

    def boxplots_for_interval(self, series, interval):
        def get_interval_name(date):
            if isinstance(date, tuple):
                date = date[0]
            if hasattr(date, 'count') and date.count('-') == 2:
                date = datetime.datetime.strptime(date, "%Y-%m-%d")
            return getattr(date, interval)

        interval_stats = []
        groups = series.groupby(get_interval_name).groups
        for interval_name, group in groups.items():
            stats = self.compute_boxplot(series[group])
            stats[interval] = interval_name
            interval_stats.append(stats)
        return interval_stats

    def compute_boxplot(self, series):
        """
        Compute boxplot for given pandas Series.
        """
        from matplotlib.cbook import boxplot_stats
        series = series[series.notnull()]
        if len(series.values) == 0:
            return {}
        elif not is_numeric_dtype(series):
            return self.non_numeric_stats(series)
        stats = boxplot_stats(list(series.values))[0]
        stats['count'] = len(series.values)
        stats['fliers'] = "|".join(map(str, stats['fliers']))
        return stats

    def non_numeric_stats(self, series):
        return {
            'count': len(series),
            'mode': series.mode()[0],
        }

    def get_group_field(self):
        """
        Categorical field to group datasets by.
        """
        return self.get_meta_option('boxplot_group')

    def get_date_field(self):
        """
        Date field to group datasets by year or month.
        """
        return self.get_meta_option('boxplot_date', False)

    def get_header_fields(self):
        """
        Additional series metadata for boxplot column headers
        """
        return self.get_meta_option('boxplot_header', [])

    def get_extra_index_fields(self):
        """
        Fields that identify each row but don't need to be considered for plot
        """
        return self.get_meta_option('boxplot_extra_index', []) RESPONSE_ERROR = (
    "Response data is a %s, not a DataFrame! "
    "Did you extend PandasMixin?"
)
class PandasBaseRenderer(BaseRenderer):
class PandasFileRenderer(PandasBaseRenderer):
class PandasHTMLRenderer(TemplateHTMLRenderer, PandasBaseRenderer):
class PandasCSVRenderer(PandasBaseRenderer):
class PandasTextRenderer(PandasCSVRenderer):
class PandasJSONRenderer(PandasBaseRenderer):
class PandasExcelRenderer(PandasFileRenderer):
class PandasOldExcelRenderer(PandasFileRenderer):
class PandasImageRenderer(PandasBaseRenderer):
class PandasPNGRenderer(PandasImageRenderer):
class PandasSVGRenderer(PandasImageRenderer):
    def render(self, data, accepted_media_type=None, renderer_context=None):
    def render_dataframe(self, data, name, *args, **kwargs):
    def init_output(self):
    def get_output(self):
    def get_pandas_args(self, data):
    def get_pandas_kwargs(self, data, renderer_context):
    def init_output(self):
    def get_pandas_args(self, data):
    def get_output(self):
    def render(self, data, accepted_media_type=None, renderer_context=None):
    def get_template_context(self, data, renderer_context):
    def get_pandas_kwargs(self, data, renderer_context):
    def get_pandas_kwargs(self, data, renderer_context):
    def get_pandas_kwargs(self, data, renderer_context):
    def render_dataframe(self, data, name, *args, **kwargs):
    def init_output(self):
    def get_pandas_args(self, data):
    def get_pandas_kwargs(self, data, renderer_context):
    def get_output(self): class TimeSeries(models.Model):
    date = models.DateField()
    value = models.FloatField()

    objects = DataFrameManager() class MultiTimeSeries(models.Model):
    # Header
    series = models.CharField(max_length=5)

    # Index
    date = models.DateField()

    # Values
    value = models.FloatField() class ComplexTimeSeries(models.Model):
    # Header
    site = models.CharField(max_length=5)
    parameter = models.CharField(max_length=5)
    units = models.CharField(max_length=5, null=True, blank=True)

    # Index
    date = models.DateField()
    type = models.CharField(max_length=10)

    # Values
    value = models.FloatField()
    flag = models.CharField(max_length=1, null=True, blank=True) class CustomIndexSeries(models.Model):
    code = models.SlugField(primary_key=True)
    value = models.FloatField() class TimeSeriesSerializer(ModelSerializer):
    date = serializers.DateField(format=None)

    class Meta:
        model = TimeSeries
        fields = '__all__' class TimeSeriesNoIdSerializer(TimeSeriesSerializer):
    class Meta:
        model = TimeSeries
        exclude = ['id'] class MultiTimeSeriesSerializer(ModelSerializer):
    class Meta:
        model = MultiTimeSeries
        exclude = ['id']

        pandas_index = ['date']
        pandas_unstacked_header = ['series']
        pandas_scatter_coord = ['series']
        pandas_boxplot_group = 'series'
        pandas_boxplot_date = 'date' class ComplexTimeSeriesSerializer(ModelSerializer):
    class Meta:
        model = ComplexTimeSeries
        exclude = ['id']

        pandas_index = ['date', 'type']
        pandas_unstacked_header = ['site', 'parameter', 'units'] class ComplexScatterSerializer(ComplexTimeSeriesSerializer):
    class Meta(ComplexTimeSeriesSerializer.Meta):
        exclude = ['id', 'flag']

        pandas_scatter_coord = ['units', 'parameter']
        pandas_scatter_header = ['site'] class ComplexBoxplotSerializer(ComplexTimeSeriesSerializer):
    class Meta(ComplexTimeSeriesSerializer.Meta):
        exclude = ['id', 'flag', 'type']
        pandas_boxplot_group = 'site'
        pandas_boxplot_date = 'date'
        pandas_boxplot_header = ['units', 'parameter'] class ComplexBoxplotExtraSerializer(ComplexTimeSeriesSerializer):
    class Meta:
        model = ComplexTimeSeries
        fields = '__all__'
        pandas_boxplot_group = 'site'
        pandas_boxplot_date = 'date'
        pandas_boxplot_extra_index = ['id']  # Ensure row uniqueness
        pandas_boxplot_header = ['units', 'parameter'] class CustomIndexSeriesSerializer(ModelSerializer):
    class Meta:
        model = CustomIndexSeries
        fields = '__all__' class CustomCSVRenderer(PandasCSVRenderer):
    def get_pandas_kwargs(self, data, renderer_context):
        kwargs = super(CustomCSVRenderer, self).get_pandas_kwargs(
            data, renderer_context
        )
        kwargs['date_format'] = '%d-%m-%Y'
        return kwargs
from rest_pandas import (
    PandasMixin, PandasSimpleView, PandasView, PandasViewSet,
    PandasUnstackedSerializer, PandasScatterSerializer, PandasBoxplotSerializer
)
from rest_framework import renderers
from rest_framework.generics import ListAPIView
from rest_pandas import renderers as pandas_renderers
from .models import (
    TimeSeries, MultiTimeSeries, ComplexTimeSeries, CustomIndexSeries,
)
from .serializers import (
    TimeSeriesSerializer, TimeSeriesNoIdSerializer,
    MultiTimeSeriesSerializer,
    ComplexTimeSeriesSerializer, ComplexScatterSerializer,
    ComplexBoxplotSerializer, ComplexBoxplotExtraSerializer,
    CustomIndexSeriesSerializer,
)
from .renderers import CustomCSVRenderer
import pandas as pd

class NoModelView(PandasSimpleView):
    def get_data(self, request, *args, **kwargs):
        return [
            {'x': 5, 'y': 7},
            {'x': 3, 'y': 2},
            {'x': 8, 'y': 6},
            {'x': 5, 'y': 4},
        ]


class FromFileView(PandasSimpleView):
    def get_data(self, request, *args, **kwargs):
        return pd.read_csv('tests/data.csv')


class TimeSeriesView(PandasView):
    """
    A simple time series view.
    """
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer

    def get_template_context(self, data):
        return {'name': data['name'] + ' Custom'}

    def get_pandas_filename(self, request, format):
        if format in ('xls', 'xlsx'):
            return self.get_view_name()
        else:
            return None


class TimeSeriesNoIdView(PandasView):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesNoIdSerializer


class TimeSeriesMixedRendererView(PandasView):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer

    renderer_classes = [
         renderers.BrowsableAPIRenderer,
         pandas_renderers.PandasCSVRenderer,
         renderers.JSONRenderer,
    ]


class TimeSeriesCustomCSVView(PandasView):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer

    renderer_classes = [
         CustomCSVRenderer,
    ]

    def transform_dataframe(self, df):
        df['date'] = df['date'].astype('datetime64')
        return df


class TimeSeriesViewSet(PandasViewSet):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer


class TimeSeriesMixinView(PandasMixin, ListAPIView):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer
    renderer_classes = [
        pandas_renderers.PandasCSVRenderer
    ]


class TimeSeriesNoMixinView(ListAPIView):
    queryset = TimeSeries.objects.all()
    serializer_class = TimeSeriesSerializer
    renderer_classes = [
        pandas_renderers.PandasCSVRenderer
    ]


class DjangoPandasView(PandasSimpleView):
    def get_data(self, request, *args, **kwargs):
        return TimeSeries.objects.to_timeseries(
            index='date',
        )


class MultiTimeSeriesView(PandasView):
    """
    Multiple time series.
    """
    queryset = MultiTimeSeries.objects.all()
    serializer_class = MultiTimeSeriesSerializer
    pandas_serializer_class = PandasUnstackedSerializer


class MultiScatterView(PandasView):
    queryset = MultiTimeSeries.objects.all()
    serializer_class = MultiTimeSeriesSerializer
    pandas_serializer_class = PandasScatterSerializer


class MultiBoxplotView(PandasView):
    queryset = MultiTimeSeries.objects.all()
    serializer_class = MultiTimeSeriesSerializer
    pandas_serializer_class = PandasBoxplotSerializer


class ComplexTimeSeriesView(PandasView):
    queryset = ComplexTimeSeries.objects.all()
    serializer_class = ComplexTimeSeriesSerializer
    pandas_serializer_class = PandasUnstackedSerializer


class ComplexScatterView(PandasView):
    queryset = ComplexTimeSeries.objects.all()
    serializer_class = ComplexScatterSerializer

    pandas_serializer_class = PandasScatterSerializer