mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
pip-installable Python API driving the bundled binary over MCP HTTP: Browser/Session with one generated method per browser tool (lightpanda/_methods.py, regenerated from tools/list), run_script() replay, a CLI trampoline entry point, pytest suite against local fixtures, and pdoc docs generation.
99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""Generate lightpanda/_methods.py from the binary's tools/list schemas.
|
|
|
|
Emits one concrete, annotated, documented method per browser tool (snake_case,
|
|
with the original tool name aliased), all forwarding to ``Session.call``.
|
|
Being real code, the methods are visible to IDEs, type checkers, and pdoc
|
|
alike. Run from bindings/python with a binary available:
|
|
|
|
uv run --no-project python scripts/generate_methods.py
|
|
|
|
CI regenerates and diffs the file to catch tool-schema drift.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import keyword
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from lightpanda import Browser # noqa: E402
|
|
from lightpanda.browser import _SESSION_TOOLS, _snake # noqa: E402
|
|
|
|
PY_TYPES = {"string": "str", "integer": "int", "number": "float", "boolean": "bool", "object": "dict", "array": "list"}
|
|
|
|
HEADER = '''\
|
|
"""Generated by scripts/generate_methods.py — do not edit.
|
|
|
|
One method per lightpanda browser tool, signatures derived from the tool
|
|
JSON schemas. camelCase aliases mirror the underlying tool names.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
class SessionMethods:
|
|
def call(self, tool: str, **kwargs: Any) -> Any:
|
|
raise NotImplementedError
|
|
'''
|
|
|
|
|
|
def docstring(text: str) -> str:
|
|
body = text.strip().replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
|
|
return f' """{body}"""'
|
|
|
|
|
|
def method_source(name: str, spec: dict) -> str:
|
|
schema = spec["schema"]
|
|
properties = schema.get("properties", {})
|
|
required = set(schema.get("required", []))
|
|
for prop in properties:
|
|
if keyword.iskeyword(prop) or not prop.isidentifier():
|
|
raise SystemExit(f"tool {name}: property {prop!r} is not a valid Python parameter")
|
|
|
|
params = ["self"]
|
|
if properties:
|
|
params.append("*")
|
|
forwards = []
|
|
for prop in sorted(properties, key=lambda p: p not in required):
|
|
py_type = PY_TYPES.get(properties[prop].get("type", ""), "Any")
|
|
if name == "extract" and prop == "schema":
|
|
py_type = "str | dict | list"
|
|
if prop in required:
|
|
params.append(f"{prop}: {py_type}")
|
|
else:
|
|
params.append(f"{prop}: {py_type} | None = None")
|
|
forwards.append(f"{prop}={prop}")
|
|
|
|
snake = _snake(name)
|
|
call_args = ", ".join([f'"{name}"'] + forwards)
|
|
lines = [f" def {snake}({', '.join(params)}) -> Any:"]
|
|
lines.append(docstring(spec["description"]))
|
|
lines.append(f" return self.call({call_args})")
|
|
if snake != name:
|
|
lines.append("")
|
|
lines.append(f" {name} = {snake}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
out = Path(__file__).parent.parent / "lightpanda" / "_methods.py"
|
|
with Browser() as browser:
|
|
tools = {k: v for k, v in browser.tools.items() if k not in _SESSION_TOOLS}
|
|
|
|
blocks = [HEADER]
|
|
for name in sorted(tools):
|
|
blocks.append(method_source(name, tools[name]))
|
|
|
|
source = "\n".join(blocks) + "\n"
|
|
compile(source, str(out), "exec")
|
|
out.write_text(source)
|
|
print(f"wrote {out} ({len(tools)} tools)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|