Skip to content

climate_ref.cli._utils #

OutputFormat #

Bases: StrEnum

Output format for tabular CLI commands.

Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
class OutputFormat(StrEnum):
    """
    Output format for tabular CLI commands.
    """

    table = "table"
    json = "json"

df_to_table(df, max_col_count=-1) #

Convert a DataFrame to a rich Table instance

Parameters:

Name Type Description Default
df DataFrame

DataFrame to convert

required
max_col_count int

Maximum number of columns to display

If the DataFrame has more columns than this, the excess columns will be truncated If set to -1, all columns will be displayed. For very wide DataFrames, then this may result in no values at all being displayed if the column-width ends up being less than 1 char.

-1

Returns:

Type Description
Rich Table instance representing the DataFrame
Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
def df_to_table(df: pd.DataFrame, max_col_count: int = -1) -> Table:
    """
    Convert a DataFrame to a rich Table instance

    Parameters
    ----------
    df
        DataFrame to convert
    max_col_count
        Maximum number of columns to display

        If the DataFrame has more columns than this, the excess columns will be truncated
        If set to -1, all columns will be displayed.
        For very wide DataFrames, then this may result in no values at all being displayed
        if the column-width ends up being less than 1 char.

    Returns
    -------
        Rich Table instance representing the DataFrame
    """
    # Initiate a Table instance to be modified
    if max_col_count > 0 and len(df.columns) > max_col_count:
        logger.warning(f"Too many columns to display ({len(df.columns)}), truncating to {max_col_count}")
        df = df.iloc[:, :max_col_count]

    table = Table(*[str(column) for column in df.columns])

    for value_list in df.values.tolist():
        row = [str(x) for x in value_list]
        table.add_row(*row)

    # Update the style of the table
    table.row_styles = ["none", "dim"]
    table.box = box.SIMPLE_HEAD

    return table

format_size(size_bytes) #

Format file size in human-readable form.

Parameters:

Name Type Description Default
size_bytes int | float

Size in bytes

required

Returns:

Type Description
str

Human-readable size string (e.g., "1.5 MB")

Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
def format_size(size_bytes: int | float) -> str:
    """
    Format file size in human-readable form.

    Parameters
    ----------
    size_bytes
        Size in bytes

    Returns
    -------
    :
        Human-readable size string (e.g., "1.5 MB")
    """
    size = float(size_bytes)
    for unit in ("B", "KB", "MB", "GB"):
        if size < _BYTES_PER_UNIT:
            return f"{size:.1f} {unit}"
        size /= _BYTES_PER_UNIT
    return f"{size:.1f} TB"

parse_facet_filters(filters) #

Parse facet filters from key=value format into a dictionary.

Multiple values for the same key are collected into a list (OR logic). Different keys are ANDed together when used for filtering.

Parameters:

Name Type Description Default
filters list[str] | None

List of filter strings in 'key=value' format. Multiple entries with the same key are ORed (e.g., ["source_id=A", "source_id=B"] matches A or B).

required

Returns:

Type Description
dict[str, list[str]]

Dictionary mapping facet keys to lists of allowed values

Raises:

Type Description
ValueError

If a filter string is not in valid 'key=value' format

Examples:

>>> parse_facet_filters(["source_id=GFDL-ESM4", "variable_id=tas"])
{'source_id': ['GFDL-ESM4'], 'variable_id': ['tas']}
>>> parse_facet_filters(["source_id=A", "source_id=B"])
{'source_id': ['A', 'B']}
Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
def parse_facet_filters(filters: list[str] | None) -> dict[str, list[str]]:
    """
    Parse facet filters from key=value format into a dictionary.

    Multiple values for the same key are collected into a list (OR logic).
    Different keys are ANDed together when used for filtering.

    Parameters
    ----------
    filters
        List of filter strings in 'key=value' format.
        Multiple entries with the same key are ORed
        (e.g., ``["source_id=A", "source_id=B"]`` matches A or B).

    Returns
    -------
    dict[str, list[str]]
        Dictionary mapping facet keys to lists of allowed values

    Raises
    ------
    ValueError
        If a filter string is not in valid 'key=value' format

    Examples
    --------
    >>> parse_facet_filters(["source_id=GFDL-ESM4", "variable_id=tas"])
    {'source_id': ['GFDL-ESM4'], 'variable_id': ['tas']}

    >>> parse_facet_filters(["source_id=A", "source_id=B"])
    {'source_id': ['A', 'B']}
    """
    if not filters:
        return {}

    parsed: dict[str, list[str]] = {}
    for filter_str in filters:
        if "=" not in filter_str:
            raise ValueError(
                f"Invalid filter format: '{filter_str}'. "
                f"Expected format: 'key=value' or 'dataset_type.key=value' "
                f"(e.g., 'source_id=GFDL-ESM4' or 'cmip6.source_id=GFDL-ESM4')"
            )

        key, value = filter_str.split("=", 1)
        key = key.strip()
        value = value.strip()

        if not key:
            raise ValueError(f"Empty key in filter: '{filter_str}'")
        if not value:
            raise ValueError(f"Empty value in filter: '{filter_str}'")

        parsed.setdefault(key, []).append(value)

    return parsed

pretty_print_df(df, console=None) #

Pretty print a DataFrame

Parameters:

Name Type Description Default
df DataFrame

DataFrame to print

required
console Console | None

Console to use for printing

If not provided, a new Console instance will be created

None
Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
def pretty_print_df(df: pd.DataFrame, console: Console | None = None) -> None:
    """
    Pretty print a DataFrame

    Parameters
    ----------
    df
        DataFrame to print
    console
        Console to use for printing

        If not provided, a new Console instance will be created
    """
    # Drop duplicates as they are not informative to CLI users.
    try:
        df = df.drop_duplicates()
    except TypeError:
        # Some columns may hold unhashable values so fallback to str
        df = df.loc[~df.astype(str).duplicated()]

    if console is None:  # pragma: no branch
        logger.debug("Creating new console for pretty printing")
        console = Console()

    max_col_count = console.width // 10
    table = df_to_table(df, max_col_count=max_col_count)

    console.print(table)

render_dataframe(df, console=None, output_format=OutputFormat.table) #

Render a DataFrame as either a rich table or machine-readable JSON.

Parameters:

Name Type Description Default
df DataFrame

DataFrame to render

required
console Console | None

Console to use for table output (ignored for JSON)

None
output_format OutputFormat

table (default) for a human-readable rich table, or json for a list of records printed to stdout.

Unlike the table output, the JSON output is not truncated and does not drop duplicate rows, so it always contains the full values (e.g. complete IDs and paths) and is suitable for scripting.

table
Source code in packages/climate-ref/src/climate_ref/cli/_utils.py
def render_dataframe(
    df: pd.DataFrame,
    console: Console | None = None,
    output_format: OutputFormat = OutputFormat.table,
) -> None:
    """
    Render a DataFrame as either a rich table or machine-readable JSON.

    Parameters
    ----------
    df
        DataFrame to render
    console
        Console to use for table output (ignored for JSON)
    output_format
        ``table`` (default) for a human-readable rich table,
        or ``json`` for a list of records printed to stdout.

        Unlike the table output, the JSON output is not truncated and does not
        drop duplicate rows, so it always contains the full values
        (e.g. complete IDs and paths) and is suitable for scripting.
    """
    if output_format == OutputFormat.json:
        records = df.to_dict(orient="records")
        print(json.dumps(records, default=str, indent=2))
    else:
        pretty_print_df(df, console=console)