# 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`. The public terminology is plugin-first, even though some internal classes and metadata fields still use `Provider` naming. --- ## Quick summary - Plugins register a plugin adapter, a `columns` definition, and a `selection_fn`. - `selection_fn` returns CLI args for a selected row. - For HTML table or list scraping, prefer `TablePluginMixin` from `SYS.plugin_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 ```py # 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 `TablePluginMixin` If your plugin scrapes HTML tables or list-like results, use `TablePluginMixin`: ```py from PluginCore.base import Provider from SYS.plugin_helpers import TablePluginMixin class MyTablePlugin(TablePluginMixin, 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) ``` `TablePluginMixin.search_table_from_url` returns `PluginCore.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 - `columns` may be a static `List[ColumnSpec]` or a factory that inspects sample rows. - `selection_fn` must accept a `ResultModel` and return a `List[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.source` is 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_.py` or follow the repo's older naming conventions when extending existing tests. - Verify `plugin.build_table(...)` produces a `ResultTable` with rows and columns. - Verify `serialize_rows()` yields `_selection_args`, `_selection_action` when applicable, and `source`. - 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_url` with a local HTML fixture or a mocked `HTTPClient`. Example test skeleton: ```py from SYS.result_table_adapters import get_plugin from plugins import example_plugin def test_example_plugin_registration(): plugin = get_plugin("example") rows = list(plugin.adapter(example_plugin.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_plugin.SAMPLE_ITEMS) assert table.provider == "example" and table.rows ``` --- ## References and examples - Read `plugins/example_plugin.py` for a compact example of a strict adapter and dynamic columns. - Read `plugins/vimm/__init__.py` for a table-oriented plugin that uses `TablePluginMixin` and converts `SearchResult` to `ResultModel` for registration. - See `docs/plugin_guide.md` for a broader plugin development checklist.