Skip to content

Index

axm_ingot

axm-ingot.

Canonical shared helpers factored out of duplicated AXM code.

Member dataclass

A single uv-workspace member.

Attributes:

Name Type Description
name str

Basename of the member directory -- not the [project].name declared in the member's pyproject.toml. Two globs resolving to same-named directories (e.g. packages/core and libs/core) therefore yield two members with equal name; callers indexing by name must account for the collision. Read the package name from the member's pyproject.toml (path locates it) when needed.

path Path

Absolute, resolved path to the member directory.

Source code in packages/axm-ingot/src/axm_ingot/uv/models.py
Python
@dataclass(frozen=True)
class Member:
    """A single uv-workspace member.

    Attributes:
        name: Basename of the member *directory* -- not the ``[project].name``
            declared in the member's ``pyproject.toml``. Two globs resolving to
            same-named directories (e.g. ``packages/core`` and ``libs/core``)
            therefore yield two members with equal ``name``; callers indexing by
            ``name`` must account for the collision. Read the package name from
            the member's ``pyproject.toml`` (``path`` locates it) when needed.
        path: Absolute, resolved path to the member directory.
    """

    name: str
    path: Path

ResolvedWorkspace dataclass

A uv workspace with its resolved members.

Attributes:

Name Type Description
root Path

Absolute path to the workspace root (holding the root pyproject).

members tuple[Member, ...]

Members sorted by name.

Source code in packages/axm-ingot/src/axm_ingot/uv/models.py
Python
@dataclass(frozen=True)
class ResolvedWorkspace:
    """A uv workspace with its resolved members.

    Attributes:
        root: Absolute path to the workspace root (holding the root pyproject).
        members: Members sorted by name.
    """

    root: Path
    members: tuple[Member, ...]

compact_table(rows, headers=None)

Render rows as a column-aligned table, optionally with a headers row.

Tolerates ragged rows (short rows are padded) and arbitrarily wide cells. None cells render as empty, never as the literal "None".

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def compact_table(
    rows: Sequence[Sequence[object]],
    headers: Sequence[object] | None = None,
) -> str:
    """Render *rows* as a column-aligned table, optionally with a *headers* row.

    Tolerates ragged rows (short rows are padded) and arbitrarily wide cells.
    ``None`` cells render as empty, never as the literal ``"None"``.
    """
    matrix = _build_matrix(rows, headers)
    if not matrix:
        return ""
    return "\n".join(_render_rows(matrix))

console_script(name, *, executable=None)

Return the path of console script name for the running environment.

Looks next to the interpreter first (where an installed entry point lands), then falls back to PATH, then to the bare name so an unusual layout — or a caller that genuinely wants PATH resolution at exec time — still works.

Parameters:

Name Type Description Default
name str

The console-script name, e.g. "axm".

required
executable str | None

Interpreter whose environment to search; defaults to :data:sys.executable (injectable for tests).

None

Returns:

Type Description
str

An absolute path when the script was found, else name unchanged.

Source code in packages/axm-ingot/src/axm_ingot/console.py
Python
def console_script(name: str, *, executable: str | None = None) -> str:
    """Return the path of console script *name* for the running environment.

    Looks next to the interpreter first (where an installed entry point lands),
    then falls back to ``PATH``, then to the bare name so an unusual layout — or
    a caller that genuinely wants ``PATH`` resolution at exec time — still works.

    Args:
        name: The console-script name, e.g. ``"axm"``.
        executable: Interpreter whose environment to search; defaults to
            :data:`sys.executable` (injectable for tests).

    Returns:
        An absolute path when the script was found, else *name* unchanged.
    """
    interpreter = Path(executable or sys.executable)
    candidate = interpreter.parent / name
    if candidate.is_file():
        return str(candidate)
    return shutil.which(name) or name

find_project_root(start)

Walk parents from start to the first directory holding any pyproject.

Returns the directory of the first ancestor (start included) that contains a pyproject.toml -- any project, not necessarily a uv workspace. start is resolved first; a file start is anchored on its parent directory. Unlike :func:find_workspace_root, this never returns None: with no pyproject.toml in any ancestor it falls back to the (resolved) starting directory.

Source code in packages/axm-ingot/src/axm_ingot/uv/resolve.py
Python
def find_project_root(start: Path) -> Path:
    """Walk parents from ``start`` to the first directory holding any pyproject.

    Returns the directory of the first ancestor (``start`` included) that
    contains a ``pyproject.toml`` -- any project, not necessarily a uv
    workspace. ``start`` is resolved first; a file ``start`` is anchored on its
    parent directory. Unlike :func:`find_workspace_root`, this never returns
    ``None``: with no ``pyproject.toml`` in any ancestor it falls back to the
    (resolved) starting directory.
    """
    current = start.resolve()
    if current.is_file():
        current = current.parent
    for candidate in (current, *current.parents):
        if (candidate / _PYPROJECT).is_file():
            return candidate
    return current

find_workspace_root(path)

Walk parents from path to the first uv-workspace root.

Returns the directory of the first ancestor (path included) whose pyproject.toml carries a [tool.uv.workspace] section, else None.

Source code in packages/axm-ingot/src/axm_ingot/uv/resolve.py
Python
def find_workspace_root(path: Path) -> Path | None:
    """Walk parents from ``path`` to the first uv-workspace root.

    Returns the directory of the first ancestor (``path`` included) whose
    ``pyproject.toml`` carries a ``[tool.uv.workspace]`` section, else ``None``.
    """
    current = path.resolve()
    for directory in (current, *current.parents):
        data = _load_pyproject(directory)
        if data is not None and _get_workspace_config(data) is not None:
            return directory
    return None

format_count(n)

Format counts in 1000 steps through no suffix, K, M, then B.

Counts below 1000 have no decimal; all abbreviated bands have one. Negative input follows the same scaling, so -1500 renders as '-1.5K'.

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def format_count(n: int) -> str:
    """Format counts in 1000 steps through no suffix, K, M, then B.

    Counts below 1000 have no decimal; all abbreviated bands have one. Negative
    input follows the same scaling, so ``-1500`` renders as ``'-1.5K'``.
    """
    magnitude = abs(n)
    if magnitude < _COUNT_STEP:
        return str(n)
    for unit, divisor in (("B", 1_000_000_000), ("M", 1_000_000), ("K", 1_000)):
        if magnitude >= divisor:
            return f"{n / divisor:.1f}{unit}"
    return str(n)

format_duration(millis)

Render a millisecond duration as a short human string.

Rendering bands are milliseconds for 0 <= millis < 1000, seconds for 1000 <= millis < 60000, minutes for 60000 <= millis < 3600000, and hours for millis >= 3600000.

450 → '450ms', 1500 → '1.5s', 90000 → '1.5min', 5400000 → '1.5h'. Bands round to at most one decimal (rounded, not truncated). Negative, non-numeric or non-finite input returns 'n/a' without raising.

Source code in packages/axm-ingot/src/axm_ingot/duration.py
Python
def format_duration(millis: float) -> str:
    """Render a millisecond duration as a short human string.

    Rendering bands are milliseconds for ``0 <= millis < 1000``, seconds for
    ``1000 <= millis < 60000``, minutes for
    ``60000 <= millis < 3600000``, and hours for ``millis >= 3600000``.

    ``450`` → ``'450ms'``, ``1500`` → ``'1.5s'``, ``90000`` → ``'1.5min'``,
    ``5400000`` → ``'1.5h'``. Bands round to at most one decimal (rounded,
    not truncated). Negative, non-numeric or non-finite input returns
    ``'n/a'`` without raising.
    """
    try:
        value = float(millis)
    except (TypeError, ValueError):
        return _FALLBACK
    if not math.isfinite(value) or value < 0:
        return _FALLBACK
    if value < _SECOND:
        return f"{int(value)}ms"
    if value < _MINUTE:
        return f"{round(value / _SECOND, 1)}s"
    if value < _HOUR:
        return f"{round(value / _MINUTE, 1)}min"
    return f"{round(value / _HOUR, 1)}h"

format_size(num_bytes)

Format bytes in 1024 steps through B, KB, MB, GB, TB, then PB.

B has no decimal; all other bands have one. Negative input follows the same scaling, so -2048 renders as '-2.0 KB'.

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def format_size(num_bytes: int) -> str:
    """Format bytes in 1024 steps through B, KB, MB, GB, TB, then PB.

    B has no decimal; all other bands have one. Negative input follows the same
    scaling, so ``-2048`` renders as ``'-2.0 KB'``.
    """
    size = float(num_bytes)
    units = ("B", "KB", "MB", "GB", "TB", "PB")
    for unit in units:
        if abs(size) < _SIZE_STEP or unit == units[-1]:
            if unit == "B":
                return f"{int(size)} {unit}"
            return f"{size:.1f} {unit}"
        size /= _SIZE_STEP
    return f"{num_bytes} B"

header(tool, summary)

Render the compact header line {tool} | {summary}.

header("audit", "3 findings") 'audit | 3 findings'

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def header(tool: str, summary: str) -> str:
    """Render the compact header line ``{tool} | {summary}``.

    >>> header("audit", "3 findings")
    'audit | 3 findings'
    """
    return f"{tool} | {summary}"

labeled_block(label, lines)

Render label followed by lines, each indented two spaces.

An empty lines yields an empty string so no dangling label is emitted. None entries render as blank lines rather than the literal "None".

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def labeled_block(label: str, lines: Sequence[str | None]) -> str:
    """Render *label* followed by *lines*, each indented two spaces.

    An empty *lines* yields an empty string so no dangling label is emitted.
    ``None`` entries render as blank lines rather than the literal ``"None"``.
    """
    if not lines:
        return ""
    body = [f"{_INDENT}{_cell(line)}" for line in lines]
    return "\n".join([label, *body])

resolve_workspace(pyproject_dir)

Resolve the uv workspace rooted at pyproject_dir.

Parses [tool.uv.workspace].members, resolves the globs to directories, subtracts the exclude globs, keeps only directories that contain a pyproject.toml (require_pyproject), and returns the members sorted by name. Returns None when pyproject_dir is not a uv workspace or its pyproject is missing/malformed.

Source code in packages/axm-ingot/src/axm_ingot/uv/resolve.py
Python
def resolve_workspace(pyproject_dir: Path) -> ResolvedWorkspace | None:
    """Resolve the uv workspace rooted at ``pyproject_dir``.

    Parses ``[tool.uv.workspace].members``, resolves the globs to directories,
    subtracts the ``exclude`` globs, keeps only directories that contain a
    ``pyproject.toml`` (``require_pyproject``), and returns the members sorted
    by name. Returns ``None`` when ``pyproject_dir`` is not a uv workspace or
    its pyproject is missing/malformed.
    """
    root = pyproject_dir.resolve()
    data = _load_pyproject(root)
    if data is None:
        return None
    workspace = _get_workspace_config(data)
    if workspace is None:
        return None

    included = _resolve_glob_dirs(root, workspace.get("members"))
    excluded = _resolve_glob_dirs(root, workspace.get("exclude"))
    members = tuple(
        sorted(
            (
                Member(name=directory.name, path=directory)
                for directory in included - excluded
                if (directory / _PYPROJECT).is_file()
            ),
            key=lambda member: member.name,
        )
    )
    return ResolvedWorkspace(root=root, members=members)

tally_outcomes(lines)

Count pytest outcome lines per kind.

Parameters:

Name Type Description Default
lines Iterable[object]

An iterable of outcome lines (typically pytest short-summary lines such as "FAILED tests/..."). Non-string / None / malformed elements are tolerated.

required

Returns:

Type Description
dict[str, int]

A dict with the fixed keys failed/error/skipped/unknown

dict[str, int]

(each always present, initialised to 0). Every input element

dict[str, int]

increments exactly one bucket, so the bucket sum equals the number of

dict[str, int]

elements.

Source code in packages/axm-ingot/src/axm_ingot/pytest_tally.py
Python
def tally_outcomes(lines: Iterable[object]) -> dict[str, int]:
    """Count pytest outcome lines per kind.

    Args:
        lines: An iterable of outcome lines (typically pytest short-summary
            lines such as ``"FAILED tests/..."``). Non-string / ``None`` /
            malformed elements are tolerated.

    Returns:
        A dict with the fixed keys ``failed``/``error``/``skipped``/``unknown``
        (each always present, initialised to ``0``). Every input element
        increments exactly one bucket, so the bucket sum equals the number of
        elements.
    """
    tally: dict[str, int] = {"failed": 0, "error": 0, "skipped": 0, "unknown": 0}
    for line in lines:
        tally[_classify(line)] += 1
    return tally

truncate(text, limit)

Bound text to limit chars, appending an ellipsis when it overflows.

Text at or under limit is returned unchanged. The overflow result has at most limit + 1 characters and ends with the ellipsis marker.

Source code in packages/axm-ingot/src/axm_ingot/render.py
Python
def truncate(text: str, limit: int) -> str:
    """Bound *text* to *limit* chars, appending an ellipsis when it overflows.

    Text at or under *limit* is returned unchanged. The overflow result has at
    most ``limit + 1`` characters and ends with the ellipsis marker.
    """
    bound = max(limit, 0)
    if len(text) <= bound:
        return text
    return text[:bound] + _ELLIPSIS