Files
OpenBB/openbb_terminal/core/scripts/sdk_audit.py
Colin Delahunty 963ca9b2b9 Audit SDK and View/Model functions (#3384)
* Initial commit

* Finalized functionality

* update script

* Allow using it without forecasting

* Update gitignore

* Update `sdk_audit.py`

* Fixed issues, found more

* Added fix for helper functions, and column for SDK type

* Checked one more thing

* Moved file

* Move files ending with models/views

* Added fix of name

* Added file path fixes

* Patch to fix sdk_audit for windows

* fix

Co-authored-by: Chavithra PARANA <chavithra@gmail.com>
2022-11-17 16:25:36 +01:00

227 lines
7.6 KiB
Python

from inspect import getmembers, isfunction
from typing import List, Callable, Tuple
from datetime import datetime
import importlib.util
from pathlib import Path
import os
import pandas as pd
try:
import darts # pylint: disable=W0611 # noqa: F401
# If you just import darts this will pass during pip install, this creates
# Failures later on, also importing utils ensures that darts is installed correctly
from darts import utils # pylint: disable=W0611 # noqa: F401
FORECASTING = True
except ImportError:
FORECASTING = False
base_path = Path(__file__).parent.parent.parent.parent / "openbb_terminal"
def load_modules(full_path: Path):
"""Loads a module from a given Path.
Parameter:
----------
full_path: Path
The path to the module
Returns:
----------
The python module
"""
mod_path = str(full_path).split("OpenBBTerminal" + os.sep)[1].replace(os.sep, ".")
spec = importlib.util.spec_from_file_location(mod_path, full_path)
if not spec:
raise ValueError(f"Could not import path: {full_path}")
mod = importlib.util.module_from_spec(spec)
if not mod:
raise ValueError(f"Could not import path: {full_path}")
if not spec.loader:
raise ValueError(f"Could not import path: {full_path}")
spec.loader.exec_module(mod)
return mod
def all_view_models() -> List[Path]:
"""Geta all files with 'view' or 'model' in the name.
Returns:
----------
List[Path]
All paths in openbb_terminal with 'view' or 'model' in the name
"""
file_list = []
all_files = os.walk(base_path)
for root, _, files in all_files:
for filename in files:
if filename.endswith(".py"):
if "view" in filename or "model" in filename:
file_list.append(f"{root}/{filename}")
clean_list = set(file_list)
return [Path(x) for x in clean_list]
def get_sdk(file_path: str = "miscellaneous/library/trail_map.csv") -> pd.DataFrame:
"""Reads the CSV that generates the sdk and converts it to a dataframe
Parameters
----------
file_path: str
The path to the sdk csv
Returns:
----------
pd.DataFrame
A dataframe with columns for sdk endpoint functiom and trail. Trails will be duplicated
because there is a model and view function
"""
df = pd.read_csv(base_path / file_path)
df_dups = len(df["trail"]) - len(df["trail"].drop_duplicates())
if df_dups > 0:
print(f"Number of duplicate sdk paths: {df_dups}")
print("This indicates that the same SDK trail is being used multiple times\n")
views = list(df[["view", "trail"]].itertuples(index=False, name=None))
models = list(df[["model", "trail"]].itertuples(index=False, name=None))
# Add in whether it is a view or a model in pandas
combined = [x + ("view",) for x in views] + [x + ("model",) for x in models]
final_df = pd.DataFrame()
final_df["name"] = [x[0] for x in combined]
final_df["trail"] = [x[1] for x in combined]
final_df["type"] = [x[2] for x in combined]
final_df = final_df.dropna()
final_df = final_df.set_index("name")
return final_df
def format_function(function: Callable) -> Tuple[str, str]:
"""Gives a function a pretty name
Parameters
----------
function: Callable
The function to get a pretty string for
Returns:
----------
Tuple[str, str]
The functions pretty name and docstring
"""
mod = str(function.__module__)[:-2].replace("/", ".")
name = function.__name__
if mod[-1] != ".":
mod = f"{mod}."
return f"{mod}{name}", str(function.__doc__)
def functions_df() -> pd.DataFrame:
"""Creates a dataframe for all functions in 'models' and 'views'.
Returns:
----------
pd.DataFrame
Information for all view and model functions
"""
modules = all_view_models()
all_formatted = []
for module in modules:
if not FORECASTING and "forecast" in str(module):
continue
loaded = load_modules(module)
# Gets all of a module's functions, but ignores imported functions
func_list = [
x[1]
for x in getmembers(loaded, isfunction)
if x[1].__module__ == loaded.__name__
]
formatted_list = [format_function(x) for x in func_list]
all_formatted.extend(formatted_list)
func_df = pd.DataFrame()
func_df["name"] = [x[0] for x in all_formatted]
func_df["docstring"] = [x[1] for x in all_formatted]
func_dups = len(func_df["name"]) - len(func_df["name"].drop_duplicates())
if func_dups > 0:
print(f"Number of duplicate functions found: {func_dups}")
print(
"This may indicate that functions are defined several times in the terminal.\n"
)
func_df = func_df.set_index("name")
return func_df
def save_df(data: pd.DataFrame) -> None:
timestamp = datetime.now().timestamp()
time_str = (str(timestamp)).replace(".", "")
output_path = f"{time_str}_sdk_audit.csv"
data.to_csv(output_path)
print(f"File saved to {output_path}")
def get_nonconforming_functions(data: pd.DataFrame) -> pd.DataFrame:
"""When we first check all functions we only look in model and view files. This means that any
function in the helpers will be ignored. This function checks any helper funcs in the sdk
treemap, so that we can get a clearer picture of whether or not functions exist
Parameters
----------
data: pd.DataFrame
The dataframe of all values, this will be searched
Returns
----------
pd.DataFrame
The dataframe with any functions that could not be found
"""
df = data[data["docstring"].isnull()]
names = df.index.tolist()
for name in names:
path_list = name.split(".")
file = "/".join(path_list[:-1])
mod_path = Path(__file__).parent.parent.parent.parent / f"{file}.py"
module = load_modules(mod_path)
command = path_list[-1]
function = getattr(module, command)
if function:
data.loc[name, "docstring"] = function.__doc__
return data
def main():
print(
"This tool checks all functions in a file with a name including 'view' or 'model'against\n"
"all functions in the sdk, which is gathered from 'trail_map.csv'. If the generated csv\n"
"has an entry for 'trail' that means it is in the SDK, and if it has an entry for\n"
"'docstring' it is in a model or view.\n"
)
funcs_df = functions_df()
sdk_df = get_sdk()
final_df = funcs_df.merge(sdk_df, how="outer", left_index=True, right_index=True)
final_df = final_df.sort_values("name")
final_df = get_nonconforming_functions(final_df)
# Do this so that the duplicated method also checks the name column
final_df = final_df.reset_index()
# Get further stats on bad data
no_doc_count = len(final_df[final_df["docstring"].isnull()].index)
if no_doc_count > 0:
print(f"The number of rows with blank docstrings is: {no_doc_count}")
print(
"This indicates a matching function does not exist, is not in a 'model' or 'view'\n"
"file, or that the trailmap does not import it from the place it is defined.\n"
)
dup_name_count = len(final_df[final_df.duplicated(keep=False)].index)
if dup_name_count > 0:
print(f"The number of duplicate functions after merge is: {dup_name_count}")
print(
"This most likely indicates that the same function is being used at "
"different SDK endpoints.\n"
)
save_df(final_df)
if __name__ == "__main__":
main()