Module redvox.api900.reader_utils

This module provides utility functions that are used by the reader but generally will not be used directly by end users.

Expand source code
"""
This module provides utility functions that are used by the reader but generally will not be used directly by end
users.
"""

import struct
import typing

from google.protobuf import json_format
from google.protobuf.internal import containers
import lz4.block
import numpy

import redvox.api900.exceptions as exceptions
import redvox.api900.lib.api900_pb2 as api900_pb2


def calculate_uncompressed_size(buf: bytes) -> int:
    """
    Given a buffer, calculate the original size of the uncompressed packet by looking at the first four bytes.
    :param buf: Buffer where first 4 big endian bytes contain the size of the original uncompressed packet.
    :return: The total number of bytes in the original uncompressed packet.
    """
    return struct.unpack(">I", buf[:4])[0]


def uncompressed_size_bytes(size: int) -> bytes:
    """
    Given the size of the original uncompressed file, return the size as an array of 4 bytes.
    :param size: The size to convert.
    :return: The 4 bytes representing the size.
    """
    return struct.pack(">I", size)


def lz4_decompress(buf: bytes) -> bytes:
    """
    Decompresses an API 900 compressed buffer.
    :param buf: The buffer to decompress.
    :return: The uncompressed buffer.
    """
    uncompressed_size = calculate_uncompressed_size(buf)

    if uncompressed_size <= 0:
        raise exceptions.ReaderException(
            "uncompressed size [{}] must be > 0".format(uncompressed_size)
        )

    return lz4.block.decompress(
        buf[4:], uncompressed_size=calculate_uncompressed_size(buf)
    )


# noinspection PyArgumentList
def lz4_compress(buf: bytes) -> bytes:
    """
    Compresses a buffer using LZ4. The compressed buffer is then prepended with the 4 bytes indicating the original
    size of uncompressed buffer.
    :param buf: The buffer to compress.
    :return: The compressed buffer with 4 bytes prepended indicated the original size of the uncompressed buffer.
    """
    uncompressed_size = uncompressed_size_bytes(len(buf))
    compressed = lz4.block.compress(buf, store_size=False)
    return uncompressed_size + compressed


def write_file(file: str, redvox_packet: api900_pb2.RedvoxPacket):
    """
    Writes a redvox file.  Specify the correct file type in the file string.
    :param file: str, File to write
    :param redvox_packet: protobuf packet to write
    :return: Nothing, compressed file is written to disk
    """
    buffer = lz4_compress(redvox_packet.SerializeToString())
    with open(file, "wb") as file_out:
        file_out.write(buffer)


def to_json(redvox_packet: api900_pb2.RedvoxPacket) -> str:
    """
    Converts a protobuf encoded API 900 RedVox packet into JSON.
    :param redvox_packet: The protobuf encoded packet.
    :return: A string containing JSON of this packet.
    """
    return json_format.MessageToJson(redvox_packet)


def from_json(redvox_packet_json: str) -> api900_pb2.RedvoxPacket:
    """
    Converts a JSON packet representing an API 900 packet into a protobuf encoded RedVox API 900 packet.
    :param redvox_packet_json: A string containing the json representing the packet.
    :return: A Python instance of an encoded API 900 packet.
    """
    return json_format.Parse(redvox_packet_json, api900_pb2.RedvoxPacket())


def payload_type(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> str:
    """
    Given a channel, return the internal protobuf string representation of the payload's data type.
    :param channel: The channel to check the data type of.
    :return: The internal protobuf string representation of the payload's data type.
    """
    if channel is None:
        return "No channel to hold payload"

    return channel.WhichOneof("payload")


def payload_len(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> int:
    """
    Given an evenly or unevenly sampled channel, extracts the entire payload.

    This will return a payload of either ints or floats and is type agnostic when it comes to the underlying
    protobuf type.
    :param channel: The protobuf channel to extract the payload from.
    :return: A numpy array of either floats or ints.
    """
    payload_type_str = payload_type(channel)

    if payload_type_str == "byte_payload":
        payload = len(channel.byte_payload.payload)
    elif payload_type_str == "uint32_payload":
        payload = len(channel.uint32_payload.payload)
    elif payload_type_str == "uint64_payload":
        payload = len(channel.uint64_payload.payload)
    elif payload_type_str == "int32_payload":
        payload = len(channel.int32_payload.payload)
    elif payload_type_str == "int64_payload":
        payload = len(channel.int64_payload.payload)
    elif payload_type_str == "float32_payload":
        payload = len(channel.float32_payload.payload)
    elif payload_type_str == "float64_payload":
        payload = len(channel.float64_payload.payload)
    else:
        payload = 0

    return payload


def extract_payload(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> numpy.ndarray:
    """
    Given an evenly or unevenly sampled channel, extracts the length of the payload.

    :param channel: The protobuf channel to extract the payload from.
    :return: Length of the payload.
    """
    payload_type_str = payload_type(channel)

    if payload_type_str == "byte_payload":
        payload = channel.byte_payload.payload
        return numpy.frombuffer(payload, numpy.uint8)
    elif payload_type_str == "uint32_payload":
        payload = channel.uint32_payload.payload
    elif payload_type_str == "uint64_payload":
        payload = channel.uint64_payload.payload
    elif payload_type_str == "int32_payload":
        payload = channel.int32_payload.payload
    elif payload_type_str == "int64_payload":
        payload = channel.int64_payload.payload
    elif payload_type_str == "float32_payload":
        payload = channel.float32_payload.payload
    elif payload_type_str == "float64_payload":
        payload = channel.float64_payload.payload
    else:
        return numpy.array([])
        # raise exceptions.ReaderException("unsupported payload type {}".format(payload_type_str))

    return numpy.array(payload)


def repeated_to_list(
    repeated: typing.Union[
        containers.RepeatedCompositeFieldContainer,
        containers.RepeatedScalarFieldContainer,
    ]
) -> typing.List:
    """
    Transforms a repeated protobuf field into a list.
    :param repeated: The repeated field to transform.
    :return: A list of the repeated items.
    """
    return repeated[0 : len(repeated)]


def repeated_to_array(
    repeated: typing.Union[
        containers.RepeatedCompositeFieldContainer,
        containers.RepeatedScalarFieldContainer,
    ]
) -> numpy.ndarray:
    """
    Transforms a repeated protobuf field into a numpy array.
    :param repeated: The repeated field to transform.
    :return: A numpy array of the repeated items.
    """
    return numpy.array(repeated_to_list(repeated))


def deinterleave_array(ndarray: numpy.ndarray, offset: int, step: int) -> numpy.ndarray:
    """
    Extracts a single channel type from an interleaved array.

    An interleaved channel contains multiple channel types in a single payload. This is useful for situations where
    a sensor produces several values for a single timestamp. For example, a GPS will produce a LATITUDE, LONGITUDE,
    ALTITUDE, and SPEED values with every update. An interleaved channel is an encoding that encodes multiple channel
    types into a single payload.

    Every channel has a field channel_types that list the channel types contained within the payload. For a GPS sensor,
    the channel_types array would look like [LATITUDE, LONGITUDE, ALTITUDE, SPEED]. The location of the channel type
    in channel_types determines the offset into the payload and the length of channel_types determines the step size.
    As such, a GPS channel payload would contain the following values:

    [LAT0, LNG0, ALT0, SPD0, LAT1, LNG1, ALT1, SPD1, ..., LATn, LNGn, ALTn, SPDn]

    This function will "deinterleave" the encoding and provide an array of a single channel type.

    :param ndarray: Interleaved array.
    :param offset: Offset into the array.
    :param step: The step size.
    :return: A numpy array of a single channel type.
    """

    if offset < 0 or offset >= len(ndarray):
        raise exceptions.ReaderException(
            "offset {} out of range [{},{})".format(offset, 0, len(ndarray))
        )

    if offset >= step:
        raise exceptions.ReaderException(
            "offset {} must be smaller than step {}".format(offset, step)
        )

    if step <= 0 or step > len(ndarray):
        raise exceptions.ReaderException(
            "step {} out of range [{},{})".format(step, 0, len(ndarray))
        )

    if len(ndarray) % step != 0:
        raise exceptions.ReaderException(
            "step {} is not a multiple of {}".format(step, len(ndarray))
        )

    # pylint: disable=C1801
    if len(ndarray) == 0:
        return empty_array()

    return ndarray[offset::step]


def to_array(values: typing.Union[typing.List, numpy.ndarray]) -> numpy.ndarray:
    """
    Takes either a list or a numpy array and returns a numpy array if the parameter was a list.
    :param values: Values to convert into numpy array if values are in a list.
    :return: A numpy array of the passed in values.
    """
    if isinstance(values, list):
        return numpy.array(values)
    return values


def interleave_arrays(arrays: typing.List[numpy.ndarray]) -> numpy.ndarray:
    """
    Interleaves multiple arrays together.
    :param arrays: Arrays to interleave.
    :return: Interleaved arrays.
    """
    if len(arrays) < 2:
        raise exceptions.ReaderException(
            "At least 2 arrays are required for interleaving"
        )

    if len(set(map(len, arrays))) > 1:
        raise exceptions.ReaderException("all arrays must be same size")

    total_arrays = len(arrays)
    total_elements = sum(map(lambda array: array.size, arrays))
    interleaved_array = numpy.empty((total_elements,), dtype=arrays[0].dtype)
    for i in range(total_arrays):
        interleaved_array[i::total_arrays] = arrays[i]

    return interleaved_array


def implements_diff(val) -> bool:
    """
    Checks if a value implements the diff method.
    :param val: Value to check
    :return: True is the value implements diff, False otherwise.
    """
    diff_atr = getattr(val, "diff", None)
    if callable(diff_atr):
        return True

    return False


# pylint: disable=R0911
def diff(val1, val2) -> typing.Tuple[bool, typing.Optional[str]]:
    """
    Determines if the two values are different.
    :param val1: The first value to check.
    :param val2: The second value to check.
    :return: False, None if the values are the same or True, and a string displaying the differences when different.
    """
    # pylint: disable=C0123
    if type(val1) != type(val2):
        return True, "type {} != type {}".format(type(val1), type(val2))

    if implements_diff(val1) and implements_diff(val2):
        diffs = val1.diff(val2)
        if len(diffs) == 0:
            return False, None

        return True, "%s" % list(diffs)

    if isinstance(val1, numpy.ndarray) and isinstance(val2, numpy.ndarray):
        if numpy.array_equal(val1, val2):
            return False, None

        return True, "{} != {}".format(val1, val2)

    if val1 != val2:
        return True, "{} != {}".format(val1, val2)

    return False, None


def safe_index_of(lst: typing.List, val: typing.Any) -> int:
    """
    Finds the index of an item in a list and instead of throwing an exception returns -1 when the item DNE.
    :param lst: List to search through.
    :param val: The value to find the index of.
    :return: The index of the first value v found or -1.
    """
    try:
        return lst.index(val)
    except ValueError:
        return -1


def empty_array() -> numpy.ndarray:
    """Returns an empty numpy array.
    :return: An empty numpy array.
    """
    return numpy.array([])


def empty_evenly_sampled_channel() -> api900_pb2.EvenlySampledChannel:
    """
    Returns an empty protobuf EvenlySampledChannel
    :return: empty EvenlySampledChannel
    """
    obj = api900_pb2.EvenlySampledChannel()
    return obj


def empty_unevenly_sampled_channel() -> api900_pb2.UnevenlySampledChannel:
    """
    Returns an empty protobuf UnevenlySampledChannel
    :return: empty UnevenlySampledChannel
    """
    obj = api900_pb2.UnevenlySampledChannel()
    return obj


def channel_type_name_from_enum(enum_constant: int) -> str:
    """
    Returns the name of a channel type given its enumeration constant.
    :param enum_constant: The constant to turn into a name.
    :return: The name of the channel.
    """
    return api900_pb2.ChannelType.Name(enum_constant)


def get_metadata(metadata: typing.List[str], k: str) -> str:
    """
    Given a meta-data key, extract the value.
    :param metadata: List of metadata to extract value from.
    :param k: The meta-data key.
    :return: The value corresponding to the key or an empty string.
    """
    if len(metadata) % 2 != 0:
        raise exceptions.ReaderException(
            "metadata list must contain an even number of items"
        )

    idx = safe_index_of(metadata, k)
    if idx < 0:
        return ""

    return metadata[idx + 1]


def extract_uneven_payload_idx_raw(
    packet: api900_pb2.RedvoxPacket, channel_type
) -> typing.Optional[int]:
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for (i, local_type) in enumerate(channel.channel_types):
            if local_type == channel_type:
                return i

    return None


def extract_uneven_payload_raw(
    packet: api900_pb2.RedvoxPacket, channel_type
) -> typing.Optional[typing.List[float]]:
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for (i, local_type) in enumerate(channel.channel_types):
            if local_type == channel_type:
                total_channels: int = len(channel.channel_types)
                return list(extract_payload(channel))[i::total_channels]

    return None


def find_uneven_channel_raw(packet: api900_pb2.RedvoxPacket, channel_types):
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for local_type in channel.channel_types:
            if local_type in channel_types:
                return channel
    return None


T = typing.TypeVar("T")


def get_metadata_or_default(
    metadata: typing.List[str],
    k: str,
    and_then: typing.Callable[[str], T],
    or_default: T,
) -> T:
    for i, v in enumerate(metadata):
        if v == k and (i + 1) < len(metadata):
            try:
                return and_then(metadata[i + 1])
            except:
                return or_default

    return or_default


def get_metadata_raw(
    metadata: typing.List[str],
    k: str,
    and_then: typing.Optional[typing.Callable[[str], T]] = None,
    or_default: typing.Optional[typing.Union[str, T]] = None,
) -> typing.Optional[typing.Union[str, T]]:
    """
    Given a meta-data key, extract the value.
    :param metadata: List of metadata to extract value from.
    :param k: The meta-data key.
    :return: The value corresponding to the key or an empty string.
    """
    for i, v in enumerate(metadata):
        if v == k and (i + 1) < len(metadata):
            try:
                r: str = metadata[i + 1]
                return r if and_then is None else and_then(r)
            except:
                return None

    return or_default


def get_metadata_as_dict(metadata: typing.List[str]) -> typing.Dict[str, str]:
    """
    Since the metadata is inherently key-value, it may be useful to turn the metadata list into a python dictionary.
    :param metadata: The metadata list.
    :return: Metadata as a python dictionary.
    """
    if not metadata:
        return {}

    if len(metadata) % 2 != 0:
        raise exceptions.ReaderException(
            "metadata list must contain an even number of items"
        )

    metadata_dict = {}
    metadata_copy = metadata.copy()
    while len(metadata_copy) >= 2:
        metadata_key = metadata_copy.pop(0)
        metadata_value = metadata_copy.pop(0)
        if metadata_key not in metadata_dict:
            metadata_dict[metadata_key] = metadata_value
    return metadata_dict


def metadata_dict_to_list(metadata_dict: typing.Dict[str, str]) -> typing.List[str]:
    """
    Converts a dictionary containing metadata into a list of metadata.
    :param metadata_dict: The dictionary of metadata.
    :return: A list of metadata.
    """
    metadata_list = []
    for key, value in metadata_dict.items():
        metadata_list.extend([key, value])
    return metadata_list

Functions

def calculate_uncompressed_size(buf: bytes) ‑> int

Given a buffer, calculate the original size of the uncompressed packet by looking at the first four bytes. :param buf: Buffer where first 4 big endian bytes contain the size of the original uncompressed packet. :return: The total number of bytes in the original uncompressed packet.

Expand source code
def calculate_uncompressed_size(buf: bytes) -> int:
    """
    Given a buffer, calculate the original size of the uncompressed packet by looking at the first four bytes.
    :param buf: Buffer where first 4 big endian bytes contain the size of the original uncompressed packet.
    :return: The total number of bytes in the original uncompressed packet.
    """
    return struct.unpack(">I", buf[:4])[0]
def channel_type_name_from_enum(enum_constant: int) ‑> str

Returns the name of a channel type given its enumeration constant. :param enum_constant: The constant to turn into a name. :return: The name of the channel.

Expand source code
def channel_type_name_from_enum(enum_constant: int) -> str:
    """
    Returns the name of a channel type given its enumeration constant.
    :param enum_constant: The constant to turn into a name.
    :return: The name of the channel.
    """
    return api900_pb2.ChannelType.Name(enum_constant)
def deinterleave_array(ndarray: numpy.ndarray, offset: int, step: int) ‑> numpy.ndarray

Extracts a single channel type from an interleaved array.

An interleaved channel contains multiple channel types in a single payload. This is useful for situations where a sensor produces several values for a single timestamp. For example, a GPS will produce a LATITUDE, LONGITUDE, ALTITUDE, and SPEED values with every update. An interleaved channel is an encoding that encodes multiple channel types into a single payload.

Every channel has a field channel_types that list the channel types contained within the payload. For a GPS sensor, the channel_types array would look like [LATITUDE, LONGITUDE, ALTITUDE, SPEED]. The location of the channel type in channel_types determines the offset into the payload and the length of channel_types determines the step size. As such, a GPS channel payload would contain the following values:

[LAT0, LNG0, ALT0, SPD0, LAT1, LNG1, ALT1, SPD1, …, LATn, LNGn, ALTn, SPDn]

This function will "deinterleave" the encoding and provide an array of a single channel type.

:param ndarray: Interleaved array. :param offset: Offset into the array. :param step: The step size. :return: A numpy array of a single channel type.

Expand source code
def deinterleave_array(ndarray: numpy.ndarray, offset: int, step: int) -> numpy.ndarray:
    """
    Extracts a single channel type from an interleaved array.

    An interleaved channel contains multiple channel types in a single payload. This is useful for situations where
    a sensor produces several values for a single timestamp. For example, a GPS will produce a LATITUDE, LONGITUDE,
    ALTITUDE, and SPEED values with every update. An interleaved channel is an encoding that encodes multiple channel
    types into a single payload.

    Every channel has a field channel_types that list the channel types contained within the payload. For a GPS sensor,
    the channel_types array would look like [LATITUDE, LONGITUDE, ALTITUDE, SPEED]. The location of the channel type
    in channel_types determines the offset into the payload and the length of channel_types determines the step size.
    As such, a GPS channel payload would contain the following values:

    [LAT0, LNG0, ALT0, SPD0, LAT1, LNG1, ALT1, SPD1, ..., LATn, LNGn, ALTn, SPDn]

    This function will "deinterleave" the encoding and provide an array of a single channel type.

    :param ndarray: Interleaved array.
    :param offset: Offset into the array.
    :param step: The step size.
    :return: A numpy array of a single channel type.
    """

    if offset < 0 or offset >= len(ndarray):
        raise exceptions.ReaderException(
            "offset {} out of range [{},{})".format(offset, 0, len(ndarray))
        )

    if offset >= step:
        raise exceptions.ReaderException(
            "offset {} must be smaller than step {}".format(offset, step)
        )

    if step <= 0 or step > len(ndarray):
        raise exceptions.ReaderException(
            "step {} out of range [{},{})".format(step, 0, len(ndarray))
        )

    if len(ndarray) % step != 0:
        raise exceptions.ReaderException(
            "step {} is not a multiple of {}".format(step, len(ndarray))
        )

    # pylint: disable=C1801
    if len(ndarray) == 0:
        return empty_array()

    return ndarray[offset::step]
def diff(val1, val2) ‑> Tuple[bool, Optional[str]]

Determines if the two values are different. :param val1: The first value to check. :param val2: The second value to check. :return: False, None if the values are the same or True, and a string displaying the differences when different.

Expand source code
def diff(val1, val2) -> typing.Tuple[bool, typing.Optional[str]]:
    """
    Determines if the two values are different.
    :param val1: The first value to check.
    :param val2: The second value to check.
    :return: False, None if the values are the same or True, and a string displaying the differences when different.
    """
    # pylint: disable=C0123
    if type(val1) != type(val2):
        return True, "type {} != type {}".format(type(val1), type(val2))

    if implements_diff(val1) and implements_diff(val2):
        diffs = val1.diff(val2)
        if len(diffs) == 0:
            return False, None

        return True, "%s" % list(diffs)

    if isinstance(val1, numpy.ndarray) and isinstance(val2, numpy.ndarray):
        if numpy.array_equal(val1, val2):
            return False, None

        return True, "{} != {}".format(val1, val2)

    if val1 != val2:
        return True, "{} != {}".format(val1, val2)

    return False, None
def empty_array() ‑> numpy.ndarray

Returns an empty numpy array. :return: An empty numpy array.

Expand source code
def empty_array() -> numpy.ndarray:
    """Returns an empty numpy array.
    :return: An empty numpy array.
    """
    return numpy.array([])
def empty_evenly_sampled_channel() ‑> api900_pb2.EvenlySampledChannel

Returns an empty protobuf EvenlySampledChannel :return: empty EvenlySampledChannel

Expand source code
def empty_evenly_sampled_channel() -> api900_pb2.EvenlySampledChannel:
    """
    Returns an empty protobuf EvenlySampledChannel
    :return: empty EvenlySampledChannel
    """
    obj = api900_pb2.EvenlySampledChannel()
    return obj
def empty_unevenly_sampled_channel() ‑> api900_pb2.UnevenlySampledChannel

Returns an empty protobuf UnevenlySampledChannel :return: empty UnevenlySampledChannel

Expand source code
def empty_unevenly_sampled_channel() -> api900_pb2.UnevenlySampledChannel:
    """
    Returns an empty protobuf UnevenlySampledChannel
    :return: empty UnevenlySampledChannel
    """
    obj = api900_pb2.UnevenlySampledChannel()
    return obj
def extract_payload(channel: Union[api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel]) ‑> numpy.ndarray

Given an evenly or unevenly sampled channel, extracts the length of the payload.

:param channel: The protobuf channel to extract the payload from. :return: Length of the payload.

Expand source code
def extract_payload(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> numpy.ndarray:
    """
    Given an evenly or unevenly sampled channel, extracts the length of the payload.

    :param channel: The protobuf channel to extract the payload from.
    :return: Length of the payload.
    """
    payload_type_str = payload_type(channel)

    if payload_type_str == "byte_payload":
        payload = channel.byte_payload.payload
        return numpy.frombuffer(payload, numpy.uint8)
    elif payload_type_str == "uint32_payload":
        payload = channel.uint32_payload.payload
    elif payload_type_str == "uint64_payload":
        payload = channel.uint64_payload.payload
    elif payload_type_str == "int32_payload":
        payload = channel.int32_payload.payload
    elif payload_type_str == "int64_payload":
        payload = channel.int64_payload.payload
    elif payload_type_str == "float32_payload":
        payload = channel.float32_payload.payload
    elif payload_type_str == "float64_payload":
        payload = channel.float64_payload.payload
    else:
        return numpy.array([])
        # raise exceptions.ReaderException("unsupported payload type {}".format(payload_type_str))

    return numpy.array(payload)
def extract_uneven_payload_idx_raw(packet: api900_pb2.RedvoxPacket, channel_type) ‑> Optional[int]
Expand source code
def extract_uneven_payload_idx_raw(
    packet: api900_pb2.RedvoxPacket, channel_type
) -> typing.Optional[int]:
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for (i, local_type) in enumerate(channel.channel_types):
            if local_type == channel_type:
                return i

    return None
def extract_uneven_payload_raw(packet: api900_pb2.RedvoxPacket, channel_type) ‑> Optional[List[float]]
Expand source code
def extract_uneven_payload_raw(
    packet: api900_pb2.RedvoxPacket, channel_type
) -> typing.Optional[typing.List[float]]:
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for (i, local_type) in enumerate(channel.channel_types):
            if local_type == channel_type:
                total_channels: int = len(channel.channel_types)
                return list(extract_payload(channel))[i::total_channels]

    return None
def find_uneven_channel_raw(packet: api900_pb2.RedvoxPacket, channel_types)
Expand source code
def find_uneven_channel_raw(packet: api900_pb2.RedvoxPacket, channel_types):
    channel: api900_pb2.UnevenlySampledChannel
    for channel in packet.unevenly_sampled_channels:
        for local_type in channel.channel_types:
            if local_type in channel_types:
                return channel
    return None
def from_json(redvox_packet_json: str) ‑> api900_pb2.RedvoxPacket

Converts a JSON packet representing an API 900 packet into a protobuf encoded RedVox API 900 packet. :param redvox_packet_json: A string containing the json representing the packet. :return: A Python instance of an encoded API 900 packet.

Expand source code
def from_json(redvox_packet_json: str) -> api900_pb2.RedvoxPacket:
    """
    Converts a JSON packet representing an API 900 packet into a protobuf encoded RedVox API 900 packet.
    :param redvox_packet_json: A string containing the json representing the packet.
    :return: A Python instance of an encoded API 900 packet.
    """
    return json_format.Parse(redvox_packet_json, api900_pb2.RedvoxPacket())
def get_metadata(metadata: List[str], k: str) ‑> str

Given a meta-data key, extract the value. :param metadata: List of metadata to extract value from. :param k: The meta-data key. :return: The value corresponding to the key or an empty string.

Expand source code
def get_metadata(metadata: typing.List[str], k: str) -> str:
    """
    Given a meta-data key, extract the value.
    :param metadata: List of metadata to extract value from.
    :param k: The meta-data key.
    :return: The value corresponding to the key or an empty string.
    """
    if len(metadata) % 2 != 0:
        raise exceptions.ReaderException(
            "metadata list must contain an even number of items"
        )

    idx = safe_index_of(metadata, k)
    if idx < 0:
        return ""

    return metadata[idx + 1]
def get_metadata_as_dict(metadata: List[str]) ‑> Dict[str, str]

Since the metadata is inherently key-value, it may be useful to turn the metadata list into a python dictionary. :param metadata: The metadata list. :return: Metadata as a python dictionary.

Expand source code
def get_metadata_as_dict(metadata: typing.List[str]) -> typing.Dict[str, str]:
    """
    Since the metadata is inherently key-value, it may be useful to turn the metadata list into a python dictionary.
    :param metadata: The metadata list.
    :return: Metadata as a python dictionary.
    """
    if not metadata:
        return {}

    if len(metadata) % 2 != 0:
        raise exceptions.ReaderException(
            "metadata list must contain an even number of items"
        )

    metadata_dict = {}
    metadata_copy = metadata.copy()
    while len(metadata_copy) >= 2:
        metadata_key = metadata_copy.pop(0)
        metadata_value = metadata_copy.pop(0)
        if metadata_key not in metadata_dict:
            metadata_dict[metadata_key] = metadata_value
    return metadata_dict
def get_metadata_or_default(metadata: List[str], k: str, and_then: Callable[[str], ~T], or_default: ~T) ‑> ~T
Expand source code
def get_metadata_or_default(
    metadata: typing.List[str],
    k: str,
    and_then: typing.Callable[[str], T],
    or_default: T,
) -> T:
    for i, v in enumerate(metadata):
        if v == k and (i + 1) < len(metadata):
            try:
                return and_then(metadata[i + 1])
            except:
                return or_default

    return or_default
def get_metadata_raw(metadata: List[str], k: str, and_then: Optional[Callable[[str], ~T]] = None, or_default: Union[str, ~T, ForwardRef(None)] = None) ‑> Union[str, ~T, ForwardRef(None)]

Given a meta-data key, extract the value. :param metadata: List of metadata to extract value from. :param k: The meta-data key. :return: The value corresponding to the key or an empty string.

Expand source code
def get_metadata_raw(
    metadata: typing.List[str],
    k: str,
    and_then: typing.Optional[typing.Callable[[str], T]] = None,
    or_default: typing.Optional[typing.Union[str, T]] = None,
) -> typing.Optional[typing.Union[str, T]]:
    """
    Given a meta-data key, extract the value.
    :param metadata: List of metadata to extract value from.
    :param k: The meta-data key.
    :return: The value corresponding to the key or an empty string.
    """
    for i, v in enumerate(metadata):
        if v == k and (i + 1) < len(metadata):
            try:
                r: str = metadata[i + 1]
                return r if and_then is None else and_then(r)
            except:
                return None

    return or_default
def implements_diff(val) ‑> bool

Checks if a value implements the diff method. :param val: Value to check :return: True is the value implements diff, False otherwise.

Expand source code
def implements_diff(val) -> bool:
    """
    Checks if a value implements the diff method.
    :param val: Value to check
    :return: True is the value implements diff, False otherwise.
    """
    diff_atr = getattr(val, "diff", None)
    if callable(diff_atr):
        return True

    return False
def interleave_arrays(arrays: List[numpy.ndarray]) ‑> numpy.ndarray

Interleaves multiple arrays together. :param arrays: Arrays to interleave. :return: Interleaved arrays.

Expand source code
def interleave_arrays(arrays: typing.List[numpy.ndarray]) -> numpy.ndarray:
    """
    Interleaves multiple arrays together.
    :param arrays: Arrays to interleave.
    :return: Interleaved arrays.
    """
    if len(arrays) < 2:
        raise exceptions.ReaderException(
            "At least 2 arrays are required for interleaving"
        )

    if len(set(map(len, arrays))) > 1:
        raise exceptions.ReaderException("all arrays must be same size")

    total_arrays = len(arrays)
    total_elements = sum(map(lambda array: array.size, arrays))
    interleaved_array = numpy.empty((total_elements,), dtype=arrays[0].dtype)
    for i in range(total_arrays):
        interleaved_array[i::total_arrays] = arrays[i]

    return interleaved_array
def lz4_compress(buf: bytes) ‑> bytes

Compresses a buffer using LZ4. The compressed buffer is then prepended with the 4 bytes indicating the original size of uncompressed buffer. :param buf: The buffer to compress. :return: The compressed buffer with 4 bytes prepended indicated the original size of the uncompressed buffer.

Expand source code
def lz4_compress(buf: bytes) -> bytes:
    """
    Compresses a buffer using LZ4. The compressed buffer is then prepended with the 4 bytes indicating the original
    size of uncompressed buffer.
    :param buf: The buffer to compress.
    :return: The compressed buffer with 4 bytes prepended indicated the original size of the uncompressed buffer.
    """
    uncompressed_size = uncompressed_size_bytes(len(buf))
    compressed = lz4.block.compress(buf, store_size=False)
    return uncompressed_size + compressed
def lz4_decompress(buf: bytes) ‑> bytes

Decompresses an API 900 compressed buffer. :param buf: The buffer to decompress. :return: The uncompressed buffer.

Expand source code
def lz4_decompress(buf: bytes) -> bytes:
    """
    Decompresses an API 900 compressed buffer.
    :param buf: The buffer to decompress.
    :return: The uncompressed buffer.
    """
    uncompressed_size = calculate_uncompressed_size(buf)

    if uncompressed_size <= 0:
        raise exceptions.ReaderException(
            "uncompressed size [{}] must be > 0".format(uncompressed_size)
        )

    return lz4.block.decompress(
        buf[4:], uncompressed_size=calculate_uncompressed_size(buf)
    )
def metadata_dict_to_list(metadata_dict: Dict[str, str]) ‑> List[str]

Converts a dictionary containing metadata into a list of metadata. :param metadata_dict: The dictionary of metadata. :return: A list of metadata.

Expand source code
def metadata_dict_to_list(metadata_dict: typing.Dict[str, str]) -> typing.List[str]:
    """
    Converts a dictionary containing metadata into a list of metadata.
    :param metadata_dict: The dictionary of metadata.
    :return: A list of metadata.
    """
    metadata_list = []
    for key, value in metadata_dict.items():
        metadata_list.extend([key, value])
    return metadata_list
def payload_len(channel: Union[api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel]) ‑> int

Given an evenly or unevenly sampled channel, extracts the entire payload.

This will return a payload of either ints or floats and is type agnostic when it comes to the underlying protobuf type. :param channel: The protobuf channel to extract the payload from. :return: A numpy array of either floats or ints.

Expand source code
def payload_len(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> int:
    """
    Given an evenly or unevenly sampled channel, extracts the entire payload.

    This will return a payload of either ints or floats and is type agnostic when it comes to the underlying
    protobuf type.
    :param channel: The protobuf channel to extract the payload from.
    :return: A numpy array of either floats or ints.
    """
    payload_type_str = payload_type(channel)

    if payload_type_str == "byte_payload":
        payload = len(channel.byte_payload.payload)
    elif payload_type_str == "uint32_payload":
        payload = len(channel.uint32_payload.payload)
    elif payload_type_str == "uint64_payload":
        payload = len(channel.uint64_payload.payload)
    elif payload_type_str == "int32_payload":
        payload = len(channel.int32_payload.payload)
    elif payload_type_str == "int64_payload":
        payload = len(channel.int64_payload.payload)
    elif payload_type_str == "float32_payload":
        payload = len(channel.float32_payload.payload)
    elif payload_type_str == "float64_payload":
        payload = len(channel.float64_payload.payload)
    else:
        payload = 0

    return payload
def payload_type(channel: Union[api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel]) ‑> str

Given a channel, return the internal protobuf string representation of the payload's data type. :param channel: The channel to check the data type of. :return: The internal protobuf string representation of the payload's data type.

Expand source code
def payload_type(
    channel: typing.Union[
        api900_pb2.EvenlySampledChannel, api900_pb2.UnevenlySampledChannel
    ]
) -> str:
    """
    Given a channel, return the internal protobuf string representation of the payload's data type.
    :param channel: The channel to check the data type of.
    :return: The internal protobuf string representation of the payload's data type.
    """
    if channel is None:
        return "No channel to hold payload"

    return channel.WhichOneof("payload")
def repeated_to_array(repeated: Union[google.protobuf.internal.containers.RepeatedCompositeFieldContainer, google.protobuf.internal.containers.RepeatedScalarFieldContainer]) ‑> numpy.ndarray

Transforms a repeated protobuf field into a numpy array. :param repeated: The repeated field to transform. :return: A numpy array of the repeated items.

Expand source code
def repeated_to_array(
    repeated: typing.Union[
        containers.RepeatedCompositeFieldContainer,
        containers.RepeatedScalarFieldContainer,
    ]
) -> numpy.ndarray:
    """
    Transforms a repeated protobuf field into a numpy array.
    :param repeated: The repeated field to transform.
    :return: A numpy array of the repeated items.
    """
    return numpy.array(repeated_to_list(repeated))
def repeated_to_list(repeated: Union[google.protobuf.internal.containers.RepeatedCompositeFieldContainer, google.protobuf.internal.containers.RepeatedScalarFieldContainer]) ‑> List

Transforms a repeated protobuf field into a list. :param repeated: The repeated field to transform. :return: A list of the repeated items.

Expand source code
def repeated_to_list(
    repeated: typing.Union[
        containers.RepeatedCompositeFieldContainer,
        containers.RepeatedScalarFieldContainer,
    ]
) -> typing.List:
    """
    Transforms a repeated protobuf field into a list.
    :param repeated: The repeated field to transform.
    :return: A list of the repeated items.
    """
    return repeated[0 : len(repeated)]
def safe_index_of(lst: List, val: Any) ‑> int

Finds the index of an item in a list and instead of throwing an exception returns -1 when the item DNE. :param lst: List to search through. :param val: The value to find the index of. :return: The index of the first value v found or -1.

Expand source code
def safe_index_of(lst: typing.List, val: typing.Any) -> int:
    """
    Finds the index of an item in a list and instead of throwing an exception returns -1 when the item DNE.
    :param lst: List to search through.
    :param val: The value to find the index of.
    :return: The index of the first value v found or -1.
    """
    try:
        return lst.index(val)
    except ValueError:
        return -1
def to_array(values: Union[List, numpy.ndarray]) ‑> numpy.ndarray

Takes either a list or a numpy array and returns a numpy array if the parameter was a list. :param values: Values to convert into numpy array if values are in a list. :return: A numpy array of the passed in values.

Expand source code
def to_array(values: typing.Union[typing.List, numpy.ndarray]) -> numpy.ndarray:
    """
    Takes either a list or a numpy array and returns a numpy array if the parameter was a list.
    :param values: Values to convert into numpy array if values are in a list.
    :return: A numpy array of the passed in values.
    """
    if isinstance(values, list):
        return numpy.array(values)
    return values
def to_json(redvox_packet: api900_pb2.RedvoxPacket) ‑> str

Converts a protobuf encoded API 900 RedVox packet into JSON. :param redvox_packet: The protobuf encoded packet. :return: A string containing JSON of this packet.

Expand source code
def to_json(redvox_packet: api900_pb2.RedvoxPacket) -> str:
    """
    Converts a protobuf encoded API 900 RedVox packet into JSON.
    :param redvox_packet: The protobuf encoded packet.
    :return: A string containing JSON of this packet.
    """
    return json_format.MessageToJson(redvox_packet)
def uncompressed_size_bytes(size: int) ‑> bytes

Given the size of the original uncompressed file, return the size as an array of 4 bytes. :param size: The size to convert. :return: The 4 bytes representing the size.

Expand source code
def uncompressed_size_bytes(size: int) -> bytes:
    """
    Given the size of the original uncompressed file, return the size as an array of 4 bytes.
    :param size: The size to convert.
    :return: The 4 bytes representing the size.
    """
    return struct.pack(">I", size)
def write_file(file: str, redvox_packet: api900_pb2.RedvoxPacket)

Writes a redvox file. Specify the correct file type in the file string. :param file: str, File to write :param redvox_packet: protobuf packet to write :return: Nothing, compressed file is written to disk

Expand source code
def write_file(file: str, redvox_packet: api900_pb2.RedvoxPacket):
    """
    Writes a redvox file.  Specify the correct file type in the file string.
    :param file: str, File to write
    :param redvox_packet: protobuf packet to write
    :return: Nothing, compressed file is written to disk
    """
    buffer = lz4_compress(redvox_packet.SerializeToString())
    with open(file, "wb") as file_out:
        file_out.write(buffer)