Skip to content

Output

cxg.output

OutputFormat

Bases: StrEnum

Output formats for commands that support tabular, JSON, and TSV.

Source code in src/cxg/output.py
class OutputFormat(StrEnum):
    """Output formats for commands that support tabular, JSON, and TSV."""

    table = "table"
    json = "json"
    tsv = "tsv"

DetailOutputFormat

Bases: StrEnum

Output formats for detail/get commands (no TSV).

Source code in src/cxg/output.py
class DetailOutputFormat(StrEnum):
    """Output formats for detail/get commands (no TSV)."""

    table = "table"
    json = "json"

fit_columns(columns, terminal_width, content_widths=None)

Drop optional columns from the end until flexible columns have enough space.

Source code in src/cxg/output.py
def fit_columns(
    columns: list[str],
    terminal_width: int,
    content_widths: dict[str, int] | None = None,
) -> list[str]:
    """Drop optional columns from the end until flexible columns have enough space."""
    result = list(columns)
    while len(result) > 1:
        fixed_total = sum(
            (content_widths.get(c, 14) if content_widths else 14) + _COLUMN_PADDING
            for c in result
            if c not in _FLEXIBLE_COLUMNS
        )
        n_flex = sum(1 for c in result if c in _FLEXIBLE_COLUMNS)
        if n_flex == 0:
            break
        available = terminal_width - fixed_total
        per_flex = available // n_flex - _COLUMN_PADDING
        if per_flex >= _MIN_FLEXIBLE_WIDTH:
            break
        for i in range(len(result) - 1, -1, -1):
            if result[i] not in REQUIRED_COLUMNS:
                result.pop(i)
                break
        else:
            break
    return result

dataset_column_value(dataset, column, *, truncate_id=False, for_table=False)

Extract and format a column value from a dataset dict.

Parameters:

Name Type Description Default
dataset dict[str, Any]

Dataset dictionary from the API.

required
column str

Column name to extract.

required
truncate_id bool

If True, truncate dataset_id to 8 characters.

False
for_table bool

If True, return Rich Text with styling and truncated lists.

False

Returns:

Type Description
str | Text

Formatted string or Rich Text object.

Source code in src/cxg/output.py
def dataset_column_value(
    dataset: dict[str, Any],
    column: str,
    *,
    truncate_id: bool = False,
    for_table: bool = False,
) -> str | Text:
    """Extract and format a column value from a dataset dict.

    Args:
        dataset: Dataset dictionary from the API.
        column: Column name to extract.
        truncate_id: If True, truncate dataset_id to 8 characters.
        for_table: If True, return Rich Text with styling and truncated lists.

    Returns:
        Formatted string or Rich Text object.
    """
    if column == "index":
        value = str(dataset.get("_index") or "")
        return Text(value, style="dim") if for_table else value
    if column == "title":
        value = dataset_title(dataset)
        return Text(value, style="bold") if for_table else value
    if column in _ONTOLOGY_COLUMNS:
        labels = get_labels(dataset, column)
        return _truncate_list(labels) if for_table else "; ".join(labels)
    if column == "collection":
        return collection_name(dataset)
    if column == "cell_count":
        count = get_cell_count(dataset)
        return humanize.intcomma(count) if for_table else str(count)
    if column == "dataset_id":
        value = dataset_id(dataset)
        value = value[:8] if truncate_id and value else value
        return Text(value, style="dim") if for_table else value
    if column == "published_at":
        raw = dataset.get("published_at")
        value = _format_datetime(raw, relative=for_table)
        return Text(value, style="dim") if for_table else value
    if column == "schema_version":
        return str(dataset.get("schema_version") or "")
    return _flatten(dataset.get(column))

sort_datasets(datasets, sort_by)

Sort datasets by the given key and direction.

The sort_by value is a column name optionally followed by a direction (asc or desc), separated by a space or colon. Defaults to descending.

Source code in src/cxg/output.py
def sort_datasets(datasets: list[dict[str, Any]], sort_by: str) -> list[dict[str, Any]]:
    """Sort datasets by the given key and direction.

    The sort_by value is a column name optionally followed by a direction
    (asc or desc), separated by a space or colon. Defaults to descending.
    """
    descending = True
    key = sort_by
    if ":" in sort_by:
        key, direction = sort_by.split(":", 1)
        descending = direction.lower() != "asc"
    elif sort_by.endswith(" desc"):
        key = sort_by[:-5]
        descending = True
    elif sort_by.endswith(" asc"):
        key = sort_by[:-4]
        descending = False

    def sort_key(dataset: dict[str, Any]) -> Any:
        if key == "cell_count":
            return get_cell_count(dataset)
        return dataset_column_value(dataset, key)

    return sorted(datasets, key=sort_key, reverse=descending)

render_datasets_table(datasets, columns, terminal_width=80)

Render a Rich table of datasets with adaptive column widths.

Source code in src/cxg/output.py
def render_datasets_table(
    datasets: list[dict[str, Any]], columns: list[str], terminal_width: int = 80
) -> Table:
    """Render a Rich table of datasets with adaptive column widths."""
    # Pre-render all cell values
    rows = [
        [dataset_column_value(ds, col, truncate_id=True, for_table=True) for col in columns]
        for ds in datasets
    ]

    # Measure content, then calculate explicit column widths
    content_widths = _measure_columns(rows, columns)
    col_widths = _calculate_column_widths(content_widths, columns, terminal_width)

    table = Table(
        box=None, show_header=True, header_style="bold", pad_edge=False, padding=(0, 2, 0, 0)
    )
    for column in columns:
        table.add_column(
            column.replace("_", " "),
            justify="right" if column in ("cell_count", "index") else "left",
            width=col_widths.get(column),
            overflow="ellipsis",
            no_wrap=True,
        )
    for row in rows:
        table.add_row(*row)
    return table

render_dataset_detail(dataset, *, full=False)

Render a polished, multi-section detail view of a single dataset.

Sections (top to bottom): collection headline + DOI, dataset title strip, side-by-side "Overview" / "Timeline" panels, a "Details" key/value block with linked ontology terms, "Assets" with linkified URLs, and a "Next" action block. With full=True, ontology lists expand to one term per line and an extra metadata/details block surfaces version IDs, citation, and the full donor list.

Source code in src/cxg/output.py
def render_dataset_detail(dataset: dict[str, Any], *, full: bool = False) -> RenderableType:
    """Render a polished, multi-section detail view of a single dataset.

    Sections (top to bottom): collection headline + DOI, dataset title strip,
    side-by-side "Overview" / "Timeline" panels, a "Details" key/value block
    with linked ontology terms, "Assets" with linkified URLs, and a "Next"
    action block. With ``full=True``, ontology lists expand to one term per
    line and an extra metadata/details block surfaces version IDs, citation,
    and the full donor list.
    """
    blank = Text("")
    sections: list[RenderableType] = [
        _render_headline(dataset),
        blank,
        _render_title_strip(dataset),
        blank,
        Columns(
            [_render_glance(dataset), _render_timeline(dataset)],
            padding=(0, 2),
            expand=False,
        ),
        blank,
        _render_details(dataset, full=full),
        blank,
        _render_assets(dataset),
    ]
    if full:
        sections.extend([blank, _render_full_extras(dataset)])
    sections.extend([blank, _render_next(dataset)])
    return Group(*sections)

render_collection_detail(collection, *, full=False)

Render a polished, multi-section detail view of a single collection.

Sections (top to bottom): collection headline + DOI/journal, ID + contact strip, optional description, side-by-side "At a glance" / "Timeline" panels, "Composition" key/value block aggregating ontology terms across nested datasets, "Datasets" table, optional "Links" table, and a "Next" action block. With full=True, the description is shown in full, ontology lists expand, and the dataset table shows every row.

Source code in src/cxg/output.py
def render_collection_detail(collection: dict[str, Any], *, full: bool = False) -> RenderableType:
    """Render a polished, multi-section detail view of a single collection.

    Sections (top to bottom): collection headline + DOI/journal, ID + contact
    strip, optional description, side-by-side "At a glance" / "Timeline"
    panels, "Composition" key/value block aggregating ontology terms across
    nested datasets, "Datasets" table, optional "Links" table, and a "Next"
    action block. With ``full=True``, the description is shown in full,
    ontology lists expand, and the dataset table shows every row.
    """
    blank = Text("")
    sections: list[RenderableType] = []

    def push(renderable: RenderableType) -> None:
        # Skip empty Group() sections so we don't emit double-blank gaps.
        if isinstance(renderable, Group) and not renderable.renderables:
            return
        if sections:
            sections.append(blank)
        sections.append(renderable)

    push(_render_collection_headline(collection))
    push(_render_collection_id_strip(collection))
    push(_render_collection_description(collection, full=full))
    push(
        Columns(
            [_render_collection_glance(collection), _render_collection_timeline(collection)],
            padding=(0, 2),
            expand=False,
        )
    )
    push(_render_collection_composition(collection, full=full))
    push(_render_collection_datasets(collection, full=full))
    push(_render_collection_links(collection))
    push(_render_collection_next(collection))

    return Group(*sections)

render_collections_table(collections)

Render a Rich table of collections with index, ID, name, and dataset count.

Source code in src/cxg/output.py
def render_collections_table(collections: list[dict[str, Any]]) -> Table:
    """Render a Rich table of collections with index, ID, name, and dataset count."""
    table = Table(
        box=None, show_header=True, header_style="bold", pad_edge=False, padding=(0, 2, 0, 0)
    )
    table.add_column("index", justify="right")
    table.add_column("collection id")
    table.add_column("name")
    table.add_column("datasets", justify="right")
    for collection in collections:
        count = collection.get("dataset_count") or len(collection.get("datasets", []))
        cid = str(collection.get("collection_id") or collection.get("id") or "")
        idx = str(collection.get("_index") or "")
        table.add_row(
            Text(idx, style="dim"),
            Text(cid, style="dim"),
            str(collection.get("name") or ""),
            str(count or 0),
        )
    return table

render_fields_list_table(rows)

Render a Rich table of filterable fields with match type and example.

Source code in src/cxg/output.py
def render_fields_list_table(rows: list[tuple[str, str, str]]) -> Table:
    """Render a Rich table of filterable fields with match type and example."""
    table = Table(
        box=None, show_header=True, header_style="bold", pad_edge=False, padding=(0, 2, 0, 0)
    )
    table.add_column("field")
    table.add_column("match")
    table.add_column("example")
    for field, match, example in rows:
        table.add_row(field, match, example)
    return table

render_fields_table(rows)

Render a Rich table of field values and their counts.

Source code in src/cxg/output.py
def render_fields_table(rows: list[tuple[str, int]]) -> Table:
    """Render a Rich table of field values and their counts."""
    table = Table(
        box=None, show_header=True, header_style="bold", pad_edge=False, padding=(0, 2, 0, 0)
    )
    table.add_column("value")
    table.add_column("count", justify="right")
    for label, count in rows:
        table.add_row(label, str(count))
    return table

print_json(console, payload)

Print syntax-highlighted JSON to the console.

Source code in src/cxg/output.py
def print_json(console: Console, payload: Any) -> None:
    """Print syntax-highlighted JSON to the console."""
    console.print_json(json.dumps(payload, ensure_ascii=True))

datasets_to_tsv(datasets, columns)

Serialize datasets to a TSV string with a header row.

Source code in src/cxg/output.py
def datasets_to_tsv(datasets: list[dict[str, Any]], columns: list[str]) -> str:
    """Serialize datasets to a TSV string with a header row."""
    buffer = io.StringIO()
    writer = csv.writer(buffer, delimiter="\t", lineterminator="\n")
    writer.writerow(columns)
    for dataset in datasets:
        writer.writerow(
            [dataset_column_value(dataset, column, truncate_id=False) for column in columns]
        )
    return buffer.getvalue()

fields_to_tsv(rows)

Serialize field value counts to a TSV string with a header row.

Source code in src/cxg/output.py
def fields_to_tsv(rows: list[tuple[str, int]]) -> str:
    """Serialize field value counts to a TSV string with a header row."""
    buffer = io.StringIO()
    writer = csv.writer(buffer, delimiter="\t", lineterminator="\n")
    writer.writerow(["value", "count"])
    for label, count in rows:
        writer.writerow([label, count])
    return buffer.getvalue()

format_age(fetched_at)

Format a datetime as a human-readable relative age string (e.g., "5m ago").

Source code in src/cxg/output.py
def format_age(fetched_at: datetime | None) -> str:
    """Format a datetime as a human-readable relative age string (e.g., "5m ago")."""
    if fetched_at is None:
        return "never"
    delta = datetime.now(tz=UTC) - fetched_at.astimezone(UTC)
    seconds = int(delta.total_seconds())
    if seconds < 60:
        return f"{seconds}s ago"
    minutes = seconds // 60
    if minutes < 60:
        return f"{minutes}m ago"
    hours = minutes // 60
    if hours < 24:
        return f"{hours}h ago"
    days = hours // 24
    return f"{days}d ago"