Skip to content

aimbat.utils

Miscellaneous helpers for AIMBAT.

Covers four areas:

  • JSON — render JSON data as Rich tables (json_to_table).
  • Sample data — download and delete the bundled sample dataset (download_sampledata, delete_sampledata).
  • Styling — shared Rich/table style helpers (make_table).
  • UUIDs — look up model records by short UUID prefix (get_by_uuid).

Modules:

Name Description
formatters

Functions:

Name Description
delete_sampledata

Delete sample data.

download_sampledata

Download sample data.

get_title_map

Creates a mapping from field names to their 'title' metadata.

mean_and_sem

Return the mean and standard error of the mean (SEM) for a list of numeric values, ignoring None values.

mean_and_sem_timedelta

Return (mean, sem) for a list of pd.Timedelta values.

rel

Cast a SQLModel relationship attribute to QueryableAttribute for use with selectinload.

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.

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:
        logger.debug(f"Extracting sample data to {settings.sampledata_dir}.")
        with ZipFile(BytesIO(zipresp.read())) as zfile:
            zfile.extractall(settings.sampledata_dir)

    logger.info("Sample data downloaded and extracted successfully.")

get_title_map cached

get_title_map(
    model_class: type[BaseModel],
) -> dict[str, str]

Creates a mapping from field names to their 'title' metadata.

Source code in src/aimbat/utils/_pydantic.py
@lru_cache(maxsize=None)
def get_title_map(model_class: type[BaseModel]) -> dict[str, str]:
    """Creates a mapping from field names to their 'title' metadata."""
    mapping: dict[str, str] = {}

    for name, info in model_class.model_fields.items():
        mapping[name] = info.title or name.replace("_", " ")

    computed_fields: dict[str, Any] = getattr(
        model_class, "__pydantic_computed_fields__", {}
    )
    for name, info in computed_fields.items():
        title = getattr(info, "title", None)
        mapping[name] = title or name.replace("_", " ")

    return mapping

mean_and_sem

mean_and_sem(
    data: Sequence[float | None],
) -> tuple[float | None, float | None]

Return the mean and standard error of the mean (SEM) for a list of numeric values, ignoring None values.

Parameters:

Name Type Description Default
data Sequence[float | None]

List of numeric values (float or int) or None.

required

Returns:

Type Description
tuple[float | None, float | None]

A tuple containing the mean and SEM of the input data, both as floats or None if not computable.

Source code in src/aimbat/utils/_maths.py
def mean_and_sem(
    data: Sequence[float | None],
) -> tuple[float | None, float | None]:
    """Return the mean and standard error of the mean (SEM) for a list of numeric values, ignoring None values.

    Args:
        data: List of numeric values (float or int) or None.

    Returns:
        A tuple containing the mean and SEM of the input data, both as floats or None if not computable.
    """
    return _mean_and_sem_tuple(tuple(data))

mean_and_sem_timedelta

mean_and_sem_timedelta(
    values: Sequence[Timedelta],
) -> tuple[Timedelta | None, Timedelta | None]

Return (mean, sem) for a list of pd.Timedelta values.

Parameters:

Name Type Description Default
values Sequence[Timedelta]

List of pd.Timedelta values.

required

Returns:

Type Description
tuple[Timedelta | None, Timedelta | None]

(None, None) when empty. SEM is None for fewer than two values.

Source code in src/aimbat/utils/_maths.py
def mean_and_sem_timedelta(
    values: Sequence[pd.Timedelta],
) -> tuple[pd.Timedelta | None, pd.Timedelta | None]:
    """Return (mean, sem) for a list of pd.Timedelta values.

    Args:
        values: List of pd.Timedelta values.

    Returns:
        `(None, None)` when empty. SEM is `None` for fewer than two values.
    """
    if not values:
        return None, None
    ns_vals = [float(td.value) for td in values]
    mean_ns, sem_ns = mean_and_sem(ns_vals)
    return (
        pd.Timedelta(int(mean_ns), unit="ns") if mean_ns is not None else None,
        pd.Timedelta(int(sem_ns), unit="ns") if sem_ns is not None else None,
    )

rel

rel(attr: Any) -> QueryableAttribute[Any]

Cast a SQLModel relationship attribute to QueryableAttribute for use with selectinload.

SQLModel types relationship fields as their Python collection type (e.g. list[Foo]), but SQLAlchemy's selectinload expects a QueryableAttribute. This helper performs the cast to satisfy mypy without requiring per-call # type: ignore comments.

Source code in src/aimbat/utils/_sqlalchemy.py
def rel(attr: Any) -> QueryableAttribute[Any]:
    """Cast a SQLModel relationship attribute to `QueryableAttribute` for use with `selectinload`.

    SQLModel types relationship fields as their Python collection type (e.g. `list[Foo]`),
    but SQLAlchemy's `selectinload` expects a `QueryableAttribute`. This helper performs
    the cast to satisfy mypy without requiring per-call `# type: ignore` comments.
    """
    return cast(QueryableAttribute[Any], attr)

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:
        resolved = uuid_set.pop()
        logger.debug(f"Resolved {id} to UUID: {resolved}")
        return resolved
    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
            continue

        matches = [u for u in relevant_pool if u.startswith(candidate)]
        if len(matches) == 1:
            logger.debug(f"Shortened {target_full} to: {candidate}")
            return candidate
        current_length += 1

    return target_full

formatters

Functions:

Name Description
fmt_bool

Format a boolean as (True) or empty string (False/None).

fmt_depth_km

Format a depth value in metres as kilometres with one decimal place.

fmt_flip

Format a boolean flip flag as (True) or empty string (False).

fmt_float

Format a float to 3 decimal places, or for None/NaN.

fmt_timedelta

Format a Timedelta as total seconds to 5 decimal places, or for None.

fmt_timestamp

Format a timestamp as YYYY-MM-DD HH:MM:SS, or for missing values.

fmt_bool

fmt_bool(val: bool | object) -> str

Format a boolean as (True) or empty string (False/None).

Source code in src/aimbat/utils/formatters.py
def fmt_bool(val: bool | object) -> str:
    """Format a boolean as `✓` (True) or empty string (False/None)."""
    return "✓" if val is True else ""

fmt_depth_km

fmt_depth_km(val: int | float | object) -> str

Format a depth value in metres as kilometres with one decimal place.

Source code in src/aimbat/utils/formatters.py
def fmt_depth_km(val: int | float | object) -> str:
    """Format a depth value in metres as kilometres with one decimal place."""
    if isinstance(val, (int, float)):
        return f"{val / 1000:.1f}"
    return str(val)

fmt_flip

fmt_flip(val: bool | object) -> str

Format a boolean flip flag as (True) or empty string (False).

Source code in src/aimbat/utils/formatters.py
def fmt_flip(val: bool | object) -> str:
    """Format a boolean flip flag as `↕` (True) or empty string (False)."""
    if isinstance(val, bool):
        return "↕" if val else ""
    return str(val)

fmt_float

fmt_float(val: float | object) -> str

Format a float to 3 decimal places, or for None/NaN.

Source code in src/aimbat/utils/formatters.py
def fmt_float(val: float | object) -> str:
    """Format a float to 3 decimal places, or ` — ` for None/NaN."""
    if val is None or (isinstance(val, float) and math.isnan(val)):
        return _MISSING_MARKER
    if isinstance(val, float):
        return f"{val:.3f}"
    return str(val)

fmt_timedelta

fmt_timedelta(val: Timedelta | object) -> str

Format a Timedelta as total seconds to 5 decimal places, or for None.

Source code in src/aimbat/utils/formatters.py
def fmt_timedelta(val: Timedelta | object) -> str:
    """Format a Timedelta as total seconds to 5 decimal places, or ` — ` for None."""
    if val is None:
        return _MISSING_MARKER
    if isinstance(val, Timedelta):
        return f"{val.total_seconds():.5f} s"
    return str(val)

fmt_timestamp

fmt_timestamp(val: Any) -> str

Format a timestamp as YYYY-MM-DD HH:MM:SS, or for missing values.

Source code in src/aimbat/utils/formatters.py
def fmt_timestamp(val: Any) -> str:
    """Format a timestamp as `YYYY-MM-DD HH:MM:SS`, or ` — ` for missing values."""
    if isinstance(val, str) and val.strip():
        try:
            val = to_datetime(val)
        except (ValueError, TypeError):
            return str(val)
    if val is None or val is NaT or val == "":
        return _MISSING_MARKER
    if hasattr(val, "strftime"):
        return val.strftime("%Y-%m-%d %H:%M:%S")
    return str(val)