rich-metadata
A Rich-based TUI toolkit for building interactive metadata browsers.
Define your entities declaratively, wire up your API, and get an interactive terminal browser with pagination, navigation, lazy loading, and image support.
Install
pip install rich-metadata
Quick start
from rich_metadata import (
BaseNavigator,
DisplayEngine,
EntityDef,
HeaderField,
HeaderLink,
SectionDef,
SummaryField,
TableColumn,
)
# 1. Define your entities
book_def = EntityDef(
type_name="book",
summary=[
SummaryField(key="title", style="bold"),
SummaryField(prefix="by ", key="author"),
SummaryField(key="year", style="dim"),
],
header_fields=[
HeaderField("Author", key="author"),
HeaderField("Year", key="year"),
HeaderField("Genre", key="genre"),
HeaderField("Pages", key="pages"),
HeaderField("Publisher", key="publisher"),
],
sections=[
SectionDef(
"chapters",
navigable=True,
columns=[
TableColumn("#", "number", width=4),
TableColumn("Title", "title", style="bold"),
TableColumn("Pages", "pages"),
],
),
SectionDef("description", lazy=True),
],
header_links=[
HeaderLink("Author: {author}", "author", ref_key="author_url"),
],
footer=["url"],
)
# 2. Create a display engine and register definitions
engine = DisplayEngine()
engine.register(book_def)
# 3. Use the engine directly
entity = {"_type": "book", "title": "Dune", "author": "Frank Herbert", "year": "1965"}
engine.details(entity) # render header + all sections
engine.summary(entity) # one-line summary
engine.header(entity) # header panel with image
# 4. Or wire up a navigator for interactive browsing
class BookAPI:
def get(self, ref: str, *, full: bool = False, **kwargs) -> dict | None:
... # fetch entity by URL/ID, return dict with "_type" key
# `full=True` is passed when the caller wants every section eagerly
# populated (e.g. `--json` / `--full` flows). Extra kwargs are forwarded.
def search(self, query: str, exact_match: bool = True) -> list[dict]:
... # return list of entity dicts (each with "_type" and "name")
navigator = BaseNavigator(
engine,
apis={"book": BookAPI(), "author": AuthorAPI()},
entity_ref_key="url",
lazy_fetchers={
("book", "description"): lambda api, entity: api.fetch_description(entity["id"]),
},
)
navigator.navigate(entity) # interactive section menu
navigator.browse(fetch_page=my_search_fn) # paginated results
Core concepts
Entity dicts
Entities are plain Python dicts with a _type key for routing:
{"_type": "book", "title": "Dune", "author": "Frank Herbert", "year": "1965"}
EntityDef
Declares how an entity type is displayed. Each EntityDef configures:
| Field | Purpose |
|---|---|
type_name |
Entity type identifier (matches _type in dicts) |
summary |
One-line display fields (SummaryField list) |
header_fields |
Key-value pairs in the detail panel (HeaderField list) |
header_image_key |
Dict key holding image bytes for the header panel |
header_title |
Custom title callable (dict) -> str |
panel_border_style |
Rich style for the header panel border |
sections |
Expandable content sections (SectionDef list) |
header_links |
Navigable links shown in the section menu (HeaderLink list) |
footer |
Keys (strings) or callables (dict) -> str | None for lines below the panel |
auto_full |
If True, render every section inline (no menu) and offer prev/next sibling navigation |
SectionDef
Defines a content section. The rendering mode is auto-detected:
- Has
columns-> table (if data is alist, rows are flat; ifdict[str, list], rows are grouped with headers) - Has
custom_render-> custom rendering function - Neither -> text panel
Key options: navigable (items can be drilled into), lazy (fetched on demand), duration_key (sums and shows total duration).
SummaryField
One segment of a one-line entity summary. Supports style, prefix (text before the value), fallback (shown when key is missing), and transform (receives value if key is set, or the whole entity dict if not).
HeaderField
A labeled row in the detail panel. Either reads from key directly, or computes via transform (receives value if key is set, or the whole entity dict if not).
TableColumn
A column in a table section. Supports style, justify, width, and transform.
HeaderLink
A navigable link shown in the section menu (e.g., "Author: Frank Herbert ->"). Uses ref_key to read a URL/ID from the entity dict, or ref_fn(entity) -> str | None for computed refs.
DisplayEngine
The rendering engine. Key methods:
engine = DisplayEngine() # uses default Rich console
engine = DisplayEngine(my_console) # custom console
engine.register(entity_def) # register an EntityDef
engine.summary(entity) # one-line summary
engine.header(entity) # detail panel with optional image
engine.section(entity, "chapters") # render a single section
engine.details(entity) # header + all non-lazy sections
engine.select_from_list(items) # numbered selection prompt
BaseNavigator
Interactive browser with pagination, back-navigation, and lazy fetching.
navigator = BaseNavigator(
engine,
apis={"book": book_api, "author": author_api},
entity_ref_key="url",
lazy_fetchers={
("book", "description"): lambda api, entity: api.fetch_desc(entity["id"]),
},
)
| Parameter | Purpose |
|---|---|
apis |
{type: api}. Each API needs .get(ref, *, full=False, **kwargs) and (for search_and_navigate) .search(query, exact_match=True) |
entity_ref_key |
Key to extract the navigable ref from item dicts (default: "url") |
lazy_fetchers |
{(type, section): callable(api, entity) -> data} for lazy sections |
Key methods:
navigate(entity)-- Interactive loop: header, section menu, lazy fetching, header link navigation, back-navigation, and prev/next sibling navigation whensiblings=is provided.display_or_navigate(entity, *, json_output=False, full=False)-- Dispatch one entity: JSON dump (internal keys stripped), full inline render, or interactivenavigate(). Used by CLIs that share one code path for--json/--full/ interactive modes.search_and_navigate(query, types, *, exact_first=True, json_output=False, full=False)-- Search acrosstypes(each API's.search()is called), prefer exact name matches whenexact_first, then select and navigate. Passesfull=to.get()so backends can fetch eagerly when needed.browse(fetch_page=..., *, render_page=None, title=None, page_size=25, full=False, loop=False)-- Paginated results with selection.fetch_page(start, count) -> (results, total).loop=Truere-displays the page after a child view returns (works with bothfulland interactive modes).browse_sources(sources, *, full=False)-- Pick from named browsable sources ([(label, fetch_page), ...]), then browse the selected one. Skips the menu when only one source is supplied.
Items with _type but no ref (no url or whatever entity_ref_key is) are treated as inline entities -- navigated directly without fetching.
QuitSignal
BaseNavigator._input raises QuitSignal (an Exception subclass) on Ctrl+C / EOF so the interactive loops can unwind cleanly. Catch it at your CLI entry point to exit quietly:
from rich_metadata import QuitSignal
try:
navigator.navigate(entity)
except QuitSignal:
pass
CLI helpers
Shared CLI utilities:
from rich_metadata import (
configure_logging, # loguru setup (debug if verbose, else warnings)
resolve_entity_type, # extract (type, query) from parsed args
strip_internal_keys, # remove _prefixed keys for JSON output
list_fetcher, # wrap a list into a fetch_page callable for browse()
page_fetcher, # adapt page-number APIs to browse()'s offset interface
parse_date, # parse 'YYYY-MM-DD' string to date
parse_date_args, # parse --from/--to argparse flags into dates
months_in_range, # list of 'YYYY-MM' strings covering a date range
)
list_fetcher and page_fetcher are adapters for BaseNavigator.browse():
# Wrap a pre-fetched list
navigator.browse(fetch_page=list_fetcher(my_items))
# Adapt a page-number API (fetch(page) -> (results, has_more))
navigator.browse(fetch_page=page_fetcher(api.search, first_page=initial_results))
Image support
Terminal image rendering for iTerm2 and Kitty:
from rich_metadata import get_image_escape, show_image_beside
# Get raw escape sequence for image bytes
escape = get_image_escape(image_bytes, width=20, height=10)
# Show an image beside a Rich renderable
show_image_beside(console, image_bytes, my_panel, img_width=20)
Duration helpers
from rich_metadata import parse_duration, format_duration
parse_duration("3:45") # 225 (seconds)
parse_duration("1:02:30") # 3750
format_duration(225) # "3:45"
format_duration(3750) # "1:02:30"
License
MIT
Metadata
Release files for rich-metadata 0.1.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| rich_metadata-0.1.9.tar.gz | 19.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rich_metadata-0.1.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.8 kB
Release files / rich_metadata-0.1.9.tar.gz
| Download URL | rich_metadata-0.1.9.tar.gz |
|---|---|
| Size | 19.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d08fed595bc94ca6adea1b139b05adbeaa63e09c34b1b3d9c5482a196506b145
|
|
BLAKE2b-256 checksum How to use checksums |
8faddd36430a08d2da2c316df12719577d9219fb66098d7ab7bd9cf83e32b8b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.
Transparency logRelease files / rich_metadata-0.1.9-py3-none-any.whl
| Download URL | rich_metadata-0.1.9-py3-none-any.whl |
|---|---|
| Size | 22.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6fc27c3f7d4885ddfaa5937084875036b89082ce875791443fe5be752b3a473d
|
|
BLAKE2b-256 checksum How to use checksums |
84bf893deff1a5711695e56cae526cb03161b7f395102bf3fbe29dd8bac04622
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.
Transparency logSecurity for rich-metadata 0.1.9
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for rich_metadata-0.1.9.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.