nielsr's picture
nielsr HF Staff
Prototype PwC MCP Space from pwc-cli@6a547659
ee0b555 verified
Raw History Blame Contribute Delete
6 kB
"""In-process access to the CLI's read-only research queries.
Embedders such as the Papers With Code MCP server run the exact CLI handlers
through the CLI's own parser. Every flag, default, validation rule, and JSON
payload therefore stays identical to ``pwc ... --json`` without a subprocess,
stdout capture, or a second implementation of the query logic.
Each call owns its parser and result sink, so concurrent callers never share
state. Only read-only research commands are reachable; authentication, paper
editing, skill installation, and version display are refused.
"""
from __future__ import annotations
import argparse
from collections.abc import Mapping
from datetime import date
from typing import Any
from pwc_cli.cli import Parser, UsageError, build_parser
from pwc_cli.transport import ResponseError
Command = tuple[str, ...]
READ_ONLY_COMMANDS: tuple[Command, ...] = (
("search",),
("paper", "info"),
("paper", "evaluations"),
("paper", "read"),
("paper", "list"),
("paper", "recent"),
("paper", "trending"),
("paper", "related"),
("paper", "lineage", "list"),
("task",),
("task", "list"),
("method",),
("method", "list"),
("conference",),
("conference", "list"),
("organization",),
("organization", "list"),
("framework",),
("framework", "list"),
("benchmark",),
("benchmark", "list"),
)
# Flags that only shape terminal rendering. JSON output ignores them, so they
# have no in-process equivalent.
PRESENTATION_FLAGS = frozenset({"json", "implementation_coverage", "flat"})
_HIDDEN_ACTIONS = (
argparse._SubParsersAction,
argparse._HelpAction,
argparse._VersionAction,
)
class _StrictParser(Parser):
"""Raise ``UsageError`` for invalid arguments instead of exiting."""
def error(self, message: str) -> None: # type: ignore[override]
raise UsageError(message)
def _command_label(command: Command) -> str:
return " ".join(("pwc", *command))
def command_parser(
command: Command, parser: argparse.ArgumentParser | None = None
) -> argparse.ArgumentParser:
"""Return the subparser that handles ``command``."""
current = parser or build_parser(parser_class=_StrictParser)
for name in command:
subparsers = next(
(
action
for action in current._actions
if isinstance(action, argparse._SubParsersAction)
),
None,
)
if subparsers is None or name not in subparsers.choices:
raise KeyError(f"unknown command: {_command_label(command)}")
current = subparsers.choices[name]
return current
def query_options(command: Command) -> dict[str, argparse.Action]:
"""Research options of one command keyed by destination.
Presentation-only flags, help, version, and nested command selectors are
excluded, leaving exactly the options an embedder must expose.
"""
parser = command_parser(command)
return {
action.dest: action
for action in parser._actions
if not isinstance(action, _HIDDEN_ACTIONS)
and action.dest not in PRESENTATION_FLAGS
}
def _text(value: Any) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, date):
return value.isoformat()
return str(value)
def build_argv(command: Command, options: Mapping[str, Any]) -> list[str]:
"""Translate structured options into the argv the CLI parser expects.
``None`` leaves a flag at its CLI default. Booleans toggle ``store_true``
flags. Lists repeat ``append`` flags or join comma-separated values, and
mappings expand to repeated ``NAME=VALUE`` entries.
"""
actions = query_options(command)
unknown = sorted(set(options) - set(actions))
if unknown:
raise UsageError(
f"unknown option(s) for {_command_label(command)}: {', '.join(unknown)}"
)
argv = list(command)
positionals: list[str] = []
for dest, action in actions.items():
value = options.get(dest)
if value is None:
continue
if not action.option_strings:
positionals.append(_text(value))
continue
flag = action.option_strings[0]
if isinstance(action, argparse._StoreTrueAction):
if value is True:
argv.append(flag)
elif value is not False:
raise UsageError(f"{flag} expects a boolean")
continue
if isinstance(value, Mapping):
value = [f"{name}={_text(item)}" for name, item in value.items()]
values = list(value) if isinstance(value, (list, tuple, set)) else [value]
if isinstance(action, argparse._AppendAction):
for item in values:
argv.extend((flag, _text(item)))
elif values:
argv.extend((flag, ",".join(_text(item) for item in values)))
if positionals:
argv.append("--")
argv.extend(positionals)
return argv
def query(command: Command, options: Mapping[str, Any], client: Any) -> Any:
"""Run one read-only CLI command in-process and return its JSON ``data``.
``client`` must provide ``get(path, params) -> Response`` like
:class:`pwc_cli.transport.Client`. Invalid options raise ``UsageError``;
catalog failures raise the CLI's ``TransportError`` or ``ResponseError``.
"""
command = tuple(command)
if command not in READ_ONLY_COMMANDS:
raise UsageError(
f"{_command_label(command)} is not a read-only research command"
)
argv = build_argv(command, options)
args = build_parser(parser_class=_StrictParser).parse_args(argv)
sink: list[Any] = []
args.json = True
args.result_sink = sink
args.handler(args, client)
if len(sink) != 1:
raise ResponseError(
f"{_command_label(command)} did not produce one JSON result"
)
return sink[0]