Skip to content

aimbat.utils

Utils used in AIMBAT.

Classes:

Name Description
TableStyling

This class is to set the colour of the table columns and elements.

Functions:

Name Description
delete_sampledata

Delete sample data.

download_sampledata

Download sample data.

json_to_table

Print a JSON dict or list of dicts as a rich table.

string_to_uuid

Determine a UUID from a string containing the first few characters.

uuid_shortener

Calculates the shortest unique prefix for a UUID, returning with dashes.

TableStyling dataclass

This class is to set the colour of the table columns and elements.

Parameters:

Name Type Description Default
id str
'bright_blue'
mine str
'cyan'
linked str
'magenta'
parameters str
'green'
Source code in src/aimbat/utils/_style.py
@dataclass(frozen=True)
class TableStyling:
    """This class is to set the colour of the table columns and elements."""

    id: str = "bright_blue"
    mine: str = "cyan"
    linked: str = "magenta"
    parameters: str = "green"

    @staticmethod
    def bool_formatter(true_or_false: bool | Any) -> str:
        if true_or_false is True:
            return "[bold green]:heavy_check_mark:[/]"
        elif true_or_false is False:
            return "[bold red]:heavy_multiplication_x:[/]"
        return true_or_false

    @staticmethod
    def timestamp_formatter(dt: Timestamp, short: bool) -> str:
        if short:
            return dt.strftime("%Y-%m-%d [light_sea_green]%H:%M:%S[/]")
        return str(dt)

delete_sampledata

delete_sampledata() -> None

Delete sample data.

Source code in src/aimbat/utils/_sampledata.py
def delete_sampledata() -> None:
    """Delete sample data."""

    logger.info(f"Deleting sample data in {settings.sampledata_dir}.")

    shutil.rmtree(settings.sampledata_dir)

download_sampledata

download_sampledata(force: bool = False) -> None

Download sample data.

Source code in src/aimbat/utils/_sampledata.py
def download_sampledata(force: bool = False) -> None:
    """Download sample data."""

    logger.info(
        f"Downloading sample data from {settings.sampledata_src} to {settings.sampledata_dir}."
    )

    if (
        settings.sampledata_dir.exists()
        and len(os.listdir(settings.sampledata_dir)) != 0
    ):
        if force is True:
            delete_sampledata()
        else:
            raise FileExistsError(
                f"The directory {settings.sampledata_dir} already exists and is non-empty."
            )

    with urlopen(settings.sampledata_src) as zipresp:
        with ZipFile(BytesIO(zipresp.read())) as zfile:
            zfile.extractall(settings.sampledata_dir)

json_to_table

json_to_table(
    data: dict[str, Any] | list[dict[str, Any]],
    title: str | None = None,
    formatters: (
        dict[str, Callable[[Any], str]] | None
    ) = None,
    skip_keys: list[str] | None = None,
    column_order: list[str] | None = None,
    column_kwargs: dict[str, dict[str, Any]] | None = None,
    common_column_kwargs: dict[str, Any] | None = None,
) -> None

Print a JSON dict or list of dicts as a rich table.

For a single dict the table has Key and Value columns with one row per key-value pair. For a list of dicts the keys become column headers and each list item becomes a row.

Parameters:

Name Type Description Default
data dict[str, Any] | list[dict[str, Any]]

A single JSON dict or a list of JSON dicts.

required
title str | None

Optional title displayed above the table.

None
formatters dict[str, Callable[[Any], str]] | None

Optional mapping of key names to callables that receive the raw value and return a string for display.

None
skip_keys list[str] | None

Optional list of keys to exclude from the table.

None
column_order list[str] | None

Optional list of keys defining the display order. Keys not listed are appended after in their original order. For a single dict this controls row order; for a list of dicts it controls column order.

None
column_kwargs dict[str, dict[str, Any]] | None

Optional mapping of key names to keyword arguments forwarded to Table.add_column (e.g. style, justify, min_width). A "header" entry overrides the displayed column header name. For a single dict the special keys "Key" and "Value" target those header columns.

None
common_column_kwargs dict[str, Any] | None

Optional keyword arguments applied to every column, merged with any per-column entries in column_kwargs. Per-column values take precedence over these defaults.

None

Examples:

>>> json_to_table({"name": "Alice", "age": 30}, title="Person")
>>> json_to_table([{"id": 1}, {"id": 2}], formatters={"id": str})
>>> json_to_table({"name": "Alice", "secret": "x"}, skip_keys=["secret"])
>>> json_to_table(
...     [{"id": 1, "name": "Alice"}],
...     column_order=["name", "id"],
...     column_kwargs={"id": {"justify": "right", "style": "cyan"}},
... )
Source code in src/aimbat/utils/_json.py
def json_to_table(
    data: dict[str, Any] | list[dict[str, Any]],
    title: str | None = None,
    formatters: dict[str, Callable[[Any], str]] | None = None,
    skip_keys: list[str] | None = None,
    column_order: list[str] | None = None,
    column_kwargs: dict[str, dict[str, Any]] | None = None,
    common_column_kwargs: dict[str, Any] | None = None,
) -> None:
    """Print a JSON dict or list of dicts as a rich table.

    For a single dict the table has ``Key`` and ``Value`` columns with one row
    per key-value pair.  For a list of dicts the keys become column headers and
    each list item becomes a row.

    Args:
        data: A single JSON dict or a list of JSON dicts.
        title: Optional title displayed above the table.
        formatters: Optional mapping of key names to callables that receive the
            raw value and return a string for display.
        skip_keys: Optional list of keys to exclude from the table.
        column_order: Optional list of keys defining the display order.  Keys
            not listed are appended after in their original order.  For a single
            dict this controls row order; for a list of dicts it controls column
            order.
        column_kwargs: Optional mapping of key names to keyword arguments
            forwarded to ``Table.add_column`` (e.g. ``style``, ``justify``,
            ``min_width``).  A ``"header"`` entry overrides the displayed column
            header name.  For a single dict the special keys ``"Key"`` and
            ``"Value"`` target those header columns.
        common_column_kwargs: Optional keyword arguments applied to every
            column, merged with any per-column entries in ``column_kwargs``.
            Per-column values take precedence over these defaults.

    Examples:
        >>> json_to_table({"name": "Alice", "age": 30}, title="Person")
        >>> json_to_table([{"id": 1}, {"id": 2}], formatters={"id": str})
        >>> json_to_table({"name": "Alice", "secret": "x"}, skip_keys=["secret"])
        >>> json_to_table(
        ...     [{"id": 1, "name": "Alice"}],
        ...     column_order=["name", "id"],
        ...     column_kwargs={"id": {"justify": "right", "style": "cyan"}},
        ... )
    """
    formatters = formatters or {}
    skip = set(skip_keys or [])
    column_kwargs = column_kwargs or {}
    common_column_kwargs = common_column_kwargs or {}
    console = Console()
    table = make_table(title=title)

    def _sorted_keys(keys: list[str]) -> list[str]:
        """Return keys reordered by column_order, with remaining keys appended."""
        if not column_order:
            return keys
        ordered = [k for k in column_order if k in keys]
        rest = [k for k in keys if k not in set(column_order)]
        return ordered + rest

    if isinstance(data, dict):
        key_kw = {**common_column_kwargs, **column_kwargs.get("Key", {})}
        val_kw = {**common_column_kwargs, **column_kwargs.get("Value", {})}
        table.add_column(key_kw.pop("header", "Key"), **key_kw)
        table.add_column(val_kw.pop("header", "Value"), **val_kw)
        keys = _sorted_keys([k for k in data if k not in skip])
        for key in keys:
            formatted = (
                formatters[key](data[key]) if key in formatters else str(data[key])
            )
            table.add_row(str(key), formatted)
    else:
        if not data:
            console.print(table)
            return
        columns = _sorted_keys([k for k in data[0].keys() if k not in skip])
        for col in columns:
            col_kw = {**common_column_kwargs, **column_kwargs.get(col, {})}
            table.add_column(col_kw.pop("header", str(col)), **col_kw)
        for item in data:
            row = []
            for col in columns:
                value = item.get(col)
                formatted = formatters[col](value) if col in formatters else str(value)
                row.append(formatted)
            table.add_row(*row)

    console.print(table)

string_to_uuid

string_to_uuid(
    session: Session,
    id: str,
    aimbat_class: type[AimbatTypes],
    custom_error: str | None = None,
) -> UUID

Determine a UUID from a string containing the first few characters.

Parameters:

Name Type Description Default
session Session

Database session.

required
id str

Input string to find UUID for.

required
aimbat_class type[AimbatTypes]

Aimbat class to use to find UUID.

required
custom_error str | None

Overrides the default error message.

None

Returns:

Type Description
UUID

The full UUID.

Raises:

Type Description
ValueError

If the UUID could not be determined.

Source code in src/aimbat/utils/_uuid.py
def string_to_uuid(
    session: Session,
    id: str,
    aimbat_class: type[AimbatTypes],
    custom_error: str | None = None,
) -> UUID:
    """Determine a UUID from a string containing the first few characters.

    Args:
        session: Database session.
        id: Input string to find UUID for.
        aimbat_class: Aimbat class to use to find UUID.
        custom_error: Overrides the default error message.

    Returns:
        The full UUID.

    Raises:
        ValueError: If the UUID could not be determined.
    """
    statement = select(aimbat_class.id).where(
        func.replace(cast(aimbat_class.id, String), "-", "").like(
            f"{id.replace('-', '')}%"
        )
    )
    uuid_set = set(session.exec(statement).all())
    if len(uuid_set) == 1:
        return uuid_set.pop()
    if len(uuid_set) == 0:
        raise ValueError(
            custom_error or f"Unable to find {aimbat_class.__name__} using id: {id}."
        )
    raise ValueError(f"Found more than one {aimbat_class.__name__} using id: {id}")

uuid_shortener

uuid_shortener(
    session: Session,
    aimbat_obj: T | type[T],
    min_length: int = 2,
    str_uuid: str | None = None,
) -> str

Calculates the shortest unique prefix for a UUID, returning with dashes.

Parameters:

Name Type Description Default
session Session

An active SQLModel/SQLAlchemy session.

required
aimbat_obj T | type[T]

Either an instance of a SQLModel or the SQLModel class itself.

required
min_length int

The starting character length for the shortened ID.

2
str_uuid str | None

The full UUID string. Required only if aimbat_obj is a class.

None

Returns:

Name Type Description
str str

The shortest unique prefix string, including hyphens where applicable.

Source code in src/aimbat/utils/_uuid.py
def uuid_shortener[T: AimbatTypes](
    session: Session,
    aimbat_obj: T | type[T],
    min_length: int = 2,
    str_uuid: str | None = None,
) -> str:
    """Calculates the shortest unique prefix for a UUID, returning with dashes.

    Args:
        session: An active SQLModel/SQLAlchemy session.
        aimbat_obj: Either an instance of a SQLModel or the SQLModel class itself.
        min_length: The starting character length for the shortened ID.
        str_uuid: The full UUID string. Required only if `aimbat_obj` is a class.

    Returns:
        str: The shortest unique prefix string, including hyphens where applicable.
    """

    if isinstance(aimbat_obj, type):
        model_class = aimbat_obj
        if str_uuid is None:
            raise ValueError("str_uuid must be provided when aimbat_obj is a class.")
        target_full = str(UUID(str_uuid))
    else:
        model_class = type(aimbat_obj)
        target_full = str(aimbat_obj.id)

    prefix_clean = target_full.replace("-", "")[:min_length]

    # select with a WHERE clause that removes dashes and compares the cleaned prefix
    statement = select(model_class.id).where(
        func.replace(cast(model_class.id, String), "-", "").like(f"{prefix_clean}%")
    )

    # Store results as standard hyphenated strings
    results = session.exec(statement).all()
    relevant_pool = [str(uid) for uid in results]

    if target_full not in relevant_pool:
        raise ValueError(f"ID {target_full} not found in table {model_class.__name__}")

    current_length = min_length
    while current_length < len(target_full):
        candidate = target_full[:current_length]
        if candidate.endswith("-"):
            current_length += 1
            candidate = target_full[:current_length]

        matches = [u for u in relevant_pool if u.startswith(candidate)]
        if len(matches) == 1:
            return candidate
        current_length += 1

    return target_full