API reference
The entire public API is re-exported from the top-level asyoulikeit
package, so from asyoulikeit import Report, Reports, report_output
is the intended import path. The sub-module documentation below is for
readers who want the full picture.
Top-level package
Utilities for enriching CLI tools with structured report output.
- exception asyoulikeit.AsyoulikeitError[source]
Base class for all exceptions raised by the asyoulikeit package.
- class asyoulikeit.Column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL)[source]
Column definition for tabular data.
- Parameters:
key – Internal identifier for the column (used in row dictionaries)
label – Display name for the column (shown in output)
header – Whether this column serves as a row header/label column
importance – Column importance level (ESSENTIAL or DETAIL)
- class asyoulikeit.DetailLevel(*values)[source]
Control which columns to include in formatted output.
AUTO: Formatter decides based on its own default behavior (TSV excludes, table includes) DETAILED: Include all columns (ESSENTIAL + DETAIL) ESSENTIAL: Include only ESSENTIAL columns
- class asyoulikeit.Extension(name: str, **kwargs)[source]
- classmethod describe(*, single_line: bool = False) str[source]
A description of the extension.
By default, this is the docstring of the extension class. Override it in the extension if you want something different.
- Parameters:
single_line – If True, return only the first non-empty line of the description. Defaults to False for full description.
- Returns:
A string describing the extension. If single_line is True, returns only the first non-empty line; otherwise returns the complete description.
- classmethod entry_point_name() str[source]
Get the entry point name (key) for this extension class.
This performs a reverse lookup from class to entry point name by searching through all extensions in the appropriate namespace. The result is cached indefinitely since entry point names are immutable.
- Returns:
The entry point name for this extension class.
- Raises:
ExtensionError – If this class is not registered as an extension.
- class asyoulikeit.Formatter(name: str, **kwargs)[source]
Base class for formatters.
Formatters convert a
Reportscollection into text suitable for a particular output channel (terminal table, TSV, JSON, etc.).- abstractmethod format(reports: Reports) str[source]
Format reports as string output.
- Parameters:
reports – A Reports object containing one or more named reports. Each report has its own data, optional styles, and formatting preferences (detail_level, header) that may have been overridden by CLI flags.
- Returns:
Formatted string representation suitable for the chosen output channel.
- exception asyoulikeit.FormatterExtensionError[source]
Exception raised when a formatter extension cannot be loaded.
- class asyoulikeit.Importance(*values)[source]
Importance level for columns and/or rows in tabular data.
ESSENTIAL columns and rows are always included in output (suitable for machine parsing). DETAIL columns provide additional information for human-friendly output but may be omitted in machine-readable formats like TSV/CSV. Moreover, information about deprecated entities can be marked as DETAIL to allow users to focus on current data.
- class asyoulikeit.Node(owner: TreeContent, values: dict[str, Any], importance: Importance)[source]
A single node in a
TreeContent.A node owns its values (a dict keyed by the tree’s column schema), its importance (ESSENTIAL / DETAIL), and a list of child
Nodeobjects in insertion order.- add_child(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) Node[source]
Append a child to this node and return the new child.
- Parameters:
_importance – The child’s
Importance. Underscore-prefixed so it cannot clash with a column key.**values – The child’s column values. Must match the owning
TreeContent’s column schema exactly — missing keys and extra keys both raiseValueError.
- Returns:
The newly-added child. Retain it to chain more descendants; to add siblings, call
add_childon the parent again.
- property importance: Importance
This node’s
Importancetag.
- class asyoulikeit.Report(data: ReportContent, styles: TableContent | None = None, title: str | None = None, description: str | None = None, detail_level: DetailLevel = DetailLevel.AUTO, header: bool | None = None)[source]
A self-contained report with data and formatting preferences.
Reports are named via dictionary keys when returned from commands. Each report contains its data, optional styling, and formatting preferences.
Report names must be valid Python identifiers (alphanumeric + underscore, not starting with digit) to ensure compatibility with JSON/JavaScript.
- data
The content of this report (a
ReportContentsubclass).
- styles
Optional styling information with same structure as data.
- Type:
- detail_level
Default detail level for this report (can be overridden by CLI).
- header
Tri-state preference for whether headers/titles/chrome are emitted.
TrueorFalseare honoured as explicit requests;None(the default) defers to the formatter, which picks a sensible default per content type. Resolution order, highest first: CLI--header/--no-header→ this field → formatter default.- Type:
bool | None
- class asyoulikeit.ReportContent[source]
Marker base class for the content carried by a
Report.Every concrete content kind (currently
TableContent) inherits from this class and declares a stable short identifier viakind(). Formatters use the identifier — or anisinstancecheck — to pick an appropriate rendering path.
- exception asyoulikeit.ReportDeclarationError[source]
Raised when a
@report_outputcommand violates itsreports=declaration.Thrown at two distinct points:
Decoration time — when the
reports=mapping is ill-formed (non-identifier keys, non-Ellipsisnon-string keys, or adefault_reportsentry naming a report that wasn’t declared).Runtime — when the handler returns a
Reportscontaining a name that wasn’t declared, and the declaration has noEllipsisslot to admit dynamic names.
- class asyoulikeit.Reports(*args, **kwargs)[source]
An immutable collection of named reports.
Reports is a value object representing a mapping from report names to Report objects. Report names are validated to be valid Python identifiers on construction, ensuring compatibility with JSON/JavaScript object keys.
This class is constructable like a regular dictionary and can be used anywhere a Mapping is expected. It provides a clean abstraction for collections of reports and leaves room for future metadata additions.
Examples
Dict-style construction:
reports = Reports({"masters": master_report, "themes": theme_report})
Keyword arguments:
reports = Reports(masters=master_report, themes=theme_report)
From iterable of tuples:
reports = Reports([("masters", master_report)])
Access like a dict:
master_report = reports["masters"] for name, report in reports.items(): print(name, report.data.title)
- Raises:
ValueError – If any report name is not a valid Python identifier
- class asyoulikeit.ScalarContent(value: Any, title: str | None = None, description: str | None = None)[source]
A single value as a
ReportContent.No columns, no rows, no children — just one opaque
valueplus optionaltitleanddescriptionmetadata. The value’s Python type is unconstrained; the formatters will coerce viastr()for text rendering and pass primitives through directly for JSON.- classmethod kind() str[source]
Return the
ReportContentkind identifier:"scalar".
- class asyoulikeit.TableContent(title: str | None = None, description: str | None = None, present_transposed: bool = False)[source]
Container for tabular data with schema validation.
TableContent enforces that all rows match a defined column schema, providing a builder-style API for constructing tables programmatically.
Example
data = TableContent(title="User List", description="Active users in the system") data.add_column("name", "Name") data.add_column("age", "Age") data.add_row(name="Alice", age=30) data.add_row(name="Bob", age=25) # Access immutable views for row in data.rows: print(row["name"], row["age"])
- add_column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL) TableContent[source]
Add a column definition to the schema.
Columns must be added before any rows. This method uses the builder pattern and returns self for method chaining.
- Parameters:
key – Internal identifier for the column (must be a valid Python identifier)
label – Display name for the column
header – Whether this column serves as a row header/label column (default: False). Can only be True for the first column. Header columns must be ESSENTIAL.
importance – Column importance level (default: ESSENTIAL). Header columns must always be ESSENTIAL. DETAIL columns provide supplementary information, and may be omitted in machine-readable output formats to reduce clutter and ease parsing.
- Returns:
Self for method chaining
- Raises:
ValueError – If rows have already been added, if column key already exists, if key is not a valid Python identifier, if header=True is set on a non-first column, or if header=True is combined with DETAIL importance
- add_row(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) TableContent[source]
Add a data row with strict validation.
All defined columns must be present in the row, and no extra columns are allowed. This enforces schema consistency across all rows.
- Parameters:
_importance – Row importance level (default: ESSENTIAL). Underscore prefix reserves this parameter name to avoid conflict with column keys.
**values – Column key-value pairs for the row
- Returns:
Self for method chaining
- Raises:
ValueError – If no columns defined, if required columns are missing, or if extra unexpected columns are present
- property columns: tuple[Column, ...]
Get an immutable view of the column definitions.
Columns are returned in the order they were added.
- Returns:
Tuple of Column objects
- property description: str | None
Get the table description.
- Returns:
The table description, or None if not set
- property detailed_columns: tuple[Column, ...]
Get columns marked as DETAIL.
Detail columns provide supplementary information for human-friendly output but may be omitted in machine-readable formats.
- Returns:
Tuple of Column objects with importance=DETAIL
- property essential_columns: tuple[Column, ...]
Get columns marked as ESSENTIAL.
Essential columns are suitable for machine-readable output formats and provide core identifying information.
- Returns:
Tuple of Column objects with importance=ESSENTIAL
- property essential_rows: tuple[dict[str, Any], ...]
Get rows marked as ESSENTIAL.
Essential rows are always included in output.
- Returns:
Tuple of row dictionaries with importance=ESSENTIAL
- classmethod from_mappings(mappings: Iterable[Mapping[str, Any]], title: str | None = None, description: str | None = None, present_transposed: bool = False) TableContent[source]
Create TableContent from an iterable of mappings (e.g., list of dicts).
Columns are inferred from the union of all keys across all mappings. Column keys and labels will be identical. Missing keys in individual mappings will be filled with None.
Column order is determined by the order of first appearance across all mappings (not sorted).
- Parameters:
mappings – Iterable of mappings (e.g., list of dictionaries)
title – Optional title for the table
description – Optional description
present_transposed – Whether to present transposed
- Returns:
A new TableContent instance
- Raises:
ValueError – If any key is not a valid Python identifier
Example
data = TableContent.from_mappings([ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25, "city": "NYC"} ]) # Results in columns: name, age, city # Row 1: Alice, 30, None # Row 2: Bob, 25, NYC
- property header_column: Column | None
Get the header column if one exists.
- Returns:
The header column, or None if no column is marked as header
- is_compatible(other: TableContent) bool[source]
Check if another TableContent has compatible structure.
Compatibility requires: - Same number of columns - Same column keys in same order - Same number of rows
Note: Column labels are NOT checked (styles may use different labels)
- Parameters:
other – Another TableContent instance to compare
- Returns:
True if structures are compatible, False otherwise
- classmethod kind() str[source]
Return the
ReportContentkind identifier:"table".
- property present_transposed: bool
Get whether the table should be presented transposed.
- Returns:
True if the table should be presented with rows and columns swapped
- property row_importances: tuple[Importance, ...]
Get row importance levels.
- Returns:
Tuple of ColumnImportance values, one per row, in same order as rows
- property rows: tuple[dict[str, Any], ...]
Get an immutable view of the data rows.
Each row is a dictionary mapping column keys to values.
- Returns:
Tuple of row dictionaries
- rows_for_detail_level(detail_level: DetailLevel) tuple[dict[str, Any], ...][source]
Get rows appropriate for the specified detail level.
This method mirrors the column filtering pattern used in formatters: - DETAILED: Returns all rows (ESSENTIAL + DETAIL) - ESSENTIAL: Returns only ESSENTIAL rows - AUTO: Treated as ESSENTIAL (should be resolved to DETAILED or ESSENTIAL by caller)
- Parameters:
detail_level – DetailLevel.DETAILED or DetailLevel.ESSENTIAL. AUTO is treated as ESSENTIAL (should be resolved by caller).
- Returns:
Tuple of row dictionaries filtered by importance
Example
>>> # In a formatter: >>> if detail_level == DetailLevel.AUTO: ... detail_level = DetailLevel.ESSENTIAL # or DETAILED >>> rows = data.rows_for_detail_level(detail_level)
- transpose(value_column_importance: Importance = Importance.ESSENTIAL) TableContent[source]
Transpose rows and columns.
Returns a new TableContent with rows and columns swapped. The header column (if present) becomes the column labels of the transposed data, and the original column labels become the first column of the transposed data (marked as header).
If no header column exists, row indices are used as column labels.
Note: Original column importance metadata is not directly preserved. Original row data does not carry importance metadata, so all value columns receive the same importance level specified by value_column_importance.
- Parameters:
value_column_importance – Importance level for all value columns in the transposed table. Defaults to ESSENTIAL (safe for machine-readable output).
- Returns:
A new transposed TableContent with present_transposed=False
- class asyoulikeit.TreeContent(title: str | None = None, description: str | None = None)[source]
Hierarchical content with a uniform column schema across nodes.
Every node — roots and descendants alike — holds a set of column values keyed by the same schema declared on the
TreeContentitself. Most trees have a single root;TreeContentalso supports a forest via repeated calls toadd_root().Construction rules:
All columns must be declared before the first
add_root()call.Exactly one column must be marked
header=True; it is the label column displayed in the tree’s first column by thedisplayformatter.Header columns must be
Importance.ESSENTIAL(the default).Every node’s values must match the column schema exactly.
Filtering on
Importanceprunes aDETAILnode and its entire subtree when the resolved detail level isESSENTIAL— a detail node’s descendants cannot out-rank their ancestor.- add_column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL) TreeContent[source]
Add a column to the tree’s schema.
- Parameters:
key – Internal identifier for the column; must be a valid Python identifier and not start with an underscore.
label – Display name shown in formatted output.
header – Whether this column is the tree’s label column. Exactly one column on a tree must be marked header.
importance –
ESSENTIAL(default) orDETAIL. Header columns must beESSENTIAL.
- Returns:
self, for method chaining.- Raises:
ValueError – If columns are added after roots exist, the key clashes with an existing column, the key is not a valid identifier (or starts with underscore), or a header column is tagged
DETAIL.
- add_root(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) Node[source]
Append a top-level node and return it.
May be called multiple times to build a forest (a
TreeContentwith more than one root). The common single-tree case is a singleadd_root()call, after whichNode.add_child()builds the rest.- Parameters:
_importance – The root’s
Importance. Underscore-prefixed to avoid clashing with a column key.**values – The root’s column values. Must match the schema exactly.
- Returns:
The newly-added root. Retain it to attach children.
- Raises:
ValueError – If no columns are declared yet, if exactly one column is not marked header, or if the values do not match the column schema.
- classmethod kind() str[source]
Return the
ReportContentkind identifier:"tree".
- asyoulikeit.create_formatter(formatter_name: str) Formatter[source]
Create a formatter instance by name.
- Parameters:
formatter_name – The name of the formatter to create (e.g., “tsv”, “json”)
- Returns:
A Formatter instance
- Raises:
FormatterExtensionError – If the formatter cannot be loaded
- asyoulikeit.describe_formatter(formatter_name: str, *, single_line: bool = False) str[source]
Get the description of a formatter.
- Parameters:
formatter_name – The name of the formatter
single_line – If True, return only the first non-empty line of the description.
- Returns:
Description string from the formatter’s docstring
- Raises:
FormatterExtensionError – If the formatter cannot be loaded
- asyoulikeit.describe_formatter_command() Command[source]
Return a Click command that prints one formatter’s full description.
The returned command is decorated with
report_output()and takes a single positionalNAMEargument restricted (viaclick.Choice) to the set of currently-registered formatters. The payload is aScalarContentwhosevalueis the full cleaned docstring and whosetitleis the formatter name — so--as tsvyields the bare description (pipe-friendly),--as jsonyields{"metadata": {"title": "<name>"}, "value": "<desc>"}, and--as displayyields<name>: <desc>.The host adds it under whatever name suits its CLI:
cli.add_command(describe_formatter_command(), name="describe-formatter")
- asyoulikeit.describe_report_command() Command[source]
Return a Click command that prints one declared report’s description.
Takes two positionals —
<command>and<report>— and surfaces the description the command declared for that report viareports={...}. Payload is aScalarContentwhosetitleiscommand.reportand whosevalueis the declared description, so--as tsvyields the bare text (pipe- friendly),--as jsongivesmetadata.title+value, and--as displayyieldscommand.report: <description>.An
<dynamic>report name works: it surfaces the description attached to theEllipsisslot of the command’s declaration.The host CLI adds it under whatever name it prefers:
cli.add_command(describe_report_command(), name="describe-report")
- asyoulikeit.format_as(reports: Reports, format_name: str) str[source]
Format reports using the specified formatter.
This is the primary dispatcher function for formatters.
- Parameters:
reports – A Reports object containing one or more named reports. Each report contains its own data, styles, and formatting preferences that may have been adjusted by CLI flags.
format_name – Name of the format (e.g., “tsv”, “json”, “display”)
- Returns:
Formatted string
- Raises:
FormatterExtensionError – If format_name is not recognized
- asyoulikeit.formatter_names() list[str][source]
Get the names of all available formatters.
- Returns:
List of formatter names (e.g., [“display”, “json”, “tsv”])
- asyoulikeit.list_formatters_command() Command[source]
Return a Click command that lists the available report-output formatters.
The returned command is decorated with
report_output()— so it inherits--as / --report / --header / --detailedand renders identically to any other asyoulikeit command. Its payload is a two-columnTableContent(Name,Description) with one row per registered formatter. The description is the formatter class’s one-line summary (first non-empty line of its docstring).The host CLI adds it to its group under whatever name it prefers:
cli.add_command(list_formatters_command(), name="list-formatters")
Because
formatter_names()is evaluated at factory-call time (not at import time), any formatter that was registered via entry point before the factory is called will appear in the listing and in--as’s choices.
- asyoulikeit.list_reports_command() Command[source]
Return a Click command that lists declared reports across the CLI.
Mirrors
list_formatters_command(): the returned command is itself decorated with@report_output, so it inherits--as / --report / --header / --detailedand renders in any format. The payload is aTreeContentwith one root node per@report_outputcommand discovered under the root Click group and children for each declared report (name + description). Becausereports=is required on every@report_outputcommand, every discovered report-output command shows its declared names; a group may also contain plain@click.commandcommands that aren’t asyoulikeit-aware, and those surface with a single<not a report-output command>marker child. Action commands declared withreports={}show<no reports>.The host CLI adds it under whatever name suits it:
cli.add_command(list_reports_command(), name="list-reports")
Usage:
mytool list-reports # every command + its reports mytool list-reports video-audit # one command only
The command walks
click.get_current_context().find_root()at invoke time, so it sees every command the host has added before invocation — no per-command wiring required.
- asyoulikeit.report_output(func: Callable = None, /, *, default_reports: None | Iterable[str] | _UniversalContainer = ALL_REPORTS, reports: Mapping[str | EllipsisType, str] | None = None) Callable[source]
Decorator factory for commands that return tabular output.
reports=is required: every@report_outputcommand must declare which report names it produces, as a mapping of{name: description}. UseEllipsisas a key to declare “this command also produces dynamically-named reports” (e.g. one report per input file), or pass an empty dict for an action command that returnsNone. There is no back-compat fallback; the declaration lets the library validate--reportvalues, generate help, run drift detection, and power the sibling introspection commands.The decorated function should return:
Reports— one or more named reports (each key must be declared inreports=or admitted by anEllipsisslot).None— silent success (nothing displayed).
- Parameters:
func – The function to decorate (when used without parentheses a
reports=kwarg must be supplied — otherwise the decoration raisesReportDeclarationError).default_reports – Which reports to show by default when no
--reportflags are specified.ALL_REPORTS(the default) shows every report;Noneshows nothing (silent action command); an iterable of names restricts the default to those names. Entries that aren’t inreports=raise at decoration time.reports – Mapping of
{name: description}declaring the report names this command produces. Keys must be valid Python identifiers;Ellipsisas a key declares the “dynamic slot” (names known only at runtime). The declaration drives decoration-time validation,--helpgeneration, parse-timeclick.Choiceon--reportwhen fully static, and runtime drift detection at the return boundary — undeclared names (and noEllipsisslot admitting them) raiseReportDeclarationError.
Adds these CLI options:
--as— output format (display / tsv / json). Defaults todisplayfor terminals,tsvfor pipes.--detailed/--essential— column inclusion override.--header/--no-header— header override.--report— filter which reports to display. Hyphens normalise to underscores (--report monthly-salesresolves tomonthly_sales).--no-reports— suppress all report rendering while still running the handler (useful for action commands whose reports are incidental confirmation).--all-reports— render every report the handler returns, regardless ofdefault_reports(useful for opting into the full picture on a command that’s normally silent or narrow).
The three selection flags (
--report,--no-reports,--all-reports) are mutually exclusive — pick at most one.Header behaviour is format-specific: TSV prefixes the first header cell with
#, display omits title/caption when headers are off, JSON ignores the flag.Examples
@report_output(reports={"users": "The system's users."})— one static report.@report_output(reports={"overall": "…", Ellipsis: "…"})— mixed static + dynamic.@report_output(reports={Ellipsis: "One per input file."})— purely dynamic.@report_output(reports={}, default_reports=None)— silent action command.
Report content: the base
Abstract base for report content.
A Report holds one ReportContent value (currently always a
TableContent — trees and other kinds are planned).
Formatters iterate reports and dispatch on the concrete content type to
render each appropriately.
The protocol is intentionally minimal in this first cut: only kind()
is required. Common concerns that might eventually be lifted up (title /
description hooks, detail-level filtering protocol, …) are deliberately
left to each concrete subclass until a second content kind exists to
triangulate what’s genuinely shared.
- class asyoulikeit.content.ReportContent[source]
Bases:
ABCMarker base class for the content carried by a
Report.Every concrete content kind (currently
TableContent) inherits from this class and declares a stable short identifier viakind(). Formatters use the identifier — or anisinstancecheck — to pick an appropriate rendering path.
Table content
Tabular data representation for CLI output formatting.
This module provides a structured way to build and represent tabular data with a defined schema, suitable for formatting into various output formats like TSV, JSON, or rich console tables.
- class asyoulikeit.tabular_data.Column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL)[source]
Bases:
objectColumn definition for tabular data.
- Parameters:
key – Internal identifier for the column (used in row dictionaries)
label – Display name for the column (shown in output)
header – Whether this column serves as a row header/label column
importance – Column importance level (ESSENTIAL or DETAIL)
- class asyoulikeit.tabular_data.DetailLevel(*values)[source]
Bases:
EnumControl which columns to include in formatted output.
AUTO: Formatter decides based on its own default behavior (TSV excludes, table includes) DETAILED: Include all columns (ESSENTIAL + DETAIL) ESSENTIAL: Include only ESSENTIAL columns
- class asyoulikeit.tabular_data.Importance(*values)[source]
Bases:
EnumImportance level for columns and/or rows in tabular data.
ESSENTIAL columns and rows are always included in output (suitable for machine parsing). DETAIL columns provide additional information for human-friendly output but may be omitted in machine-readable formats like TSV/CSV. Moreover, information about deprecated entities can be marked as DETAIL to allow users to focus on current data.
- class asyoulikeit.tabular_data.Report(data: ReportContent, styles: TableContent | None = None, title: str | None = None, description: str | None = None, detail_level: DetailLevel = DetailLevel.AUTO, header: bool | None = None)[source]
Bases:
objectA self-contained report with data and formatting preferences.
Reports are named via dictionary keys when returned from commands. Each report contains its data, optional styling, and formatting preferences.
Report names must be valid Python identifiers (alphanumeric + underscore, not starting with digit) to ensure compatibility with JSON/JavaScript.
- data
The content of this report (a
ReportContentsubclass).
- styles
Optional styling information with same structure as data.
- Type:
- detail_level
Default detail level for this report (can be overridden by CLI).
- header
Tri-state preference for whether headers/titles/chrome are emitted.
TrueorFalseare honoured as explicit requests;None(the default) defers to the formatter, which picks a sensible default per content type. Resolution order, highest first: CLI--header/--no-header→ this field → formatter default.- Type:
bool | None
- class asyoulikeit.tabular_data.Reports(*args, **kwargs)[source]
-
An immutable collection of named reports.
Reports is a value object representing a mapping from report names to Report objects. Report names are validated to be valid Python identifiers on construction, ensuring compatibility with JSON/JavaScript object keys.
This class is constructable like a regular dictionary and can be used anywhere a Mapping is expected. It provides a clean abstraction for collections of reports and leaves room for future metadata additions.
Examples
Dict-style construction:
reports = Reports({"masters": master_report, "themes": theme_report})
Keyword arguments:
reports = Reports(masters=master_report, themes=theme_report)
From iterable of tuples:
reports = Reports([("masters", master_report)])
Access like a dict:
master_report = reports["masters"] for name, report in reports.items(): print(name, report.data.title)
- Raises:
ValueError – If any report name is not a valid Python identifier
- class asyoulikeit.tabular_data.TableContent(title: str | None = None, description: str | None = None, present_transposed: bool = False)[source]
Bases:
ReportContentContainer for tabular data with schema validation.
TableContent enforces that all rows match a defined column schema, providing a builder-style API for constructing tables programmatically.
Example
data = TableContent(title="User List", description="Active users in the system") data.add_column("name", "Name") data.add_column("age", "Age") data.add_row(name="Alice", age=30) data.add_row(name="Bob", age=25) # Access immutable views for row in data.rows: print(row["name"], row["age"])
- add_column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL) TableContent[source]
Add a column definition to the schema.
Columns must be added before any rows. This method uses the builder pattern and returns self for method chaining.
- Parameters:
key – Internal identifier for the column (must be a valid Python identifier)
label – Display name for the column
header – Whether this column serves as a row header/label column (default: False). Can only be True for the first column. Header columns must be ESSENTIAL.
importance – Column importance level (default: ESSENTIAL). Header columns must always be ESSENTIAL. DETAIL columns provide supplementary information, and may be omitted in machine-readable output formats to reduce clutter and ease parsing.
- Returns:
Self for method chaining
- Raises:
ValueError – If rows have already been added, if column key already exists, if key is not a valid Python identifier, if header=True is set on a non-first column, or if header=True is combined with DETAIL importance
- add_row(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) TableContent[source]
Add a data row with strict validation.
All defined columns must be present in the row, and no extra columns are allowed. This enforces schema consistency across all rows.
- Parameters:
_importance – Row importance level (default: ESSENTIAL). Underscore prefix reserves this parameter name to avoid conflict with column keys.
**values – Column key-value pairs for the row
- Returns:
Self for method chaining
- Raises:
ValueError – If no columns defined, if required columns are missing, or if extra unexpected columns are present
- property columns: tuple[Column, ...]
Get an immutable view of the column definitions.
Columns are returned in the order they were added.
- Returns:
Tuple of Column objects
- property description: str | None
Get the table description.
- Returns:
The table description, or None if not set
- property detailed_columns: tuple[Column, ...]
Get columns marked as DETAIL.
Detail columns provide supplementary information for human-friendly output but may be omitted in machine-readable formats.
- Returns:
Tuple of Column objects with importance=DETAIL
- property essential_columns: tuple[Column, ...]
Get columns marked as ESSENTIAL.
Essential columns are suitable for machine-readable output formats and provide core identifying information.
- Returns:
Tuple of Column objects with importance=ESSENTIAL
- property essential_rows: tuple[dict[str, Any], ...]
Get rows marked as ESSENTIAL.
Essential rows are always included in output.
- Returns:
Tuple of row dictionaries with importance=ESSENTIAL
- classmethod from_mappings(mappings: Iterable[Mapping[str, Any]], title: str | None = None, description: str | None = None, present_transposed: bool = False) TableContent[source]
Create TableContent from an iterable of mappings (e.g., list of dicts).
Columns are inferred from the union of all keys across all mappings. Column keys and labels will be identical. Missing keys in individual mappings will be filled with None.
Column order is determined by the order of first appearance across all mappings (not sorted).
- Parameters:
mappings – Iterable of mappings (e.g., list of dictionaries)
title – Optional title for the table
description – Optional description
present_transposed – Whether to present transposed
- Returns:
A new TableContent instance
- Raises:
ValueError – If any key is not a valid Python identifier
Example
data = TableContent.from_mappings([ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25, "city": "NYC"} ]) # Results in columns: name, age, city # Row 1: Alice, 30, None # Row 2: Bob, 25, NYC
- property header_column: Column | None
Get the header column if one exists.
- Returns:
The header column, or None if no column is marked as header
- is_compatible(other: TableContent) bool[source]
Check if another TableContent has compatible structure.
Compatibility requires: - Same number of columns - Same column keys in same order - Same number of rows
Note: Column labels are NOT checked (styles may use different labels)
- Parameters:
other – Another TableContent instance to compare
- Returns:
True if structures are compatible, False otherwise
- property present_transposed: bool
Get whether the table should be presented transposed.
- Returns:
True if the table should be presented with rows and columns swapped
- property row_importances: tuple[Importance, ...]
Get row importance levels.
- Returns:
Tuple of ColumnImportance values, one per row, in same order as rows
- property rows: tuple[dict[str, Any], ...]
Get an immutable view of the data rows.
Each row is a dictionary mapping column keys to values.
- Returns:
Tuple of row dictionaries
- rows_for_detail_level(detail_level: DetailLevel) tuple[dict[str, Any], ...][source]
Get rows appropriate for the specified detail level.
This method mirrors the column filtering pattern used in formatters: - DETAILED: Returns all rows (ESSENTIAL + DETAIL) - ESSENTIAL: Returns only ESSENTIAL rows - AUTO: Treated as ESSENTIAL (should be resolved to DETAILED or ESSENTIAL by caller)
- Parameters:
detail_level – DetailLevel.DETAILED or DetailLevel.ESSENTIAL. AUTO is treated as ESSENTIAL (should be resolved by caller).
- Returns:
Tuple of row dictionaries filtered by importance
Example
>>> # In a formatter: >>> if detail_level == DetailLevel.AUTO: ... detail_level = DetailLevel.ESSENTIAL # or DETAILED >>> rows = data.rows_for_detail_level(detail_level)
- transpose(value_column_importance: Importance = Importance.ESSENTIAL) TableContent[source]
Transpose rows and columns.
Returns a new TableContent with rows and columns swapped. The header column (if present) becomes the column labels of the transposed data, and the original column labels become the first column of the transposed data (marked as header).
If no header column exists, row indices are used as column labels.
Note: Original column importance metadata is not directly preserved. Original row data does not carry importance metadata, so all value columns receive the same importance level specified by value_column_importance.
- Parameters:
value_column_importance – Importance level for all value columns in the transposed table. Defaults to ESSENTIAL (safe for machine-readable output).
- Returns:
A new transposed TableContent with present_transposed=False
- asyoulikeit.tabular_data.validate_styles_compatibility(data: TableContent, styles: TableContent) None[source]
Verify styles table is compatible with data table.
- Parameters:
data – The data table
styles – The styles table
- Raises:
ValueError – If tables are not compatible with detailed message
Tree content
Hierarchical report content with a uniform column schema across nodes.
Where TableContent carries a flat table of rows
against a column schema, TreeContent carries a hierarchical
forest: one or more root nodes, each optionally with children, where
every node holds values keyed by the same column schema. Think file
trees, syntax trees, project organisation hierarchies — anywhere the
shape is hierarchical but the per-node data is homogeneous.
Construction mirrors TableContent’s builder
pattern; TreeContent.add_root() starts a top-level node and
Node.add_child() grows the tree downward. Both return the
newly-added node so the caller can retain a handle for adding its
own children or siblings.
Example:
tree = (
TreeContent(title="/usr")
.add_column("name", "Name", header=True)
.add_column("size", "Size")
)
root = tree.add_root(name="/usr", size=0)
bin_dir = root.add_child(name="bin", size=4096)
bin_dir.add_child(name="ls", size=150_296)
bin_dir.add_child(name="cat", size=52_024)
TreeContent supports a forest rather than strictly a tree: call
add_root() more than once to add multiple top-level
nodes.
- class asyoulikeit.tree_data.Node(owner: TreeContent, values: dict[str, Any], importance: Importance)[source]
Bases:
objectA single node in a
TreeContent.A node owns its values (a dict keyed by the tree’s column schema), its importance (ESSENTIAL / DETAIL), and a list of child
Nodeobjects in insertion order.- add_child(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) Node[source]
Append a child to this node and return the new child.
- Parameters:
_importance – The child’s
Importance. Underscore-prefixed so it cannot clash with a column key.**values – The child’s column values. Must match the owning
TreeContent’s column schema exactly — missing keys and extra keys both raiseValueError.
- Returns:
The newly-added child. Retain it to chain more descendants; to add siblings, call
add_childon the parent again.
- property importance: Importance
This node’s
Importancetag.
- class asyoulikeit.tree_data.TreeContent(title: str | None = None, description: str | None = None)[source]
Bases:
ReportContentHierarchical content with a uniform column schema across nodes.
Every node — roots and descendants alike — holds a set of column values keyed by the same schema declared on the
TreeContentitself. Most trees have a single root;TreeContentalso supports a forest via repeated calls toadd_root().Construction rules:
All columns must be declared before the first
add_root()call.Exactly one column must be marked
header=True; it is the label column displayed in the tree’s first column by thedisplayformatter.Header columns must be
Importance.ESSENTIAL(the default).Every node’s values must match the column schema exactly.
Filtering on
Importanceprunes aDETAILnode and its entire subtree when the resolved detail level isESSENTIAL— a detail node’s descendants cannot out-rank their ancestor.- add_column(key: str, label: str, header: bool = False, importance: Importance = Importance.ESSENTIAL) TreeContent[source]
Add a column to the tree’s schema.
- Parameters:
key – Internal identifier for the column; must be a valid Python identifier and not start with an underscore.
label – Display name shown in formatted output.
header – Whether this column is the tree’s label column. Exactly one column on a tree must be marked header.
importance –
ESSENTIAL(default) orDETAIL. Header columns must beESSENTIAL.
- Returns:
self, for method chaining.- Raises:
ValueError – If columns are added after roots exist, the key clashes with an existing column, the key is not a valid identifier (or starts with underscore), or a header column is tagged
DETAIL.
- add_root(*, _importance: Importance = Importance.ESSENTIAL, **values: Any) Node[source]
Append a top-level node and return it.
May be called multiple times to build a forest (a
TreeContentwith more than one root). The common single-tree case is a singleadd_root()call, after whichNode.add_child()builds the rest.- Parameters:
_importance – The root’s
Importance. Underscore-prefixed to avoid clashing with a column key.**values – The root’s column values. Must match the schema exactly.
- Returns:
The newly-added root. Retain it to attach children.
- Raises:
ValueError – If no columns are declared yet, if exactly one column is not marked header, or if the values do not match the column schema.
Scalar content
A single-value ReportContent kind.
Where TableContent carries a table of rows and
TreeContent carries a hierarchy, ScalarContent
carries one value: a title, a number, a status string, an address — the
kind of thing a command like disc title image produces. It exists so
that such commands can flow through @report_output with the same
--as / --report / --header machinery as the other content
kinds, rather than skipping the decorator (and losing those affordances)
or wrapping the scalar in a 1×1 transposed table (which reads as ceremony
around a single cell and clutters JSON output with empty tabular shape).
Example:
from asyoulikeit import (
Report, Reports, ScalarContent, report_output,
)
@click.command()
@report_output(reports={"title": "The disc image's title."})
def disc_title(image):
return Reports(title=Report(data=ScalarContent(
value=_read_title(image),
title="Disc title",
)))
In a terminal the user sees Disc title: My Disc Image; piped to
another tool with | pbcopy they get just My Disc Image (the
TSV formatter’s per-content default for scalars is “no header line,
just the value”). JSON output is always self-describing — consumers
parse .reports.title.value via jq.
- class asyoulikeit.scalar_data.ScalarContent(value: Any, title: str | None = None, description: str | None = None)[source]
Bases:
ReportContentA single value as a
ReportContent.No columns, no rows, no children — just one opaque
valueplus optionaltitleanddescriptionmetadata. The value’s Python type is unconstrained; the formatters will coerce viastr()for text rendering and pass primitives through directly for JSON.
The report-output decorator
Report output decorator for Click commands.
Provides the report_output() decorator for commands that produce
formatted report output with support for multiple reports, format selection
(display/TSV/JSON), and column filtering.
- asyoulikeit.cli.describe_formatter_command() Command[source]
Return a Click command that prints one formatter’s full description.
The returned command is decorated with
report_output()and takes a single positionalNAMEargument restricted (viaclick.Choice) to the set of currently-registered formatters. The payload is aScalarContentwhosevalueis the full cleaned docstring and whosetitleis the formatter name — so--as tsvyields the bare description (pipe-friendly),--as jsonyields{"metadata": {"title": "<name>"}, "value": "<desc>"}, and--as displayyields<name>: <desc>.The host adds it under whatever name suits its CLI:
cli.add_command(describe_formatter_command(), name="describe-formatter")
- asyoulikeit.cli.describe_report_command() Command[source]
Return a Click command that prints one declared report’s description.
Takes two positionals —
<command>and<report>— and surfaces the description the command declared for that report viareports={...}. Payload is aScalarContentwhosetitleiscommand.reportand whosevalueis the declared description, so--as tsvyields the bare text (pipe- friendly),--as jsongivesmetadata.title+value, and--as displayyieldscommand.report: <description>.An
<dynamic>report name works: it surfaces the description attached to theEllipsisslot of the command’s declaration.The host CLI adds it under whatever name it prefers:
cli.add_command(describe_report_command(), name="describe-report")
- asyoulikeit.cli.list_formatters_command() Command[source]
Return a Click command that lists the available report-output formatters.
The returned command is decorated with
report_output()— so it inherits--as / --report / --header / --detailedand renders identically to any other asyoulikeit command. Its payload is a two-columnTableContent(Name,Description) with one row per registered formatter. The description is the formatter class’s one-line summary (first non-empty line of its docstring).The host CLI adds it to its group under whatever name it prefers:
cli.add_command(list_formatters_command(), name="list-formatters")
Because
formatter_names()is evaluated at factory-call time (not at import time), any formatter that was registered via entry point before the factory is called will appear in the listing and in--as’s choices.
- asyoulikeit.cli.list_reports_command() Command[source]
Return a Click command that lists declared reports across the CLI.
Mirrors
list_formatters_command(): the returned command is itself decorated with@report_output, so it inherits--as / --report / --header / --detailedand renders in any format. The payload is aTreeContentwith one root node per@report_outputcommand discovered under the root Click group and children for each declared report (name + description). Becausereports=is required on every@report_outputcommand, every discovered report-output command shows its declared names; a group may also contain plain@click.commandcommands that aren’t asyoulikeit-aware, and those surface with a single<not a report-output command>marker child. Action commands declared withreports={}show<no reports>.The host CLI adds it under whatever name suits it:
cli.add_command(list_reports_command(), name="list-reports")
Usage:
mytool list-reports # every command + its reports mytool list-reports video-audit # one command only
The command walks
click.get_current_context().find_root()at invoke time, so it sees every command the host has added before invocation — no per-command wiring required.
- asyoulikeit.cli.report_output(func: Callable = None, /, *, default_reports: None | Iterable[str] | _UniversalContainer = ALL_REPORTS, reports: Mapping[str | EllipsisType, str] | None = None) Callable[source]
Decorator factory for commands that return tabular output.
reports=is required: every@report_outputcommand must declare which report names it produces, as a mapping of{name: description}. UseEllipsisas a key to declare “this command also produces dynamically-named reports” (e.g. one report per input file), or pass an empty dict for an action command that returnsNone. There is no back-compat fallback; the declaration lets the library validate--reportvalues, generate help, run drift detection, and power the sibling introspection commands.The decorated function should return:
Reports— one or more named reports (each key must be declared inreports=or admitted by anEllipsisslot).None— silent success (nothing displayed).
- Parameters:
func – The function to decorate (when used without parentheses a
reports=kwarg must be supplied — otherwise the decoration raisesReportDeclarationError).default_reports – Which reports to show by default when no
--reportflags are specified.ALL_REPORTS(the default) shows every report;Noneshows nothing (silent action command); an iterable of names restricts the default to those names. Entries that aren’t inreports=raise at decoration time.reports – Mapping of
{name: description}declaring the report names this command produces. Keys must be valid Python identifiers;Ellipsisas a key declares the “dynamic slot” (names known only at runtime). The declaration drives decoration-time validation,--helpgeneration, parse-timeclick.Choiceon--reportwhen fully static, and runtime drift detection at the return boundary — undeclared names (and noEllipsisslot admitting them) raiseReportDeclarationError.
Adds these CLI options:
--as— output format (display / tsv / json). Defaults todisplayfor terminals,tsvfor pipes.--detailed/--essential— column inclusion override.--header/--no-header— header override.--report— filter which reports to display. Hyphens normalise to underscores (--report monthly-salesresolves tomonthly_sales).--no-reports— suppress all report rendering while still running the handler (useful for action commands whose reports are incidental confirmation).--all-reports— render every report the handler returns, regardless ofdefault_reports(useful for opting into the full picture on a command that’s normally silent or narrow).
The three selection flags (
--report,--no-reports,--all-reports) are mutually exclusive — pick at most one.Header behaviour is format-specific: TSV prefixes the first header cell with
#, display omits title/caption when headers are off, JSON ignores the flag.Examples
@report_output(reports={"users": "The system's users."})— one static report.@report_output(reports={"overall": "…", Ellipsis: "…"})— mixed static + dynamic.@report_output(reports={Ellipsis: "One per input file."})— purely dynamic.@report_output(reports={}, default_reports=None)— silent action command.
Formatters
Formatter extension point for tabular data output.
- class asyoulikeit.formatter.Formatter(name: str, **kwargs)[source]
Bases:
ExtensionBase class for formatters.
Formatters convert a
Reportscollection into text suitable for a particular output channel (terminal table, TSV, JSON, etc.).- abstractmethod format(reports: Reports) str[source]
Format reports as string output.
- Parameters:
reports – A Reports object containing one or more named reports. Each report has its own data, optional styles, and formatting preferences (detail_level, header) that may have been overridden by CLI flags.
- Returns:
Formatted string representation suitable for the chosen output channel.
- exception asyoulikeit.formatter.FormatterExtensionError[source]
Bases:
ExtensionErrorException raised when a formatter extension cannot be loaded.
- asyoulikeit.formatter.create_formatter(formatter_name: str) Formatter[source]
Create a formatter instance by name.
- Parameters:
formatter_name – The name of the formatter to create (e.g., “tsv”, “json”)
- Returns:
A Formatter instance
- Raises:
FormatterExtensionError – If the formatter cannot be loaded
- asyoulikeit.formatter.describe_formatter(formatter_name: str, *, single_line: bool = False) str[source]
Get the description of a formatter.
- Parameters:
formatter_name – The name of the formatter
single_line – If True, return only the first non-empty line of the description.
- Returns:
Description string from the formatter’s docstring
- Raises:
FormatterExtensionError – If the formatter cannot be loaded
- asyoulikeit.formatter.format_as(reports: Reports, format_name: str) str[source]
Format reports using the specified formatter.
This is the primary dispatcher function for formatters.
- Parameters:
reports – A Reports object containing one or more named reports. Each report contains its own data, styles, and formatting preferences that may have been adjusted by CLI flags.
format_name – Name of the format (e.g., “tsv”, “json”, “display”)
- Returns:
Formatted string
- Raises:
FormatterExtensionError – If format_name is not recognized
- asyoulikeit.formatter.formatter_names() list[str][source]
Get the names of all available formatters.
- Returns:
List of formatter names (e.g., [“display”, “json”, “tsv”])
- asyoulikeit.formatter.formatter_type(formatter_name: str) Type[Formatter][source]
Obtain the type of a formatter by name.
- Parameters:
formatter_name – The name of a formatter. Available formatter names can be obtained from
formatter_names().- Returns:
The type (i.e. class) of the requested formatter.
- Raises:
FormatterExtensionError – If the requested formatter could not be found.
Extension machinery
Generic extension / plug-in machinery built on top of stevedore.
Extension classes are loaded via entry points. Each Extension
subclass declares a kind (a short identifier such as "formatter")
and is registered under a namespace of the form "<prefix>.<kind>" in
pyproject.toml. Consumers then use create_extension(),
extension(), or list_extensions() to load them.
- class asyoulikeit.extension.Extension(name: str, **kwargs)[source]
Bases:
ABC- classmethod describe(*, single_line: bool = False) str[source]
A description of the extension.
By default, this is the docstring of the extension class. Override it in the extension if you want something different.
- Parameters:
single_line – If True, return only the first non-empty line of the description. Defaults to False for full description.
- Returns:
A string describing the extension. If single_line is True, returns only the first non-empty line; otherwise returns the complete description.
- classmethod entry_point_name() str[source]
Get the entry point name (key) for this extension class.
This performs a reverse lookup from class to entry point name by searching through all extensions in the appropriate namespace. The result is cached indefinitely since entry point names are immutable.
- Returns:
The entry point name for this extension class.
- Raises:
ExtensionError – If this class is not registered as an extension.
- exception asyoulikeit.extension.ExtensionError[source]
Bases:
AsyoulikeitError
- asyoulikeit.extension.create_extension(kind, namespace, name, exception_type=None, **kwargs) Extension[source]
Instantiate an extension.
- Parameters:
kind – The kind of extension.
namespace – The namespace for the extension.
name – The name of the extension to be loaded.
exception_type – The exception type to be raised if the extension couldn’t be loaded.
**kwargs – Keyword arguments forwarded to the extension constructor.
- Returns:
An extension.
- asyoulikeit.extension.describe_extension(kind, namespace, name, exception_type=None, *, single_line: bool = False) str[source]
Describe an extension by name.
- Parameters:
kind – The kind of extension.
namespace – The namespace for the extension.
name – The name of the extension.
exception_type – The exception type to be raised if the extension couldn’t be loaded.
single_line – If True, return only the first non-empty line of the description.
- Returns:
A string describing the extension.
- asyoulikeit.extension.extension(kind: str, namespace: str, name: str, exception_type: BaseException) Type[Extension][source]
Get the extension class without instantiating it.
- Parameters:
kind – The kind of extension.
namespace – The namespace for the extension.
name – The name of the extension to be loaded.
exception_type – The exception type to be raised if the extension couldn’t be loaded.
- Returns:
The type (i.e. class) of an extension.
- asyoulikeit.extension.extension_name_from_class(namespace: str, extension_class: Type[Extension]) str[source]
Get the entry point name for an extension class.
This performs a reverse lookup from class to entry point name by iterating through all extensions in the namespace until finding one whose plugin matches the given class.
- Parameters:
namespace – The namespace to search (e.g., ‘asyoulikeit.formatter’)
extension_class – The extension class to find
- Returns:
The entry point name (key) for the extension
- Raises:
ExtensionError – If the extension class is not found in the namespace
Exceptions
Exception hierarchy for the asyoulikeit package.
- exception asyoulikeit.exceptions.AsyoulikeitError[source]
Bases:
ExceptionBase class for all exceptions raised by the asyoulikeit package.
- exception asyoulikeit.exceptions.ReportDeclarationError[source]
Bases:
AsyoulikeitErrorRaised when a
@report_outputcommand violates itsreports=declaration.Thrown at two distinct points:
Decoration time — when the
reports=mapping is ill-formed (non-identifier keys, non-Ellipsisnon-string keys, or adefault_reportsentry naming a report that wasn’t declared).Runtime — when the handler returns a
Reportscontaining a name that wasn’t declared, and the declaration has noEllipsisslot to admit dynamic names.