pybufrkit/bufr.py
class BufrMessage(object):
    """
    This class represents a single BUFR message that is comprised of different
    sections. Note this is different from BufrTemplateData which is only part of
    the overall message and dedicates to data associated to the Template.

    Properties of this class are proxies to actual fields of its sections. They
    are set by the sections when they are processed. The proxy approach allows
    these properties to be referenced in a consistent way no matter where they
    actually come from. This makes sections loosely coupled, i.e. one section
    does not need to know about other sections, and free to change if needed.
    """

    def __init__(self, filename=''):
        self.filename = filename
        self.sections = []
        self.serialized_bytes = None
        self.table_group_key = None
        self._edition = None
        # Default to zero for edition 1 which does not have this parameter
        self._master_table_number = SectionParameter('master_table_number',
                                                     0, 'unit', None, True, 0)
        # Default to zero for edition 1 and 2 which do not have this parameter
        self._originating_subcentre = SectionParameter('originating_subcentre',
                                                       0, 'unit', None, True, 0)

    def add_section(self, section):
        """
        Add a section to the message

        :param BufrSection section: The Bufr Section to add
        """
        self.sections.append(section)

    def build_template(self, tables_root_dir, normalize=1):
        """
        Build the BufrTemplate object using the list of unexpanded descriptors
        and corresponding table group.

        :param tables_root_dir: The root directory to find BUFR tables
        :param normalize: Whether to use some default table group if the specific
            one is not available.
        :return: A tuple of BufrTemplate and the associated TableGroup
        """
        table_group = TableGroupCacheManager.get_table_group(
            tables_root_dir=tables_root_dir,
            master_table_number=self.master_table_number.value,
            originating_centre=self.originating_centre.value,
            originating_subcentre=self.originating_subcentre.value,
            master_table_version=self.master_table_version.value,
            local_table_version=self.local_table_version.value,
            normalize=normalize
        )
        self.table_group_key = table_group.key

        return table_group.template_from_ids(*self.unexpanded_descriptors.value), table_group

    def wire(self):
        """
        Wire the flat list of descriptors and values to a full hierarchical
        structure. Also allocate all attributes to their corresponding
        descriptors.
        """
        self.template_data.value.wire()

    def subset(self, subset_indices):
        if max(subset_indices) >= self.n_subsets.value:
            raise PyBufrKitError('maximum subset index out of range')
        if min(subset_indices) < 0:
            raise PyBufrKitError('minimum subset index out of range')

        data = []
        for section in self.sections:
            section_data = []
            for parameter in section:
                if parameter.type == PARAMETER_TYPE_TEMPLATE_DATA:
                    section_data.append(
                        [v for i, v in enumerate(parameter.value.decoded_values_all_subsets)
                         if i in subset_indices]
                    )
                else:
                    section_data.append(
                        len(subset_indices) if parameter.name == 'n_subsets'
                        else parameter.value
                    )
            data.append(section_data)
        return data

    # Proxy properties follows
    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, new_value):
        self._length = new_value

    @property
    def edition(self):
        return self._edition

    @edition.setter
    def edition(self, new_value):
        self._edition = new_value

    @property
    def master_table_number(self):
        return self._master_table_number

    @master_table_number.setter
    def master_table_number(self, new_value):
        self._master_table_number = new_value

    @property
    def originating_centre(self):
        return self._originating_centre

    @originating_centre.setter
    def originating_centre(self, new_value):
        self._originating_centre = new_value

    @property
    def originating_subcentre(self):
        return self._originating_subcentre

    @originating_subcentre.setter
    def originating_subcentre(self, new_value):
        self._originating_subcentre = new_value

    @property
    def master_table_version(self):
        return self._master_table_version

    @master_table_version.setter
    def master_table_version(self, new_value):
        self._master_table_version = new_value

    @property
    def local_table_version(self):
        return self._local_table_version

    @local_table_version.setter
    def local_table_version(self, new_value):
        self._local_table_version = new_value

    @property
    def timestamp(self):
        # TODO: 1900 correct?
        return datetime(
            self.year.value if self.edition.value >= 4 else self.year.value + 1900,
            self.month.value, self.day.value,
            self.hour.value, self.minute.value, self.second.value
        )

    @property
    def year(self):
        return self._year

    @year.setter
    def year(self, new_value):
        self._year = new_value

    @property
    def month(self):
        return self._month

    @month.setter
    def month(self, new_value):
        self._month = new_value

    @property
    def day(self):
        return self._day

    @day.setter
    def day(self, new_value):
        self._day = new_value

    @property
    def hour(self):
        return self._hour

    @hour.setter
    def hour(self, new_value):
        self._hour = new_value

    @property
    def minute(self):
        return self._minute

    @minute.setter
    def minute(self, new_value):
        self._minute = new_value

    @property
    def second(self):
        return self._second

    @second.setter
    def second(self, new_value):
        self._second = new_value

    @property
    def is_section2_presents(self):
        return self._is_section2_presents

    @is_section2_presents.setter
    def is_section2_presents(self, new_value):
        self._is_section2_presents = new_value

    @property
    def data_category(self):
        return self._data_category

    @data_category.setter
    def data_category(self, new_value):
        self._data_category = new_value

    @property
    def n_subsets(self):
        return self._n_subsets

    @n_subsets.setter
    def n_subsets(self, new_value):
        self._n_subsets = new_value

    @property
    def is_observation(self):
        return self._is_observation

    @is_observation.setter
    def is_observation(self, new_value):
        self._is_observation = new_value

    @property
    def is_compressed(self):
        return self._is_compressed

    @is_compressed.setter
    def is_compressed(self, new_value):
        self._is_compressed = new_value

    @property
    def unexpanded_descriptors(self):
        return self._unexpanded_descriptors

    @unexpanded_descriptors.setter
    def unexpanded_descriptors(self, new_value):
        self._unexpanded_descriptors = new_value

    @property
    def template_data(self):
        return self._template_data

    @template_data.setter
    def template_data(self, new_value):
        self._template_data = new_value
pybufrkit/descriptors.py
class OperatorDescriptor(Descriptor):
    """
    Operator Descriptor 2XXYYY
    """

    def __init__(self, id_):
        super(OperatorDescriptor, self).__init__(id_)

    @property
    def operator_code(self):
        return self.id // 1000

    @property
    def operand_value(self):
        return self.id % 1000
pybufrkit/constants.py
INDENT_CHARS = '    '
pybufrkit/descriptors.py
class AssociatedDescriptor(Descriptor):
    """
    Associated field for element descriptor

    :param int nbits: Number of bits used by this descriptor.
    """

    def __init__(self, id_, nbits):
        super(AssociatedDescriptor, self).__init__(id_)
        self.nbits = nbits
        # Dummy unit so it does not fail on unit checking in other functions
        self.unit = 'ASSOCIATED'

    def __str__(self):
        return 'A{:05d}'.format(self.id)
pybufrkit/templatedata.py
class TemplateData(object):
    """
    This class is dedicated to the data section of a BUFR message and produces a
    fully hierarchical structure for the otherwise flat list of decoded
    descriptors and values. Attributes like associated fields and statistical
    values are properly allocated to their corresponding referred elements.
    """

    def __init__(self,
                 template,
                 is_compressed,
                 decoded_descriptors_all_subsets,
                 decoded_values_all_subsets,
                 bitmap_links_all_subsets):

        self.template = template
        self.is_compressed = is_compressed
        self.decoded_descriptors_all_subsets = decoded_descriptors_all_subsets
        self.decoded_values_all_subsets = decoded_values_all_subsets
        self.bitmap_links_all_subsets = bitmap_links_all_subsets
        self.n_subsets = len(self.decoded_descriptors_all_subsets)

        # The most important difference between decoded nodes and decoded
        # descriptors is that the decoded descriptors are flat while decoded
        # nodes are nested. For an example, the replication is a single node
        # (with its own member nodes) in the decoded nodes but a series of flat
        # descriptors in decoded descriptors.

        if self.is_compressed:
            self.decoded_nodes_all_subsets = [[]] * self.n_subsets
        else:
            self.decoded_nodes_all_subsets = [[] for _ in range(self.n_subsets)]

        self.decoded_nodes = [] if not self.decoded_nodes_all_subsets else self.decoded_nodes_all_subsets[0]

        self._is_wired = False

    def wire(self):
        """
        From the flat list of descriptors and values, construct a fully
        hierarchical structure of data including sequence descriptors and
        correctly set bitmapped values to their corresponding node as
        attributes.
        """
        # Do not wire more than once
        if self._is_wired:
            return
        else:
            self._is_wired = True

        # For compressed data, the wiring is the same for all subsets.
        n_subsets = 1 if self.is_compressed else self.n_subsets

        for idx_subset in range(n_subsets):
            self.decoded_nodes = self.decoded_nodes_all_subsets[idx_subset]
            self.decoded_descriptors = self.decoded_descriptors_all_subsets[idx_subset]
            self.decoded_values = self.decoded_values_all_subsets[idx_subset]
            self.bitmap_links = self.bitmap_links_all_subsets[idx_subset]

            # The index is used to index into the decoded descriptors/values.
            # The is used to have links between flat indices and nested nodes,
            # so that attributes can be associated to their bit-mapped nodes.
            self.index_to_node = {}
            self.next_index = functools.partial(next, itertools.count())
            self.nbits_associated_list = []  # 204 YYY
            self.data_not_present_count = 0  # 221
            self.waiting_for_qa_info_meaning = False
            self.waiting_for_1st_order_stats_meaning = False
            self.waiting_for_difference_stats_meaning = False

            self.wire_members(self.template.members)

            # release memory
            del self.index_to_node

    def get_next_descriptor_and_index(self):
        index = self.next_index()
        return self.decoded_descriptors[index], index

    def add_node(self, node):
        self.decoded_nodes.append(node)
        if not isinstance(node, NoValueDataNode):
            self.index_to_node[node.index] = node
        return node

    def add_value_node(self):
        node = ValueDataNode(*self.get_next_descriptor_and_index())
        self.decoded_nodes.append(node)
        self.index_to_node[node.index] = node
        return node

    def add_delayed_replication_factor_node(self):
        factor_node = ValueDataNode(*self.get_next_descriptor_and_index())
        self.index_to_node[factor_node.index] = factor_node
        return factor_node

    def wire_element_descriptor(self, descriptor):
        # Read associated field if exists
        if self.nbits_associated_list and descriptor.X != 31:
            assoc_node = AssociatedFieldNode(*self.get_next_descriptor_and_index())
            assoc_node.add_attribute(self.associated_field_meaning)
            node = ValueDataNode(*self.get_next_descriptor_and_index())
            node.add_attribute(assoc_node)
            self.add_node(node)

        else:
            if descriptor.X == 33 and self.waiting_for_qa_info_meaning:
                node = self.add_node(QualityInfoNode(*self.get_next_descriptor_and_index()))
                self.index_to_node[self.bitmap_links[node.index]].add_attribute(node)

            else:
                node = self.add_value_node()
                if descriptor.id == 31021 and self.nbits_associated_list:
                    self.associated_field_meaning = node

                elif descriptor.id == 8023 and self.waiting_for_1st_order_stats_meaning:
                    self.first_order_stats_meaning = node
                    self.waiting_for_1st_order_stats_meaning = False

                elif descriptor.id == 8024 and self.waiting_for_difference_stats_meaning:
                    self.difference_stats_meaning = node
                    self.waiting_for_difference_stats_meaning = False

    def wire_fixed_replication_descriptor(self, descriptor):
        """
        :param FixedReplicationDescriptor descriptor:
        """
        fixed_replication_node = FixedReplicationNode(descriptor)
        nodes = self.decoded_nodes
        self.decoded_nodes = fixed_replication_node.members
        for _ in range(descriptor.n_repeats):
            self.wire_members(descriptor.members)
        self.decoded_nodes = nodes

        self.add_node(fixed_replication_node)

    def wire_delayed_replication_descriptor(self, descriptor):
        """
        :param DelayedReplicationDescriptor descriptor:
        """
        delayed_replication_node = DelayedReplicationNode(descriptor)
        nodes = self.decoded_nodes
        self.decoded_nodes = delayed_replication_node.members

        # Add the delayed replication factor node to the node indices as well as
        # it is possible to have attributes attached to it. For an example,
        # ocea_133.bufr from benchmark data has QA info attached to 031001.
        factor_node = self.add_delayed_replication_factor_node()
        delayed_replication_node.factor = factor_node
        for _ in range(self.decoded_values[factor_node.index]):
            self.wire_members(descriptor.members)
        self.decoded_nodes = nodes

        self.add_node(delayed_replication_node)

    def wire_sequence_descriptor(self, descriptor):
        sequence_node = SequenceNode(descriptor)
        nodes = self.decoded_nodes
        self.decoded_nodes = sequence_node.members
        self.wire_members(descriptor.members)
        self.decoded_nodes = nodes

        self.add_node(sequence_node)

    def wire_bitmap_attribute(self, attr_node):
        self.index_to_node[self.bitmap_links[attr_node.index]].add_attribute(attr_node)

    def wire_operator_descriptor(self, descriptor):
        """
        :param OperatorDescriptor descriptor:
        :return:
        """
        operator_code, operand_value = descriptor.operator_code, descriptor.operand_value

        if operator_code in (201, 202, 203, 206, 207, 208,):
            # nbits offset, scale offset, new refval, skip local, increment, change string length
            self.add_node(NoValueDataNode(descriptor))

        elif operator_code == 204:  # associated field
            if operand_value == 0:
                self.nbits_associated_list.pop()
            else:
                self.nbits_associated_list.append(operand_value)
            self.add_node(NoValueDataNode(descriptor))

        elif operator_code == 205:  # read string of YYY bytes
            self.add_value_node()

        # Data not present for following YYY descriptors except class 0-9 and 31
        elif operator_code == 221:
            self.data_not_present_count = operand_value
            self.add_node(NoValueDataNode(descriptor))

        elif operator_code == 222:  # quality info follows
            self.waiting_for_qa_info_meaning = True
            self.add_value_node()

        elif operator_code == 223:  # substituted value
            self.waiting_for_qa_info_meaning = False
            if operand_value == 0:
                self.add_value_node()
            else:
                node = self.add_node(SubstitutionNode(*self.get_next_descriptor_and_index()))
                self.wire_bitmap_attribute(node)

        elif operator_code == 224:  # 1st order stats
            self.waiting_for_qa_info_meaning = False
            if operand_value == 0:
                self.waiting_for_1st_order_stats_meaning = True
                self.add_value_node()
            else:
                node = self.add_node(FirstOrderStatsNode(*self.get_next_descriptor_and_index()))
                node.add_attribute(self.first_order_stats_meaning)
                self.wire_bitmap_attribute(node)

        elif operator_code == 225:  # difference stats
            self.waiting_for_qa_info_meaning = False
            if operand_value == 0:
                self.waiting_for_difference_stats_meaning = True
                self.add_value_node()
            else:
                node = self.add_node(DifferenceStatsNode(*self.get_next_descriptor_and_index()))
                node.add_attribute(self.difference_stats_meaning)
                self.wire_bitmap_attribute(node)

        elif operator_code == 232:  # replaced/retained value
            self.waiting_for_qa_info_meaning = False
            if operand_value == 0:
                self.add_value_node()
            else:
                node = self.add_node(ReplacementNode(*self.get_next_descriptor_and_index()))
                self.wire_bitmap_attribute(node)

        elif operator_code == 235:  # cancel all backwards data reference
            self.waiting_for_qa_info_meaning = False
            self.add_node(NoValueDataNode(descriptor))

        elif operator_code == 236:
            self.add_value_node()

        elif operator_code == 237:
            self.add_value_node()

        else:  # TODO: 241, 242, 243
            raise NotImplementedError('Operator Descriptor {} not implemented'.format(descriptor))

    def wire_skippable_local_descriptor(self):
        self.add_value_node()

    def wire_members(self, members):
        for member in members:

            # 221 YYY data not present for following YYY descriptors except class 0-9 and 31
            if self.data_not_present_count:
                self.data_not_present_count -= 1
                if isinstance(member, ElementDescriptor):
                    X = member.X
                    if not (1 <= X <= 9 or X == 31):  # skipping
                        self.add_node(NoValueDataNode(member))
                        continue

            # Now process normally
            if isinstance(member, ElementDescriptor):
                self.wire_element_descriptor(member)

            elif isinstance(member, FixedReplicationDescriptor):
                self.wire_fixed_replication_descriptor(member)

            elif isinstance(member, DelayedReplicationDescriptor):
                self.wire_delayed_replication_descriptor(member)

            elif isinstance(member, OperatorDescriptor):
                self.wire_operator_descriptor(member)

            elif isinstance(member, SequenceDescriptor):
                self.wire_sequence_descriptor(member)

            elif isinstance(member, SkippedLocalDescriptor):
                self.wire_skippable_local_descriptor()

            elif isinstance(member, UndefinedElementDescriptor):
                # TODO: assume any undefined element descriptor here is a skipped local
                self.wire_skippable_local_descriptor()

            else:
                raise PyBufrKitError('Cannot wire descriptor type: {}'.format(type(member)))
pybufrkit/descriptors.py
class Descriptor(object):
    """
    This class is the base class of all BUFR descriptors. It provides common
    machinery for Descriptors.

    :param int id_: The descriptor ID.
    """

    def __init__(self, id_):
        self.id = id_

    def __str__(self):
        return '{:06d}'.format(self.id)

    def __repr__(self):
        return '<{}>'.format(self)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.id == other.id

    @property
    def F(self):
        """
        The F value of the descriptor.
        """
        return self.id // 100000

    @property
    def X(self):
        """
        The X value of the descriptor.
        """
        return self.id // 1000 % 100

    @property
    def Y(self):
        """
        THe Y value of the descriptor.
        """
        return self.id % 1000

    def accept(self, visitor):
        visitor.visit_descriptor(self)
pybufrkit/templatedata.py
class SequenceNode(NoValueDataNode):
    def __init__(self, descriptor):
        super(SequenceNode, self).__init__(descriptor)
        self.members = []
pybufrkit/descriptors.py
class FixedReplicationDescriptor(ReplicationDescriptor):
    """
    Fixed replication Descriptor 1XXYYY
    """

    def __init__(self, id_, members=None):
        super(FixedReplicationDescriptor, self).__init__(id_, members)

    @property
    def n_repeats(self):
        """
        Number of times to perform the replication. This value is decoded
        directly from the descriptor ID.
        """
        return self.id % 1000

    def accept(self, visitor):
        visitor.visit_replication_descriptor(self)
        if not self.members:
            visitor.visit_replication_descriptor_end(self)
            return
        for member in self.members:
            member.accept(visitor)
        visitor.visit_replication_descriptor_end(self)
pybufrkit/descriptors.py
class MarkerDescriptor(ElementDescriptor):
    """
    A marker descriptor is useful in the case when marker operator descriptors
    are used to signify a statistical value of an element descriptor. For an
    example, 224255 and 225255.
    """

    def __str__(self):
        return '{}{:05d}'.format(
            marker_descriptor_prefix.get(self.marker_id, 'M'),
            self.id
        )

    @staticmethod
    def from_element_descriptor(ed, marker_id,
                                scale=None, refval=None, nbits=None):
        """
        Create from a given element descriptor with the option to override its
        scale, refval and nbits.

        :param ElementDescriptor ed: The element descriptor
        :param int marker_id: The marker operator ID
        :param int scale: Overridden value for scale.
        :param int refval: Overridden value for reference.
        :param int nbits: Overridden value for number of bits.
        :rtype: MarkerDescriptor
        """
        md = MarkerDescriptor(
            ed.id, ed.name, ed.unit,
            ed.scale if scale is None else scale,
            ed.refval if refval is None else refval,
            ed.nbits if nbits is None else nbits,
            ed.crex_unit, ed.crex_scale, ed.crex_nchars
        )
        md.marker_id = marker_id
        return md
pybufrkit/constants.py
PARAMETER_TYPE_TEMPLATE_DATA = 'template_data'
pybufrkit/descriptors.py
class SkippedLocalDescriptor(Descriptor):
    """
    The skipped local descriptor is a placeholder for any descriptors followed by
    operator descriptor 206YYY.
    """

    def __init__(self, id_, nbits):
        # TODO: If a local descriptor does not exist in tables, it will be
        # created as an undefined descriptor. So how it should be converted
        # later to SkippedLocalDescriptor if it has a preceding 206YYY? During
        # template lint?
        super(SkippedLocalDescriptor, self).__init__(id_)
        self.nbits = nbits
        # Dummy unit so it does not fail on unit checking in other functions
        self.unit = 'SKIPPED'

    def __str__(self):
        return 'S{:05d}'.format(self.id)
pybufrkit/utils.py
def fixed_width_repr_of_int(value, width, pad_left=True):
    """
    Format the given integer and ensure the result string is of the given
    width. The string will be padded space on the left if the number is 
    small or replaced as a string of asterisks if the number is too big.
    
    :param int value: An integer number to format 
    :param int width: The result string must have the exact width 
    :return: A string representation of the given integer. 
    """
    ret = '{:{pad_dir}{width}d}'.format(value, pad_dir='>' if pad_left else '>', width=width)
    return '*' * width if len(ret) > width else ret
pybufrkit/templatedata.py
class NoValueDataNode(DataNode):
    """
    A no value node is for any descriptors that cannot have a value, e.g.
    replication descriptors, sequence descriptors and some operator descriptors,
    e.g. 201YYY.
    """

    def __init__(self, descriptor):
        super(NoValueDataNode, self).__init__(descriptor)
pybufrkit/templatedata.py
class DelayedReplicationNode(NoValueDataNode):
    def __init__(self, descriptor):
        super(DelayedReplicationNode, self).__init__(descriptor)
        self.members = []
        self.factor = None
pybufrkit/errors.py
class PyBufrKitError(Exception):
    """
    This is the root exception object of the package.
    """

    def __init__(self, message=''):
        self.message = message

    def __str__(self):
        return 'Error: {}'.format(self.message)
pybufrkit/descriptors.py
class SequenceDescriptor(Descriptor):
    """
    Sequence Descriptor 3XXYYY
    """

    def __init__(self, id_, name, members=None):
        super(SequenceDescriptor, self).__init__(id_)
        self.members = members
        self.name = name

    def __iter__(self):
        return iter(self.members)

    def __getitem__(self, item):
        return self.members.__getitem__(item)

    def accept(self, visitor):
        visitor.visit_sequence_descriptor(self)
        if not self.members:
            return
        for member in self.members:
            member.accept(visitor)
        visitor.visit_sequence_descriptor_end(self)
pybufrkit/descriptors.py
class DelayedReplicationDescriptor(ReplicationDescriptor):
    """
    Delayed replication Descriptor 1XX000
    """

    def __init__(self, id_, members=None, factor=None):
        super(DelayedReplicationDescriptor, self).__init__(id_, members)
        self.factor = factor

    @property
    def n_repeats(self):
        raise PyBufrKitError('Cannot access n_repeats for Delayed Replication')

    def accept(self, visitor):
        visitor.visit_replication_descriptor(self)
        visitor.visit_replication_factor(self.factor)
        if not self.members:
            visitor.visit_replication_descriptor_end(self)
            return
        for member in self.members:
            member.accept(visitor)
        visitor.visit_replication_descriptor_end(self)
pybufrkit/descriptors.py
class ElementDescriptor(Descriptor):
    """
    Element Descriptor 0XXYYY

    :param int id_: The descriptor ID
    :param str name: Name of the descriptor
    :param str unit: Units of the descriptor
    :param int scale: Scale factor of the descriptor value
    :param int refval: Reference value of the descriptor value
    :param int nbits: The number of bits used by the descriptor
    :param str crex_unit: Units of the descriptor for CREX spec
    :param int crex_scale: Scale factor of the descriptor value for CREX Spec
    :param int crex_nchars: Number of characters used by the descriptor for CREX Spec
    """

    def __init__(self, id_, name, unit, scale, refval, nbits,
                 crex_unit, crex_scale, crex_nchars):
        super(ElementDescriptor, self).__init__(id_)
        self.name, self.unit, self.scale, self.refval, self.nbits = (
            name, unit, scale, refval, nbits
        )
        self.crex_unit, self.crex_scale, self.crex_nchars = crex_unit, crex_scale, crex_nchars

    def as_list(self):
        return [self.id, self.name, self.unit, self.scale, self.refval, self.nbits]

    def as_dict(self):
        return {self.id: (self.name, self.unit, self.scale, self.refval, self.nbits)}

    def accept(self, visitor):
        visitor.visit_element_descriptor(self)
pybufrkit/dataquery.py
class QueryResult(object):
    """
    This class represents the query result.
    """

    def __init__(self, path_expr=''):
        self.path_expr = path_expr
        self.results = OrderedDict()

    def add_subset(self, i_subset, values):
        self.results[i_subset] = values

    def subset_indices(self):
        return list(self.results.keys())

    def get_values(self, i_subset, flat=False):
        values = self.results[i_subset]
        return flatten_list(values) if flat else values

    def all_values(self, flat=False):
        if flat:
            return [self.get_values(i, flat=True) for i in self.subset_indices()]
        else:
            return list(self.results.values())

    def __iter__(self):
        return iter(self.results.viewitems())
pybufrkit/templatedata.py
class FixedReplicationNode(NoValueDataNode):
    def __init__(self, descriptor):
        super(FixedReplicationNode, self).__init__(descriptor)
        self.members = []


Question: import abc
from collections import OrderedDict
from six.moves import range, zip
from pybufrkit.constants import INDENT_CHARS, PARAMETER_TYPE_TEMPLATE_DATA
from pybufrkit.errors import PyBufrKitError
from pybufrkit.utils import fixed_width_repr_of_int
from pybufrkit.bufr import BufrMessage
from pybufrkit.descriptors import (Descriptor, ElementDescriptor, FixedReplicationDescriptor,
                                   DelayedReplicationDescriptor, OperatorDescriptor,
                                   SequenceDescriptor, SkippedLocalDescriptor,
                                   AssociatedDescriptor, MarkerDescriptor)
from pybufrkit.templatedata import (TemplateData, NoValueDataNode, SequenceNode,
                                    FixedReplicationNode, DelayedReplicationNode)
from pybufrkit.dataquery import QueryResult
            return self._render_bufr_message(obj)
        elif isinstance(obj, TemplateData):
            return self._render_template_data(obj)
        elif isinstance(obj, Descriptor):
            return self._render_descriptor(obj)
        elif isinstance(obj, QueryResult):
            return self._render_query_result(obj)
        else:
            raise PyBufrKitError('Unknown object {} for rendering'.format(type(obj)))

    @abc.abstractmethod
    def _render_bufr_message(self, bufr_message):
        """Render a bufr message"""

    @abc.abstractmethod
    def _render_template_data(self, template_data):
        """Render the template data"""

    @abc.abstractmethod
    def _render_descriptor(self, descriptor):
        """Render a Descriptor (including all of its subclasses)"""

    @abc.abstractmethod
    def _render_query_result(self, query_result):
        """Render a QueryResult object"""


class FlatTextRenderer(Renderer):
    """
    This renderer converts the given object by flatten all its sub-structures.
    """

    def _render_bufr_message(self, bufr_message):
        ret = [str(bufr_message.table_group_key)]
        for section in bufr_message.sections:
            ret.append('<<<<<< section {} >>>>>>'.format(section.get_metadata('index')))
            for parameter in section:
                if parameter.type == PARAMETER_TYPE_TEMPLATE_DATA:
                    ret.extend(
                        self._render_template_data(parameter.value).split('\n')
                    )
                else:
                    ret.append('{} = {!r}'.format(parameter.name, parameter.value))

        return '\n'.join(ret)

    def _render_template_data(self, template_data):
        """
        Format the template data so that they have an user-friend display.
        """
        ret = []
        for idx_subset in range(template_data.n_subsets):
            ret.append('###### subset {} of {} ######'.format(idx_subset + 1, template_data.n_subsets))
            descriptors = template_data.decoded_descriptors_all_subsets[idx_subset]
            bitmap_links = template_data.bitmap_links_all_subsets[idx_subset]
            values = template_data.decoded_values_all_subsets[idx_subset]
            for idx, (descriptor, value) in enumerate(zip(descriptors, values)):
                if value is not None and hasattr(descriptor, 'unit') and descriptor.unit == 'FLAG TABLE':
                    value = (
                        value,
                        [(i + 1) for i, bit in enumerate(
                            '{:0{}b}'.format(value, descriptor.nbits)
                        ) if bit == '1']
                    )

                if idx in bitmap_links:
                    ret.append('{} {:64.64} -> {} {!r}'.format(
                        fixed_width_repr_of_int(idx + 1, 5),
                        self._render_descriptor(descriptor),
                        fixed_width_repr_of_int(bitmap_links[idx] + 1, 6, pad_left=False),
                        value)
                    )
                else:
                    ret.append('{} {:74.74} {!r}'.format(
                        fixed_width_repr_of_int(idx + 1, 5),
                        self._render_descriptor(descriptor),
                        value)
                    )
        return '\n'.join(ret)

    def _render_descriptor(self, descriptor):
        lines = self._render_descriptor_helper(descriptor, '')
        return '\n'.join(lines)

    def _render_query_result(self, query_result):
        lines = []
        for idx_subset in query_result.subset_indices():
            values = query_result.get_values(idx_subset, flat=True)
            lines.append('###### subset {} of {} ######'.format(idx_subset + 1, query_result.n_subsets))
            lines.append(','.join('{!r}'.format(v) for v in values))
        return '\n'.join(lines)

    def _render_descriptor_helper(self, descriptor, indent):
        lines = []

        if isinstance(descriptor, SequenceDescriptor):
            lines.append('{}{} {}'.format(indent, descriptor, descriptor.name))
            for member in descriptor.members:
                lines.extend(self._render_descriptor_helper(member, indent + INDENT_CHARS))

        elif isinstance(descriptor, SkippedLocalDescriptor):
            lines.append('{}{} {} bits'.format(indent, descriptor, descriptor.nbits))

        elif isinstance(descriptor, MarkerDescriptor):
            lines.append('{}{}'.format(indent, descriptor))

        elif isinstance(descriptor, ElementDescriptor):
            lines.append('{}{} {}'.format(indent, descriptor, descriptor.name))

        elif isinstance(descriptor, FixedReplicationDescriptor):
            lines.append('{}{}'.format(indent, descriptor))
            for member in descriptor.members:
                lines.extend(self._render_descriptor_helper(member, indent + INDENT_CHARS))

        elif isinstance(descriptor, DelayedReplicationDescriptor):
            lines.append('{}{}'.format(indent, descriptor))
            lines.extend(self._render_descriptor_helper(descriptor.factor, indent + '.' * len(INDENT_CHARS)))
            for member in descriptor.members:
                lines.extend(self._render_descriptor_helper(member, indent + INDENT_CHARS))


Answer:         elif isinstance(descriptor, OperatorDescriptor):