import json
import shutil
import sys
from functools import reduce
from pathlib import Path
from textwrap import shorten
from typing import Any, Dict, List, Literal, Optional
import requests
# Paths
WEBSITE_PATH = Path(__file__).parent.absolute()
CONTENT_PATH = WEBSITE_PATH / "content"
XL_FUNCS_PATH = CONTENT_PATH / "excel" / "functions.json"
XL_OPENBB_FUNCS_PATH = CONTENT_PATH / "excel" / "openbb-functions.json"
XL_PLATFORM_PATH = CONTENT_PATH / "excel" / "openapi.json"
SEO_METADATA_PATH = WEBSITE_PATH / "metadata" / "platform_v4_seo_metadata.json"
# URLs: the platform url should match the backend being used by excel.openbb.co
XL_FUNCS_URL = "https://excel.openbb.co/assets/functions.json"
XL_OPENBB_FUNCS_URL = "https://excel.openbb.co/assets/openbb-functions.json"
XL_PLATFORM_URL = "https://sdk.openbb.co/openapi.json"
class CommandLib:
"""Command library."""
XL_TYPE_MAP = {
"bool": "Boolean",
"float": "Number",
"int": "Number",
"integer": "Number",
"str": "Text",
"string": "Text",
}
API_PREFIX = "/api/v1"
EXAMPLES: Dict[str, Dict] = {
"/get": {
"Example 0": '=OBB.GET({"a","b","c";"d","e","f"})',
"Example 1": '=OBB.GET({"a","b","c";"d","e","f"},"d","c")',
},
"/byod": {
"Example 0": '=OBB.BYOD("widget_name")',
"Example 1": '=OBB.BYOD("widget_name","backend_name")',
},
}
def __init__(self):
self.xl_funcs = self.read_xl_funcs()
self.xl_openbb_funcs = self.read_xl_openbb_funcs()
self.openapi = self.read_openapi()
self.seo_metadata = self.read_seo_metadata()
@staticmethod
def fetch_xl_funcs():
"""Fetch the excel functions."""
r = requests.get(XL_FUNCS_URL, timeout=10)
with open(XL_FUNCS_PATH, "w") as f:
json.dump(r.json(), f, indent=2)
@staticmethod
def fetch_xl_openbb_funcs():
"""Fetch the openbb version of excel functions."""
r = requests.get(XL_OPENBB_FUNCS_URL, timeout=10)
with open(XL_OPENBB_FUNCS_PATH, "w") as f:
json.dump(r.json(), f, indent=2)
@staticmethod
def fetch_openapi():
"""Fetch the openapi.json."""
r = requests.get(XL_PLATFORM_URL, timeout=10)
with open(XL_PLATFORM_PATH, "w") as f:
json.dump(r.json(), f, indent=2)
@staticmethod
def _traverse(
parts: List[str], map_: dict, exclude: Optional[List[str]] = None
) -> dict:
"""Traverse the map."""
parts = [p for p in parts if p not in (exclude or [])]
try:
return reduce(lambda x, y: x[y], parts, map_)
except KeyError:
return {}
def read_seo_metadata(self) -> dict:
"""Get the SEO metadata."""
with open(SEO_METADATA_PATH) as f:
metadata = json.load(f)
return {"/" + k.replace(".", "/").lower(): v for k, v in metadata.items()}
def read_xl_funcs(self) -> Dict[str, dict]:
"""Get a list of all the commands in the docs."""
with open(XL_FUNCS_PATH) as f:
funcs = json.load(f)
return {
"/" + func["name"].replace(".", "/").lower(): func
for func in funcs["functions"]
}
def read_xl_openbb_funcs(self) -> Dict[str, Any]:
"""Read the Excel openbb functions file."""
with open(XL_OPENBB_FUNCS_PATH) as f:
funcs = json.load(f)
return funcs
def read_openapi(self) -> dict:
"""Get the openapi.json."""
with open(XL_PLATFORM_PATH) as f:
return json.load(f)
def to_xl(self, type_: str) -> str:
"""Convert a type to an Excel type."""
return self.XL_TYPE_MAP.get(type_, type_).title()
def get_func(self, cmd: str) -> str:
"""Get the func of the command."""
return self.xl_funcs.get(cmd, {}).get("name", ".").split(".")[-1].lower()
def _get_signature(self, cmd: str, parameters: dict, sep: str = ",") -> str:
"""Get the signature of the command."""
sig = "=OBB." + self.xl_funcs[cmd].get("name", "")
sig += "("
for p_name, p_info in parameters.items():
if p_info["required"]:
sig += f"{p_name}"
else:
sig += f"[{p_name}]"
sig += sep
sig = sig.strip(f"{sep} ") + ")"
return sig
def _get_parameters(self, cmd: str) -> dict:
"""Get the parameters of the command."""
parameters = {}
for p in self.xl_funcs[cmd]["parameters"]:
parameters[p["name"]] = {
"type": self.to_xl(str(p["type"])),
"description": p["description"].replace("\n", " "),
"required": not p.get("optional", False),
}
return parameters
def _get_data(self, cmd: str) -> dict:
"""Get the data of the command from the openapi."""
schema = self._traverse(
[
"paths",
self.API_PREFIX + cmd,
"get",
"responses",
"200",
"content",
"application/json",
"schema",
],
self.openapi,
)
if "$ref" in schema and (
inner_schema := self._traverse(
schema["$ref"].split("/"), self.openapi, ["#"]
)
):
models = self._traverse(["properties", "results", "anyOf"], inner_schema)[0]
models = (
models["items"].get("oneOf", [models["items"]])
if models.get("type") == "array"
else [models]
)
# Get the available providers from the provider parameter enum
parameters = self._traverse(
[
"paths",
self.API_PREFIX + cmd,
"get",
"parameters",
],
self.openapi,
)
providers = []
for p in parameters:
if p.get("name") == "provider":
providers = p.get("schema", {}).get("enum", [])
if providers:
d = {}
for i, model in enumerate(models):
ref = model.get("$ref", "")
model_schema = self._traverse(ref.split("/"), self.openapi, ["#"])
provider = providers[i]
d[provider] = {
name: {
"description": info.get("description", "").replace("\n", "")
}
for name, info in model_schema["properties"].items()
}
return d
return {}
def _get_examples(
self, cmd: str, sig_parameters: Dict, sep: str = ","
) -> Dict[str, str]:
"""Get the examples of the command."""
if cmd in ("/get", "/byod"):
return self.EXAMPLES[cmd]
# API examples
examples = self._traverse(
["paths", self.API_PREFIX + cmd, "get", "examples"], self.openapi
)
sig = "=OBB." + self.xl_funcs[cmd].get("name", "")
ex_reference: Dict[str, str] = {}
for i, ex in enumerate(examples):
ex_code = ""
if ex.get("scope") == "api":
ex_code += sig + "("
ex_parameters = ex.get("parameters", {})
for p_name, p_info in sig_parameters.items():
p_type = p_info.get("type", {})
if p_value := ex_parameters.get(p_name):
if p_type == "Text":
ex_code += f'"{p_value}"{sep}'
elif p_type == "Boolean":
p_type = "TRUE" if bool(p_value) else "FALSE"
ex_code += f"{p_type}{sep}"
else:
ex_code += f"{p_value}{sep}"
else:
ex_code += sep
ex_code = ex_code.strip(sep)
ex_code += ")"
# Some example are repeated, we only want to add them once
if ex_code not in ex_reference.values():
ex_reference[f"Example {i}"] = ex_code
return ex_reference
def get_info(self, cmd: str) -> Dict[str, Any]:
"""Get the info for a command."""
name = self.get_func(cmd)
if not name:
return {}
description = self.xl_funcs[cmd].get("description", "").replace("\n", " ")
parameters = self._get_parameters(cmd)
function = self.xl_funcs[cmd].get("name", "")
signature_ = self._get_signature(cmd, parameters)
data = self._get_data(cmd)
return_ = self.xl_funcs[cmd].get("result", {}).get("dimensionality", "")
examples = self._get_examples(cmd, parameters)
providers = self.xl_openbb_funcs.get(function, {}).get("providers", [])
return {
"name": name,
"description": description,
"function": function,
"signature": signature_,
"parameters": parameters,
"data": data,
"return": return_,
"examples": examples,
"providers": providers,
}
class Editor:
"""Editor for the website docs."""
def __init__(
self,
directory: Path,
interface: Literal["excel"],
main_folder: str,
cmd_lib: CommandLib,
) -> None:
"""Initialize the editor."""
self.directory = directory
self.interface = interface
self.main_folder = main_folder
self.target_dir = directory / interface / main_folder
self.cmd_lib = cmd_lib
@staticmethod
def write(path: Path, content: str):
with open(path, "w", encoding="utf-8", newline="\n") as f: # type: ignore
f.write(content)
def generate_md(self, path: Path, cmd: str, cmd_info: dict):
def get_header() -> str:
header = ""
metadata = self.cmd_lib.seo_metadata.get(cmd, {})
if metadata:
title = metadata["title"].upper()
description = metadata["description"]
keywords = metadata["keywords"]
header = "---\n"
header += f"title: {title}\n"
header += f"description: {description}\n"
header += "keywords: \n"
for kw in keywords:
header += f"- {kw}\n"
header += "---\n\n"
else:
title = cmd_info["name"].upper()
header = "---\n"
header += f"title: {title}\n"
header += "---\n\n"
return header
def get_head_title() -> str:
func = cmd_info["function"]
title = "\n"
title += "import HeadTitle from '@site/src/components/General/HeadTitle.tsx';\n\n"
title += f'