<commit_before># This file is part of RinohType, the Python document preparation system.
#
# Copyright (c) Brecht Machiels.
#
# Use of this source code is subject to the terms of the GNU Affero General
# Public License v3. See the LICENSE file or http://www.gnu.org/licenses/.

from collections import Iterable
from itertools import chain
from functools import partial
from math import sqrt

from .draw import Line, Rectangle, ShapeStyle
from .flowable import Flowable, FlowableStyle, FlowableState
from .layout import MaybeContainer, VirtualContainer, EndOfContainer
from .structure import StaticGroupedFlowables, GroupedFlowablesStyle
from .style import Styled
from .util import ReadAliasAttribute

__all__ = ['Table', 'TableSection', 'TableHead', 'TableBody', 'TableRow',
           'TableCell', 'TableCellStyle', 'TableCellBorder',
           'TableCellBackground',
           'TOP', 'MIDDLE', 'BOTTOM']

TOP = 'top'
MIDDLE = 'middle'
BOTTOM = 'bottom'

class TableState(FlowableState):
    def __init__(self, column_widths, body_row_index=0):
        super().__init__()
        self.column_widths = column_widths
        self.body_row_index = body_row_index

    @property
    def body_row_index(self):
        return self._body_row_index

    @body_row_index.setter
    def body_row_index(self, body_row_index):
        self._body_row_index = body_row_index
        self.initial = body_row_index == 0

    def __copy__(self):
        return self.__class__(self.column_widths, self.body_row_index)

class TableStyle(FlowableStyle):
    attributes = {'split_minimum_rows': 5,
                  'repeat_head': False}

NEVER_SPLIT = float('+inf')

class Table(Flowable):
    style_class = TableStyle

    def __init__(self, body, head=None, column_widths=None,
                 id=None, style=None, parent=None):
        super().__init__(id=id, style=style, parent=parent)
        self.head = head
        if head:
            head.parent = self
        self.body = body
        body.parent = self
        self.column_widths = column_widths

    def render(self, container, last_descender, state=None):
        # TODO: allow data to override style (align)
        get_style = partial(self.get_style, document=container.document)
        state = state or TableState(self._size_columns(container))
        with MaybeContainer(container) as maybe_container:
            def render_rows(section, next_row_index=0):
                rows = section[next_row_index:]
                for rendered_rows in self._render_section(container, rows,
                                                          state.column_widths):
                    sum_row_heights = sum(row.height for row in rendered_rows)
                    if sum_row_heights > maybe_container.remaining_height:
                        break
                    self._place_rows_and_render_borders(maybe_container,
                                                        rendered_rows)
                    next_row_index += len(rendered_rows)
                return next_row_index

            # head rows
            if self.head and (state.initial or get_style('repeat_head')):
                if render_rows(self.head) != len(self.head):
                    raise EndOfContainer(state)
            # body rows
            next_row_index = render_rows(self.body, state.body_row_index)
            rows_left = len(self.body) - next_row_index
            if rows_left > 0:
                split_minimum_rows = get_style('split_minimum_rows')
                if min(next_row_index, rows_left) >= split_minimum_rows:
                    state.body_row_index = next_row_index
                raise EndOfContainer(state)
        return sum(state.column_widths), 0

    def _size_columns(self, container):
        """Calculate the table's column sizes constrained by:

        - requested relative widths
        - container width (= available width)
        - cell contents

        """
        def calculate_column_widths(max_cell_width):
            """Calculate required column widths given a maximum cell width"""
            def cell_content_width(cell):
                buffer = VirtualContainer(container, width=max_cell_width)
                width, _ = cell.flow(buffer, None)
                return float(width)

            widths = [0] * self.body.num_columns
            for row in chain(self.head or [], self.body):
                for cell in (cell for cell in row if cell.colspan == 1):
                    col = int(cell.column_index)
                    widths[col] = max(widths[col], cell_content_width(cell))
            for row in chain(self.head or [], self.body):
                for cell in (cell for cell in row if cell.colspan > 1):
                    c = int(cell.column_index)
                    c_end = c + cell.colspan
                    padding = cell_content_width(cell) - sum(widths[c:c_end])
                    if padding > 0:
                        per_column_padding = padding / cell.colspan
                        for i in range(cell.colspan):
                            widths[c + i] += per_column_padding
            return widths

        max_column_widths = calculate_column_widths(float('+inf'))
        if sum(max_column_widths) < container.width:
            if self.column_widths:
                factor = max(maximum / required for maximum, required
                             in zip(max_column_widths, self.column_widths))
                if sum(self.column_widths) * factor < container.width:
                    return [width * factor for width in self.column_widths]
            else:
                return max_column_widths
        # this point is reached when the table cannot fit the available width
        # without line-wrapping the cells' contents
        if self.column_widths:
            rel_column_widths = self.column_widths
        else:   # automatically choose the relative column widths
            min_column_widths = calculate_column_widths(0)
            rel_column_widths = [sqrt(minimum * maximum) for minimum, maximum
                                 in zip(min_column_widths, max_column_widths)]
        scale = float(container.width) / sum(rel_column_widths)
        return [width * scale for width in rel_column_widths]

    @classmethod
    def _render_section(cls, container, rows, column_widths):
        rendered_rows = []
        rows_left_in_span = 0
        for row in rows:
            rows_left_in_span = max(row.maximum_rowspan, rows_left_in_span) - 1
            rendered_row = cls._render_row(column_widths, container, row)
            rendered_rows.append(rendered_row)
            if rows_left_in_span == 0:
                yield cls._vertically_size_cells(rendered_rows)
                rendered_rows = []
        assert not rendered_rows

    @staticmethod
    def _render_row(column_widths, container, row):
        row_index = int(row.index)
        rendered_row = RenderedRow(row_index, row)
        for cell in row:
            col_idx = int(cell.column_index)
            left = sum(column_widths[:col_idx])
            cell_width = sum(column_widths[col_idx:col_idx + cell.colspan])
            buffer = VirtualContainer(container, cell_width)
            width, descender = cell.flow(buffer, None)
            rendered_cell = RenderedCell(cell, buffer, left)
            rendered_row.append(rendered_cell)
        return rendered_row

    @staticmethod
    def _vertically_size_cells(rendered_rows):
        """Grow row heights to cater for vertically spanned cells that do not
        fit in the available space."""
        for r, rendered_row in enumerate(rendered_rows):
            for rendered_cell in rendered_row:
                if rendered_cell.rowspan > 1:
                    row_height = sum(row.height for row in
                                     rendered_rows[r:r + rendered_cell.rowspan])
                    extra_height_needed = rendered_cell.height - row_height
                    if extra_height_needed > 0:
                        padding = extra_height_needed / rendered_cell.rowspan
                        for i in range(r, r + rendered_cell.rowspan):
                            rendered_rows[i].height += padding
        return rendered_rows

    @staticmethod
    def _place_rows_and_render_borders(container, rendered_rows):
        """Place the rendered cells onto the page canvas and draw borders around
        them."""
        def draw_cell_border(rendered_cell, cell_height, container):
            cell_width = rendered_cell.width
            background = TableCellBackground((0, 0), cell_width, cell_height,
                                             parent=rendered_cell.cell)
            background.render(container)
            for position in ('top', 'right', 'bottom', 'left'):
                border = TableCellBorder(rendered_cell, cell_height, position)
                border.render(container)

        document = container.document
        y_cursor = container.cursor
        for r, rendered_row in enumerate(rendered_rows):
            container.advance(rendered_row.height)
            for c, rendered_cell in enumerate(rendered_row):
                cell_height = sum(rendered_row.height for rendered_row in
                                  rendered_rows[r:r + rendered_cell.rowspan])
                x_cursor = rendered_cell.x_position
                y_pos = float(y_cursor + cell_height)
                cell_container = VirtualContainer(container)
                draw_cell_border(rendered_cell, cell_height, cell_container)
                cell_container.place_at(container, x_cursor, y_pos)
                vertical_align = rendered_cell.cell.get_style('vertical_align',
                                                              document)
                if vertical_align == TOP:
                    vertical_offset = 0
                elif vertical_align == MIDDLE:
                    vertical_offset = (cell_height - rendered_cell.height) / 2
                elif vertical_align == BOTTOM:
                    vertical_offset = (cell_height - rendered_cell.height)
                y_offset = float(y_cursor + vertical_offset)
                rendered_cell.container.place_at(container, x_cursor, y_offset)
            y_cursor += rendered_row.height

class TableSection(Styled, list):
    def __init__(self, rows, style=None, parent=None):
        Styled.__init__(self, style=style, parent=parent)
        list.__init__(self, rows)
        for row in rows:
            row.parent = self

    def prepare(self, document):
        for row in self:
            row.prepare(document)

    @property
    def num_columns(self):
        return sum(cell.colspan for cell in self[0])

class TableHead(TableSection):
    pass

class TableBody(TableSection):
    pass

class TableRow(Styled, list):
    section = ReadAliasAttribute('parent')

    def __init__(self, cells, style=None, parent=None):
        Styled.__init__(self, style=style, parent=parent)
        list.__init__(self, cells)
        for cell in cells:
            cell.parent = self

    @property
    def maximum_rowspan(self):
        return max(cell.rowspan for cell in self)

    def prepare(self, document):
        for cells in self:
            cells.prepare(document)

    @property
    def index(self):
        return RowIndex(self)

    def get_rowspanned_columns(self):
        """Return a dictionary mapping column indices to the number of columns
        spanned."""
        spanned_columns = {}
        current_row_index = int(self.index)
        current_row_cols = sum(cell.colspan for cell in self)
        prev_rows = iter(reversed(self.section[:current_row_index]))
        while current_row_cols < self.section.num_columns:
            row = next(prev_rows)
            min_rowspan = current_row_index - int(row.index)
            if row.maximum_rowspan > min_rowspan:
                for cell in row:
                    if cell.rowspan > min_rowspan:
                        col_index = int(cell.column_index)
                        spanned_columns[col_index] = cell.colspan
                        current_row_cols += cell.colspan
        return spanned_columns

class TableCellStyle(GroupedFlowablesStyle):
    attributes = {'vertical_align': MIDDLE,
                  'background_color': None}

class TableCell(StaticGroupedFlowables):
    style_class = TableCellStyle

    row = ReadAliasAttribute('parent')

    def __init__(self, flowables, rowspan=1, colspan=1,
                 id=None, style=None, parent=None):
        super().__init__(flowables, id=id, style=style, parent=parent)
        self.rowspan = rowspan
        self.colspan = colspan

    @property
    def row_index(self):
        return self.row.index

    @property
    def column_index(self):
        return ColumnIndex(self)

class Index(object):
    def __init__(self, element):
        self.element = element

    @property
    def parent(self):
        return self.element.parent

    def __int__(self):
        raise NotImplementedError

    def __eq__(self, other):
        if isinstance(other, slice):
            elements = self.parent[other]
        elif isinstance(other, Iterable):
            elements = (self.parent[index] for index in other)
        else:
            elements = (self.parent[other], )
        return any(self.element is elem for elem in elements)

class RowIndex(Index):
    section = ReadAliasAttribute('parent')
    row = ReadAliasAttribute('element')

    def __int__(self):
        for row_index, row in enumerate(self.section):
            if row is self.row:
                return row_index

class ColumnIndex(Index):
    row = ReadAliasAttribute('parent')
    cell = ReadAliasAttribute('element')

    def __int__(self):
        spanned_columns = self.row.get_rowspanned_columns()
        column_index = 0
        cells = iter(self.row)
        for col_index in range(self.cell.row.section.num_columns):
            if col_index in spanned_columns:
                column_index += spanned_columns[col_index]
            else:
                cell = next(cells)
                if cell is self.element:
                    return column_index
                column_index += cell.colspan

class RenderedCell(object):
    def __init__(self, cell, container, x_position):
        self.cell = cell
        self.container = container
        self.x_position = x_position

    @property
    def width(self):
        return float(self.container.width)

    @property
    def height(self):
        return float(self.container.height)

    @property
    def rowspan(self):
        return self.cell.rowspan

class RenderedRow(list):
    def __init__(self, index, row):
        super().__init__()
        self.index = index
        self.row = row
        self.height = 0

    def append(self, rendered_cell):
        if rendered_cell.cell.rowspan == 1:
            self.height = max(self.height, rendered_cell.height)
        super().append(rendered_cell)

class TableCellBorderStyle(ShapeStyle):
    attributes = {'stroke_width': None}

class TableCellBorder(Line):
    style_class = TableCellBorderStyle

    def __init__(self, rendered_cell, cell_height, position, style=None):
        left, bottom, right, top = 0, 0, rendered_cell.width, cell_height
        if position == 'top':
            start, end = (left, top), (right, top)
        if position == 'right':
            start, end = (right, top), (right, bottom)
        if position == 'bottom':
            start, end = (left, bottom), (right, bottom)
        if position == 'left':
            start, end = (left, bottom), (left, top)
        super().__init__(start, end, style=style, parent=rendered_cell.cell)
        self.position = position

class TableCellBackgroundStyle(ShapeStyle):
    attributes = {'fill_color': None,
                  'stroke_color': None}

class TableCellBackground(Rectangle):
    style_class = TableCellBackgroundStyle
<commit_msg>Fix Index.__eq__()<commit_after># This file is part of RinohType, the Python document preparation system.
#
# Copyright (c) Brecht Machiels.
#
# Use of this source code is subject to the terms of the GNU Affero General
# Public License v3. See the LICENSE file or http://www.gnu.org/licenses/.

from collections import Iterable
from itertools import chain
from functools import partial
from math import sqrt

from .draw import Line, Rectangle, ShapeStyle
from .flowable import Flowable, FlowableStyle, FlowableState
from .layout import MaybeContainer, VirtualContainer, EndOfContainer
from .structure import StaticGroupedFlowables, GroupedFlowablesStyle
from .style import Styled
from .util import ReadAliasAttribute

__all__ = ['Table', 'TableSection', 'TableHead', 'TableBody', 'TableRow',
           'TableCell', 'TableCellStyle', 'TableCellBorder',
           'TableCellBackground',
           'TOP', 'MIDDLE', 'BOTTOM']

TOP = 'top'
MIDDLE = 'middle'
BOTTOM = 'bottom'

class TableState(FlowableState):
    def __init__(self, column_widths, body_row_index=0):
        super().__init__()
        self.column_widths = column_widths
        self.body_row_index = body_row_index

    @property
    def body_row_index(self):
        return self._body_row_index

    @body_row_index.setter
    def body_row_index(self, body_row_index):
        self._body_row_index = body_row_index
        self.initial = body_row_index == 0

    def __copy__(self):
        return self.__class__(self.column_widths, self.body_row_index)

class TableStyle(FlowableStyle):
    attributes = {'split_minimum_rows': 5,
                  'repeat_head': False}

NEVER_SPLIT = float('+inf')

class Table(Flowable):
    style_class = TableStyle

    def __init__(self, body, head=None, column_widths=None,
                 id=None, style=None, parent=None):
        super().__init__(id=id, style=style, parent=parent)
        self.head = head
        if head:
            head.parent = self
        self.body = body
        body.parent = self
        self.column_widths = column_widths

    def render(self, container, last_descender, state=None):
        # TODO: allow data to override style (align)
        get_style = partial(self.get_style, document=container.document)
        state = state or TableState(self._size_columns(container))
        with MaybeContainer(container) as maybe_container:
            def render_rows(section, next_row_index=0):
                rows = section[next_row_index:]
                for rendered_rows in self._render_section(container, rows,
                                                          state.column_widths):
                    sum_row_heights = sum(row.height for row in rendered_rows)
                    if sum_row_heights > maybe_container.remaining_height:
                        break
                    self._place_rows_and_render_borders(maybe_container,
                                                        rendered_rows)
                    next_row_index += len(rendered_rows)
                return next_row_index

            # head rows
            if self.head and (state.initial or get_style('repeat_head')):
                if render_rows(self.head) != len(self.head):
                    raise EndOfContainer(state)
            # body rows
            next_row_index = render_rows(self.body, state.body_row_index)
            rows_left = len(self.body) - next_row_index
            if rows_left > 0:
                split_minimum_rows = get_style('split_minimum_rows')
                if min(next_row_index, rows_left) >= split_minimum_rows:
                    state.body_row_index = next_row_index
                raise EndOfContainer(state)
        return sum(state.column_widths), 0

    def _size_columns(self, container):
        """Calculate the table's column sizes constrained by:

        - requested relative widths
        - container width (= available width)
        - cell contents

        """
        def calculate_column_widths(max_cell_width):
            """Calculate required column widths given a maximum cell width"""
            def cell_content_width(cell):
                buffer = VirtualContainer(container, width=max_cell_width)
                width, _ = cell.flow(buffer, None)
                return float(width)

            widths = [0] * self.body.num_columns
            for row in chain(self.head or [], self.body):
                for cell in (cell for cell in row if cell.colspan == 1):
                    col = int(cell.column_index)
                    widths[col] = max(widths[col], cell_content_width(cell))
            for row in chain(self.head or [], self.body):
                for cell in (cell for cell in row if cell.colspan > 1):
                    c = int(cell.column_index)
                    c_end = c + cell.colspan
                    padding = cell_content_width(cell) - sum(widths[c:c_end])
                    if padding > 0:
                        per_column_padding = padding / cell.colspan
                        for i in range(cell.colspan):
                            widths[c + i] += per_column_padding
            return widths

        max_column_widths = calculate_column_widths(float('+inf'))
        if sum(max_column_widths) < container.width:
            if self.column_widths:
                factor = max(maximum / required for maximum, required
                             in zip(max_column_widths, self.column_widths))
                if sum(self.column_widths) * factor < container.width:
                    return [width * factor for width in self.column_widths]
            else:
                return max_column_widths
        # this point is reached when the table cannot fit the available width
        # without line-wrapping the cells' contents
        if self.column_widths:
            rel_column_widths = self.column_widths
        else:   # automatically choose the relative column widths
            min_column_widths = calculate_column_widths(0)
            rel_column_widths = [sqrt(minimum * maximum) for minimum, maximum
                                 in zip(min_column_widths, max_column_widths)]
        scale = float(container.width) / sum(rel_column_widths)
        return [width * scale for width in rel_column_widths]

    @classmethod
    def _render_section(cls, container, rows, column_widths):
        rendered_rows = []
        rows_left_in_span = 0
        for row in rows:
            rows_left_in_span = max(row.maximum_rowspan, rows_left_in_span) - 1
            rendered_row = cls._render_row(column_widths, container, row)
            rendered_rows.append(rendered_row)
            if rows_left_in_span == 0:
                yield cls._vertically_size_cells(rendered_rows)
                rendered_rows = []
        assert not rendered_rows

    @staticmethod
    def _render_row(column_widths, container, row):
        row_index = int(row.index)
        rendered_row = RenderedRow(row_index, row)
        for cell in row:
            col_idx = int(cell.column_index)
            left = sum(column_widths[:col_idx])
            cell_width = sum(column_widths[col_idx:col_idx + cell.colspan])
            buffer = VirtualContainer(container, cell_width)
            width, descender = cell.flow(buffer, None)
            rendered_cell = RenderedCell(cell, buffer, left)
            rendered_row.append(rendered_cell)
        return rendered_row

    @staticmethod
    def _vertically_size_cells(rendered_rows):
        """Grow row heights to cater for vertically spanned cells that do not
        fit in the available space."""
        for r, rendered_row in enumerate(rendered_rows):
            for rendered_cell in rendered_row:
                if rendered_cell.rowspan > 1:
                    row_height = sum(row.height for row in
                                     rendered_rows[r:r + rendered_cell.rowspan])
                    extra_height_needed = rendered_cell.height - row_height
                    if extra_height_needed > 0:
                        padding = extra_height_needed / rendered_cell.rowspan
                        for i in range(r, r + rendered_cell.rowspan):
                            rendered_rows[i].height += padding
        return rendered_rows

    @staticmethod
    def _place_rows_and_render_borders(container, rendered_rows):
        """Place the rendered cells onto the page canvas and draw borders around
        them."""
        def draw_cell_border(rendered_cell, cell_height, container):
            cell_width = rendered_cell.width
            background = TableCellBackground((0, 0), cell_width, cell_height,
                                             parent=rendered_cell.cell)
            background.render(container)
            for position in ('top', 'right', 'bottom', 'left'):
                border = TableCellBorder(rendered_cell, cell_height, position)
                border.render(container)

        document = container.document
        y_cursor = container.cursor
        for r, rendered_row in enumerate(rendered_rows):
            container.advance(rendered_row.height)
            for c, rendered_cell in enumerate(rendered_row):
                cell_height = sum(rendered_row.height for rendered_row in
                                  rendered_rows[r:r + rendered_cell.rowspan])
                x_cursor = rendered_cell.x_position
                y_pos = float(y_cursor + cell_height)
                cell_container = VirtualContainer(container)
                draw_cell_border(rendered_cell, cell_height, cell_container)
                cell_container.place_at(container, x_cursor, y_pos)
                vertical_align = rendered_cell.cell.get_style('vertical_align',
                                                              document)
                if vertical_align == TOP:
                    vertical_offset = 0
                elif vertical_align == MIDDLE:
                    vertical_offset = (cell_height - rendered_cell.height) / 2
                elif vertical_align == BOTTOM:
                    vertical_offset = (cell_height - rendered_cell.height)
                y_offset = float(y_cursor + vertical_offset)
                rendered_cell.container.place_at(container, x_cursor, y_offset)
            y_cursor += rendered_row.height

class TableSection(Styled, list):
    def __init__(self, rows, style=None, parent=None):
        Styled.__init__(self, style=style, parent=parent)
        list.__init__(self, rows)
        for row in rows:
            row.parent = self

    def prepare(self, document):
        for row in self:
            row.prepare(document)

    @property
    def num_columns(self):
        return sum(cell.colspan for cell in self[0])

class TableHead(TableSection):
    pass

class TableBody(TableSection):
    pass

class TableRow(Styled, list):
    section = ReadAliasAttribute('parent')

    def __init__(self, cells, style=None, parent=None):
        Styled.__init__(self, style=style, parent=parent)
        list.__init__(self, cells)
        for cell in cells:
            cell.parent = self

    @property
    def maximum_rowspan(self):
        return max(cell.rowspan for cell in self)

    def prepare(self, document):
        for cells in self:
            cells.prepare(document)

    @property
    def index(self):
        return RowIndex(self)

    def get_rowspanned_columns(self):
        """Return a dictionary mapping column indices to the number of columns
        spanned."""
        spanned_columns = {}
        current_row_index = int(self.index)
        current_row_cols = sum(cell.colspan for cell in self)
        prev_rows = iter(reversed(self.section[:current_row_index]))
        while current_row_cols < self.section.num_columns:
            row = next(prev_rows)
            min_rowspan = current_row_index - int(row.index)
            if row.maximum_rowspan > min_rowspan:
                for cell in row:
                    if cell.rowspan > min_rowspan:
                        col_index = int(cell.column_index)
                        spanned_columns[col_index] = cell.colspan
                        current_row_cols += cell.colspan
        return spanned_columns

class TableCellStyle(GroupedFlowablesStyle):
    attributes = {'vertical_align': MIDDLE,
                  'background_color': None}

class TableCell(StaticGroupedFlowables):
    style_class = TableCellStyle

    row = ReadAliasAttribute('parent')

    def __init__(self, flowables, rowspan=1, colspan=1,
                 id=None, style=None, parent=None):
        super().__init__(flowables, id=id, style=style, parent=parent)
        self.rowspan = rowspan
        self.colspan = colspan

    @property
    def row_index(self):
        return self.row.index

    @property
    def column_index(self):
        return ColumnIndex(self)

class Index(object):
    def __init__(self, element):
        self.element = element

    @property
    def parent(self):
        return self.element.parent

    def __eq__(self, other):
        if isinstance(other, slice):
            indices = range(*other.indices(self.num_items))
        elif isinstance(other, Iterable):
            indices = other
        else:
            indices = (other, )
        return int(self) in (self.num_items + index if index < 0 else index
                             for index in indices)

    def __int__(self):
        raise NotImplementedError

    def num_items(self):
        raise NotImplementedError

class RowIndex(Index):
    section = ReadAliasAttribute('parent')
    row = ReadAliasAttribute('element')

    def __int__(self):
        for row_index, row in enumerate(self.section):
            if row is self.row:
                return row_index

    @property
    def num_items(self):
        return len(self.section)

class ColumnIndex(Index):
    row = ReadAliasAttribute('parent')
    cell = ReadAliasAttribute('element')

    def __int__(self):
        spanned_columns = self.row.get_rowspanned_columns()
        column_index = 0
        cells = iter(self.row)
        for col_index in range(self.cell.row.section.num_columns):
            if col_index in spanned_columns:
                column_index += spanned_columns[col_index]
            else:
                cell = next(cells)
                if cell is self.element:
                    return column_index
                column_index += cell.colspan

    @property
    def num_items(self):
        return self.row.section.num_columns

class RenderedCell(object):
    def __init__(self, cell, container, x_position):
        self.cell = cell
        self.container = container
        self.x_position = x_position

    @property
    def width(self):
        return float(self.container.width)

    @property
    def height(self):
        return float(self.container.height)

    @property
    def rowspan(self):
        return self.cell.rowspan

class RenderedRow(list):
    def __init__(self, index, row):
        super().__init__()
        self.index = index
        self.row = row
        self.height = 0

    def append(self, rendered_cell):
        if rendered_cell.cell.rowspan == 1:
            self.height = max(self.height, rendered_cell.height)
        super().append(rendered_cell)

class TableCellBorderStyle(ShapeStyle):
    attributes = {'stroke_width': None}

class TableCellBorder(Line):
    style_class = TableCellBorderStyle

    def __init__(self, rendered_cell, cell_height, position, style=None):
        left, bottom, right, top = 0, 0, rendered_cell.width, cell_height
        if position == 'top':
            start, end = (left, top), (right, top)
        if position == 'right':
            start, end = (right, top), (right, bottom)
        if position == 'bottom':
            start, end = (left, bottom), (right, bottom)
        if position == 'left':
            start, end = (left, bottom), (left, top)
        super().__init__(start, end, style=style, parent=rendered_cell.cell)
        self.position = position

class TableCellBackgroundStyle(ShapeStyle):
    attributes = {'fill_color': None,
                  'stroke_color': None}

class TableCellBackground(Rectangle):
    style_class = TableCellBackgroundStyle
<|endoftext|>


<commit_before>"""
MakeMKV CLI Wrapper

This class acts as a python wrapper to the MakeMKV CLI.

Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.2, 2013-01-20 11:08:00 CST $;
@author     Jason Millward <jason@jcode.me>
@license    http://opensource.org/licenses/MIT
"""

#
#   IMPORTS
#

import commands
import imdb
import os
import re

#
#   CODE
#

class makeMKV(object):

    def __init__(self):
        self.movieName = ""
        self.imdbScaper = imdb.IMDb()

    def _queueMovie(self):
        home = os.path.expanduser("~")
        if os.path.exists('%s/.makemkvautoripper' % home) == False:
            os.makedirs('%s/.makemkvautoripper' % home)

        os.chdir('%s/%s' % (self.path, self.movieName))
        for files in os.listdir("."):
            if files.endswith(".mkv"):
                movie = files
                break

        with open("%s/.makemkvautoripper/queue" % home, "a+") as myfile:
            myfile.write("%s|%s|%s|%s.mkv\n"
                %
                (self.path, self.movieName, movie, self.movieName))

    def _cleanTitle(self):
        tmpName = self.movieName
        # A little fix for extended editions (eg; Die Hard 4)
        tmpName = tmpName.title().replace("Extended_Edition", "")

        # Remove Special Edition
        tmpName = tmpName.replace("Special_Edition", "")

        # Remove Disc X from the title
        tmpName = re.sub(r"Disc_(\d)", "", tmpName)

        # Clean up the disc title so IMDb can identify it easier
        tmpName = tmpName.replace("\"", "").replace("_", " ")

        # Clean up the edges and remove whitespace
        self.movieName = tmpName.strip()

    def ripDisc(self, path, length, cache, queue):
        self.path = path
        # Start the making of the mkv
        commands.getstatusoutput(
            'makemkvcon mkv disc:%s 0 "%s/%s" --cache=%d --noscan --minlength=%d'
            %
            (self.discIndex, self.path, self.movieName, cache, length))

        # Add some checking here
        # Seriously, check to see if makemkv worked like it should :(
        if queue:
            self._queueMovie()
        return True

    def findDisc(self, output):
        # Execute the info gathering
        # Save output into /tmp/ for interpreting 3 or 4 lines later
        commands.getstatusoutput('makemkvcon -r info > %s' % output)

        # Open the info file from /tmp/
        tempFile = open(output, 'r')
        for line in tempFile.readlines():
            if line[:4] == "DRV:":
                if "/dev/" in line:
                    drive = line.split(',')
                    self.discIndex = drive[0].replace("DRV:", "")
                    self.movieName = drive[5]
                    break

        if len(self.discIndex) == 0:
            print "No disc detected"
            return False
        else:
            print "Disc detected in drive %s" % self.discIndex
            return True

    def getTitle(self):
        self._cleanTitle()

        result = self.imdbScaper.search_movie(self.movieName, results=1)

        if len(result) > 0:
            self.movieName = result[0]

        return self.movieName
<commit_msg>Commiting before the big file change<commit_after>"""
MakeMKV CLI Wrapper

This class acts as a python wrapper to the MakeMKV CLI.

Released under the MIT license
Copyright (c) 2012, Jason Millward

@category   misc
@version    $Id: 1.2, 2013-01-20 11:08:00 CST $;
@author     Jason Millward <jason@jcode.me>
@license    http://opensource.org/licenses/MIT
"""

#
#   IMPORTS
#

import commands
import imdb
import os
import re

#
#   CODE
#

class makeMKV(object):

    """ Function:   __init__
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def __init__(self):
        self.movieName = ""
        self.imdbScaper = imdb.IMDb()

    """ Function:   _queueMovie
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def _queueMovie(self):
        home = os.path.expanduser("~")
        if os.path.exists('%s/.makemkvautoripper' % home) == False:
            os.makedirs('%s/.makemkvautoripper' % home)

        os.chdir('%s/%s' % (self.path, self.movieName))
        for files in os.listdir("."):
            if files.endswith(".mkv"):
                movie = files
                break

        with open("%s/.makemkvautoripper/queue" % home, "a+") as myfile:
            myfile.write("%s|%s|%s|%s.mkv\n"
                %
                (self.path, self.movieName, movie, self.movieName))

    """ Function:   _cleanTitle
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def _cleanTitle(self):
        tmpName = self.movieName
        # A little fix for extended editions (eg; Die Hard 4)
        tmpName = tmpName.title().replace("Extended_Edition", "")

        # Remove Special Edition
        tmpName = tmpName.replace("Special_Edition", "")

        # Remove Disc X from the title
        tmpName = re.sub(r"Disc_(\d)", "", tmpName)

        # Clean up the disc title so IMDb can identify it easier
        tmpName = tmpName.replace("\"", "").replace("_", " ")

        # Clean up the edges and remove whitespace
        self.movieName = tmpName.strip()

    """ Function:   ripDisc
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def ripDisc(self, path, length, cache, queue):
        self.path = path
        # Start the making of the mkv
        commands.getstatusoutput(
            'makemkvcon mkv disc:%s 0 "%s/%s" --cache=%d --noscan --minlength=%d'
            %
            (self.discIndex, self.path, self.movieName, cache, length))

        # Add some checking here
        # Seriously, check to see if makemkv worked like it should :(
        if queue:
            self._queueMovie()
        return True

    """ Function:   findDisc
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def findDisc(self, output):
        # Execute the info gathering
        # Save output into /tmp/ for interpreting 3 or 4 lines later
        commands.getstatusoutput('makemkvcon -r info > %s' % output)

        # Open the info file from /tmp/
        tempFile = open(output, 'r')
        for line in tempFile.readlines():
            if line[:4] == "DRV:":
                if "/dev/" in line:
                    drive = line.split(',')
                    self.discIndex = drive[0].replace("DRV:", "")
                    self.movieName = drive[5]
                    break

        if len(self.discIndex) == 0:
            print "No disc detected"
            return False
        else:
            print "Disc detected in drive %s" % self.discIndex
            return True

    """ Function:   getTitle
            Removes the log file and the input movie because these files are
                no longer needed by this script

        Inputs:
            log         (Str): File path of the log to remove
            oldMovie    (Str): File path of the movie to remove

        Outputs:
            None
    """
    def getTitle(self):
        self._cleanTitle()

        result = self.imdbScaper.search_movie(self.movieName, results=1)

        if len(result) > 0:
            self.movieName = result[0]

        return self.movieName
<|endoftext|>


<commit_before>from django.conf.urls import patterns, include, url

from rockit.foundation.core import models

urlpatterns = list()

for a in models.Association.objects.all():
    try:
        prefix = ''

        if('plugins' in a.namespace):
            prefix = 'plugins/'

        urlpatterns += patterns('', url(r'^%s%s/' % (prefix,a.entry), include(a.namespace + '.urls')))
    except ImportError:
        continue<commit_msg>Updated urls<commit_after>from django.conf.urls import patterns, include, url

from rockit.foundation.core import models
from rockit import settings

urlpatterns = list()

for a in models.Association.objects.all():
    try:
        prefix = ''

        if('plugins' in a.namespace):
            prefix = 'plugins/'

        urlpatterns += patterns('', url(r'^%s%s/' % (prefix,a.entry), include(a.namespace + '.urls')))
    except ImportError:
        continue

#for app in settings.INSTALLED_APPS:
#    if app.startswith('rockit'):
#        entry = 'rockit' if 'plugins' not in app else app.split('.')[-1]
#        urls  = '%s.urls' % app
#        urlpatterns += patterns('', url(r'^%s/' % entry, include(app)))
<|endoftext|>


<commit_before>import os
import datetime
import requests

from flask import jsonify, request
from dateutil.parser import parse

from server import app, sqldb
from penn.base import APIError
from .models import StudySpacesBooking, User
from .penndata import studyspaces
from .penndata import wharton
from .base import cached_route

def get_sessionid():
    return os.environ['sessionid'] # "l87ugy7ha9fabhxil71zwshbsl9ogl7x"

@app.route('/studyspaces/gsr', methods=['GET'])
def get_wharton_gsrs_temp_route():
    """ Temporary endpoint to allow non-authenticated users to access the list of GSRs. """
    return jsonify(wharton.get_wharton_gsrs(get_sessionid(), request))

@app.route('/studyspaces/availability/<int:building>', methods=['GET'])
def parse_times(building):
    """
    Returns JSON containing all rooms for a given building.

    Usage:
        /studyspaces/availability/<building> gives all rooms for the next 24 hours
        /studyspaces/availability/<building>?start=2018-25-01 gives all rooms in the start date
        /studyspaces/availability/<building>?start=...&end=... gives all rooms between the two days
    """
    if 'date' in request.args:
        start = request.args.get('date')
        end = request.args.get('date')
    else:
        start = request.args.get('start')
        end = request.args.get('end')

    if building == 1:
        sessionid = get_sessionid()
        rooms = wharton.get_wharton_gsrs_formatted(sessionid)
    else:
        try:
            rooms = studyspaces.get_rooms(building, start, end)
            rooms["location_id"] = rooms["id"]
            rooms["rooms"] = []
            for room_list in rooms["categories"]:
                for room in room_list["rooms"]:
                    room["thumbnail"] = room["image"]
                    del room["image"]
                    room["room_id"] = room["id"]
                    del room["id"]
                    room["gid"] = room_list["cid"]
                    room["lid"] = building
                    room["times"] = room["availability"]
                    del room["availability"]
                    for time in room["times"]:
                        time["available"] = True
                        time["start"] = time["from"]
                        time["end"] = time["to"]
                        del time["from"]
                        del time["to"]
                    rooms["rooms"].append(room)
        except APIError as e:
            return jsonify({"error": str(e)})
    return jsonify(rooms)

@app.route('/studyspaces/locations', methods=['GET'])
def display_id_pairs():
    """
    Returns JSON containing a list of buildings with their ids.
    """
    def get_data():
        return {"locations": studyspaces.get_buildings() + [{"lid": 1, "name": "Huntsman Hall", "service": "wharton"}]}

    return cached_route('studyspaces:locations', datetime.timedelta(days=1), get_data)

@app.route('/studyspaces/cancel', methods=['POST'])
def cancel_room():
    """
    Cancels a booked room.
    """
    booking_id = request.form.get("booking_id")
    if not booking_id:
        return jsonify({"error": "No booking id sent to server!"})

    # ensure that the server was the one that booked the room
    for bid in booking_id.strip().split(","):
        exists = sqldb.session.query(sqldb.exists().where(StudySpacesBooking.booking_id == bid)).scalar()
        if not exists:
            return jsonify({"error": "Cancellation request aborted because of booking '{}'.".format(bid)})

    resp = studyspaces.cancel_room(booking_id)
    return jsonify(resp)

@app.route('/studyspaces/book', methods=['POST'])
def book_room():
    """
    Books a room.
    """

    try:
        room = int(request.form["room"])
    except (KeyError, ValueError):
        return jsonify({"results": False, "error": "Please specify a correct room id!"})

    try:
        start = parse(request.form["start"])
        end = parse(request.form["end"])
    except KeyError:
        return jsonify({"results": False, "error": "No start and end parameters passed to server!"})

    contact = {}
    for arg, field in [("fname", "firstname"), ("lname", "lastname"), ("email", "email"), ("nickname", "groupname")]:
        try:
            contact[arg] = request.form[field]
        except KeyError:
            return jsonify({"results": False, "error": "'{}' is a required parameter!".format(field)})

    contact["custom"] = {}
    for arg, field in [("q2533", "phone"), ("q2555", "size"), ("q2537", "size")]:
        try:
            contact["custom"][arg] = request.form[field]
        except KeyError:
            pass

    resp = studyspaces.book_room(room, start.isoformat(), end.isoformat(), **contact)
    if "error" not in resp:
        save_booking(
            rid=room,
            email=contact["email"],
            start=start.replace(tzinfo=None),
            end=end.replace(tzinfo=None),
            booking_id=resp.get("booking_id")
        )
    return jsonify(resp)

def save_booking(**info):
    try:
        user = User.get_or_create()
    except ValueError:
        user = None

    if user is None:
        return

    info['user'] = user.id

    item = StudySpacesBooking(**info)

    sqldb.session.add(item)
    sqldb.session.commit()
<commit_msg>changed parameter to date from request<commit_after>import os
import datetime
import requests

from flask import jsonify, request
from dateutil.parser import parse

from server import app, sqldb
from penn.base import APIError
from .models import StudySpacesBooking, User
from .penndata import studyspaces
from .penndata import wharton
from .base import cached_route

def get_sessionid():
    return os.environ['sessionid'] # "l87ugy7ha9fabhxil71zwshbsl9ogl7x"

@app.route('/studyspaces/gsr', methods=['GET'])
def get_wharton_gsrs_temp_route():
    """ Temporary endpoint to allow non-authenticated users to access the list of GSRs. """
    date = request.args.get('date')
    return jsonify(wharton.get_wharton_gsrs(get_sessionid(), date))

@app.route('/studyspaces/availability/<int:building>', methods=['GET'])
def parse_times(building):
    """
    Returns JSON containing all rooms for a given building.

    Usage:
        /studyspaces/availability/<building> gives all rooms for the next 24 hours
        /studyspaces/availability/<building>?start=2018-25-01 gives all rooms in the start date
        /studyspaces/availability/<building>?start=...&end=... gives all rooms between the two days
    """
    if 'date' in request.args:
        start = request.args.get('date')
        end = request.args.get('date')
    else:
        start = request.args.get('start')
        end = request.args.get('end')

    if building == 1:
        sessionid = get_sessionid()
        rooms = wharton.get_wharton_gsrs_formatted(sessionid)
    else:
        try:
            rooms = studyspaces.get_rooms(building, start, end)
            rooms["location_id"] = rooms["id"]
            rooms["rooms"] = []
            for room_list in rooms["categories"]:
                for room in room_list["rooms"]:
                    room["thumbnail"] = room["image"]
                    del room["image"]
                    room["room_id"] = room["id"]
                    del room["id"]
                    room["gid"] = room_list["cid"]
                    room["lid"] = building
                    room["times"] = room["availability"]
                    del room["availability"]
                    for time in room["times"]:
                        time["available"] = True
                        time["start"] = time["from"]
                        time["end"] = time["to"]
                        del time["from"]
                        del time["to"]
                    rooms["rooms"].append(room)
        except APIError as e:
            return jsonify({"error": str(e)})
    return jsonify(rooms)

@app.route('/studyspaces/locations', methods=['GET'])
def display_id_pairs():
    """
    Returns JSON containing a list of buildings with their ids.
    """
    def get_data():
        return {"locations": studyspaces.get_buildings() + [{"lid": 1, "name": "Huntsman Hall", "service": "wharton"}]}

    return cached_route('studyspaces:locations', datetime.timedelta(days=1), get_data)

@app.route('/studyspaces/cancel', methods=['POST'])
def cancel_room():
    """
    Cancels a booked room.
    """
    booking_id = request.form.get("booking_id")
    if not booking_id:
        return jsonify({"error": "No booking id sent to server!"})

    # ensure that the server was the one that booked the room
    for bid in booking_id.strip().split(","):
        exists = sqldb.session.query(sqldb.exists().where(StudySpacesBooking.booking_id == bid)).scalar()
        if not exists:
            return jsonify({"error": "Cancellation request aborted because of booking '{}'.".format(bid)})

    resp = studyspaces.cancel_room(booking_id)
    return jsonify(resp)

@app.route('/studyspaces/book', methods=['POST'])
def book_room():
    """
    Books a room.
    """

    try:
        room = int(request.form["room"])
    except (KeyError, ValueError):
        return jsonify({"results": False, "error": "Please specify a correct room id!"})

    try:
        start = parse(request.form["start"])
        end = parse(request.form["end"])
    except KeyError:
        return jsonify({"results": False, "error": "No start and end parameters passed to server!"})

    contact = {}
    for arg, field in [("fname", "firstname"), ("lname", "lastname"), ("email", "email"), ("nickname", "groupname")]:
        try:
            contact[arg] = request.form[field]
        except KeyError:
            return jsonify({"results": False, "error": "'{}' is a required parameter!".format(field)})

    contact["custom"] = {}
    for arg, field in [("q2533", "phone"), ("q2555", "size"), ("q2537", "size")]:
        try:
            contact["custom"][arg] = request.form[field]
        except KeyError:
            pass

    resp = studyspaces.book_room(room, start.isoformat(), end.isoformat(), **contact)
    if "error" not in resp:
        save_booking(
            rid=room,
            email=contact["email"],
            start=start.replace(tzinfo=None),
            end=end.replace(tzinfo=None),
            booking_id=resp.get("booking_id")
        )
    return jsonify(resp)

def save_booking(**info):
    try:
        user = User.get_or_create()
    except ValueError:
        user = None

    if user is None:
        return

    info['user'] = user.id

    item = StudySpacesBooking(**info)

    sqldb.session.add(item)
    sqldb.session.commit()
<|endoftext|>


<commit_before>"""Methods common to all handlers."""

import os
from google.appengine.api import users
import django
from django import shortcuts
import models

HELP_CONTACT = 'al@rebuildingtogetherpeninsula.org'

def GetUser(request, user=None):
  if user is None:
    user = users.GetCurrentUser()
  captain = models.Captain.all().filter('email = ', user.email()).get()
  user.captain = captain
  staff = models.Staff.all().filter('email = ', user.email()).get()
  user.staff = staff
  return user, captain, staff

def Respond(request, template_name, params=None):
  """Helper to render a response, passing standard stuff to the response.

  Args:
    request: The request object.
    template_name: The template name; '.html' is appended automatically.
    params: A dict giving the template parameters; modified in-place.

  Returns:
    Whatever render_to_response(template_name, params) returns.

  Raises:
    Whatever render_to_response(template_name, params) raises.
  """
  user, _, _ = GetUser(request)
  if params is None:
    params = {}
  if user:
    params['user'] = user
    params['sign_out'] = users.CreateLogoutURL('/')
    params['is_admin'] = (users.IsCurrentUserAdmin() and
                          'Dev' in os.getenv('SERVER_SOFTWARE'))
  else:
    params['sign_in'] = users.CreateLoginURL(request.path)
  params['help_contact'] = HELP_CONTACT
  if not template_name.endswith('.html'):
    template_name += '.html'
  return shortcuts.render_to_response(template_name, params)

<commit_msg>changed help link form al to cari<commit_after>"""Methods common to all handlers."""

import os
from google.appengine.api import users
import django
from django import shortcuts
import models

HELP_CONTACT = 'cari@rebuildingtogetherpeninsula.org'

def GetUser(request, user=None):
  if user is None:
    user = users.GetCurrentUser()
  captain = models.Captain.all().filter('email = ', user.email()).get()
  user.captain = captain
  staff = models.Staff.all().filter('email = ', user.email()).get()
  user.staff = staff
  return user, captain, staff

def Respond(request, template_name, params=None):
  """Helper to render a response, passing standard stuff to the response.

  Args:
    request: The request object.
    template_name: The template name; '.html' is appended automatically.
    params: A dict giving the template parameters; modified in-place.

  Returns:
    Whatever render_to_response(template_name, params) returns.

  Raises:
    Whatever render_to_response(template_name, params) raises.
  """
  user, _, _ = GetUser(request)
  if params is None:
    params = {}
  if user:
    params['user'] = user
    params['sign_out'] = users.CreateLogoutURL('/')
    params['is_admin'] = (users.IsCurrentUserAdmin() and
                          'Dev' in os.getenv('SERVER_SOFTWARE'))
  else:
    params['sign_in'] = users.CreateLoginURL(request.path)
  params['help_contact'] = HELP_CONTACT
  if not template_name.endswith('.html'):
    template_name += '.html'
  return shortcuts.render_to_response(template_name, params)

<|endoftext|>


<commit_before># Copyright (c) 2013 by California Institute of Technology
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the California Institute of Technology nor
#    the names of its contributors may be used to endorse or promote
#    products derived from this software without specific prior
#    written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
"""
Convert labeled graph to dot using
pydot and custom filtering
"""
from warnings import warn
from collections import Iterable
from textwrap import fill
from cStringIO import StringIO

import networkx as nx

try:
    import pydot
except ImportError:
    warn('pydot package not found.\nHence dot export not unavailable.')
    pydot = None

try:
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    matplotlib = True
except ImportError:
    warn('matplotlib package not found.\nSo no loading of dot plots.')
    matplotlib = None

try:
    from IPython.display import display, Image
    IPython = True
except ImportError:
    warn('IPython not found.\nSo loaded dot images not inline.')
    IPython = None

from ..mathset import dprint

def _states2dot_str(states, to_pydot_graph, wrap=10):
        """Copy nodes to given Pydot graph, with attributes for dot export.
        """
        def add_incoming_edge(g, state):
            phantom_node = 'phantominit' +str(state)

            g.add_node(phantom_node, label='""', shape='none', width='0')
            g.add_edge(phantom_node, state)

        def form_node_label(state, state_data, label_def,
                            label_format, width=10):
            # node itself
            state_str = fill(str(state), width=width)
            state_str.replace('\n', '\\n')
            node_dot_label = '"' +state_str +'\\n'

            # add node annotations from action, AP sets etc
            # other key,values in state attr_dict ignored
            sep_label_sets = label_format['separator']
            for (label_type, label_value) in state_data.iteritems():
                if label_type in label_def:
                    # label formatting
                    type_name = label_format[label_type]
                    sep_type_value = label_format['type?label']

                    # avoid turning strings to lists,
                    # or non-iterables to lists
                    if isinstance(label_value, str):
                        label_str = fill(label_value, width=width)
                    elif isinstance(label_value, Iterable): # and not str
                        label_str = fill(str(list(label_value) ), width=width)
                    else:
                        label_str = fill(str(label_value), width=width)
                    label_str.replace('\n', '\\n')

                    node_dot_label += type_name +sep_type_value
                    node_dot_label += label_str +sep_label_sets
            node_dot_label += '"'

            return node_dot_label

        def decide_node_shape(graph, state):
            node_shape = graph.dot_node_shape['normal']

            # check if accepting states defined
            if not states._exist_accepting_states(warn=False):
                return node_shape

            # check for accepting states
            if state in states.accepting:
                node_shape = graph.dot_node_shape['accepting']

            return node_shape

        # get labeling def
        if states._exist_labels():
            label_def = states.graph._state_label_def
            label_format = states.graph._state_dot_label_format

        for (state_id, state_data) in states.graph.nodes_iter(data=True):
            state = states._int2mutant(state_id)

            if state in states.initial:
                add_incoming_edge(to_pydot_graph, state_id)

            node_shape = decide_node_shape(states.graph, state)

            # state annotation
            if states._exist_labels():
                node_dot_label = form_node_label(
                    state, state_data, label_def, label_format, wrap
                )
            else:
                node_dot_label = fill(str(state), width=wrap)
                node_dot_label.replace('\n', '\\n')

            # state boundary color
            if state_data.has_key('color'):
                node_color = state_data['color']
            else:
                node_color = '"black"'

            # state interior color
            node_style = '"rounded'
            if state_data.has_key('fillcolor'):
                node_style += ',filled"'
                fill_color = state_data['fillcolor']
            else:
                node_style += '"'
                fill_color = "none"

            # TODO option to replace with int to reduce size,
            # TODO generate separate LaTeX legend table (PNG option ?)
            to_pydot_graph.add_node(
                state_id, label=node_dot_label, shape=node_shape,
                style=node_style, color=node_color, fillcolor=fill_color)

def _transitions2dot_str(trans, to_pydot_graph):
        """Return label for dot export.
        """
        def form_edge_label(edge_data, label_def, label_format):
            edge_dot_label = '"'
            sep_label_sets = label_format['separator']
            for (label_type, label_value) in edge_data.iteritems():
                if label_type in label_def:
                    # label formatting
                    type_name = label_format[label_type]
                    sep_type_value = label_format['type?label']

                    # avoid turning strings to lists
                    if isinstance(label_value, str):
                        label_str = label_value
                    elif isinstance(label_value, Iterable):
                        label_str = str(list(label_value) )
                    else:
                        label_str = str(label_value)

                    edge_dot_label += type_name +sep_type_value
                    edge_dot_label += label_str +sep_label_sets
            edge_dot_label += '"'

            return edge_dot_label

        trans._exist_labels()

        # get labeling def
        label_def = trans.graph._transition_label_def
        label_format = trans.graph._transition_dot_label_format

        for (u, v, key, edge_data) in \
        trans.graph.edges_iter(data=True, keys=True):
            edge_dot_label = form_edge_label(edge_data,
                                             label_def, label_format)
            to_pydot_graph.add_edge(u, v, key=key,
                                    label=edge_dot_label)

def _pydot_missing(self):
        if pydot is None:
            msg = 'Attempted calling _to_pydot.\n'
            msg += 'Unavailable due to pydot not installed.\n'
            warn(msg)
            return True

        return False

def _graph2pydot(graph, wrap=10):
    """Convert (possibly labeled) state graph to dot str.

    @type graph: LabeledStateDiGraph

    @rtype: str
    """
    if graph._pydot_missing():
        return None

    dummy_nx_graph = nx.MultiDiGraph()

    _states2dot_str(graph.states, dummy_nx_graph, wrap)
    _transitions2dot_str(graph.transitions, dummy_nx_graph)

    pydot_graph = nx.to_pydot(dummy_nx_graph)
    pydot_graph.set_overlap(False)

    return pydot_graph

def graph2dot_str(graph, wrap=10):
    """Convert graph to dot string.

    Requires pydot.

    @type graph: LabeledStateDiGraph

    @rtype: str
    """
    pydot_graph = _graph2pydot(graph, wrap=wrap)

    return pydot_graph.to_string()

def save_dot(graph, path, fileformat, rankdir, prog, wrap):
    """Save state graph to dot file.

    @type graph: LabeledStateDiGraph

    @return: True upon success
    @rtype: bool
    """
    pydot_graph = _graph2pydot(graph, wrap=wrap)
    if pydot_graph is None:
        # graph2dot must have printed warning already
        return False
    pydot_graph.set_rankdir(rankdir)
    pydot_graph.set_splines('true')
    pydot_graph.write(path, format=fileformat, prog=prog)
    return True

def plot_pydot(graph, prog='dot', rankdir='LR', wrap=10):
    """Plot a networkx or pydot graph using dot.

    No files written or deleted from the disk.

    Note that all networkx graph classes are inherited
    from networkx.Graph

    see also
    --------
    dot & pydot documentation

    @param graph: to plot
    @type graph: networkx.Graph | pydot.Graph

    @param prog: GraphViz programto use
    @type prog: 'dot' | 'neato' | 'circo' | 'twopi'
        | 'fdp' | 'sfdp' | etc

    @param rankdir: direction to layout nodes
    @type rankdir: 'LR' | 'TB'
    """
    if pydot is None:
        msg = 'Using plot_pydot requires that pydot be installed.'
        warn(msg)
        return

    try:
        pydot_graph = _graph2pydot(graph, wrap=wrap)
    except:
        if isinstance(graph, nx.Graph):
            pydot_graph = nx.to_pydot(graph)
        else:
            raise TypeError('graph not networkx or pydot class.' +
                'Got instead: ' +str(type(graph) ) )
    pydot_graph.set_rankdir(rankdir)
    pydot_graph.set_splines('true')
    pydot_graph.set_bgcolor('gray')

    png_str = pydot_graph.create_png(prog=prog)

    # installed ?
    if IPython:
        dprint('IPython installed.')

        # called by IPython ?
        try:
            cfg = get_ipython().config
            dprint('Script called by IPython.')

            # Caution!!! : not ordinary dict, but IPython.config.loader.Config

            # qtconsole ?
            if cfg['IPKernelApp']:
                dprint('Within IPython QtConsole.')
                display(Image(data=png_str) )
                return True
        except:
            print('IPython installed, but not called from it.')
    else:
        dprint('IPython not installed.')

    # not called from IPython QtConsole, try Matplotlib...

    # installed ?
    if matplotlib:
        dprint('Matplotlib installed.')

        sio = StringIO()
        sio.write(png_str)
        sio.seek(0)
        img = mpimg.imread(sio)
        imgplot = plt.imshow(img, aspect='equal')
        plt.show(block=False)
        return imgplot
    else:
        dprint('Matplotlib not installed.')

    warn('Neither IPython QtConsole nor Matplotlib available.')
    return None
<commit_msg>move functions<commit_after># Copyright (c) 2013 by California Institute of Technology
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the California Institute of Technology nor
#    the names of its contributors may be used to endorse or promote
#    products derived from this software without specific prior
#    written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
"""
Convert labeled graph to dot using
pydot and custom filtering
"""
from warnings import warn
from collections import Iterable
from textwrap import fill
from cStringIO import StringIO

import networkx as nx

try:
    import pydot
except ImportError:
    warn('pydot package not found.\nHence dot export not unavailable.')
    pydot = None

try:
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    matplotlib = True
except ImportError:
    warn('matplotlib package not found.\nSo no loading of dot plots.')
    matplotlib = None

try:
    from IPython.display import display, Image
    IPython = True
except ImportError:
    warn('IPython not found.\nSo loaded dot images not inline.')
    IPython = None

from ..mathset import dprint

def _states2dot_str(states, to_pydot_graph, wrap=10):
        """Copy nodes to given Pydot graph, with attributes for dot export.
        """
        # get labeling def
        if states._exist_labels():
            label_def = states.graph._state_label_def
            label_format = states.graph._state_dot_label_format

        for (state_id, state_data) in states.graph.nodes_iter(data=True):
            state = states._int2mutant(state_id)

            if state in states.initial:
                _add_incoming_edge(to_pydot_graph, state_id)

            node_shape = _decide_node_shape(states.graph, state)

            # state annotation
            if states._exist_labels():
                node_dot_label = _form_node_label(
                    state, state_data, label_def, label_format, wrap
                )
            else:
                node_dot_label = fill(str(state), width=wrap)
                node_dot_label.replace('\n', '\\n')

            # state boundary color
            if state_data.has_key('color'):
                node_color = state_data['color']
            else:
                node_color = '"black"'

            # state interior color
            node_style = '"rounded'
            if state_data.has_key('fillcolor'):
                node_style += ',filled"'
                fill_color = state_data['fillcolor']
            else:
                node_style += '"'
                fill_color = "none"

            # TODO option to replace with int to reduce size,
            # TODO generate separate LaTeX legend table (PNG option ?)
            to_pydot_graph.add_node(
                state_id, label=node_dot_label, shape=node_shape,
                style=node_style, color=node_color, fillcolor=fill_color)

def _add_incoming_edge(g, state):
    phantom_node = 'phantominit' +str(state)

    g.add_node(phantom_node, label='""', shape='none', width='0')
    g.add_edge(phantom_node, state)

def _form_node_label(state, state_data, label_def,
                            label_format, width=10):
    # node itself
    state_str = fill(str(state), width=width)
    state_str.replace('\n', '\\n')
    node_dot_label = '"' +state_str +'\\n'

    # add node annotations from action, AP sets etc
    # other key,values in state attr_dict ignored
    sep_label_sets = label_format['separator']
    for (label_type, label_value) in state_data.iteritems():
        if label_type in label_def:
            # label formatting
            type_name = label_format[label_type]
            sep_type_value = label_format['type?label']

            # avoid turning strings to lists,
            # or non-iterables to lists
            if isinstance(label_value, str):
                label_str = fill(label_value, width=width)
            elif isinstance(label_value, Iterable): # and not str
                label_str = fill(str(list(label_value) ), width=width)
            else:
                label_str = fill(str(label_value), width=width)
            label_str.replace('\n', '\\n')

            node_dot_label += type_name +sep_type_value
            node_dot_label += label_str +sep_label_sets
    node_dot_label += '"'

    return node_dot_label

def _decide_node_shape(graph, state):
    node_shape = graph.dot_node_shape['normal']

    # check if accepting states defined
    if not graph.states._exist_accepting_states(warn=False):
        return node_shape

    # check for accepting states
    if state in graph.states.accepting:
        node_shape = graph.dot_node_shape['accepting']

    return node_shape

def _transitions2dot_str(trans, to_pydot_graph):
        """Return label for dot export.
        """
        trans._exist_labels()

        # get labeling def
        label_def = trans.graph._transition_label_def
        label_format = trans.graph._transition_dot_label_format

        for (u, v, key, edge_data) in \
        trans.graph.edges_iter(data=True, keys=True):
            edge_dot_label = _form_edge_label(edge_data,
                                             label_def, label_format)
            to_pydot_graph.add_edge(u, v, key=key,
                                    label=edge_dot_label)

def _form_edge_label(edge_data, label_def, label_format):
            edge_dot_label = '"'
            sep_label_sets = label_format['separator']
            for (label_type, label_value) in edge_data.iteritems():
                if label_type in label_def:
                    # label formatting
                    type_name = label_format[label_type]
                    sep_type_value = label_format['type?label']

                    # avoid turning strings to lists
                    if isinstance(label_value, str):
                        label_str = label_value
                    elif isinstance(label_value, Iterable):
                        label_str = str(list(label_value) )
                    else:
                        label_str = str(label_value)

                    edge_dot_label += type_name +sep_type_value
                    edge_dot_label += label_str +sep_label_sets
            edge_dot_label += '"'

            return edge_dot_label

def _pydot_missing(self):
        if pydot is None:
            msg = 'Attempted calling _to_pydot.\n'
            msg += 'Unavailable due to pydot not installed.\n'
            warn(msg)
            return True

        return False

def _graph2pydot(graph, wrap=10):
    """Convert (possibly labeled) state graph to dot str.

    @type graph: LabeledStateDiGraph

    @rtype: str
    """
    if graph._pydot_missing():
        return None

    dummy_nx_graph = nx.MultiDiGraph()

    _states2dot_str(graph.states, dummy_nx_graph, wrap)
    _transitions2dot_str(graph.transitions, dummy_nx_graph)

    pydot_graph = nx.to_pydot(dummy_nx_graph)
    pydot_graph.set_overlap(False)

    return pydot_graph

def graph2dot_str(graph, wrap=10):
    """Convert graph to dot string.

    Requires pydot.

    @type graph: LabeledStateDiGraph

    @rtype: str
    """
    pydot_graph = _graph2pydot(graph, wrap=wrap)

    return pydot_graph.to_string()

def save_dot(graph, path, fileformat, rankdir, prog, wrap):
    """Save state graph to dot file.

    @type graph: LabeledStateDiGraph

    @return: True upon success
    @rtype: bool
    """
    pydot_graph = _graph2pydot(graph, wrap=wrap)
    if pydot_graph is None:
        # graph2dot must have printed warning already
        return False
    pydot_graph.set_rankdir(rankdir)
    pydot_graph.set_splines('true')
    pydot_graph.write(path, format=fileformat, prog=prog)
    return True

def plot_pydot(graph, prog='dot', rankdir='LR', wrap=10):
    """Plot a networkx or pydot graph using dot.

    No files written or deleted from the disk.

    Note that all networkx graph classes are inherited
    from networkx.Graph

    see also
    --------
    dot & pydot documentation

    @param graph: to plot
    @type graph: networkx.Graph | pydot.Graph

    @param prog: GraphViz programto use
    @type prog: 'dot' | 'neato' | 'circo' | 'twopi'
        | 'fdp' | 'sfdp' | etc

    @param rankdir: direction to layout nodes
    @type rankdir: 'LR' | 'TB'
    """
    if pydot is None:
        msg = 'Using plot_pydot requires that pydot be installed.'
        warn(msg)
        return

    try:
        pydot_graph = _graph2pydot(graph, wrap=wrap)
    except:
        if isinstance(graph, nx.Graph):
            pydot_graph = nx.to_pydot(graph)
        else:
            raise TypeError('graph not networkx or pydot class.' +
                'Got instead: ' +str(type(graph) ) )
    pydot_graph.set_rankdir(rankdir)
    pydot_graph.set_splines('true')
    pydot_graph.set_bgcolor('gray')

    png_str = pydot_graph.create_png(prog=prog)

    # installed ?
    if IPython:
        dprint('IPython installed.')

        # called by IPython ?
        try:
            cfg = get_ipython().config
            dprint('Script called by IPython.')

            # Caution!!! : not ordinary dict, but IPython.config.loader.Config

            # qtconsole ?
            if cfg['IPKernelApp']:
                dprint('Within IPython QtConsole.')
                display(Image(data=png_str) )
                return True
        except:
            print('IPython installed, but not called from it.')
    else:
        dprint('IPython not installed.')

    # not called from IPython QtConsole, try Matplotlib...

    # installed ?
    if matplotlib:
        dprint('Matplotlib installed.')

        sio = StringIO()
        sio.write(png_str)
        sio.seek(0)
        img = mpimg.imread(sio)
        imgplot = plt.imshow(img, aspect='equal')
        plt.show(block=False)
        return imgplot
    else:
        dprint('Matplotlib not installed.')

    warn('Neither IPython QtConsole nor Matplotlib available.')
    return None
<|endoftext|>


<commit_before># Copyright (C) 2006-2022  Music Technology Group - Universitat Pompeu Fabra
#
# This file is part of Essentia
#
# Essentia is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the Affero GNU General Public License
# version 3 along with this program. If not, see http://www.gnu.org/licenses/

from essentia_test import *
import numpy as np

class TestChordsDetectionBeats(TestCase):
    def testEmpty(self):
        with self.assertRaises(EssentiaException):
            ChordsDetectionBeats()(np.zeros(shape=(0, 12), dtype=np.single), np.zeros(shape=(0,), dtype=np.single))

    @staticmethod
    def get_hpcp(audio_path):
        audio = MonoLoader(filename=audio_path)()
        w = Windowing(type='blackmanharris62', size=4096)
        spec = Spectrum(size=4096)
        s_peak = SpectralPeaks(minFrequency=30)
        hpcp_algo = HPCP()
        pcp = []
        for frame in FrameGenerator(audio, frameSize=4096, hopSize=2048):
            peak_freq, peak_mag = s_peak(spec(w(frame)))
            pcp.append(hpcp_algo(peak_freq, peak_mag))

        return np.array(pcp, dtype=np.single)

    def testSynthesized(self):
        pcp = TestChordsDetectionBeats.get_hpcp(
            join(testdata.audio_dir, "generated", "synthesised", "chord_detection", "Chord_Detection_Test.flac"))

        ticks = [i * 0.75 for i in range(25)]

        chords, strength = ChordsDetectionBeats()(pcp, ticks)
        chords_groundtruth = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
        chords_groundtruth = chords_groundtruth + [s+"m" for s in chords_groundtruth]

        for detected_chord, expected_chord, strength in zip(chords, chords_groundtruth, strength):
            self.assertEqual(detected_chord, expected_chord)
            self.assertTrue(strength > 0.8)

    def testRecorded(self):
        pcp = TestChordsDetectionBeats.get_hpcp(
            join(testdata.audio_dir, "recorded", "guitar_triads.flac")
        )

        ticks = [2.2, 5.7, 9.0, 12.0, 16.0, 18.8, 22.6, 25.4, 28.7, 31.6,
                 34.7, 38.5, 40.6, 43.0, 45.2, 48.5, 50.5, 53.3, 56.5]

        chords, strength = ChordsDetectionBeats()(pcp, ticks)
        strength_threshold = [0.8] * 18
        chords_groundtruth = ['Em', 'Am', 'Dm', 'G', 'C', 'F#', 'Bm', 'B',
                              'Em', 'Em', 'E', 'A', 'D', 'E', 'E', 'A', 'D', 'Em']
        strength_threshold[3] = 0.4

        print(chords, strength)
        for detected_chord, expected_chord, s, threshold in \
                zip(chords, chords_groundtruth, strength, strength_threshold):
            self.assertEqual(detected_chord, expected_chord)
            self.assertTrue(s > threshold)

suite = allTests(TestChordsDetectionBeats)

if __name__ == '__main__':
    TextTestRunner(verbosity=2).run(suite)
<commit_msg>More tests added, document modified<commit_after># Copyright (C) 2006-2022  Music Technology Group - Universitat Pompeu Fabra
#
# This file is part of Essentia
#
# Essentia is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the Affero GNU General Public License
# version 3 along with this program. If not, see http://www.gnu.org/licenses/

from essentia_test import *
import numpy as np

class TestChordsDetectionBeats(TestCase):
    def testEmpty(self):
        with self.assertRaises(EssentiaException):
            ChordsDetectionBeats()(np.zeros(shape=(0, 12), dtype=np.single), np.zeros(shape=(0,), dtype=np.single))

        with self.assertRaises(EssentiaException):
            ChordsDetectionBeats()(
                np.random.rand(100, 12).astype(dtype=np.single), np.zeros(shape=(0,), dtype=np.single))

        chords, strength = ChordsDetectionBeats()(
            np.zeros(shape=(0, 12), dtype=np.single), [0.75 * i for i in range(10)]
        )
        self.assertEqual(len(chords), 0)

    @staticmethod
    def get_hpcp(audio_path):
        audio = MonoLoader(filename=audio_path)()
        w = Windowing(type='blackmanharris62', size=4096)
        spec = Spectrum(size=4096)
        s_peak = SpectralPeaks(minFrequency=30)
        hpcp_algo = HPCP()
        pcp = []
        for frame in FrameGenerator(audio, frameSize=4096, hopSize=2048):
            peak_freq, peak_mag = s_peak(spec(w(frame)))
            pcp.append(hpcp_algo(peak_freq, peak_mag))

        return np.array(pcp, dtype=np.single)

    def testSynthesized(self):
        pcp = TestChordsDetectionBeats.get_hpcp(
            join(testdata.audio_dir, "generated", "synthesised", "chord_detection", "Chord_Detection_Test.flac"))

        ticks = [i * 0.75 for i in range(25)]

        chords, strength = ChordsDetectionBeats()(pcp, ticks)
        chords_groundtruth = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
        chords_groundtruth = chords_groundtruth + [s+"m" for s in chords_groundtruth]

        for detected_chord, expected_chord, strength in zip(chords, chords_groundtruth, strength):
            self.assertEqual(detected_chord, expected_chord)
            self.assertTrue(strength > 0.8)

    def testRecorded(self):
        pcp = TestChordsDetectionBeats.get_hpcp(
            join(testdata.audio_dir, "recorded", "guitar_triads.flac")
        )

        ticks = [2.2, 5.7, 9.0, 12.0, 16.0, 18.8, 22.6, 25.4, 28.7, 31.6,
                 34.7, 38.5, 40.6, 43.0, 45.2, 48.5, 50.5, 53.3, 56.5]

        chords, strength = ChordsDetectionBeats()(pcp, ticks)
        strength_threshold = [0.8] * 18
        chords_groundtruth = ['Em', 'Am', 'Dm', 'G', 'C', 'F#', 'Bm', 'B',
                              'Em', 'Em', 'E', 'A', 'D', 'E', 'E', 'A', 'D', 'Em']
        strength_threshold[3] = 0.4

        for detected_chord, expected_chord, s, threshold in \
                zip(chords, chords_groundtruth, strength, strength_threshold):
            self.assertEqual(detected_chord, expected_chord)
            self.assertTrue(s > threshold)

suite = allTests(TestChordsDetectionBeats)

if __name__ == '__main__':
    TextTestRunner(verbosity=2).run(suite)
<|endoftext|>


<commit_before>import os
import logging
from flask import Flask
from slack_sdk.web import WebClient
from slackeventsapi import SlackEventAdapter
from onboarding_tutorial import OnboardingTutorial

# Initialize a Flask app to host the events adapter
app = Flask(__name__)
slack_events_adapter = SlackEventAdapter(os.environ["SLACK_SIGNING_SECRET"], "/slack/events", app)

# Initialize a Web API client
slack_web_client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])

# For simplicity we'll store our app data in-memory with the following data structure.
# onboarding_tutorials_sent = {"channel": {"user_id": OnboardingTutorial}}
onboarding_tutorials_sent = {}

def start_onboarding(user_id: str, channel: str):
    # Create a new onboarding tutorial.
    onboarding_tutorial = OnboardingTutorial(channel)

    # Get the onboarding message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the onboarding message in Slack
    response = slack_web_client.chat_postMessage(**message)

    # Capture the timestamp of the message we've just posted so
    # we can use it to update the message after a user
    # has completed an onboarding task.
    onboarding_tutorial.timestamp = response["ts"]

    # Store the message sent in onboarding_tutorials_sent
    if channel not in onboarding_tutorials_sent:
        onboarding_tutorials_sent[channel] = {}
    onboarding_tutorials_sent[channel][user_id] = onboarding_tutorial

# ================ Team Join Event =============== #
# When the user first joins a team, the type of the event will be 'team_join'.
# Here we'll link the onboarding_message callback to the 'team_join' event.
@slack_events_adapter.on("team_join")
def onboarding_message(payload):
    """Create and send an onboarding welcome message to new users. Save the
    time stamp of this message so we can update this message in the future.
    """
    event = payload.get("event", {})

    # Get the id of the Slack user associated with the incoming event
    user_id = event.get("user", {}).get("id")

    # Open a DM with the new user.
    response = slack_web_client.conversations_open(users=user_id)
    channel = response["channel"]["id"]

    # Post the onboarding message.
    start_onboarding(user_id, channel)

# ============= Reaction Added Events ============= #
# When a users adds an emoji reaction to the onboarding message,
# the type of the event will be 'reaction_added'.
# Here we'll link the update_emoji callback to the 'reaction_added' event.
@slack_events_adapter.on("reaction_added")
def update_emoji(payload):
    """Update the onboarding welcome message after receiving a "reaction_added"
    event from Slack. Update timestamp for welcome message as well.
    """
    event = payload.get("event", {})

    channel_id = event.get("item", {}).get("channel")
    user_id = event.get("user")

    if channel_id not in onboarding_tutorials_sent:
        return

    # Get the original tutorial sent.
    onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id]

    # Mark the reaction task as completed.
    onboarding_tutorial.reaction_task_completed = True

    # Get the new message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the updated message in Slack
    updated_message = slack_web_client.chat_update(**message)

    # Update the timestamp saved on the onboarding tutorial object
    onboarding_tutorial.timestamp = updated_message["ts"]

# =============== Pin Added Events ================ #
# When a users pins a message the type of the event will be 'pin_added'.
# Here we'll link the update_pin callback to the 'pin_added' event.
@slack_events_adapter.on("pin_added")
def update_pin(payload):
    """Update the onboarding welcome message after receiving a "pin_added"
    event from Slack. Update timestamp for welcome message as well.
    """
    event = payload.get("event", {})

    channel_id = event.get("channel_id")
    user_id = event.get("user")

    # Get the original tutorial sent.
    onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id]

    # Mark the pin task as completed.
    onboarding_tutorial.pin_task_completed = True

    # Get the new message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the updated message in Slack
    updated_message = slack_web_client.chat_update(**message)

    # Update the timestamp saved on the onboarding tutorial object
    onboarding_tutorial.timestamp = updated_message["ts"]

# ============== Message Events ============= #
# When a user sends a DM, the event type will be 'message'.
# Here we'll link the message callback to the 'message' event.
@slack_events_adapter.on("message")
def message(payload):
    """Display the onboarding welcome message after receiving a message
    that contains "start".
    """
    event = payload.get("event", {})

    channel_id = event.get("channel")
    user_id = event.get("user")
    text = event.get("text")

    if text and text.lower() == "start":
        return start_onboarding(user_id, channel_id)

if __name__ == "__main__":
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.StreamHandler())
    app.run(port=3000)
<commit_msg>the timestamp for Slack-post never changes, it is effectivly an ID<commit_after>import os
import logging
from flask import Flask
from slack_sdk.web import WebClient
from slackeventsapi import SlackEventAdapter
from onboarding_tutorial import OnboardingTutorial

# Initialize a Flask app to host the events adapter
app = Flask(__name__)
slack_events_adapter = SlackEventAdapter(os.environ["SLACK_SIGNING_SECRET"], "/slack/events", app)

# Initialize a Web API client
slack_web_client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])

# For simplicity we'll store our app data in-memory with the following data structure.
# onboarding_tutorials_sent = {"channel": {"user_id": OnboardingTutorial}}
onboarding_tutorials_sent = {}

def start_onboarding(user_id: str, channel: str):
    # Create a new onboarding tutorial.
    onboarding_tutorial = OnboardingTutorial(channel)

    # Get the onboarding message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the onboarding message in Slack
    response = slack_web_client.chat_postMessage(**message)

    # Capture the timestamp of the message we've just posted so
    # we can use it to update the message after a user
    # has completed an onboarding task.
    onboarding_tutorial.timestamp = response["ts"]

    # Store the message sent in onboarding_tutorials_sent
    if channel not in onboarding_tutorials_sent:
        onboarding_tutorials_sent[channel] = {}
    onboarding_tutorials_sent[channel][user_id] = onboarding_tutorial

# ================ Team Join Event =============== #
# When the user first joins a team, the type of the event will be 'team_join'.
# Here we'll link the onboarding_message callback to the 'team_join' event.
@slack_events_adapter.on("team_join")
def onboarding_message(payload):
    """Create and send an onboarding welcome message to new users. Save the
    time stamp of this message so we can update this message in the future.
    """
    event = payload.get("event", {})

    # Get the id of the Slack user associated with the incoming event
    user_id = event.get("user", {}).get("id")

    # Open a DM with the new user.
    response = slack_web_client.conversations_open(users=user_id)
    channel = response["channel"]["id"]

    # Post the onboarding message.
    start_onboarding(user_id, channel)

# ============= Reaction Added Events ============= #
# When a users adds an emoji reaction to the onboarding message,
# the type of the event will be 'reaction_added'.
# Here we'll link the update_emoji callback to the 'reaction_added' event.
@slack_events_adapter.on("reaction_added")
def update_emoji(payload):
    """Update the onboarding welcome message after receiving a "reaction_added"
    event from Slack. Update timestamp for welcome message as well.
    """
    event = payload.get("event", {})

    channel_id = event.get("item", {}).get("channel")
    user_id = event.get("user")

    if channel_id not in onboarding_tutorials_sent:
        return

    # Get the original tutorial sent.
    onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id]

    # Mark the reaction task as completed.
    onboarding_tutorial.reaction_task_completed = True

    # Get the new message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the updated message in Slack
    updated_message = slack_web_client.chat_update(**message)

# =============== Pin Added Events ================ #
# When a users pins a message the type of the event will be 'pin_added'.
# Here we'll link the update_pin callback to the 'pin_added' event.
@slack_events_adapter.on("pin_added")
def update_pin(payload):
    """Update the onboarding welcome message after receiving a "pin_added"
    event from Slack. Update timestamp for welcome message as well.
    """
    event = payload.get("event", {})

    channel_id = event.get("channel_id")
    user_id = event.get("user")

    # Get the original tutorial sent.
    onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id]

    # Mark the pin task as completed.
    onboarding_tutorial.pin_task_completed = True

    # Get the new message payload
    message = onboarding_tutorial.get_message_payload()

    # Post the updated message in Slack
    updated_message = slack_web_client.chat_update(**message)

# ============== Message Events ============= #
# When a user sends a DM, the event type will be 'message'.
# Here we'll link the message callback to the 'message' event.
@slack_events_adapter.on("message")
def message(payload):
    """Display the onboarding welcome message after receiving a message
    that contains "start".
    """
    event = payload.get("event", {})

    channel_id = event.get("channel")
    user_id = event.get("user")
    text = event.get("text")

    if text and text.lower() == "start":
        return start_onboarding(user_id, channel_id)

if __name__ == "__main__":
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.StreamHandler())
    app.run(port=3000)
<|endoftext|>