Files
OpenBB/website/generate_terminal_markdown.py
Jeroen Bouma 0ee2ebcf1e Refractor stocks/fa by incorporating stocks/dd and making the menu less reliant on Yahoo Finance (#4055)
* I missed some sheet_name ones :)

* Move quote to main menu and combine yahoo finance commands

* Update stocks for quote

* First update to move yfinance away from main functionality

* Small updates

* Update tests to reflect changes

* Fix some little things

* Update docs a little too

* Remove the Due Diligence menu

* Update tests for new change

* Update fa and stocks with new text and move some stuff around

* Bye bye dd

* Pls work

* work..

* work..

* work..

* Work..|

* Add tests back in

* Remove tests

* Fix fix fix

* Remove tests to fix conflicts

* Improve the `stocks/search` command (#4084)

* Improve search command

* Make it more intuitive

* Fix tests

* Update the docs

* Ok done

* Ok done 2

* Ok done 3

* Git update tests that failed

* Update commands with --sheet-name that didn't work (#4088)

* Update commands with --sheet-name that didn't work

* Fix test

* oops

* Adds auto completion to `news` (#4089)

* Fix plot look when using eval and add query to choices (#3881)

* convert index to datetime and update choices

* uncomment economy integration test

* fix treasury concat bug

* fix datasets concat on duplciates

* Lock ruff version so that new lints dont break our CI (#3905)

* Lock ruff version so that new lints dont break our CI

* Bumped pre-commit ruff version

* returning always a dataframe

* Revert "returning always a dataframe"

This reverts commit d5dbf3132f.

* quick fix to auto complete args

---------

Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>

* Hotfix/Windows-CLI (#4087)

* init

* Update windows10_build.yml

* Update windows10_build.yml

* Update windows10_build.yml

* Update windows10_build.yml

* Update windows10_build.yml

* Update windows10_build.yml

* oops

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>

* Adds end date to `forex/load` (#4077)

* Fix plot look when using eval and add query to choices (#3881)

* convert index to datetime and update choices

* uncomment economy integration test

* fix treasury concat bug

* fix datasets concat on duplciates

* Lock ruff version so that new lints dont break our CI (#3905)

* Lock ruff version so that new lints dont break our CI

* Bumped pre-commit ruff version

* returning always a dataframe

* Revert "returning always a dataframe"

This reverts commit d5dbf3132f.

* adding end date to the forex load

* added some tests

* added example

* always show msg

* rewriting test

* updating tests

* updating tests

* Redo load tests

---------

Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>

* Update tests to reflect changes

* work..

* Remove tests

* Fix fix fix

* Remove tests

* Update the whole test folder

* Small updates

* Revert change

* Update eodhd to reflect it requires a paid plan and update tests

* Fix all the tests

* Update tests

* Update integration tests

* Removing tests so no whining happens

* ADD ALL THE TESTS

* Fix linting

* gitignore.  thanks jer

* Add back dd and redirect to fa

* Update to just display a message

* Update OpenBB SDK commands

* Fixing issues with exporting

* Fix ruff messing up

* Fix tests

* Fix the tests by disabling isort to run on tests folder

---------

Co-authored-by: Jeroen Bouma <jeroenbouma@Jeroens-MacBook-Pro.local>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
Co-authored-by: Henrique Joaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: teh_coderer <me@tehcoderer.com>
2023-02-06 23:24:15 +01:00

357 lines
12 KiB
Python

import json
import os
import re
import shutil
import traceback
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Union
from openbb_terminal.core.config.paths import USER_DATA_DIRECTORY
from openbb_terminal.rich_config import console
from website.controller_doc_classes import (
ControllerDoc,
LoadControllersDoc,
sub_names_full as subnames,
)
website_path = Path(__file__).parent.absolute()
USER_PATH = (f"{USER_DATA_DIRECTORY}", "`USER_DATA_DIRECTORY`")
def existing_markdown_file_examples(
trailmap: str, cmd_dict: Dict[str, str]
) -> Dict[str, Union[str, List[str]]]:
"""Get existing markdown file examples
Parameters
----------
trailmap : str
The trailmap to the function
cmd_dict : Dict[str, str]
The dictionary of the command
Returns
-------
Dict[str, Union[str, List[str]]]
Updated dictionary of the command with examples and images keys
"""
trail = trailmap.split(".")
for sub in trail:
if sub in ["ba", "ta", "qa"]:
trail.remove(trailmap.split(".")[0])
examples_path = (
f"old_content/terminal/{'/'.join(trail)}/{cmd_dict['cmd_name']}/_index.md"
)
examples_dict: Dict[str, Union[str, List[str]]] = {}
if os.path.exists(website_path / examples_path):
with open(website_path / examples_path, encoding="utf-8") as f:
content = f.read()
examples: str = ""
if "Example:" in content:
example_split = content.split("Example:")[1].split("```")
if example_split and len(example_split) > 1:
examples = f"{example_split[1].strip()}"
examples_dict["example"] = examples
images = [
x for x in content.split("\n") if x.startswith("!") and "TODO" not in x
]
examples_dict["images"] = images
return examples_dict
# pylint: disable=isinstance-second-argument-not-valid-type
def process_cmd_parsers(ctrl: ControllerDoc) -> List[Dict[str, str]]:
"""Process command parsers from ControllerDoc"""
commands = []
for cmd, parser in ctrl.cmd_parsers.items():
actions = []
for action in parser._actions: # pylint: disable=protected-access
if action.dest == "help":
continue
if (default := action.default) is not None:
if isinstance(default, list):
default = ", ".join([str(x) for x in default])
elif isinstance(default, datetime):
if "start" in action.dest:
default = "datetime.now() - timedelta(days=365)"
elif "end" in action.dest or "date" in action.dest:
default = "datetime.now()"
if (choices := action.choices) is not None:
# We do this so that range(0, N) objects are not converted to a list
if isinstance(choices, (dict, type({}.keys()), list)):
listdict: list = []
for choice in choices:
# We check for nested dicts
if isinstance(choice, (dict, type({}.keys()))):
listdict.append([f"{k}" for k in choice])
choices = listdict or [f"{x}" for x in choices]
choices = ", ".join(choices) if len(choices) > 0 else "None"
if action.dest == "file":
new_desc = "File in `EXPORTS` or `CUSTOM_IMPORTS` directories"
if (file_ext := re.compile(r"\.(\w{3,5})").findall(choices)) != []:
exts = set(file_ext)
ext_examples = "file_name." + ", file_name.".join(exts)
new_desc += f" (e.g: `{ext_examples}`)"
action.choices = [f"`{ext_examples}`"]
choices = new_desc
if (doc := action.help) is not None:
# We do this to fix multiline docstrings for the markdown
doc = " ".join(doc.split()).replace(*USER_PATH)
if (flags := action.option_strings) is not None:
flags = " ".join(flags)
actions.append(
{
"opt_name": action.dest if action.dest else "",
"flags": flags if flags else "",
"doc": doc if doc else "",
"default": f"{default}".replace(*USER_PATH),
"optional": not action.required,
"choices": choices,
}
)
if (desc := parser.description) is not None:
# We do this to fix multiline docstrings for the markdown
desc = " ".join(desc.split()).replace(*USER_PATH)
param = {
"cmd_name": cmd.replace("call_", ""),
"actions": actions,
"usage": " ".join(parser.format_usage().split()).replace("usage: ", ""),
"description": desc if desc else "",
}
commands.append(param)
return commands
def generate_markdown(
cmd_meta: Dict[str, str], examples: Dict[str, Union[str, List[str]]]
) -> str:
"""Generate markdown section
Parameters
----------
cmd_meta : Dict[str, str]
Command metadata
examples : Dict[str, Union[str, List[str]]]
Command examples
Returns
-------
str
Markdown string
"""
if not cmd_meta:
raise ValueError("No command metadata found")
markdown = f"""---
########### THIS FILE IS AUTO GENERATED - ANY CHANGES WILL BE VOID ###########
title: {cmd_meta["cmd_name"]}
description: OpenBB Terminal Function
---\n\n"""
# head meta https://docusaurus.io/docs/markdown-features/head-metadata
markdown += f"# {cmd_meta['cmd_name']}\n\n{cmd_meta['description']}\n\n"
markdown += f"### Usage\n\n```python wordwrap\n{cmd_meta['usage']}\n```\n\n"
markdown += "---\n\n## Parameters\n\n"
if cmd_meta["actions"]:
markdown += (
"| Name | Parameter | Description | Default | Optional | Choices |\n"
)
markdown += (
"| ---- | --------- | ----------- | ------- | -------- | ------- |\n"
)
for param in cmd_meta["actions"]:
if isinstance(param, dict):
markdown += (
f"| {param['opt_name']} | {param['flags']} | {param['doc']} "
f"| {param['default']} | {param['optional']} | {param['choices']} |\n"
)
else:
markdown += "This command has no parameters\n\n"
if examples.get("example", ""):
markdown += "\n\n---\n\n## Examples\n\n"
markdown += f"```python\n{examples['example']}\n```"
markdown += "\n"
if isinstance(examples.get("images", None), list):
for image in examples["images"]:
markdown += f"{image}\n\n"
markdown += "---\n"
return markdown.replace("<", "").replace(">", "")
def add_todict(d: dict, location_path: list, cmd_name: str, full_path: str) -> dict:
"""Adds the trailmap to the dictionary. This function creates the dictionary paths to the function.
Parameters
----------
d : dict
The dictionary to add the path to
location_path : list
The path to the function
cmd_name : str
The name of the function
full_path : str
The full path to the function
Returns
-------
dict
The updated dictionary
"""
if location_path[0] not in d:
d[location_path[0]] = {}
if len(location_path) > 1:
add_todict(d[location_path[0]], location_path[1:], cmd_name, full_path)
else:
d[location_path[0]][
cmd_name
] = f"/terminal/reference/{'/'.join(full_path)}/{cmd_name}"
return d
def main() -> bool:
"""Main function to generate markdown files"""
console.print(
"[bright_yellow]Loading Controllers... Please wait and ignore any errors, this is normal.[/bright_yellow]"
)
load_ctrls = LoadControllersDoc()
ctrls = load_ctrls.available_controllers()
wopen_kwargs = {"encoding": "utf-8", "newline": "\n"}
console.print(
"[bright_yellow]Generating markdown files... Don't ignore any errors now[/bright_yellow]"
)
content_path: Path = website_path / "content/terminal/reference"
terminal_ref: Dict[str, dict] = {}
for file in content_path.glob("*"):
if file.is_file():
file.unlink()
else:
shutil.rmtree(file)
for ctrl_trailmap in ctrls:
try:
ctrl = load_ctrls.get_controller_doc(ctrl_trailmap)
ctrl_cmds = process_cmd_parsers(ctrl)
for cmd in ctrl_cmds:
examples = existing_markdown_file_examples(ctrl_trailmap, cmd)
markdown = generate_markdown(cmd, examples)
if cmd["cmd_name"] == "index":
cmd["cmd_name"] = "index_cmd"
trail = ctrl_trailmap.split(".")
terminal_ref = add_todict(terminal_ref, trail, cmd["cmd_name"], trail)
filepath = content_path / f"{'/'.join(trail)}/{cmd['cmd_name']}.md"
filepath.parent.mkdir(parents=True, exist_ok=True)
with open(filepath, "w", **wopen_kwargs) as f: # type: ignore
f.write(markdown)
except Exception as e:
traceback.print_exc()
console.print(
f"[red]Failed to generate markdown for {ctrl_trailmap}: {e}[/red]"
)
return False
terminal_ref = {
k: dict(sorted(v.items(), key=lambda item: item[0]))
for k, v in sorted(terminal_ref.items(), key=lambda item: item[0])
}
# Generate root "_category_.json" file
with open(content_path / "_category_.json", "w", **wopen_kwargs) as f: # type: ignore
f.write(json.dumps({"label": "Terminal Reference", "position": 4}, indent=2))
# Generate root "index.md" file
with open(content_path / "index.md", "w", **wopen_kwargs) as f: # type: ignore
f.write(
f"# OpenBB Terminal Features\n\n{generate_index_markdown('', terminal_ref, 2)}"
)
def gen_category_json(fname: str, path: Path):
"""Generate category json"""
fname = subnames[fname.lower()] if fname.lower() in subnames else fname.title()
with open(path / "_category_.json", "w", **wopen_kwargs) as f: # type: ignore
f.write(json.dumps({"label": fname}, indent=2))
def gen_category_recursive(nested_path: Path):
"""Generate category json recursively"""
for folder in nested_path.iterdir():
if folder.is_dir():
gen_category_json(folder.name, folder)
gen_category_recursive(folder) # pylint: disable=cell-var-from-loop
# Generate modules/sub category json and index files
gen_category_recursive(content_path)
console.print(
"[green]Markdown files generated, check the website/content/terminal/reference/ folder[/green]"
)
return True
def generate_index_markdown(markdown: str, d: dict, level: int) -> str:
"""Generate index markdown
Parameters
----------
markdown : str
The markdown to add to
d : dict
The dictionary to recursively generate markdown from
level : int
The level of the markdown header
Returns
-------
str
Generated index file markdown string
"""
for key in d:
if isinstance(d[key], dict):
markdown += f"\n{'#' * level} {key}\n\n"
markdown = generate_index_markdown(markdown, d[key], level + 1)
else:
markdown += f"- [{key}]({d[key]})\n"
return markdown
if __name__ == "__main__":
main()