Skip to main content

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 a list, rows are flat; if dict[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 when siblings= is provided.
  • display_or_navigate(entity, *, json_output=False, full=False) -- Dispatch one entity: JSON dump (internal keys stripped), full inline render, or interactive navigate(). 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 across types (each API's .search() is called), prefer exact name matches when exact_first, then select and navigate. Passes full= 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=True re-displays the page after a child view returns (works with both full and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rich_metadata-0.1.7.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rich_metadata-0.1.7-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

Details for the file rich_metadata-0.1.7.tar.gz.

File metadata

  • Download URL: rich_metadata-0.1.7.tar.gz
  • Upload date:
  • Size: 18.9 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

Hashes for rich_metadata-0.1.7.tar.gz
Algorithm Hash digest
SHA256 09b034257f6141529bae90b004c1cbff17dcf2d79363af18518ee978b675bf2f
MD5 37eb4faa63fb91ee134e53548698a613
BLAKE2b-256 e3441b2cc53771217d1491ea12517d40189799edcd3906efb0af2972ca896e55

See more details on using hashes here.

File details

Details for the file rich_metadata-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: rich_metadata-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 22.5 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

Hashes for rich_metadata-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 9b353f559178b4f71cafd2a140c327ac7ce1860768c50d73a3930002ed4e66bb
MD5 499638ae67bb776f7a4b988406815456
BLAKE2b-256 393a3920c036fc97bb60c8caf2193105d28d1fb35ec33ad0229ee8260f4e8b69

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page