# Source code for uqrng_direct.utils

    from dataclasses import dataclass
    from typing import TypedDict, List
    import numpy as np
    import grpc

    class resultNISTdetail(TypedDict):
        """
        Dictionary containing detailed results from NIST-STS returned within
        :class:`.resultNIST` generated by either
        :meth:`.UqrngClient.FetchHealthTest` or
        :meth:`.UqrngClient.HealthTest`.

        :param test_name: a list of names for randomness tests
        :param p_value: a list of p_values associated with the tests.
        :param proportion: a list of proportion of pass vs fail for test.
        :param passed: list of bools with passing tests indicated by True
            and failing tests returning False.
        :param elapsed_time_mins: returns time in fractional mins since
            last health check was completed.
        """
        test_name: List[str]
        p_value: List[float]
        proportion: List[float]
        passed: List[bool]
        elapsed_time_mins: float

    class resultNIST(TypedDict):
        """
        Dictionary containing results summary from NIST-STS generated by
        :meth:`.UqrngClient.FetchHealthTest` or
        :meth:`.UqrngClient.HealthTest`.

        :param all_pass: indicates whether all tests in health check
            passed if any test failed then returns False.
        :param tests_detail: detailed test results of form
            :class:`.resultNISTdetail`
        :param summary_table: a string formatted to print as a table which
           summarizes the randomness test detailed results.
        """
        all_pass: bool
        tests_detail: resultNISTdetail
        summary_table: str

    class SystemInfoDict(TypedDict):
        """
        Dictionary structure for :meth:`.client.UqrngClient.SystemInfo`

        :param device_name: the type of device
        :param server_version: the current semantic version for the
           device server
        :param test_interval_mins: current number of
            minutes between consecutive health test for server. For
            information on how this value is set see
            :meth:`.client.UqrngClient.ScheduleHealthTest`.
        """
        device_type: str
        server_version: str
        test_interval_mins: int

    @dataclass
    class SysStatus:
        """
        Status codes for system paired with their descriptions.
        """

        IDLE = {"status_code": 0, "status_desc": "IDLE"}
        SAMPLING = {"status_code": 1, "status_desc": "SAMPLING"}

    class StatusDict(TypedDict):
        """
        Status message

        :param status_code: integer code for response
        :param status_desc: description for status code
        """
        status_code: int
        status_desc: str

    def message_to_dict(grpc_message) -> dict:
        """Convert a gRPC message to a dictionary."""
        result = {}
        for descriptor in grpc_message.DESCRIPTOR.fields:
            field = getattr(grpc_message, descriptor.name)
            if descriptor.type == descriptor.TYPE_MESSAGE:
                if descriptor.label == descriptor.LABEL_REPEATED:
                    if field:
                        result[descriptor.name] = [message_to_dict(item) for item in field]
                    else:
                        result[descriptor.name] = []
                else:
                    if field:
                        result[descriptor.name] = message_to_dict(field)
                    else:
                        result[descriptor.name] = {}
            else:
                result[descriptor.name] = field
        return result

    def create_summary_table(detail_result: resultNISTdetail) -> str:
        column_names = ["STATISTICAL_TEST", "P-VALUE", "PROPORTION", "PASS"]
        prop_pass_print = np.round(detail_result["proportion_pass"], 6)
        pval_print = np.round(detail_result["p_value"],6)
        prop_col_width = max(len("PROPORTION"),max([len(str(num)) for num in prop_pass_print]))
        test_col_width = max(len("STATISTICAL TEST"),
                             max([len(str(num)) for num in detail_result["test_name"]]))
        pval_col_width = max(len("P-VALUE"),max([len(str(num)) for num in pval_print]))
        pass_col_width = max(len("PASS"),max([len(str(num)) for num in detail_result["passed"]]))
        col_widths = [test_col_width, pval_col_width, prop_col_width, pass_col_width]
        # Calculate column widths
        for i in range(len(column_names)):
            header = " | ".join(f"{col:{col_widths[i]}}" for i, col in enumerate(column_names))
        str_table = header + "\n"
        str_table += ("-" * len(header)+"\n")

        # Print rows
        for t_name, p_val, p_pass, pass_val in zip(detail_result["test_name"],
                                             pval_print,
                                             prop_pass_print,
                                             detail_result["passed"]):
            entry = " | ".join(f"{str(val):{col_widths[i]}}" for i, val in enumerate([t_name,
                                                                             p_val,
                                                                             p_pass,
                                                                             pass_val]))
            str_table += (entry+"\n")
        return str_table

    def check_qrng_busy_error(rpc_err: grpc.RpcError):
        """
        Utility function used in waiting loop to determine if error is from
        busy or another RPC error which can't be handled

        :param rpc_error: RpcError to check
        :return: bool whether error is from QRNG is in use
        """
        if rpc_err.code() == grpc.StatusCode.UNAVAILABLE:
            error_details = rpc_err.details()
            return True
        else:
            return False