mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 13:01:38 +00:00
* stash some changes * add more robust testing * mypy * point PR at V5 * introduce spec file * codespell * test fix * fix workflow environment setup * fix workflow environment setup * fix workflow environment setup * add pyyaml to dependencies * split lint jobs * fix workflow environment setup * fix workflow environment setup * workflow env setup * workflow env setup * clean up code comments * add auth hook entrypoints * codespell * add codegen feature * codespell * move _unpack into dispatchers for consistency with codegen packages * surface nested models in the response * fix missing coverage in CI * socrata updates * test fix * detect plotly output * add --include and --exclude flags from generate-extension command * cap test matrix at python 3.14 * no useless comments * platform controller command description split * merge URL overloads from path params * exclude none and unset from model dump --------- Co-authored-by: deeleeramone <> Co-authored-by: Copilot <copilot@github.com>
117 lines
5.1 KiB
Python
117 lines
5.1 KiB
Python
"""Multi-spec dispatcher — routes namespaced commands to per-spec backends."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from openbb_cli.dispatchers.http import HttpDispatcher
|
|
from openbb_cli.dispatchers.protocol import Request, Response, ResponseError
|
|
|
|
|
|
class MultiSpecDispatcher:
|
|
"""Routes ``<namespace>.<command>`` to per-spec ``HttpDispatcher``s."""
|
|
|
|
def __init__(self, dispatchers: dict[str, HttpDispatcher]) -> None:
|
|
if not dispatchers:
|
|
raise ValueError("MultiSpecDispatcher requires at least one namespace.")
|
|
self._dispatchers = dict(dispatchers)
|
|
self._spec_doc = self._merge_spec_docs()
|
|
|
|
def _merge_spec_docs(self) -> dict[str, Any]:
|
|
"""Build an aggregated spec doc for the spec-aware parser path."""
|
|
merged_commands: dict[str, Any] = {}
|
|
merged_routers: dict[str, str] = {}
|
|
merged_reference_paths: dict[str, dict[str, Any]] = {}
|
|
merged_reference_routers: dict[str, dict[str, Any]] = {}
|
|
for namespace, dispatcher in self._dispatchers.items():
|
|
doc = dispatcher._spec_doc
|
|
for cmd, entry in doc.get("commands", {}).items():
|
|
merged_commands[f"{namespace}.{cmd}"] = entry
|
|
merged_routers[namespace] = "menu"
|
|
reference = doc.get("reference") or {}
|
|
for path, meta in (reference.get("paths") or {}).items():
|
|
prefixed = (
|
|
"/" + namespace + (path if path.startswith("/") else "/" + path)
|
|
)
|
|
merged_reference_paths[prefixed] = meta
|
|
for router, meta in (reference.get("routers") or {}).items():
|
|
if router == "":
|
|
merged_reference_routers[namespace] = meta
|
|
else:
|
|
merged_reference_routers[f"{namespace}/{router}"] = meta
|
|
return {
|
|
"commands": merged_commands,
|
|
"routers": merged_routers,
|
|
"reference": {
|
|
"paths": merged_reference_paths,
|
|
"routers": merged_reference_routers,
|
|
},
|
|
}
|
|
|
|
async def dispatch(self, request: Request, method: str | None = None) -> Response:
|
|
"""Route ``request`` to the namespace named by its leading command segment."""
|
|
if request.command == "__commands__":
|
|
return await self._aggregate_list_commands(request)
|
|
if request.command == "__schema__":
|
|
return await self._forward_schema(request)
|
|
namespace, _, inner = request.command.partition(".")
|
|
if not inner or namespace not in self._dispatchers:
|
|
return Response(
|
|
id=request.id,
|
|
ok=False,
|
|
error=ResponseError(
|
|
type="UnknownNamespace",
|
|
message=(
|
|
f"command {request.command!r} does not start with a "
|
|
f"declared spec namespace. Available: "
|
|
f"{', '.join(sorted(self._dispatchers))}."
|
|
),
|
|
),
|
|
)
|
|
forwarded = Request(id=request.id, command=inner, params=request.params)
|
|
return await self._dispatchers[namespace].dispatch(forwarded, method=method)
|
|
|
|
async def _aggregate_list_commands(self, request: Request) -> Response:
|
|
"""Concatenate every namespace's ``__commands__`` with prefixed names."""
|
|
rows: list[dict[str, Any]] = []
|
|
for namespace in sorted(self._dispatchers):
|
|
sub = await self._dispatchers[namespace].dispatch(
|
|
Request(id=None, command="__commands__")
|
|
)
|
|
if not sub.ok or not isinstance(sub.result, list):
|
|
continue
|
|
for entry in sub.result:
|
|
if not isinstance(entry, dict) or "name" not in entry:
|
|
continue
|
|
rows.append({**entry, "name": f"{namespace}.{entry['name']}"})
|
|
rows.sort(key=lambda r: r["name"])
|
|
return Response(id=request.id, ok=True, result=rows, error=None)
|
|
|
|
async def _forward_schema(self, request: Request) -> Response:
|
|
"""Strip the leading namespace from ``params['name']`` and forward."""
|
|
params = dict(request.params or {})
|
|
name = params.get("name") or ""
|
|
namespace, _, inner = name.partition(".")
|
|
if not inner or namespace not in self._dispatchers:
|
|
return Response(
|
|
id=request.id,
|
|
ok=False,
|
|
error=ResponseError(
|
|
type="UnknownNamespace",
|
|
message=(
|
|
f"__schema__ name {name!r} does not start with a "
|
|
f"declared spec namespace. Available: "
|
|
f"{', '.join(sorted(self._dispatchers))}."
|
|
),
|
|
),
|
|
)
|
|
params["name"] = inner
|
|
return await self._dispatchers[namespace].dispatch(
|
|
Request(id=request.id, command="__schema__", params=params)
|
|
)
|
|
|
|
async def aclose(self) -> None:
|
|
"""Close every owned dispatcher."""
|
|
for dispatcher in self._dispatchers.values():
|
|
await dispatcher.aclose()
|