2026-05-16 15:26:08 -07:00
# Plugin authoring: ResultTable and plugin adapters
2026-01-06 01:38:59 -08:00
2026-05-16 15:26:08 -07:00
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` .
2026-05-21 16:19:17 -07:00
The public terminology is plugin-first, even though some internal classes and
metadata fields still use `Provider` naming.
2026-01-06 01:38:59 -08:00
---
## Quick summary
2026-05-16 15:26:08 -07:00
- Plugins register a plugin adapter, a `columns` definition, and a `selection_fn` .
- `selection_fn` returns CLI args for a selected row.
2026-05-21 16:19:17 -07:00
- For HTML table or list scraping, prefer `TablePluginMixin` from `SYS.plugin_helpers` .
2026-01-06 01:38:59 -08:00
## Runtime dependency policy
2026-05-16 15:26:08 -07:00
- 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.
2026-01-06 01:38:59 -08:00
---
2026-05-16 15:26:08 -07:00
## Minimal plugin template
2026-01-06 01:38:59 -08:00
``` py
2026-04-19 00:41:09 -07:00
# plugins/my_plugin.py
2026-01-06 01:38:59 -08:00
from typing import Any , Dict , Iterable , List
2026-05-16 15:26:08 -07:00
from SYS . result_table_api import ResultModel , ColumnSpec , title_column
2026-04-19 00:41:09 -07:00
from SYS . result_table_adapters import register_plugin
2026-01-06 01:38:59 -08:00
SAMPLE_ITEMS = [
2026-05-16 15:26:08 -07:00
{
" name " : " Example File.pdf " ,
" path " : " https://example.com/x.pdf " ,
" ext " : " pdf " ,
" size " : 1024 ,
" source " : " myplugin " ,
} ,
2026-01-06 01:38:59 -08:00
]
2026-05-16 15:26:08 -07:00
2026-01-06 01:38:59 -08:00
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 ) ,
2026-05-16 15:26:08 -07:00
source = str ( it . get ( " source " ) ) if it . get ( " source " ) else " myplugin " ,
2026-01-06 01:38:59 -08:00
)
2026-05-16 15:26:08 -07:00
2026-01-06 01:38:59 -08:00
def columns_factory ( rows : List [ ResultModel ] ) - > List [ ColumnSpec ] :
cols = [ title_column ( ) ]
2026-05-16 15:26:08 -07:00
if any ( ( row . metadata or { } ) . get ( " size " ) for row in rows ) :
cols . append ( ColumnSpec ( " size " , " Size " , lambda row : row . size_bytes or " " ) )
2026-01-06 01:38:59 -08:00
return cols
2026-05-16 15:26:08 -07:00
2026-01-06 01:38:59 -08:00
def selection_fn ( row : ResultModel ) - > List [ str ] :
if row . path :
return [ " -path " , row . path ]
return [ " -title " , row . title or " " ]
2026-05-16 15:26:08 -07:00
register_plugin ( " myplugin " , adapter , columns = columns_factory , selection_fn = selection_fn )
2026-01-06 01:38:59 -08:00
```
---
2026-05-21 16:19:17 -07:00
## Table scraping with `TablePluginMixin`
2026-01-06 01:38:59 -08:00
2026-05-21 16:19:17 -07:00
If your plugin scrapes HTML tables or list-like results, use `TablePluginMixin` :
2026-01-06 01:38:59 -08:00
``` py
2026-05-21 16:19:17 -07:00
from PluginCore . base import Provider
from SYS . plugin_helpers import TablePluginMixin
2026-01-06 01:38:59 -08:00
2026-05-16 15:26:08 -07:00
2026-05-21 16:19:17 -07:00
class MyTablePlugin ( TablePluginMixin , Provider ) :
2026-01-06 01:38:59 -08:00
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 )
```
2026-05-21 16:19:17 -07:00
`TablePluginMixin.search_table_from_url` returns
`PluginCore.base.SearchResult` entries. If you want to integrate the plugin
2026-05-16 15:26:08 -07:00
with the strict `ResultTable` registry, add a small adapter that converts
`SearchResult` to `ResultModel` and register it using `register_plugin` .
2026-01-06 01:38:59 -08:00
---
2026-05-16 15:26:08 -07:00
## 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.
2026-01-06 01:38:59 -08:00
---
2026-05-16 15:26:08 -07:00
## 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.
2026-01-06 01:38:59 -08:00
---
2026-05-16 15:26:08 -07:00
## Testing and examples
- Write `tests/test_plugin_<name>.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` .
2026-01-06 01:38:59 -08:00
2026-05-16 15:26:08 -07:00
Example test skeleton:
2026-01-06 01:38:59 -08:00
``` py
2026-05-16 15:26:08 -07:00
from SYS . result_table_adapters import get_plugin
2026-05-21 16:19:17 -07:00
from plugins import example_plugin
2026-01-06 01:38:59 -08:00
2026-05-16 15:26:08 -07:00
def test_example_plugin_registration ( ) :
2026-04-19 00:41:09 -07:00
plugin = get_plugin ( " example " )
2026-05-21 16:19:17 -07:00
rows = list ( plugin . adapter ( example_plugin . SAMPLE_ITEMS ) )
2026-01-06 01:38:59 -08:00
assert rows and rows [ 0 ] . title
2026-05-16 15:26:08 -07:00
cols = plugin . get_columns ( rows )
assert any ( col . name == " title " for col in cols )
2026-05-21 16:19:17 -07:00
table = plugin . build_table ( example_plugin . SAMPLE_ITEMS )
2026-01-06 01:38:59 -08:00
assert table . provider == " example " and table . rows
```
---
2026-05-16 15:26:08 -07:00
## References and examples
2026-05-21 16:19:17 -07:00
- 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.