Skip to main content

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

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.9.tar.gz (19.1 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.9-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: rich_metadata-0.1.9.tar.gz
  • Upload date:
  • Size: 19.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for rich_metadata-0.1.9.tar.gz
Algorithm Hash digest
SHA256 d08fed595bc94ca6adea1b139b05adbeaa63e09c34b1b3d9c5482a196506b145
MD5 b810a671482b7c08c85b89dec3d894cd
BLAKE2b-256 8faddd36430a08d2da2c316df12719577d9219fb66098d7ab7bd9cf83e32b8b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for rich_metadata-0.1.9.tar.gz:

Publisher: publish.yml on gabriel-jung/rich-metadata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: rich_metadata-0.1.9-py3-none-any.whl
  • Upload date:
  • Size: 22.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for rich_metadata-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 6fc27c3f7d4885ddfaa5937084875036b89082ce875791443fe5be752b3a473d
MD5 4fd300570fae9cc5b7e6483829aecbaa
BLAKE2b-256 84bf893deff1a5711695e56cae526cb03161b7f395102bf3fbe29dd8bac04622

See more details on using hashes here.

Provenance

The following attestation bundles were made for rich_metadata-0.1.9-py3-none-any.whl:

Publisher: publish.yml on gabriel-jung/rich-metadata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.9 This release

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page