A Rich-based TUI toolkit for building metadata browsers.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rich_metadata-0.1.5.tar.gz.
File metadata
- Download URL: rich_metadata-0.1.5.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d4ddf0a40ff59ea123e6dfe7262a9d047a5b1348e1c8356ed33d5b14bbd9229
|
|
| MD5 |
7b7324dd6cb74a7ace8e3959fb65bcbc
|
|
| BLAKE2b-256 |
fd30e26c3b660c8791a15652697ae583099c5f80b7771891f352f3f13734b068
|
File details
Details for the file rich_metadata-0.1.5-py3-none-any.whl.
File metadata
- Download URL: rich_metadata-0.1.5-py3-none-any.whl
- Upload date:
- Size: 21.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b884ee11f320baf0967c0c7c81c540e1620834cf149aa90ecf68d5f30a34fd84
|
|
| MD5 |
7298adb1f55e0eb7a8cd6558b6c75e9f
|
|
| BLAKE2b-256 |
de6087cb24f77100597c33d8029e05bd878c06cb631bbcbdf5556c4a7a8c19ab
|