cleanup and rename provider to plugin
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# 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_<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`.
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user