| 654 | | class Transformer(Command): |
| 655 | | """The base class for transformer commands.""" |
| 656 | | |
| 657 | | supported_input = [PRODUCER] |
| 658 | | supported_output = [PRODUCER, RENDERER] |
| 659 | | |
| 660 | | def produce(self, connection=None, data=None): |
| 661 | | """Perform the command.""" |
| 662 | | if connection is None: |
| 663 | | connection = self.app.make_connection() |
| 664 | | try: |
| 665 | | product = self.execute(connection, data) |
| 666 | | except: |
| 667 | | connection.rollback() |
| 668 | | raise |
| 669 | | else: |
| 670 | | connection.commit() |
| 671 | | else: |
| 672 | | product = self.execute(connection, data) |
| 673 | | return product |
| 674 | | |
| 675 | | def execute(self, connection, data=None): |
| 676 | | """Execute the previous command and transform the output.""" |
| 677 | | product = self.input.execute(connection, data) |
| 678 | | if not product: |
| 679 | | raise errors.InvalidQuery("no output data is generated", self.mark) |
| 680 | | return self.convert(product) |
| 681 | | |
| 682 | | def convert(self, product): |
| 683 | | assert False, "abstract method" |
| 684 | | |
| 685 | | def render(self, environ): |
| 686 | | """Generate HTTP output.""" |
| 687 | | container = self.app.make_commands() |
| 688 | | command = container.by_http_accept(environ, self) |
| 689 | | return command.render(environ) |
| 690 | | |
| 691 | | |
| 692 | | class FlattenGenerator(object): |
| 693 | | |
| 694 | | def __init__(self, generator, children, widths): |
| 695 | | self.generator = generator |
| 696 | | self.children = children |
| 697 | | self.widths = widths |
| 698 | | try: |
| 699 | | label, values = generator.next() |
| 700 | | except StopIteration: |
| 701 | | label, values = None, None |
| 702 | | self.current_label = label |
| 703 | | self.current_values = values |
| 704 | | |
| 705 | | def __iter__(self): |
| 706 | | for row in self.flatten_children(None): |
| 707 | | yield (0, row) |
| 708 | | |
| 709 | | def flatten_children(self, label): |
| 710 | | if not self.children[label]: |
| 711 | | if label is not None: |
| 712 | | yield [] |
| 713 | | return |
| 714 | | children_rows = [] |
| 715 | | for child_label in self.children[label]: |
| 716 | | is_last = (child_label == self.children[label][-1]) |
| 717 | | child_rows = self.flatten_segment(child_label, is_last) |
| 718 | | children_rows.append(child_rows) |
| 719 | | is_first = (label is not None) |
| 720 | | while True: |
| 721 | | is_empty = (not is_first) |
| 722 | | is_first = False |
| 723 | | row = [] |
| 724 | | for child_rows in children_rows: |
| 725 | | child_row, is_child_empty = child_rows.next() |
| 726 | | row += child_row |
| 727 | | is_empty &= is_child_empty |
| 728 | | if is_empty: |
| 729 | | return |
| 730 | | yield row |
| 731 | | |
| 732 | | def flatten_segment(self, label, is_last=False): |
| 733 | | iterator = self.emit_segment(label) |
| 734 | | if not is_last: |
| 735 | | iterator = list(iterator) |
| 736 | | return self.loop_segment(label, iterator) |
| 737 | | |
| 738 | | def emit_segment(self, label): |
| 739 | | while self.current_label == label: |
| 740 | | base = self.current_values |
| 741 | | try: |
| 742 | | next_label, next_values = self.generator.next() |
| 743 | | except StopIteration: |
| 744 | | next_label, next_values = None, None |
| 745 | | self.current_label = next_label |
| 746 | | self.current_values = next_values |
| 747 | | for children in self.flatten_children(label): |
| 748 | | yield base+children |
| 749 | | |
| 750 | | def loop_segment(self, label, iterator): |
| 751 | | for row in iterator: |
| 752 | | yield (row, False) |
| 753 | | empty = [None]*self.widths[label] |
| 754 | | while True: |
| 755 | | yield (empty, True) |
| 756 | | |
| 757 | | |
| 758 | | class Flatten(Transformer): |
| 759 | | """The ``flatten()`` transformer.""" |
| 760 | | |
| 761 | | name = ('htsql', 'flatten') |
| 762 | | |
| 763 | | def convert(self, product): |
| 764 | | if len(product.profile.segments) <= 1: |
| 765 | | return product |
| 766 | | new_generator = self.convert_generator(product.generator, |
| 767 | | product.profile) |
| 768 | | new_profile = self.convert_profile(product.profile) |
| 769 | | new_product = Product(new_generator, new_profile, None) |
| 770 | | return new_product |
| 771 | | |
| 772 | | def convert_generator(self, generator, profile): |
| 773 | | children = { None: [] } |
| 774 | | for label, segment in enumerate(profile.segments): |
| 775 | | parent_label = None |
| 776 | | if segment.parent is not None: |
| 777 | | parent_label = profile.segments.index(segment.parent) |
| 778 | | children[label] = [] |
| 779 | | children[parent_label].append(label) |
| 780 | | widths = {} |
| 781 | | for label, segment in reversed(list(enumerate(profile.segments))): |
| 782 | | parent_label = None |
| 783 | | if segment.parent is not None: |
| 784 | | parent_label = profile.segments.index(segment.parent) |
| 785 | | widths.setdefault(label, 0) |
| 786 | | widths.setdefault(parent_label, 0) |
| 787 | | widths[label] += len(segment.elements) |
| 788 | | widths[parent_label] += widths[label] |
| 789 | | generator = FlattenGenerator(generator, children, widths) |
| 790 | | return iter(generator) |
| 791 | | |
| 792 | | def convert_profile(self, profile): |
| 793 | | root_segments = [segment for segment in profile.segments |
| 794 | | if segment.parent is None] |
| 795 | | is_forest = (len(root_segments) != 1) |
| 796 | | if is_forest: |
| 797 | | new_segment_title = descriptions.Title('') |
| 798 | | else: |
| 799 | | new_segment_title = root_segments[0].title |
| 800 | | new_elements = [] |
| 801 | | for segment in profile.segments: |
| 802 | | parent_title = None |
| 803 | | if segment.parent is not None or is_forest: |
| 804 | | parent_title = segment.title |
| 805 | | for element in segment.elements: |
| 806 | | new_title = element.title |
| 807 | | if parent_title is not None: |
| 808 | | new_title = descriptions.CompositeTitle(parent_title, |
| 809 | | new_title) |
| 810 | | new_element = descriptions.Element(new_title, element.domain, |
| 811 | | element.entity) |
| 812 | | new_elements.append(new_element) |
| 813 | | new_segment = descriptions.Segment(None, new_segment_title, |
| 814 | | new_elements) |
| 815 | | new_request = descriptions.Request(profile.authority, |
| 816 | | profile.perspective, |
| 817 | | profile.title, [new_segment]) |
| 818 | | return new_request |
| 819 | | |
| 820 | | |
| 821 | | class Pivot(Command): |
| 822 | | """The ``pivot()`` transformer.""" |
| 823 | | |
| 824 | | name = ('htsql', 'pivot') |
| 825 | | supported_input = [PRODUCER] |
| 826 | | supported_output = [PRODUCER, RENDERER] |
| 827 | | declaration = [ |
| 828 | | ('on', INT(), -2), |
| 829 | | ('by', INT(), -1), |
| 830 | | ] |
| 831 | | |
| 832 | | def produce(self, connection=None, data=None): |
| 833 | | """Perform the command.""" |
| 834 | | if connection is None: |
| 835 | | connection = self.app.make_connection() |
| 836 | | try: |
| 837 | | product = self.execute(connection, data) |
| 838 | | except: |
| 839 | | connection.rollback() |
| 840 | | raise |
| 841 | | else: |
| 842 | | connection.commit() |
| 843 | | else: |
| 844 | | product = self.execute(connection, data) |
| 845 | | return product |
| 846 | | |
| 847 | | def execute(self, connection, data=None): |
| 848 | | """Execute the request.""" |
| 849 | | product = self.input.execute(connection, data) |
| 850 | | if product is None: |
| 851 | | raise errors.InvalidQuery("null input", self.mark) |
| 852 | | if len(product.description.segments) != 1: |
| 853 | | raise errors.InvalidQuery("pivot requires a single segment", |
| 854 | | self.mark) |
| 855 | | segment = product.description.segments[0] |
| 856 | | on = self.on |
| 857 | | if on > 0: |
| 858 | | on -= 1 |
| 859 | | else: |
| 860 | | on += len(segment.elements) |
| 861 | | by = self.by |
| 862 | | if by > 0: |
| 863 | | by -= 1 |
| 864 | | else: |
| 865 | | by += len(segment.elements) |
| 866 | | if not (0 <= on < len(segment.elements) and |
| 867 | | 0 <= by < len(segment.elements) and on != by): |
| 868 | | raise errors.InvalidQuery("invalid arguments", self.mark) |
| 869 | | return self.convert(product, on, by) |
| 870 | | |
| 871 | | def convert(self, product, on, by): |
| 872 | | rows = list(product) |
| 873 | | pivot_values = set() |
| 874 | | for row in rows: |
| 875 | | pivot_values.add(row[on]) |
| 876 | | pivot_values = list(pivot_values) |
| 877 | | pivot_values.sort() |
| 878 | | description = product.description |
| 879 | | segment_description = description.segments[0] |
| 880 | | new_elements = [] |
| 881 | | domain = segment_description.elements[by].domain |
| 882 | | for index, element in enumerate(segment_description.elements): |
| 883 | | if index == on: |
| 884 | | for value in pivot_values: |
| 885 | | title = descriptions.Title(str(value)) |
| 886 | | element = descriptions.Element(title, domain) |
| 887 | | new_elements.append(element) |
| 888 | | elif index != by: |
| 889 | | new_elements.append(element) |
| 890 | | new_segment_description = descriptions.Segment( |
| 891 | | segment_description.title, new_elements) |
| 892 | | new_description = descriptions.Request( |
| 893 | | description.title, [new_segment_description]) |
| 894 | | key_to_position = {} |
| 895 | | key_to_values = {} |
| 896 | | for row in rows: |
| 897 | | key = tuple(value for index, value in enumerate(row) |
| 898 | | if index not in (on, by)) |
| 899 | | if key not in key_to_values: |
| 900 | | key_to_position[key] = len(key_to_values) |
| 901 | | key_to_values[key] = [None]*len(pivot_values) |
| 902 | | values = key_to_values[key] |
| 903 | | value_index = pivot_values.index(row[on]) |
| 904 | | if row[by] is not None: |
| 905 | | if values[value_index] is not None \ |
| 906 | | and values[value_index] != row[by]: |
| 907 | | raise errors.InvalidQuery("duplicate row", self.mark) |
| 908 | | values[value_index] = row[by] |
| 909 | | new_rows = [] |
| 910 | | for key in sorted(key_to_values, key=(lambda k: key_to_position[k])): |
| 911 | | values = key_to_values[key] |
| 912 | | key = list(key) |
| 913 | | key[on:on] = values |
| 914 | | new_rows.append((0, key)) |
| 915 | | return Product(iter(new_rows), new_description, None) |
| 916 | | |
| 917 | | def render(self, environ): |
| 918 | | """Generate HTTP output.""" |
| 919 | | container = self.app.make_commands() |
| 920 | | command = container.by_http_accept(environ, self) |
| 921 | | return command.render(environ) |
| 922 | | |
| 923 | | |