cleanup and rename provider to plugin
This commit is contained in:
@@ -98,7 +98,7 @@ from SYS.result_table import Table
|
||||
|
||||
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
|
||||
from SYS.pipeline import PipelineExecutor
|
||||
from ProviderCore.registry import plugin_inline_query_choices
|
||||
from PluginCore.registry import plugin_inline_query_choices
|
||||
|
||||
|
||||
|
||||
@@ -505,7 +505,7 @@ class CmdletIntrospection:
|
||||
if normalized_arg == "plugin":
|
||||
canonical_cmd = (cmd_name or "").replace("_", "-").lower()
|
||||
try:
|
||||
from ProviderCore.registry import (
|
||||
from PluginCore.registry import (
|
||||
list_configured_plugin_names_with_capability,
|
||||
list_plugin_names_for_cmdlet,
|
||||
)
|
||||
@@ -546,7 +546,7 @@ class CmdletIntrospection:
|
||||
|
||||
if normalized_arg == "scrape":
|
||||
try:
|
||||
from plugins.metadata_provider import list_metadata_plugins
|
||||
from plugins.metadata_plugin import list_metadata_plugins
|
||||
|
||||
metadata_plugins = list_metadata_plugins(config) or {}
|
||||
if metadata_plugins:
|
||||
@@ -765,7 +765,7 @@ class CmdletCompleter(Completer):
|
||||
return []
|
||||
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
from PluginCore.registry import get_plugin_class
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from SYS.logger import log, debug
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
|
||||
|
||||
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Plugin core modules.
|
||||
|
||||
This package contains the plugin framework (base types, registry, and shared
|
||||
helpers). Bundled plugins live in the `plugins/` package.
|
||||
"""
|
||||
@@ -6,8 +6,8 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, List, Optional
|
||||
import logging
|
||||
from ProviderCore.commands import get_primary_command_object
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.commands import get_primary_command_object
|
||||
from PluginCore.registry import get_plugin
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ def extract_records(doc_or_html: Any, base_url: Optional[str] = None, xpaths: Op
|
||||
|
||||
# Small convenience: convert records to SearchResult. Providers can call this or
|
||||
# use their own mapping when they need full SearchResult objects.
|
||||
from ProviderCore.base import SearchResult # local import to avoid circular issues
|
||||
from PluginCore.base import SearchResult # local import to avoid circular issues
|
||||
|
||||
|
||||
def records_to_search_results(records: List[Dict[str, str]], table: str = "provider") -> List[SearchResult]:
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
from SYS.yt_metadata import extract_ytdlp_tags
|
||||
|
||||
try: # Optional; used when available for richer metadata fetches
|
||||
|
||||
+4
-4
@@ -1562,7 +1562,7 @@ class PipelineExecutor:
|
||||
_add(getattr(item, "table", None))
|
||||
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin, is_known_plugin_name
|
||||
from PluginCore.registry import get_plugin, is_known_plugin_name
|
||||
except Exception:
|
||||
get_plugin = None # type: ignore
|
||||
is_known_plugin_name = None # type: ignore
|
||||
@@ -1679,7 +1679,7 @@ class PipelineExecutor:
|
||||
_add(getattr(item, "source", None))
|
||||
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin, is_known_plugin_name
|
||||
from PluginCore.registry import get_plugin, is_known_plugin_name
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -2313,7 +2313,7 @@ class PipelineExecutor:
|
||||
auto_stage = None
|
||||
if isinstance(table_type, str) and table_type:
|
||||
try:
|
||||
from ProviderCore.registry import selection_auto_stage_for_table
|
||||
from PluginCore.registry import selection_auto_stage_for_table
|
||||
|
||||
auto_stage = selection_auto_stage_for_table(table_type)
|
||||
except Exception:
|
||||
@@ -3098,7 +3098,7 @@ class PipelineExecutor:
|
||||
auto_stage = None
|
||||
if isinstance(table_type, str) and table_type:
|
||||
try:
|
||||
from ProviderCore.registry import selection_auto_stage_for_table
|
||||
from PluginCore.registry import selection_auto_stage_for_table
|
||||
|
||||
# Preserve historical behavior: only forward selection-stage args
|
||||
# to the auto stage when we are appending a new last stage.
|
||||
|
||||
@@ -6,7 +6,7 @@ import pkgutil
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
from SYS.config import global_config
|
||||
from ProviderCore.registry import get_plugin_class, list_plugins
|
||||
from PluginCore.registry import get_plugin_class, list_plugins
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -194,7 +194,7 @@ def get_required_config_keys(item_type: str, item_name: str) -> List[str]:
|
||||
|
||||
def get_configurable_store_types() -> List[str]:
|
||||
"""Return configurable multi-instance plugin types (formerly 'store types')."""
|
||||
from ProviderCore.registry import REGISTRY
|
||||
from PluginCore.registry import REGISTRY
|
||||
options: List[str] = []
|
||||
for info in REGISTRY.iter_plugins():
|
||||
plugin_cls = info.plugin_class
|
||||
@@ -205,7 +205,7 @@ def get_configurable_store_types() -> List[str]:
|
||||
|
||||
def get_configurable_plugin_types() -> List[str]:
|
||||
"""Return all plugin types that can be configured: those with a schema or MULTI_INSTANCE flag."""
|
||||
from ProviderCore.registry import REGISTRY
|
||||
from PluginCore.registry import REGISTRY
|
||||
options: List[str] = []
|
||||
for info in REGISTRY.iter_plugins():
|
||||
plugin_cls = info.plugin_class
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Convenience mixins and helpers for table-based providers.
|
||||
"""Convenience mixins and helpers for table-based plugins.
|
||||
|
||||
Provides a small `TableProviderMixin` that handles HTTP fetch + table extraction
|
||||
Provides a small `TablePluginMixin` that handles HTTP fetch + table extraction
|
||||
(using `SYS.html_table.extract_records`) and converts records into
|
||||
`ProviderCore.base.SearchResult` rows with sane default column ordering.
|
||||
`PluginCore.base.SearchResult` rows with sane default column ordering.
|
||||
|
||||
Providers can subclass this mixin to implement search quickly:
|
||||
Plugins can subclass this mixin to implement search quickly:
|
||||
|
||||
class MyProvider(TableProviderMixin, Provider):
|
||||
class MyPlugin(TablePluginMixin, Provider):
|
||||
URL = ("https://example.org/search",)
|
||||
|
||||
def search(self, query, limit=50, **kwargs):
|
||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
|
||||
from API.HTTP import HTTPClient
|
||||
from ProviderCore.base import SearchResult
|
||||
from PluginCore.base import SearchResult
|
||||
from SYS.html_table import extract_records
|
||||
import lxml.html as lxml_html
|
||||
|
||||
@@ -29,8 +29,8 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TableProviderMixin:
|
||||
"""Mixin to simplify providers that scrape table/list results from HTML.
|
||||
class TablePluginMixin:
|
||||
"""Mixin to simplify plugins that scrape table/list results from HTML.
|
||||
|
||||
Methods:
|
||||
- search_table_from_url(url, limit, xpaths): fetches HTML, extracts records, returns SearchResults
|
||||
@@ -59,7 +59,7 @@ class TableProviderMixin:
|
||||
resp = client.get(url)
|
||||
content = resp.content
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch URL %s for provider %s", url, getattr(self, 'name', '<provider>'))
|
||||
logger.exception("Failed to fetch URL %s for plugin %s", url, getattr(self, 'name', '<plugin>'))
|
||||
return []
|
||||
|
||||
# Ensure we pass an lxml document or string (httpx returns bytes)
|
||||
@@ -99,14 +99,14 @@ class TableProviderMixin:
|
||||
|
||||
results.append(
|
||||
SearchResult(
|
||||
table=(getattr(self, "name", "provider") or "provider"),
|
||||
table=(getattr(self, "name", "plugin") or "plugin"),
|
||||
title=title,
|
||||
path=path,
|
||||
detail="",
|
||||
annotations=[],
|
||||
media_kind="file",
|
||||
size_bytes=None,
|
||||
tag={getattr(self, "name", "provider")},
|
||||
tag={getattr(self, "name", "plugin") or "plugin"},
|
||||
columns=cols,
|
||||
full_metadata={"raw_record": rec},
|
||||
)
|
||||
+2
-2
@@ -88,7 +88,7 @@ def _load_root_modules() -> None:
|
||||
|
||||
def _load_helper_modules() -> None:
|
||||
# Provider-specific module pre-loading removed; providers are loaded lazily
|
||||
# through ProviderCore.registry when first referenced.
|
||||
# through PluginCore.registry when first referenced.
|
||||
#
|
||||
# Keep explicit imports for cmdlets moved into subpackages so they remain
|
||||
# registered under their legacy command names.
|
||||
@@ -118,7 +118,7 @@ def _register_native_commands() -> None:
|
||||
|
||||
def _register_plugin_commands() -> None:
|
||||
try:
|
||||
from ProviderCore.commands import register_plugin_commands
|
||||
from PluginCore.commands import register_plugin_commands
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
|
||||
+3
-3
@@ -276,7 +276,7 @@ class SharedArgs:
|
||||
|
||||
# Plugin-based multi-instance backends (config["plugin"] / config["provider"] sections)
|
||||
try:
|
||||
from ProviderCore.registry import REGISTRY
|
||||
from PluginCore.registry import REGISTRY
|
||||
plugin_instances = REGISTRY.list_storage_plugin_instances(config)
|
||||
for _plugin_name, instance_names in plugin_instances.items():
|
||||
names.update(instance_names)
|
||||
@@ -1448,7 +1448,7 @@ def fetch_hydrus_metadata(
|
||||
client = hydrus_client
|
||||
hydrus_provider = None
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||
except Exception:
|
||||
@@ -4177,7 +4177,7 @@ def check_url_exists_in_storage(
|
||||
|
||||
hydrus_provider = None
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||
except Exception:
|
||||
|
||||
+3
-3
@@ -63,7 +63,7 @@ class _CommandDependencies:
|
||||
|
||||
def get_plugin(self, name: str) -> Optional[Any]:
|
||||
"""Cached plugin lookup by name."""
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
norm_name = str(name or "").strip().lower()
|
||||
if not norm_name:
|
||||
@@ -77,7 +77,7 @@ class _CommandDependencies:
|
||||
|
||||
def get_plugin_with_capability(self, name: str, capability: str) -> Optional[Any]:
|
||||
"""Cached plugin lookup with capability check."""
|
||||
from ProviderCore.registry import get_plugin_with_capability
|
||||
from PluginCore.registry import get_plugin_with_capability
|
||||
|
||||
norm_name = str(name or "").strip().lower()
|
||||
if not norm_name:
|
||||
@@ -2336,7 +2336,7 @@ class Add_File(Cmdlet):
|
||||
delete_after: bool,
|
||||
) -> int:
|
||||
"""Handle uploading via an upload plugin (e.g. 0x0)."""
|
||||
from ProviderCore.registry import (
|
||||
from PluginCore.registry import (
|
||||
get_plugin_with_capability,
|
||||
list_plugin_names_with_capability,
|
||||
list_plugins_with_capability,
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Sequence, Set
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from SYS.logger import log
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
from SYS.item_accessors import get_http_url, get_sha256_hex, get_store_name
|
||||
from SYS.utils import extract_hydrus_hash_from_url
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
from SYS.logger import debug, log
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
from Store import Store
|
||||
from .. import _shared as sh
|
||||
from SYS import pipeline as ctx
|
||||
|
||||
@@ -992,8 +992,8 @@ class Download_File(Cmdlet):
|
||||
def _load_provider_registry() -> Dict[str, Any]:
|
||||
"""Lightweight accessor for plugin helpers without hard dependencies."""
|
||||
try:
|
||||
from ProviderCore import registry as provider_registry # type: ignore
|
||||
from ProviderCore.base import SearchResult # type: ignore
|
||||
from PluginCore import registry as provider_registry # type: ignore
|
||||
from PluginCore.base import SearchResult # type: ignore
|
||||
|
||||
return {
|
||||
"get_plugin": getattr(provider_registry, "get_plugin", None),
|
||||
|
||||
@@ -154,7 +154,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
if urls_to_download and len(urls_to_download) >= 2:
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin_for_url
|
||||
from PluginCore.registry import get_plugin_for_url
|
||||
|
||||
expanded: List[Dict[str, Any]] = []
|
||||
downloaded_any = False
|
||||
|
||||
@@ -15,7 +15,7 @@ from urllib.parse import urlparse, parse_qs, unquote, urljoin
|
||||
|
||||
from SYS.logger import log, debug, debug_panel
|
||||
from SYS.payload_builders import build_file_result_payload, normalize_file_extension
|
||||
from ProviderCore.registry import get_plugin_with_capability, list_plugins_with_capability
|
||||
from PluginCore.registry import get_plugin_with_capability, list_plugins_with_capability
|
||||
from SYS.rich_display import (
|
||||
show_plugin_config_panel,
|
||||
show_store_config_panel,
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
|
||||
from SYS.detail_view_helpers import create_detail_view, prepare_detail_metadata
|
||||
from SYS.logger import log
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
from SYS.result_table_helpers import add_row_columns
|
||||
from SYS.selection_builder import build_hash_store_selection
|
||||
from SYS.result_publication import publish_result_table
|
||||
|
||||
@@ -9,7 +9,7 @@ import sys
|
||||
|
||||
from SYS.logger import log
|
||||
from SYS.item_accessors import get_sha256_hex, get_store_name
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
from SYS import pipeline as ctx
|
||||
from .. import _shared as sh
|
||||
|
||||
@@ -14,18 +14,18 @@ import sys
|
||||
|
||||
from SYS.logger import log, debug
|
||||
|
||||
# plugins.metadata_provider is deferred: it transitively loads yt_dlp, Cryptodome,
|
||||
# plugins.metadata_plugin is deferred: it transitively loads yt_dlp, Cryptodome,
|
||||
# imdbinfo, musicbrainzngs and ~1400 modules (~1.5s). Import lazily on first use.
|
||||
_METADATA_PROVIDER_MOD: Optional[Any] = None
|
||||
_METADATA_PLUGIN_MOD: Optional[Any] = None
|
||||
|
||||
|
||||
def _mp() -> Any:
|
||||
"""Return the (lazily imported) plugins.metadata_provider module."""
|
||||
global _METADATA_PROVIDER_MOD
|
||||
if _METADATA_PROVIDER_MOD is None:
|
||||
import plugins.metadata_provider as _m
|
||||
_METADATA_PROVIDER_MOD = _m
|
||||
return _METADATA_PROVIDER_MOD
|
||||
"""Return the (lazily imported) plugins.metadata_plugin module."""
|
||||
global _METADATA_PLUGIN_MOD
|
||||
if _METADATA_PLUGIN_MOD is None:
|
||||
import plugins.metadata_plugin as _m
|
||||
_METADATA_PLUGIN_MOD = _m
|
||||
return _METADATA_PLUGIN_MOD
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -42,7 +42,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
select_raw = parsed.get("select")
|
||||
|
||||
try:
|
||||
provider = get_plugin(plugin_name)
|
||||
plugin = get_plugin(plugin_name)
|
||||
except Exception:
|
||||
log(f"Unknown plugin: {plugin_name}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -52,7 +52,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if use_sample:
|
||||
# Try to locate SAMPLE_ITEMS in the adapter's module (convention only)
|
||||
try:
|
||||
mod = __import__(provider.adapter.__module__, fromlist=["*"])
|
||||
mod = __import__(plugin.adapter.__module__, fromlist=["*"])
|
||||
items = getattr(mod, "SAMPLE_ITEMS", None)
|
||||
if items is None:
|
||||
log("Plugin does not expose SAMPLE_ITEMS; no sample available", file=sys.stderr)
|
||||
@@ -69,14 +69,14 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
items = inputs
|
||||
|
||||
try:
|
||||
table = provider.build_table(items)
|
||||
table = plugin.build_table(items)
|
||||
except Exception as exc:
|
||||
log(f"Plugin '{provider.name}' failed: {exc}", file=sys.stderr)
|
||||
log(f"Plugin '{plugin.name}' failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Emit rows for downstream pipeline consumption (pipable behavior).
|
||||
try:
|
||||
for item in provider.serialize_rows(table.rows):
|
||||
for item in plugin.serialize_rows(table.rows):
|
||||
try:
|
||||
ctx.emit(item)
|
||||
except Exception:
|
||||
@@ -115,7 +115,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
return 1
|
||||
|
||||
selected = table.rows[select_idx]
|
||||
sel_args = provider.selection_args(selected)
|
||||
sel_args = plugin.selection_args(selected)
|
||||
|
||||
if not run_cmd:
|
||||
# Print selection args for caller
|
||||
@@ -41,7 +41,7 @@ def _provider_config_map(config: dict) -> dict[str, Any]:
|
||||
|
||||
def _iter_registered_plugin_infos() -> tuple[Any, ...]:
|
||||
try:
|
||||
from ProviderCore.registry import REGISTRY
|
||||
from PluginCore.registry import REGISTRY
|
||||
|
||||
return tuple(
|
||||
sorted(
|
||||
|
||||
+412
-16
@@ -12,6 +12,13 @@ from SYS.config import (
|
||||
)
|
||||
from SYS.database import LOG_DB_PATH, db
|
||||
from SYS.logger import log
|
||||
from SYS.plugin_config import (
|
||||
build_default_plugin_config,
|
||||
build_default_tool_config,
|
||||
get_configurable_plugin_types,
|
||||
get_configurable_store_types,
|
||||
get_configurable_tool_types,
|
||||
)
|
||||
from SYS import pipeline as ctx
|
||||
from SYS.result_table import Table
|
||||
from cmdnat._parsing import (
|
||||
@@ -26,6 +33,7 @@ from cmdnat._parsing import (
|
||||
_PREFERENCES_BROWSE_PATH = "__preferences__"
|
||||
_PLUGINS_BROWSE_PATH = "__plugins__"
|
||||
_PLUGIN_CATEGORY_KEYS = ("plugin", "provider", "tool")
|
||||
_CREATE_INSTANCE_FLAG = "-create-instance"
|
||||
_KNOWN_SECTION_LABELS = {
|
||||
"plugin": "Plugins",
|
||||
"provider": "Plugins",
|
||||
@@ -52,6 +60,18 @@ _SENSITIVE_CONFIG_KEYS = {
|
||||
"secret",
|
||||
"token",
|
||||
}
|
||||
_CONFIG_ITEM_FIELDS = (
|
||||
"kind",
|
||||
"key",
|
||||
"title",
|
||||
"browse_path",
|
||||
"name",
|
||||
"value",
|
||||
"value_display",
|
||||
"type",
|
||||
"display_path",
|
||||
"instance_target",
|
||||
)
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".config",
|
||||
@@ -271,6 +291,168 @@ def _format_config_entry_count(value: Any) -> str:
|
||||
return f"{count} entries"
|
||||
|
||||
|
||||
def _get_configurable_plugin_names() -> List[str]:
|
||||
try:
|
||||
return [
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_plugin_types() or [])
|
||||
if str(name).strip()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get_configurable_tool_names() -> List[str]:
|
||||
try:
|
||||
return [
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_tool_types() or [])
|
||||
if str(name).strip()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get_multi_instance_plugin_names() -> set[str]:
|
||||
try:
|
||||
return {
|
||||
str(name).strip().lower()
|
||||
for name in (get_configurable_store_types() or [])
|
||||
if str(name).strip()
|
||||
}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _split_config_path(value: Optional[str]) -> List[str]:
|
||||
return [part for part in str(value or "").split(".") if part]
|
||||
|
||||
|
||||
def _is_multi_instance_plugin_name(name: str) -> bool:
|
||||
return str(name or "").strip().lower() in _get_multi_instance_plugin_names()
|
||||
|
||||
|
||||
def _is_multi_instance_plugin_root_path(browse_path: Optional[str]) -> bool:
|
||||
parts = _split_config_path(browse_path)
|
||||
return (
|
||||
len(parts) == 2
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
)
|
||||
|
||||
|
||||
def _plugin_schema_field_keys(plugin_name: str) -> set[str]:
|
||||
defaults = build_default_plugin_config(plugin_name)
|
||||
if not isinstance(defaults, dict):
|
||||
return set()
|
||||
return {
|
||||
str(key or "").strip().lower()
|
||||
for key in defaults.keys()
|
||||
if str(key or "").strip()
|
||||
}
|
||||
|
||||
|
||||
def _looks_like_single_instance_branch(plugin_name: str, branch: Any) -> bool:
|
||||
if not isinstance(branch, dict) or not branch:
|
||||
return False
|
||||
|
||||
schema_keys = _plugin_schema_field_keys(plugin_name)
|
||||
entry_keys = {str(key or "").strip().lower() for key in branch.keys()}
|
||||
looks_like_single = bool(schema_keys and entry_keys.intersection(schema_keys))
|
||||
if not looks_like_single:
|
||||
looks_like_single = not all(isinstance(value, dict) for value in branch.values())
|
||||
return looks_like_single
|
||||
|
||||
|
||||
def _normalize_multi_instance_branch(plugin_name: str, branch: Any) -> Dict[str, Any]:
|
||||
if not isinstance(branch, dict):
|
||||
return {}
|
||||
if _looks_like_single_instance_branch(plugin_name, branch):
|
||||
return {"default": dict(branch)}
|
||||
return {
|
||||
str(key): value
|
||||
for key, value in _visible_config_entries(branch)
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
|
||||
|
||||
def _build_create_instance_item(category: str, plugin_name: str) -> Dict[str, Any]:
|
||||
target = f"{category}.{plugin_name}"
|
||||
return {
|
||||
"kind": "create_instance",
|
||||
"key": f"{target}.__new_instance__",
|
||||
"title": "Add Instance",
|
||||
"name": "add_instance",
|
||||
"value": None,
|
||||
"value_display": "Create with @N | .config <name>",
|
||||
"display_path": f"{_format_config_path_label(target)} / Add Instance",
|
||||
"type": "action",
|
||||
"instance_target": target,
|
||||
}
|
||||
|
||||
|
||||
def _build_synthetic_plugin_branch(category: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
normalized_category = str(category or "").strip().lower()
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
return None
|
||||
|
||||
if normalized_category == "tool":
|
||||
branch = build_default_tool_config(normalized_name)
|
||||
return dict(branch) if isinstance(branch, dict) else None
|
||||
|
||||
branch = build_default_plugin_config(normalized_name)
|
||||
if not isinstance(branch, dict):
|
||||
return None
|
||||
if normalized_name in _get_multi_instance_plugin_names():
|
||||
return {"default": dict(branch)}
|
||||
return dict(branch)
|
||||
|
||||
|
||||
def _find_configured_plugin_branch(
|
||||
config_data: Dict[str, Any],
|
||||
category: str,
|
||||
name: str,
|
||||
) -> Optional[tuple[str, Dict[str, Any]]]:
|
||||
category_block = config_data.get(category)
|
||||
if not isinstance(category_block, dict):
|
||||
return None
|
||||
|
||||
target = str(name or "").strip().lower()
|
||||
for raw_name, raw_value in _visible_config_entries(category_block):
|
||||
if str(raw_name or "").strip().lower() != target or not isinstance(raw_value, dict):
|
||||
continue
|
||||
return raw_name, raw_value
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_plugin_branch(
|
||||
config_data: Dict[str, Any],
|
||||
category: str,
|
||||
name: str,
|
||||
) -> Optional[tuple[str, Dict[str, Any], bool]]:
|
||||
found = _find_configured_plugin_branch(config_data, category, name)
|
||||
if found is not None:
|
||||
resolved_name, resolved_value = found
|
||||
return resolved_name, resolved_value, True
|
||||
|
||||
normalized_category = str(category or "").strip().lower()
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
return None
|
||||
|
||||
if normalized_category == "tool":
|
||||
if normalized_name not in _get_configurable_tool_names():
|
||||
return None
|
||||
elif normalized_name not in _get_configurable_plugin_names():
|
||||
return None
|
||||
|
||||
synthetic = _build_synthetic_plugin_branch(normalized_category, normalized_name)
|
||||
if synthetic is None:
|
||||
return None
|
||||
return normalized_name, synthetic, False
|
||||
|
||||
|
||||
def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any]]:
|
||||
branches: List[tuple[str, str, Any]] = []
|
||||
if not isinstance(config_data, dict):
|
||||
@@ -285,9 +467,41 @@ def _iter_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, A
|
||||
return branches
|
||||
|
||||
|
||||
def _iter_available_plugin_branches(config_data: Dict[str, Any]) -> List[tuple[str, str, Any, bool]]:
|
||||
branches: List[tuple[str, str, Any, bool]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for category, name, value in _iter_plugin_branches(config_data):
|
||||
normalized_name = str(name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
continue
|
||||
branches.append((category, name, value, True))
|
||||
seen.add(normalized_name)
|
||||
|
||||
for name in _get_configurable_plugin_names():
|
||||
if name in seen:
|
||||
continue
|
||||
synthetic = _build_synthetic_plugin_branch("plugin", name)
|
||||
if synthetic is None:
|
||||
continue
|
||||
branches.append(("plugin", name, synthetic, False))
|
||||
seen.add(name)
|
||||
|
||||
for name in _get_configurable_tool_names():
|
||||
if name in seen:
|
||||
continue
|
||||
synthetic = _build_synthetic_plugin_branch("tool", name)
|
||||
if synthetic is None:
|
||||
continue
|
||||
branches.append(("tool", name, synthetic, False))
|
||||
seen.add(name)
|
||||
|
||||
return branches
|
||||
|
||||
|
||||
def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
plugin_items: Dict[str, Dict[str, Any]] = {}
|
||||
for category, name, value in _iter_plugin_branches(config_data):
|
||||
for category, name, value, is_configured in _iter_available_plugin_branches(config_data):
|
||||
key = str(name or "").strip().lower()
|
||||
if not key:
|
||||
continue
|
||||
@@ -299,7 +513,7 @@ def _collect_plugin_root_items(config_data: Dict[str, Any]) -> List[Dict[str, An
|
||||
"browse_path": f"{category}.{name}",
|
||||
"summary": _format_config_entry_count(value),
|
||||
"type": "section",
|
||||
"description": "Plugin configuration",
|
||||
"description": "Plugin configuration" if is_configured else "Plugin configuration (available to configure)",
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -337,8 +551,22 @@ def _resolve_config_branch(
|
||||
for item in _collect_plugin_root_items(config_data)
|
||||
}
|
||||
|
||||
parts = [part for part in text.split(".") if part]
|
||||
if len(parts) >= 2 and parts[0] in _PLUGIN_CATEGORY_KEYS:
|
||||
resolved = _resolve_plugin_branch(config_data, parts[0], parts[1])
|
||||
if resolved is None:
|
||||
return None
|
||||
_, current, _ = resolved
|
||||
if parts[0] in {"plugin", "provider"} and _is_multi_instance_plugin_name(parts[1]):
|
||||
current = _normalize_multi_instance_branch(parts[1], current)
|
||||
for part in parts[2:]:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
return current if isinstance(current, dict) else None
|
||||
|
||||
current: Any = config_data
|
||||
for part in text.split("."):
|
||||
for part in parts:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(part)
|
||||
@@ -386,6 +614,66 @@ def _build_value_item(
|
||||
}
|
||||
|
||||
|
||||
def _create_or_get_plugin_instance(
|
||||
config_data: Dict[str, Any],
|
||||
instance_target: str,
|
||||
instance_name: str,
|
||||
) -> tuple[str, bool]:
|
||||
parts = _split_config_path(instance_target)
|
||||
if len(parts) != 2 or parts[0] not in {"plugin", "provider"}:
|
||||
raise ValueError(f"Unsupported instance target '{instance_target}'")
|
||||
|
||||
category, plugin_name = parts
|
||||
raw_instance_name = str(instance_name or "").strip()
|
||||
if not raw_instance_name:
|
||||
raise ValueError("Instance name is required")
|
||||
if raw_instance_name.startswith("_"):
|
||||
raise ValueError("Instance names cannot start with '_' characters")
|
||||
|
||||
category_block = config_data.get(category)
|
||||
if not isinstance(category_block, dict):
|
||||
category_block = {}
|
||||
config_data[category] = category_block
|
||||
|
||||
plugin_block = category_block.get(plugin_name)
|
||||
if not isinstance(plugin_block, dict):
|
||||
plugin_block = {}
|
||||
category_block[plugin_name] = plugin_block
|
||||
|
||||
if _looks_like_single_instance_branch(plugin_name, plugin_block):
|
||||
existing_default = dict(plugin_block)
|
||||
plugin_block.clear()
|
||||
plugin_block["default"] = existing_default
|
||||
|
||||
target_key = None
|
||||
lowered_target = raw_instance_name.lower()
|
||||
for existing_key in plugin_block.keys():
|
||||
if str(existing_key or "").strip().lower() == lowered_target:
|
||||
target_key = str(existing_key)
|
||||
break
|
||||
|
||||
if target_key is not None and isinstance(plugin_block.get(target_key), dict):
|
||||
return f"{category}.{plugin_name}.{target_key}", False
|
||||
|
||||
plugin_block[raw_instance_name] = dict(build_default_plugin_config(plugin_name))
|
||||
return f"{category}.{plugin_name}.{raw_instance_name}", True
|
||||
|
||||
|
||||
def _resolve_update_key(config_data: Dict[str, Any], selection_key: str) -> str:
|
||||
parts = _split_config_path(selection_key)
|
||||
if (
|
||||
len(parts) >= 4
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and parts[2].lower() == "default"
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
):
|
||||
category_block = config_data.get(parts[0])
|
||||
plugin_block = category_block.get(parts[1]) if isinstance(category_block, dict) else None
|
||||
if _looks_like_single_instance_branch(parts[1], plugin_block):
|
||||
return ".".join([parts[0], parts[1], *parts[3:]])
|
||||
return selection_key
|
||||
|
||||
|
||||
def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
items: List[Dict[str, Any]] = []
|
||||
visible_entries = _visible_config_entries(config_data)
|
||||
@@ -444,7 +732,13 @@ def _build_nested_config_items(
|
||||
|
||||
section_items: List[Dict[str, Any]] = []
|
||||
value_items: List[Dict[str, Any]] = []
|
||||
action_items: List[Dict[str, Any]] = []
|
||||
is_preferences_view = browse_path == _PREFERENCES_BROWSE_PATH
|
||||
parts = _split_config_path(browse_path)
|
||||
is_multi_instance_root = _is_multi_instance_plugin_root_path(browse_path)
|
||||
|
||||
if is_multi_instance_root:
|
||||
branch = _normalize_multi_instance_branch(parts[1], branch)
|
||||
|
||||
for key, value in _visible_config_entries(branch):
|
||||
full_key = key if is_preferences_view else f"{browse_path}.{key}"
|
||||
@@ -467,7 +761,9 @@ def _build_nested_config_items(
|
||||
|
||||
section_items.sort(key=lambda item: str(item.get("title") or "").lower())
|
||||
value_items.sort(key=lambda item: str(item.get("name") or "").lower())
|
||||
return section_items + value_items
|
||||
if is_multi_instance_root:
|
||||
action_items.append(_build_create_instance_item(parts[0], parts[1]))
|
||||
return section_items + value_items + action_items
|
||||
|
||||
|
||||
def _build_config_items(
|
||||
@@ -493,12 +789,31 @@ def _build_config_header_lines(browse_path: Optional[str]) -> List[str]:
|
||||
return [
|
||||
"Use @N on a section to drill in. Use @.. to go back.",
|
||||
]
|
||||
if _is_multi_instance_plugin_root_path(text):
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N on an instance to drill in. Use @N | .config <name> on Add Instance to create a new instance, then update its fields in the table that opens. Use @.. to go back.",
|
||||
]
|
||||
parts = _split_config_path(text)
|
||||
if (
|
||||
len(parts) == 3
|
||||
and parts[0] in {"plugin", "provider"}
|
||||
and _is_multi_instance_plugin_name(parts[1])
|
||||
):
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N | .config <value> to update a setting. After creating an instance, set its path, credentials, or other fields here. Use @.. to go back.",
|
||||
]
|
||||
return [
|
||||
f"Path: {_format_config_path_label(text)}",
|
||||
"Use @N on a section to drill in. Use @N | .config <value> to update a setting. Use @.. to go back.",
|
||||
]
|
||||
|
||||
|
||||
def _extract_create_instance_target(args: Sequence[str]) -> Optional[str]:
|
||||
return _extract_arg_value(args, flags={_CREATE_INSTANCE_FLAG, "--create-instance"}, allow_positional=False)
|
||||
|
||||
|
||||
def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
|
||||
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
|
||||
|
||||
@@ -529,18 +844,49 @@ def _get_selected_config_item() -> Optional[Dict[str, Any]]:
|
||||
idx = indices[0]
|
||||
if idx < 0 or idx >= len(items):
|
||||
return None
|
||||
item = items[idx]
|
||||
if isinstance(item, dict):
|
||||
return item
|
||||
return _normalize_config_item(items[idx])
|
||||
|
||||
|
||||
def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]:
|
||||
if candidate is None:
|
||||
return None
|
||||
|
||||
normalized: Dict[str, Any] = {}
|
||||
for key in ("kind", "key", "title", "browse_path", "name", "value", "value_display", "type"):
|
||||
sources: List[Any] = [candidate]
|
||||
|
||||
if isinstance(candidate, dict):
|
||||
extra = candidate.get("extra")
|
||||
if isinstance(extra, dict):
|
||||
sources.append(extra)
|
||||
else:
|
||||
try:
|
||||
value = getattr(item, key, None)
|
||||
extra = getattr(candidate, "extra", None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
extra = None
|
||||
if isinstance(extra, dict):
|
||||
sources.append(extra)
|
||||
|
||||
for source in sources:
|
||||
if isinstance(source, dict):
|
||||
getter = source.get
|
||||
for key in _CONFIG_ITEM_FIELDS:
|
||||
if key in normalized:
|
||||
continue
|
||||
value = getter(key)
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
continue
|
||||
|
||||
for key in _CONFIG_ITEM_FIELDS:
|
||||
if key in normalized:
|
||||
continue
|
||||
try:
|
||||
value = getattr(source, key, None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None:
|
||||
normalized[key] = value
|
||||
|
||||
return normalized or None
|
||||
|
||||
|
||||
@@ -573,6 +919,11 @@ def _show_config_table(
|
||||
idx,
|
||||
[".config", "-browse", str(item.get("browse_path"))],
|
||||
)
|
||||
elif item.get("kind") == "create_instance" and item.get("instance_target"):
|
||||
table.set_row_selection_action(
|
||||
idx,
|
||||
[".config", _CREATE_INSTANCE_FLAG, str(item.get("instance_target"))],
|
||||
)
|
||||
|
||||
ctx.set_last_result_table(table, items)
|
||||
ctx.set_current_stage_table(table)
|
||||
@@ -602,6 +953,15 @@ def _resolve_direct_browse_path(
|
||||
return _PREFERENCES_BROWSE_PATH
|
||||
if lowered in {"plugins", "plugin", "providers", "provider", "tools", "tool"}:
|
||||
return _PLUGINS_BROWSE_PATH
|
||||
|
||||
plugin_branch = _resolve_plugin_branch(config_data, "plugin", lowered)
|
||||
if plugin_branch is not None:
|
||||
return f"plugin.{plugin_branch[0]}"
|
||||
|
||||
tool_branch = _resolve_plugin_branch(config_data, "tool", lowered)
|
||||
if tool_branch is not None:
|
||||
return f"tool.{tool_branch[0]}"
|
||||
|
||||
branch = _resolve_config_branch(config_data, text)
|
||||
if isinstance(branch, dict):
|
||||
return text
|
||||
@@ -629,27 +989,63 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
||||
if browse_path:
|
||||
return _show_config_table(current_config, browse_path=browse_path)
|
||||
|
||||
selection_item = _get_selected_config_item()
|
||||
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
|
||||
|
||||
create_instance_target = _extract_create_instance_target(args)
|
||||
if create_instance_target:
|
||||
print(
|
||||
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(create_instance_target)}', then set its fields in the table that opens."
|
||||
)
|
||||
return 0
|
||||
|
||||
value_from_pipe = _extract_piped_value(piped_result)
|
||||
selection_kind = str((selection_item or {}).get("kind") or "").strip().lower()
|
||||
selection_key = str((selection_item or {}).get("key") or "").strip() or None
|
||||
selection_browse_path = str((selection_item or {}).get("browse_path") or "").strip() or None
|
||||
selection_display_path = str((selection_item or {}).get("display_path") or selection_key or "").strip() or selection_key
|
||||
selection_instance_target = str((selection_item or {}).get("instance_target") or "").strip() or None
|
||||
|
||||
if selection_kind == "section" and selection_browse_path and not args and value_from_pipe is None:
|
||||
return _show_config_table(current_config, browse_path=selection_browse_path)
|
||||
|
||||
if selection_kind == "create_instance" and selection_instance_target:
|
||||
new_instance_name = value_from_pipe or _extract_selected_update_value(args)
|
||||
if new_instance_name is None:
|
||||
print(
|
||||
f"Use @N | .config <instance_name> to create a new instance under '{_format_config_path_label(selection_instance_target)}', then set its fields in the table that opens."
|
||||
)
|
||||
return 0
|
||||
new_instance_name = _strip_value_quotes(new_instance_name)
|
||||
try:
|
||||
new_browse_path, created = _create_or_get_plugin_instance(
|
||||
current_config,
|
||||
selection_instance_target,
|
||||
new_instance_name,
|
||||
)
|
||||
_save_updated_config(current_config, new_browse_path)
|
||||
status_text = "Created" if created else "Using existing"
|
||||
print(
|
||||
f"{status_text} instance '{new_instance_name}' at '{_format_config_path_label(new_browse_path)}'. "
|
||||
"Configure its fields in the table below."
|
||||
)
|
||||
return _show_config_table(current_config, browse_path=new_browse_path)
|
||||
except Exception as exc:
|
||||
log(f"Error creating config instance '{selection_instance_target}': {exc}")
|
||||
print(f"Error creating config instance: {exc}")
|
||||
return 1
|
||||
|
||||
if selection_kind == "value" and selection_key:
|
||||
new_value = value_from_pipe or _extract_selected_update_value(args)
|
||||
if new_value is not None:
|
||||
new_value = _strip_value_quotes(new_value)
|
||||
target_key = _resolve_update_key(current_config, selection_key)
|
||||
try:
|
||||
set_nested_config(current_config, selection_key, new_value)
|
||||
_save_updated_config(current_config, selection_key)
|
||||
set_nested_config(current_config, target_key, new_value)
|
||||
_save_updated_config(current_config, target_key)
|
||||
print(f"Updated '{selection_display_path}' to '{new_value}'")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log(f"Error updating config '{selection_key}': {exc}")
|
||||
log(f"Error updating config '{target_key}': {exc}")
|
||||
print(f"Error updating config: {exc}")
|
||||
return 1
|
||||
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
|
||||
## Unreleased (2026-01-05)
|
||||
|
||||
- **docs:** Add `docs/provider_authoring.md` as a plugin authoring Quick Start for the strict `ResultTable` API (ResultModel/ColumnSpec/selection_fn). The file keeps its legacy name for compatibility.
|
||||
- **docs:** Add `docs/plugin_authoring.md` as a plugin authoring Quick Start for the strict `ResultTable` API (ResultModel/ColumnSpec/selection_fn).
|
||||
- **docs:** Add a link from `docs/result_table.md` to the plugin authoring guide.
|
||||
- **tests:** Add `tests/test_provider_author_examples.py` validating example plugin registration and adapter behavior. The test file keeps its legacy name for compatibility.
|
||||
- **notes:** Existing example plugins (`plugins/example_provider.py`, `plugins/vimm/__init__.py`) are referenced as canonical patterns.
|
||||
- **tests:** Add plugin-author example coverage validating example plugin registration and adapter behavior.
|
||||
- **notes:** Existing example plugins (`plugins/example_plugin.py`, `plugins/vimm/__init__.py`) are referenced as canonical patterns.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
PR Title: docs: Add plugin authoring doc, examples, and tests
|
||||
|
||||
Summary:
|
||||
- Add `docs/provider_authoring.md` describing the strict `ResultModel`-based plugin adapter pattern, `ColumnSpec` usage, `selection_fn`, and `TableProviderMixin` for HTML table scraping.
|
||||
- Add `docs/plugin_authoring.md` describing the strict `ResultModel`-based plugin adapter pattern, `ColumnSpec` usage, `selection_fn`, and `TablePluginMixin` for HTML table scraping.
|
||||
- Link the new doc from `docs/result_table.md`.
|
||||
- Add `tests/test_provider_author_examples.py` to validate the example plugin integration with the registry.
|
||||
- Add plugin-author example coverage to validate the example plugin integration with the registry.
|
||||
|
||||
Why:
|
||||
- Provide a short, focused Quick Start to help contributors author plugins that integrate with the strict ResultTable API.
|
||||
@@ -5,16 +5,15 @@ 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.
|
||||
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 `TableProviderMixin` from `SYS.provider_helpers`.
|
||||
- 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.
|
||||
@@ -74,16 +73,16 @@ register_plugin("myplugin", adapter, columns=columns_factory, selection_fn=selec
|
||||
|
||||
---
|
||||
|
||||
## Table scraping with `TableProviderMixin`
|
||||
## Table scraping with `TablePluginMixin`
|
||||
|
||||
If your plugin scrapes HTML tables or list-like results, use `TableProviderMixin`:
|
||||
If your plugin scrapes HTML tables or list-like results, use `TablePluginMixin`:
|
||||
|
||||
```py
|
||||
from ProviderCore.base import Provider
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from PluginCore.base import Provider
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
|
||||
|
||||
class MyTablePlugin(TableProviderMixin, Provider):
|
||||
class MyTablePlugin(TablePluginMixin, Provider):
|
||||
URL = ("https://example.org/search",)
|
||||
|
||||
def validate(self) -> bool:
|
||||
@@ -94,8 +93,8 @@ class MyTablePlugin(TableProviderMixin, Provider):
|
||||
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
|
||||
`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`.
|
||||
|
||||
@@ -128,22 +127,22 @@ Example test skeleton:
|
||||
|
||||
```py
|
||||
from SYS.result_table_adapters import get_plugin
|
||||
from plugins import example_provider
|
||||
from plugins import example_plugin
|
||||
|
||||
|
||||
def test_example_plugin_registration():
|
||||
plugin = get_plugin("example")
|
||||
rows = list(plugin.adapter(example_provider.SAMPLE_ITEMS))
|
||||
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_provider.SAMPLE_ITEMS)
|
||||
table = plugin.build_table(example_plugin.SAMPLE_ITEMS)
|
||||
assert table.provider == "example" and table.rows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References and examples
|
||||
- Read `plugins/example_provider.py` for a compact example of a strict adapter and dynamic columns.
|
||||
- Read `plugins/vimm/__init__.py` for a table-oriented plugin that uses `TableProviderMixin` and converts `SearchResult` to `ResultModel` for registration.
|
||||
- See `docs/provider_guide.md` for a broader plugin development checklist.
|
||||
- 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.
|
||||
@@ -4,9 +4,8 @@
|
||||
This guide describes how to write, test, and register a plugin so the
|
||||
application can discover and use it as a pluggable component.
|
||||
|
||||
Note: this file keeps its historical `provider_guide` name, but the public
|
||||
model is plugin-first. Some runtime classes still use `Provider` naming
|
||||
internally.
|
||||
The public model is plugin-first, even though the internal base class still
|
||||
uses `Provider` naming.
|
||||
|
||||
Keep plugin code small, focused, and well-tested. Bundled plugins and drop-in
|
||||
plugins share the same `plugins/` layout.
|
||||
@@ -15,7 +14,7 @@ plugins share the same `plugins/` layout.
|
||||
|
||||
## Anatomy of a plugin
|
||||
A plugin is a Python class that currently extends the internal base class
|
||||
`ProviderCore.base.Provider` and implements a few key methods and attributes.
|
||||
`PluginCore.base.Provider` and implements a few key methods and attributes.
|
||||
|
||||
Minimum expectations:
|
||||
- `class MyPlugin(Provider):` subclasses the current internal base plugin class.
|
||||
@@ -31,7 +30,7 @@ Optional but common:
|
||||
---
|
||||
|
||||
## SearchResult
|
||||
Use `ProviderCore.base.SearchResult` to describe results returned by `search()`.
|
||||
Use `PluginCore.base.SearchResult` to describe results returned by `search()`.
|
||||
|
||||
Important fields:
|
||||
- `table` (str): plugin table name
|
||||
@@ -55,7 +54,7 @@ Return a list of `SearchResult(...)` objects or simple dicts convertible with `.
|
||||
Example:
|
||||
|
||||
```python
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
|
||||
|
||||
class HelloPlugin(Provider):
|
||||
@@ -132,7 +131,7 @@ pytest -q
|
||||
- Bundled plugins live under `plugins/` and are auto-discovered from that package.
|
||||
- External plugins can be dropped into `plugins/` or any directory listed in `MM_PLUGIN_PATH` or `MEDEIA_PLUGIN_PATH`.
|
||||
- Package directories are preferred so plugin-specific files travel with the plugin.
|
||||
- Plugin authors should import from `ProviderCore.*`.
|
||||
- Plugin authors should import from `PluginCore.*`.
|
||||
|
||||
If a plugin supports multiple configured endpoints or accounts, the user-facing
|
||||
concept is a plugin instance. Some stored config still lives under legacy key
|
||||
@@ -181,4 +181,4 @@ another, such as artist to discography drill-in.
|
||||
- ResultTable implementation: `SYS/result_table.py`
|
||||
- Pipeline helpers: `SYS/pipeline.py`
|
||||
- Row replay and selection flow: `SYS/pipeline.py`
|
||||
- Plugin authoring guide: `docs/provider_authoring.md`
|
||||
- Plugin authoring guide: `docs/plugin_authoring.md`
|
||||
|
||||
+2
-2
@@ -29,13 +29,13 @@ drop-in plugin search paths are:
|
||||
|
||||
Plugin rules:
|
||||
- A plugin can be a single `.py` file or a package directory with `__init__.py`.
|
||||
- Current plugin classes inherit from `ProviderCore.base.Provider`.
|
||||
- Current plugin classes inherit from `PluginCore.base.Provider`.
|
||||
- Give the plugin a stable name using `PLUGIN_NAME` or the class name.
|
||||
|
||||
Example skeleton:
|
||||
|
||||
```python
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
|
||||
|
||||
class MyPlugin(Provider):
|
||||
|
||||
@@ -13,8 +13,8 @@ from urllib.parse import urlparse
|
||||
|
||||
from API.HTTP import HTTPClient, _download_direct_file
|
||||
from plugins.alldebrid.api import AllDebridClient, parse_magnet_or_hash, is_torrent_file
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
from SYS.item_accessors import get_field as _extract_value
|
||||
from SYS.utils import sanitize_filename
|
||||
from SYS.logger import log, debug, debug_panel
|
||||
@@ -637,7 +637,7 @@ def adjust_output_dir_for_alldebrid(
|
||||
return output_dir
|
||||
|
||||
|
||||
class AllDebrid(TableProviderMixin, Provider):
|
||||
class AllDebrid(TablePluginMixin, Provider):
|
||||
"""AllDebrid account provider with magnet folder/file browsing and downloads.
|
||||
|
||||
This provider uses the new table system (strict ResultTable adapter pattern) for
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from urllib.parse import urlparse
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import log, debug, debug_panel
|
||||
|
||||
from tool.playwright import PlaywrightTool
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Example plugin that uses the new `ResultTable` API.
|
||||
|
||||
This module demonstrates a minimal provider adapter that yields `ResultModel`
|
||||
This module demonstrates a minimal plugin adapter that yields `ResultModel`
|
||||
instances, a set of `ColumnSpec` definitions, and a tiny CLI-friendly renderer
|
||||
(`render_table`) for demonstration.
|
||||
|
||||
Run this to see sample output:
|
||||
python -m Provider.example_provider
|
||||
python -m plugins.example_plugin
|
||||
|
||||
Example usage (piped selector):
|
||||
plugin-table -plugin example -sample | select -select 1 | add-file -instance default
|
||||
@@ -44,7 +44,7 @@ SAMPLE_ITEMS = [
|
||||
|
||||
|
||||
def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
||||
"""Convert provider-specific items into `ResultModel` instances.
|
||||
"""Convert plugin-specific items into `ResultModel` instances.
|
||||
|
||||
This adapter enforces the strict API requirement: it yields only
|
||||
`ResultModel` instances (no legacy dict objects).
|
||||
@@ -62,7 +62,7 @@ def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
||||
|
||||
|
||||
# Columns are intentionally *not* mandated. Create a factory that inspects
|
||||
# sample rows and builds only columns that make sense for the provider data.
|
||||
# sample rows and builds only columns that make sense for the plugin data.
|
||||
from SYS.result_table_api import metadata_column
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ register_plugin(
|
||||
adapter,
|
||||
columns=columns_factory,
|
||||
selection_fn=selection_fn,
|
||||
metadata={"description": "Example provider demonstrating dynamic columns and selectors"},
|
||||
metadata={"description": "Example plugin demonstrating dynamic columns and selectors"},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ProviderCore.base import Provider
|
||||
from PluginCore.base import Provider
|
||||
from SYS.logger import log
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
|
||||
|
||||
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
||||
|
||||
@@ -5,7 +5,7 @@ This minimal plugin demonstrates the typical hooks a plugin may implement:
|
||||
- `search()` to return `SearchResult` items
|
||||
- `download()` to persist a sample file (useful for local tests)
|
||||
|
||||
See `docs/provider_guide.md` for authoring guidance.
|
||||
See `docs/plugin_guide.md` for authoring guidance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
|
||||
|
||||
class HelloProvider(Provider):
|
||||
|
||||
@@ -16,7 +16,7 @@ from plugins.tidal.api import (
|
||||
extract_artists,
|
||||
stringify,
|
||||
)
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from SYS.field_access import get_field
|
||||
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
||||
from SYS import pipeline as pipeline_context
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
from API.HTTP import _download_direct_file
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.utils import sanitize_filename, unique_path
|
||||
from SYS.logger import log
|
||||
from SYS.config import get_provider_block
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urljoin, urlparse, unquote
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.utils import sanitize_filename
|
||||
from SYS.logger import log, debug
|
||||
from SYS.models import ProgressBar
|
||||
@@ -383,7 +383,7 @@ def _enrich_book_tags_from_isbn(isbn: str,
|
||||
|
||||
# 2) isbnsearch metadata plugin fallback.
|
||||
try:
|
||||
from plugins.metadata_provider import get_metadata_plugin
|
||||
from plugins.metadata_plugin import get_metadata_plugin
|
||||
|
||||
provider = get_metadata_plugin("isbnsearch",
|
||||
config or {})
|
||||
@@ -660,7 +660,7 @@ class Libgen(Provider):
|
||||
"libgen": ["download-file"],
|
||||
}
|
||||
# Domains that should be routed to this provider when the user supplies a URL.
|
||||
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
||||
# (Used by PluginCore.registry.match_provider_name_for_url)
|
||||
URL_DOMAINS = (
|
||||
"libgen.gl",
|
||||
"libgen.li",
|
||||
@@ -1056,7 +1056,7 @@ class Libgen(Provider):
|
||||
def download_url(self, url: str, output_dir: Path) -> Optional[Path]:
|
||||
"""Download a direct LibGen URL using the regular mirror logic."""
|
||||
try:
|
||||
from ProviderCore.base import SearchResult
|
||||
from PluginCore.base import SearchResult
|
||||
sr = SearchResult(
|
||||
table="libgen",
|
||||
title="libgen",
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from plugins.loc.api import LOCClient
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.cli_syntax import get_free_text, parse_query
|
||||
from SYS.logger import log
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ProviderCore.base import Provider
|
||||
from PluginCore.base import Provider
|
||||
from SYS.metadata import write_metadata, write_tags
|
||||
from SYS.utils import sanitize_filename, sha256_file, unique_path
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ from urllib.parse import quote
|
||||
from API.requests_client import get_requests_session
|
||||
from SYS.utils import ffprobe as probe_media_metadata
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
from SYS.logger import log
|
||||
|
||||
_MATRIX_INIT_CHECK_CACHE: Dict[str,
|
||||
@@ -276,7 +276,7 @@ def _matrix_health_check(*,
|
||||
return False, str(exc)
|
||||
|
||||
|
||||
class Matrix(TableProviderMixin, Provider):
|
||||
class Matrix(TablePluginMixin, Provider):
|
||||
"""Matrix (Element) room provider with file uploads and selection.
|
||||
|
||||
This provider uses the new table system (strict ResultTable adapter pattern) for
|
||||
|
||||
@@ -22,7 +22,7 @@ from SYS.command_parsing import (
|
||||
normalize_to_list as _normalize_to_list,
|
||||
)
|
||||
from SYS import pipeline as ctx
|
||||
from ProviderCore.registry import get_plugin, get_plugin_for_url
|
||||
from PluginCore.registry import get_plugin, get_plugin_for_url
|
||||
|
||||
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
|
||||
_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text"
|
||||
|
||||
@@ -10,7 +10,7 @@ import subprocess
|
||||
|
||||
from API.HTTP import HTTPClient
|
||||
from API.requests_client import get_requests_session
|
||||
from ProviderCore.base import SearchResult
|
||||
from PluginCore.base import SearchResult
|
||||
try:
|
||||
from plugins.tidal import Tidal
|
||||
except ImportError: # pragma: no cover - optional
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from pathlib import Path
|
||||
from SYS.cmdlet_spec import Cmdlet, CmdletArg, parse_cmdlet_args
|
||||
from ProviderCore.registry import get_plugin, get_plugin_for_url, list_plugin_names_with_capability
|
||||
from PluginCore.registry import get_plugin, get_plugin_for_url, list_plugin_names_with_capability
|
||||
from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, set_thread_stream
|
||||
from SYS.result_table import Table
|
||||
from plugins.mpv.mpv_ipc import MPV
|
||||
|
||||
@@ -1170,7 +1170,7 @@ def _infer_hydrus_store_from_url_target(*, target: str, config: dict) -> Optiona
|
||||
is_hydrus_backend = backend_type == "hydrusnetwork" or backend_class == "hydrusnetwork"
|
||||
if not is_hydrus_backend:
|
||||
try:
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
hydrus_provider = get_plugin("hydrusnetwork", config)
|
||||
checker = getattr(hydrus_provider, "is_backend", None) if hydrus_provider is not None else None
|
||||
|
||||
@@ -70,7 +70,7 @@ from SYS.config import load_config, reload_config # noqa: E402
|
||||
from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
|
||||
from SYS.repl_queue import enqueue_repl_command # noqa: E402
|
||||
from SYS.utils import format_bytes # noqa: E402
|
||||
from ProviderCore.registry import get_plugin, get_plugin_class # noqa: E402
|
||||
from PluginCore.registry import get_plugin, get_plugin_class # noqa: E402
|
||||
from tool.ytdlp import get_display_format_id, get_selection_format_id # noqa: E402
|
||||
|
||||
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
||||
|
||||
@@ -18,11 +18,11 @@ import requests
|
||||
|
||||
from API.HTTP import HTTPClient
|
||||
from API.requests_client import get_requests_session
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.utils import sanitize_filename
|
||||
from SYS.cli_syntax import get_field, get_free_text, parse_query
|
||||
from SYS.logger import log
|
||||
from plugins.metadata_provider import (
|
||||
from plugins.metadata_plugin import (
|
||||
archive_item_metadata_to_tags,
|
||||
fetch_archive_item_metadata,
|
||||
)
|
||||
@@ -654,7 +654,7 @@ class OpenLibrary(Provider):
|
||||
]
|
||||
|
||||
# Domains that should be routed to this provider when the user supplies a URL.
|
||||
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
||||
# (Used by PluginCore.registry.match_provider_name_for_url)
|
||||
URL_DOMAINS = (
|
||||
"openlibrary.org",
|
||||
"archive.org",
|
||||
|
||||
@@ -5,7 +5,7 @@ import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import log
|
||||
from SYS.utils import format_bytes
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from urllib.parse import quote, unquote, urlparse
|
||||
import paramiko
|
||||
from scp import SCPClient
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
|
||||
|
||||
def _coerce_bool(value: Any, default: bool = False) -> bool:
|
||||
|
||||
@@ -11,7 +11,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import log, debug, debug_panel
|
||||
from SYS.models import ProgressBar
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import debug
|
||||
|
||||
_TELEGRAM_DEFAULT_TIMESTAMP_STEM_RE = re.compile(
|
||||
|
||||
@@ -9,7 +9,7 @@ from SYS.command_parsing import has_flag as _has_flag, normalize_to_list as _nor
|
||||
from SYS.logger import log
|
||||
from SYS.result_table import Table
|
||||
from SYS import pipeline as ctx
|
||||
from ProviderCore.registry import get_plugin
|
||||
from PluginCore.registry import get_plugin
|
||||
|
||||
_TELEGRAM_PENDING_ITEMS_KEY = "telegram_pending_items"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from plugins.tidal.api import (
|
||||
extract_artists,
|
||||
stringify,
|
||||
)
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.field_access import get_field
|
||||
from plugins.tidal_manifest import resolve_tidal_manifest_path
|
||||
from SYS import pipeline as pipeline_context
|
||||
@@ -216,7 +216,7 @@ class Tidal(Provider):
|
||||
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
||||
"""Parse inline `key:value` query arguments.
|
||||
|
||||
Unlike the generic parser in ProviderCore, this supports multi-word
|
||||
Unlike the generic parser in PluginCore, this supports multi-word
|
||||
values (e.g. `artist:elliott smith`).
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
||||
import requests
|
||||
|
||||
from API.requests_client import get_requests_session
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from PluginCore.base import Provider, SearchResult
|
||||
from SYS.logger import debug, log
|
||||
try: # Preferred HTML parser
|
||||
from lxml import html as lxml_html
|
||||
|
||||
@@ -16,14 +16,14 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
from API.HTTP import HTTPClient
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from ProviderCore.inline_utils import resolve_filter
|
||||
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from PluginCore.inline_utils import resolve_filter
|
||||
from SYS.logger import debug, debug_panel
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
from tool.playwright import PlaywrightTool
|
||||
|
||||
|
||||
class Vimm(TableProviderMixin, Provider):
|
||||
class Vimm(TablePluginMixin, Provider):
|
||||
"""Minimal provider for vimm.net vault listings using TableProvider mixin.
|
||||
|
||||
NOTES / HOW-TO (selection & auto-download):
|
||||
@@ -142,7 +142,7 @@ class Vimm(TableProviderMixin, Provider):
|
||||
],
|
||||
"region": REGION_CHOICES,
|
||||
}
|
||||
# ProviderCore still looks for INLINE_QUERY_FIELD_CHOICES, so expose this
|
||||
# PluginCore still looks for INLINE_QUERY_FIELD_CHOICES, so expose this
|
||||
# mapping once and keep QUERY_ARG_CHOICES as the readable name we prefer.
|
||||
INLINE_QUERY_FIELD_CHOICES = QUERY_ARG_CHOICES
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from PluginCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from SYS.plugin_helpers import TablePluginMixin
|
||||
from SYS.logger import debug, log
|
||||
from SYS.models import DownloadError, DownloadMediaResult, DownloadOptions
|
||||
from SYS.payload_builders import build_file_result_payload, build_table_result_payload
|
||||
@@ -501,7 +501,7 @@ def _build_pipe_objects(
|
||||
return pipe_objects
|
||||
|
||||
|
||||
class ytdlp(TableProviderMixin, Provider):
|
||||
class ytdlp(TablePluginMixin, Provider):
|
||||
"""yt-dlp-backed search and direct download plugin."""
|
||||
|
||||
PLUGIN_NAME = "ytdlp"
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from ProviderCore.base import Provider
|
||||
from PluginCore.base import Provider
|
||||
from SYS.logger import log
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Plugins are the main integration surface for the app.
|
||||
- Plugins can expose named instances, so one plugin can target multiple endpoints, accounts, or servers via `-instance <name>`.
|
||||
- Bundled and external plugins use the same `plugins/<name>/` layout.
|
||||
- External plugin search paths include the repo `plugins/` folder, the current working directory `plugins/` folder, `MM_PLUGIN_PATH`, and `MEDEIA_PLUGIN_PATH`.
|
||||
- Plugin authoring still uses the current Python base class name `ProviderCore.base.Provider`. That is an implementation detail rather than the preferred user-facing term.
|
||||
- Plugin authoring still uses the current Python base class name `PluginCore.base.Provider`. That is an implementation detail rather than the preferred user-facing term.
|
||||
|
||||
See [plugins/README.md](plugins/README.md) for plugin packaging and discovery details.
|
||||
|
||||
@@ -140,8 +140,8 @@ The exact meaning of `@1` depends on the current table and plugin. For example,
|
||||
|
||||
- [docs/tag_template_syntax.md](docs/tag_template_syntax.md)
|
||||
- [plugins/README.md](plugins/README.md)
|
||||
- [docs/provider_guide.md](docs/provider_guide.md)
|
||||
- [docs/provider_authoring.md](docs/provider_authoring.md)
|
||||
- [docs/plugin_guide.md](docs/plugin_guide.md)
|
||||
- [docs/plugin_authoring.md](docs/plugin_authoring.md)
|
||||
- [docs/ftp_plugin_tutorial.md](docs/ftp_plugin_tutorial.md)
|
||||
- [docs/scp_plugin_tutorial.md](docs/scp_plugin_tutorial.md)
|
||||
- [docs/BOOTSTRAP_TROUBLESHOOTING.md](docs/BOOTSTRAP_TROUBLESHOOTING.md)
|
||||
|
||||
@@ -121,8 +121,7 @@ packages = [
|
||||
"cmdnat",
|
||||
"API",
|
||||
"Store",
|
||||
"ProviderCore",
|
||||
"Provider",
|
||||
"PluginCore",
|
||||
"SYS",
|
||||
"tool",
|
||||
"TUI",
|
||||
|
||||
Reference in New Issue
Block a user