5.5 KiB
Plugin authoring: ResultTable and plugin adapters
This short guide explains how to write plugins that integrate with the strict
ResultTable API: adapters yield ResultModel instances, and plugins register
via SYS.result_table_adapters.register_plugin with columns and a
selection_fn.
Note: this file keeps its historical provider_authoring name, but the public
terminology is plugin-first. Some internal classes and metadata fields still use
Provider naming.
Quick summary
- Plugins register a plugin adapter, a
columnsdefinition, and aselection_fn. selection_fnreturns CLI args for a selected row.- For HTML table or list scraping, prefer
TableProviderMixinfromSYS.provider_helpers.
Runtime dependency policy
- Treat required runtime dependencies such as Playwright as mandatory: import them unconditionally and let missing dependencies fail fast.
- Use guarded imports only for truly optional dependencies such as
pandas. - Keep plugin code minimal and explicit: fail early and document required runtime dependencies in README and installation notes.
Minimal plugin template
# plugins/my_plugin.py
from typing import Any, Dict, Iterable, List
from SYS.result_table_api import ResultModel, ColumnSpec, title_column
from SYS.result_table_adapters import register_plugin
SAMPLE_ITEMS = [
{
"name": "Example File.pdf",
"path": "https://example.com/x.pdf",
"ext": "pdf",
"size": 1024,
"source": "myplugin",
},
]
def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
for it in items:
title = it.get("name") or it.get("title") or str(it.get("path") or "")
yield ResultModel(
title=str(title),
path=str(it.get("path")) if it.get("path") else None,
ext=str(it.get("ext")) if it.get("ext") else None,
size_bytes=int(it.get("size")) if it.get("size") is not None else None,
metadata=dict(it),
source=str(it.get("source")) if it.get("source") else "myplugin",
)
def columns_factory(rows: List[ResultModel]) -> List[ColumnSpec]:
cols = [title_column()]
if any((row.metadata or {}).get("size") for row in rows):
cols.append(ColumnSpec("size", "Size", lambda row: row.size_bytes or ""))
return cols
def selection_fn(row: ResultModel) -> List[str]:
if row.path:
return ["-path", row.path]
return ["-title", row.title or ""]
register_plugin("myplugin", adapter, columns=columns_factory, selection_fn=selection_fn)
Table scraping with TableProviderMixin
If your plugin scrapes HTML tables or list-like results, use TableProviderMixin:
from ProviderCore.base import Provider
from SYS.provider_helpers import TableProviderMixin
class MyTablePlugin(TableProviderMixin, Provider):
URL = ("https://example.org/search",)
def validate(self) -> bool:
return True
def search(self, query: str, limit: int = 50, **kwargs):
url = f"{self.URL[0]}?q={quote_plus(query)}"
return self.search_table_from_url(url, limit=limit)
TableProviderMixin.search_table_from_url returns
ProviderCore.base.SearchResult entries. If you want to integrate the plugin
with the strict ResultTable registry, add a small adapter that converts
SearchResult to ResultModel and register it using register_plugin.
Columns and selection
columnsmay be a staticList[ColumnSpec]or a factory that inspects sample rows.selection_fnmust accept aResultModeland return aList[str]representing CLI args.- For downloadable file rows, prefer explicit URL args such as
['-url', row.path]so downstream downloaders interpret the row unambiguously. - Ensure
ResultModel.sourceis set directly or falls back to the registered plugin name during serialization.
Optional pandas support
SYS.html_table.extract_records prefers a pure-lxml path but can fall back to
pandas.read_html when pandas is installed and the helper detects it works for
the input table. This is optional. Document whether your plugin requires
pandas and emit a clear error or log message when it is missing.
Testing and examples
- Write
tests/test_plugin_<name>.pyor follow the repo's older naming conventions when extending existing tests. - Verify
plugin.build_table(...)produces aResultTablewith rows and columns. - Verify
serialize_rows()yields_selection_args,_selection_actionwhen applicable, andsource. - When you need an exact CLI stage sequence, call
table.set_row_selection_action(row_index, tokens)so replay uses the row action verbatim. - For table-oriented plugins, test
search_table_from_urlwith a local HTML fixture or a mockedHTTPClient.
Example test skeleton:
from SYS.result_table_adapters import get_plugin
from plugins import example_provider
def test_example_plugin_registration():
plugin = get_plugin("example")
rows = list(plugin.adapter(example_provider.SAMPLE_ITEMS))
assert rows and rows[0].title
cols = plugin.get_columns(rows)
assert any(col.name == "title" for col in cols)
table = plugin.build_table(example_provider.SAMPLE_ITEMS)
assert table.provider == "example" and table.rows
References and examples
- Read
plugins/example_provider.pyfor a compact example of a strict adapter and dynamic columns. - Read
plugins/vimm/__init__.pyfor a table-oriented plugin that usesTableProviderMixinand convertsSearchResulttoResultModelfor registration. - See
docs/provider_guide.mdfor a broader plugin development checklist.