From 577a42461d9a123b0a717603aed0fb4e306b69eb Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Tue, 27 Feb 2024 16:50:28 +0530 Subject: [PATCH] [Feature] - Platform V4 Markdown Generator V2 (#6094) * add recent `openapi.json` * fix data types in models * removed `openapi.json` * set default date value as None in PolygonCurrencyPairs class * add function to generate reference.json file * reworked function to improve readability * reword function to add seo metadata * add functions for creating markdown sections * add code to extract data card markdown title * add additional functions for generating index and data models file * extract first sentence of the description for cards * code cleanup and documentation * linting * linting polygon models * add openbb import statement in create_reference_markdown_examples function * add POST method functions * cleanup; reworked generate_reference_index_files function * moved development section to 7th position in the sidebar * fix POST function params default value add standard flag to QueryParams and Data fields cleanup * add type expansion from package_builder.MethodDefinition * sort data models cards alphabetically make printing less verbose * make MAX_CARDS global display less content in cards in Commannds section * cleanup * Remove '_' from the cards under Commands section * " to ' in econometrics/causality * replace ' with " in ReferenceCard for reference dir index files * remove extra . from the quantile function description * shoutout to @deeleeramone for finding POST method description bug! * set correct value for standard field * handle BaseModel types in provider data fields * unit tests for the platform markdown generator v2 * yeet 'Default' and 'Optional' columns in the 'Data' section * last minute bug fix * add info for multiple symbols * make multiple items info same as platform static * organize sections properly * sort reference sub-directories alphabetically * extra space in 'OBBject extra' description * add type expansion for fields with multiple items POST method cleanup --------- Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com> Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> --- .../openbb_core/app/static/package_builder.py | 2 +- .../econometrics_router.py | 4 +- .../rolling/rolling_router.py | 2 +- .../openbb_polygon/models/currency_pairs.py | 3 +- .../openbb_polygon/models/equity_nbbo.py | 3 +- .../sec/openbb_sec/models/schema_files.py | 2 +- .../platform/development/_category_.json | 4 +- website/generate_platform_v4_markdown.py | 1610 ++++++++++------- website/tests/test_platform_docs.py | 308 ++++ 9 files changed, 1240 insertions(+), 698 deletions(-) create mode 100644 website/tests/test_platform_docs.py diff --git a/openbb_platform/core/openbb_core/app/static/package_builder.py b/openbb_platform/core/openbb_core/app/static/package_builder.py index 9e75ce8fc31..abfad97eda6 100644 --- a/openbb_platform/core/openbb_core/app/static/package_builder.py +++ b/openbb_platform/core/openbb_core/app/static/package_builder.py @@ -843,7 +843,7 @@ class DocstringGenerator: " List of warnings.\n" " chart : Optional[Chart]\n" " Chart object.\n" - " extra: Dict[str, Any]\n" + " extra : Dict[str, Any]\n" " Extra info.\n" ) obbject_description = obbject_description.replace("NoneType", "None") diff --git a/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py b/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py index 0c7b52529a0..2a1a941e553 100644 --- a/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py +++ b/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py @@ -353,10 +353,10 @@ def causality( x_column: str, lag: PositiveInt = 3, ) -> OBBject[Data]: - """Perform Granger causality test to determine if X "causes" y. + """Perform Granger causality test to determine if X 'causes' y. The Granger causality test is a statistical hypothesis test to determine if one time series is useful in - forecasting another. While "causality" in this context does not imply a cause-and-effect relationship in + forecasting another. While 'causality' in this context does not imply a cause-and-effect relationship in the philosophical sense, it does test whether changes in one variable are systematically followed by changes in another variable, suggesting a predictive relationship. By specifying a lag, you set the number of periods to look back in the time series to assess this relationship. This test is particularly useful in economic and diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py index 6e464083aea..c2cc5c7a1ae 100644 --- a/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py +++ b/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py @@ -230,7 +230,7 @@ def quantile( Quantiles are points dividing the range of a probability distribution into intervals with equal probabilities, or dividing the sample in the same way. This function is useful for understanding the distribution of data - within a specified window, allowing for analysis of trends, identification of outliers, and assessment of risk.. + within a specified window, allowing for analysis of trends, identification of outliers, and assessment of risk. Parameters: data: List[Data] diff --git a/openbb_platform/providers/polygon/openbb_polygon/models/currency_pairs.py b/openbb_platform/providers/polygon/openbb_polygon/models/currency_pairs.py index 66adef15c48..2ea951781d2 100644 --- a/openbb_platform/providers/polygon/openbb_polygon/models/currency_pairs.py +++ b/openbb_platform/providers/polygon/openbb_polygon/models/currency_pairs.py @@ -26,7 +26,7 @@ class PolygonCurrencyPairsQueryParams(CurrencyPairsQueryParams): default=None, description="Symbol of the pair to search." ) date: Optional[dateType] = Field( - default=datetime.now().date(), description=QUERY_DESCRIPTIONS.get("date", "") + default=None, description=QUERY_DESCRIPTIONS.get("date", "") ) search: Optional[str] = Field( default="", @@ -159,6 +159,7 @@ class PolygonCurrencyPairsFetcher( return all_data + # pylint: disable=unused-argument @staticmethod def transform_data( query: PolygonCurrencyPairsQueryParams, diff --git a/openbb_platform/providers/polygon/openbb_polygon/models/equity_nbbo.py b/openbb_platform/providers/polygon/openbb_polygon/models/equity_nbbo.py index ab92bc81e02..e332745c99e 100644 --- a/openbb_platform/providers/polygon/openbb_polygon/models/equity_nbbo.py +++ b/openbb_platform/providers/polygon/openbb_polygon/models/equity_nbbo.py @@ -92,7 +92,7 @@ class PolygonEquityNBBOData(EquityNBBOData): conditions: Optional[Union[str, List[int], List[str]]] = Field( default=None, description="A list of condition codes.", alias="conditions" ) - indicators: Optional[List] = Field( + indicators: Optional[List[int]] = Field( default=None, description="A list of indicator codes.", alias="indicators" ) sequence_num: Optional[int] = Field( @@ -208,6 +208,7 @@ class PolygonEquityNBBOFetcher( return data + # pylint: disable=unused-argument @staticmethod def transform_data( query: PolygonEquityNBBOQueryParams, diff --git a/openbb_platform/providers/sec/openbb_sec/models/schema_files.py b/openbb_platform/providers/sec/openbb_sec/models/schema_files.py index 93ef045baf7..682d03985b1 100644 --- a/openbb_platform/providers/sec/openbb_sec/models/schema_files.py +++ b/openbb_platform/providers/sec/openbb_sec/models/schema_files.py @@ -23,7 +23,7 @@ class SecSchemaFilesQueryParams(CotSearchQueryParams): class SecSchemaFilesData(Data): """SEC Schema Files List Data.""" - files: List = Field(description="Dictionary of URLs to SEC Schema Files") + files: List[str] = Field(description="Dictionary of URLs to SEC Schema Files") class SecSchemaFilesFetcher(Fetcher[SecSchemaFilesQueryParams, SecSchemaFilesData]): diff --git a/website/content/platform/development/_category_.json b/website/content/platform/development/_category_.json index 8219ce2ae74..7a45206fc82 100644 --- a/website/content/platform/development/_category_.json +++ b/website/content/platform/development/_category_.json @@ -1,4 +1,4 @@ { "label": "Development", - "position": 6 -} + "position": 7 +} \ No newline at end of file diff --git a/website/generate_platform_v4_markdown.py b/website/generate_platform_v4_markdown.py index c377a68f70c..66842d771f4 100644 --- a/website/generate_platform_v4_markdown.py +++ b/website/generate_platform_v4_markdown.py @@ -1,602 +1,698 @@ +"""Platform V4 Markdown Generator Script.""" + +# pylint: disable=too-many-lines + import inspect import json import re import shutil -from inspect import Parameter, _empty, signature from pathlib import Path -from textwrap import shorten -from typing import Any, Callable, Dict, List, TextIO, Tuple, Union +from typing import Any, Callable, Dict, List -from docstring_parser import parse from openbb_core.app.provider_interface import ProviderInterface -from openbb_core.app.static.package_builder import ( - DocstringGenerator, - MethodDefinition, - PathHandler, -) +from openbb_core.app.router import RouterLoader +from openbb_core.app.static.package_builder import MethodDefinition from openbb_core.provider import standard_models -from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined -website_path = Path(__file__).parent.absolute() -SEO_META: Dict[str, Dict[str, Union[str, List[str]]]] = json.loads( - (website_path / "metadata/platform_v4_seo_metadata.json").read_text() -) +# Number of spaces to substitute tabs for indentation +TAB_WIDTH = 4 -REFERENCE_IMPORT_UL = """import ReferenceCard from "@site/src/components/General/NewReferenceCard"; +# Maximum number of commands to display on the cards +MAX_COMMANDS = 8 -\n\n" + + # Commands section for markdown files + if markdown_files: + index_content += "### Commands\n" + index_content += PLATFORM_REFERENCE_UL_ELEMENT + "\n" + for file in markdown_files: + # Check if the current file is not the index file to avoid self-referencing + if file.name != "index.mdx": + # Extract the file name without extension to use as a title + title = file.stem.replace("_", " ") + # Generate a relative file path from the PLATFORM_REFERENCE_PATH, + # remove the file extension, and convert it to POSIX path format + # for consistency across OS + file_path = file.relative_to(PLATFORM_REFERENCE_PATH).with_suffix("").as_posix() # fmt: skip + # Attempt to fetch the file's description from reference_content + # using its path,split by the first period to get the first sentence, + # and default to an empty string if not found + file_description = reference_content.get(f"/{file_path}", "").split(".")[0] # fmt: skip + url = f"/platform/reference/{file_path}" + index_content += f'\n' + index_content += "\n\n" + + # Save the index.mdx file + with open(path / "index.mdx", "w", encoding="utf-8") as f: + f.write(index_content) + + # Recursively generate for sub-directories + for i, sub_dir in enumerate(sub_dirs, start=1): + generate_index_and_category(sub_dir, sub_dir.name.capitalize(), i) + + # Start the recursive generation from the PLATFORM_REFERENCE_PATH + generate_index_and_category(PLATFORM_REFERENCE_PATH) + + +def generate_reference_top_level_index() -> None: + """Generate the top-level index.mdx file for the reference directory.""" + + # Get the sub-directories in the reference directory + reference_dirs = [d for d in PLATFORM_REFERENCE_PATH.iterdir() if d.is_dir()] + reference_dirs.sort() + reference_cards_content = "" + + for dir_path in reference_dirs: + # Sub-directory name is used as the title for the ReferenceCard component + title = dir_path.name + markdown_files = [] + + # Recursively find all markdown files in the directory and subdirectories + for file in dir_path.rglob("*.md"): + markdown_files.append(file.stem) + + # Format description as a comma-separated string + if len(markdown_files) <= MAX_COMMANDS: + description_str = f"{', '.join(markdown_files)}" + else: + description_str = f"{', '.join(markdown_files[:MAX_COMMANDS])},..." + + reference_cards_content += ( + f"\n" + ) + + index_content = ( + "# Reference\n\n" + f"{PLATFORM_REFERENCE_IMPORT}\n\n" + "
    \n" + f"{reference_cards_content}" + "
\n" + ) + + # Generate the top-level index.mdx file for the reference directory + with (PLATFORM_REFERENCE_PATH / "index.mdx").open("w", encoding="utf-8") as f: + f.write(index_content) + + +def create_data_models_index(title: str, description: str, model: str) -> str: + """Create the index content for the data models. + + Args: + title (str): Title of the data model + description (str): Description of the data model + model (str): Model name + + Returns: + str: Index content for the data models + """ + + # Get the first sentence of the description + description = description.split(".")[0].strip() + + # For easy debugging of the created strings + index_content = ( + "\n" + ) + + return index_content + + +def generate_data_models_index_files(content: str) -> None: + """Generate index.mdx and _category_.json files for the data_models directory. + + Args: + content (str): Content for the data models index file + """ + + index_content = ( + "# Data Models\n\n" + f"{PLATFORM_REFERENCE_IMPORT}\n\n" + "
    \n" + f"{content}\n" + "
\n" + ) + + # Generate the index.mdx file for the data_models directory + with open(PLATFORM_DATA_MODELS_PATH / "index.mdx", "w", encoding="utf-8") as f: + f.write(index_content) + + # Generate the _category_.json file for the data_models directory + category_content = {"label": "Data Models", "position": 6} + with open( + PLATFORM_DATA_MODELS_PATH / "_category_.json", "w", encoding="utf-8" + ) as f: + json.dump(category_content, f, indent=2) + + +def generate_markdown_file(path: str, markdown_content: str, directory: str) -> None: + """Generate markdown file using the content of the specified path and directory. + + Args: + path (str): Path to the markdown file + markdown_content (str): Content for the markdown file + directory (str): Directory to save the markdown file + + Raises: + ValueError: If the content type is invalid + """ + + # For reference, split the path to separate the + # directory structure from the file name + if directory == "reference": + parts = path.strip("/").split("/") + file_name = f"{parts[-1]}.md" + directory_path = PLATFORM_REFERENCE_PATH / "/".join(parts[:-1]) + # For data models, the file name is derived from the last + # part of the path, and there's no additional directory structure + elif directory == "data_models": + file_name = f"{path.split('/')[-1]}.md" + directory_path = PLATFORM_DATA_MODELS_PATH + else: + raise ValueError(f"Invalid directory: {directory}") + + # Ensure the directory exists + directory_path.mkdir(parents=True, exist_ok=True) + + # Generate the markdown file for the specified path and directory + with open(directory_path / file_name, "w", encoding="utf-8") as md_file: + md_file.write(markdown_content) def generate_platform_markdown() -> None: """Generate markdown files for OpenBB Docusaurus website.""" - route_map = PathHandler.build_route_map() - path_list = sorted(PathHandler.build_path_list(route_map=route_map)) - print("Generating markdown files...") - kwargs = {"encoding": "utf-8", "newline": "\n"} - content_path = website_path / "content/platform/reference" - data_models_path = website_path / "content/platform/data_models" - reference_cards: Dict[Path, List[Dict[str, str]]] = {} - data_reference_cards: Dict[Path, List[Dict[str, str]]] = {} + data_models_index_content = [] + reference_index_content_dict = {} - for file in content_path.glob("*"): - if file.is_file(): - file.unlink() - else: - shutil.rmtree(file) - for file in data_models_path.glob("*"): - if file.is_file(): - file.unlink() - else: - shutil.rmtree(file) + print("[CRITICAL] Ensure all the extensions are installed before running this script!") # fmt: skip - for path in path_list: - meta_command = get_command_meta(path, route_map) - if not meta_command: - continue - func = PathHandler.get_route(path=path, route_map=route_map).endpoint + # Generate and read the reference.json file + print("[INFO] Generating the reference.json file...") + generate_reference_file() + with open(PLATFORM_CONTENT_PATH / "reference.json") as f: + reference = json.load(f) - func_name = func.__name__ - if func_name == "index": - func_name = "index_cmd" + # Clear the platform/reference folder + print("[INFO] Clearing the platform/reference folder...") + shutil.rmtree(PLATFORM_REFERENCE_PATH, ignore_errors=True) - folder = "/".join(path.strip("/").split("/")[:-1]) - filepath = content_path / folder / f"{func_name}.md" + # Clear the platform/data_models folder + print("[INFO] Clearing the platform/data_models folder...") + shutil.rmtree(PLATFORM_DATA_MODELS_PATH, ignore_errors=True) - markdown = generate_markdown(meta_command=meta_command) + print(f"[INFO] Generating the markdown files for the {PLATFORM_REFERENCE_PATH} sub-directories...") # fmt: skip + print(f"[INFO] Generating the markdown files for the {PLATFORM_DATA_MODELS_PATH} directory...") # fmt: skip - if data_model := meta_command.get("model", None): - ## title is the desc here - clean this later - ( - data_model_card_title, - data_model_card_description, - ) = generate_data_model_card_info(meta_command) + for path, path_data in reference.items(): + reference_markdown_content = "" + data_markdown_content = "" - title = re.sub( + description = path_data["description"] + path_parameters_fields = path_data["parameters"] + path_data_fields = path_data["data"] + + reference_index_content_dict[path] = description + + reference_markdown_content = create_reference_markdown_seo( + path[1:], description + ) + reference_markdown_content += create_reference_markdown_intro( + path[1:], description, path_data["deprecated"] + ) + reference_markdown_content += create_reference_markdown_examples( + path_data["examples"] + ) + + if path_parameters_fields := path_data["parameters"]: + reference_markdown_content += create_reference_markdown_tabular_section( + path_parameters_fields, "Parameters" + ) + + reference_markdown_content += create_reference_markdown_returns_section( + path_data["returns"]["OBBject"] + ) + + if path_data_fields := path_data["data"]: + reference_markdown_content += create_reference_markdown_tabular_section( + path_data_fields, "Data" + ) + + generate_markdown_file(path, reference_markdown_content, "reference") + + if model := path_data["model"]: + data_markdown_title = re.sub( r"([A-Z]{1}[a-z]+)|([A-Z]{3}|[SP500]|[EU])([A-Z]{1}[a-z]+)|([A-Z]{5,})", # noqa: W605 lambda m: ( f"{m.group(1) or m.group(4)} ".title() if not any([m.group(2), m.group(3)]) else f"{m.group(2)} {m.group(3)} " ), - data_model, + path_data["model"], ).strip() - - data_markdown = ( - f"---\ntitle: {title}\n" - f"description: {data_model_card_title}\n---\n\n" - "\n\n" - "import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n" + data_markdown_content = create_data_model_markdown( + data_markdown_title, + description, + model, + ) + data_markdown_content += create_reference_markdown_tabular_section( + path_parameters_fields, "Parameters" + ) + data_markdown_content += create_reference_markdown_tabular_section( + path_data_fields, "Data" + ) + data_models_index_content.append( + create_data_models_index(data_markdown_title, description, model) ) - data_markdown += generate_implementation_details_markdown_section( - data_model - ) + generate_markdown_file(model, data_markdown_content, "data_models") - data_model_markdown = generate_data_markdown_section(meta_command) - data_markdown += "\n\n## Parameters\n\n" - if meta_command["params"]: - data_markdown += generate_params_markdown_section(meta_command) - - data_markdown += data_model_markdown - markdown += data_model_markdown - - data_filepath = data_models_path / f"{data_model}.md" - - data_reference_cards.setdefault(data_filepath.parent, []).append( - dict( - title=title, - description=data_model_card_title or "", - url=data_models_path.relative_to(data_models_path) / data_model, - ) - ) - data_filepath.parent.mkdir(parents=True, exist_ok=True) - with open(data_filepath, "w", **kwargs) as f: # type: ignore - f.write(data_markdown) - - reference_cards.setdefault(filepath.parent, []).append( - dict( - title=func_name, - description=func.__doc__ or "", - url="/".join((content_path / folder).relative_to(content_path).parts), - ) - ) - - filepath.parent.mkdir(parents=True, exist_ok=True) - with open(filepath, "w", **kwargs) as f: # type: ignore - f.write(markdown) - - reference_cards = dict(sorted(reference_cards.items(), key=lambda item: item[0])) - data_reference_cards = { - folder: sorted(cards, key=lambda item: item["title"]) - for folder, cards in data_reference_cards.items() - } - with open(content_path / "index.mdx", "w", **kwargs) as f: # type: ignore - fname = "OpenBB Platform Reference" - rel_path = content_path.relative_to(content_path) - write_reference_index(reference_cards, fname, content_path, rel_path, f) - - with open(data_models_path / "index.mdx", "w", **kwargs) as f: # type: ignore - fname = "Data Models" - rel_path = data_models_path.relative_to(data_models_path) - write_reference_index( - data_reference_cards, fname, data_models_path, rel_path, f, True - ) - - with open(content_path / "_category_.json", "w", **kwargs) as f: # type: ignore - # Generate category json - f.write(json.dumps({"label": "Reference", "position": 5}, indent=2)) - - def gen_category_json(fname: str, path: Path, position: int = 1): - """Generate category json""" - with open(path / "_category_.json", "w", **kwargs) as f: # type: ignore - f.write( - json.dumps({"label": fname.title(), "position": position}, indent=2) - ) - - with open(path / "index.mdx", "w", **kwargs) as f: # type: ignore - rel_path = path.relative_to(content_path) - write_reference_index(reference_cards, fname, path, rel_path, f) - - def gen_category_recursive(nested_path: Path): - """Generate category json recursively""" - position = 1 - for folder in nested_path.iterdir(): - if folder.is_dir(): - gen_category_json(folder.name, folder, position) - gen_category_recursive(folder) # pylint: disable=cell-var-from-loop - position += 1 - - gen_category_recursive(content_path) - print(f"Markdown files generated, check the {content_path} folder.") - - -def save_metadata(path: Path) -> Dict[str, Dict[str, Union[str, List[str]]]]: - """Save SEO metadata""" - regex = re.compile( - r"---\ntitle: (.*)\ndescription: (.*)\nkeywords:(.*)\n---\n\nimport HeadTitle", - re.MULTILINE | re.DOTALL, + # Generate the index.mdx and _category_.json files for the reference directory + print(f"[INFO] Generating the index files for the {PLATFORM_REFERENCE_PATH} sub-directories...") # fmt: skip + generate_reference_index_files(reference_index_content_dict) + print( + f"[INFO] Generating the index files for the {PLATFORM_REFERENCE_PATH} directory..." ) + generate_reference_top_level_index() - metadata = {} - for file in path.rglob("*/**/*.md"): - context = file.read_text(encoding="utf-8") - match = regex.search(context) - if match: - title, description, keywords = match.groups() - key = file.relative_to(path).as_posix().removesuffix(".md") - metadata[key.replace("/", ".")] = { - "title": title, - "description": description, - "keywords": [ - keyword.strip() for keyword in keywords.split("\n- ") if keyword - ], - } + # Sort the data models index content alphabetically to display in the same order + data_models_index_content.sort() + data_models_index_content_str = "".join(data_models_index_content) - filepath = website_path / "metadata/platform_v4_seo_metadata2.json" - with open(filepath, "w", encoding="utf-8", newline="\n") as f: - json.dump(metadata, f, indent=2) - - return metadata + # Generate the index.mdx and _category_.json files for the data_models directory + print(f"[INFO] Generating the index files for the {PLATFORM_DATA_MODELS_PATH} directory...") # fmt: skip + generate_data_models_index_files(data_models_index_content_str) + print("[INFO] Markdown files generated successfully!") if __name__ == "__main__": diff --git a/website/tests/test_platform_docs.py b/website/tests/test_platform_docs.py new file mode 100644 index 00000000000..e8565e80d0b --- /dev/null +++ b/website/tests/test_platform_docs.py @@ -0,0 +1,308 @@ +import datetime +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import List, Literal, Optional, Union + +import pytest +from pydantic import BaseModel, Field + +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +# pylint: disable=unused-argument,redefined-outer-name,wrong-import-position + +from generate_platform_v4_markdown import ( # noqa: E402 + get_field_data_type, + get_function_params_default_value, + get_post_method_parameters_info, + get_post_method_returns_info, + get_provider_field_params, + get_provider_parameter_info, +) + + +# Mock Classes +class MockField: + def __init__(self, annotation): + self.annotation = annotation + + +class MockReturnType: + def __init__(self): + self.model_fields = {"results": MockField(annotation="List[str]")} + + +@dataclass +class MockProvider(BaseModel): + provider: Literal["provider1", "provider2"] = "provider1" + + +class MockModel(BaseModel): + provider: Optional[MockProvider] = Field(default=None) + + +# Mock Pydantic models to simulate your data structure +class OpenBBQueryParams(BaseModel): + field1: Optional[int] = Field(default=None, description="A sample integer field") + date: datetime.date = Field( + default=None, description="A datetime.date type field (expansion type)" + ) + + +class OpenBBData(BaseModel): + field1: Literal["a", "b"] = Field( + default="a", description="A sample Union field with Literal['a', 'b']" + ) + field2: Optional[int] = Field( + default=10, + description="A field with annotated type", + gt=0, + ) + + +class Provider1QueryParams(BaseModel): + field1: Optional[List[str]] = Field( + default=None, description="A sample List[str] field" + ) + + +class EmptyModel(BaseModel): + pass + + +@pytest.fixture +def mock_data(): + return { + "openbb": { + "QueryParams": {"fields": OpenBBQueryParams().model_fields}, + "Data": {"fields": OpenBBData().model_fields}, + }, + "provider1": { + "QueryParams": {"fields": Provider1QueryParams().model_fields}, + "Data": {"fields": EmptyModel().model_fields}, + }, + } + + +@pytest.fixture +def mock_endpoint(): + def endpoint(param1: int, param2: Optional[str] = "default"): + pass # Mock endpoint function + + return endpoint + + +class MockEndpointSetup: + def __init__(self): + self.mock_endpoint = self._create_mock_endpoint() + + def _create_mock_endpoint(self): + def mock_endpoint(provider_choices: Optional[MockProvider] = None): + return ["result1", "result2"] + + # Simulate annotations with a dataclass model + mock_endpoint.__annotations__ = { + "provider_choices": Optional[MockProvider], + "return": List[str], + } + + return mock_endpoint + + +@pytest.fixture +def mock_endpoint_setup(): + return MockEndpointSetup() + + +# Test Classes +class TestFieldDataType: + @pytest.mark.parametrize( + "input_type,expected_output", + [ + (int, "int"), + (str, "str"), + (Optional[int], "int"), + (Union[int, None], "int"), + ], + ) + def test_get_field_data_type(self, input_type, expected_output): + assert get_field_data_type(input_type) == expected_output + + +class TestProviderFieldParams: + def test_get_provider_field_params(self, mock_data, monkeypatch): + monkeypatch.setattr( + "generate_platform_v4_markdown.get_field_data_type", + get_field_data_type, + ) + # Define expected_result based on mock_data and logic in get_provider_field_params + expected_result_1 = [ + { + "name": "field1", + "type": "int", + "description": "A sample integer field", + "default": "None", + "optional": True, + "standard": True, + }, + { + "name": "date", + "type": "Union[str, date]", + "description": "A datetime.date type field (expansion type)", + "default": "None", + "optional": True, + "standard": True, + }, + ] + + expected_result_2 = [ + { + "name": "field1", + "type": "List[str]", + "description": "A sample List[str] field", + "default": "None", + "optional": True, + "standard": False, + }, + ] + + expected_result_3 = [ + { + "name": "field1", + "type": "Literal['a', 'b']", + "description": "A sample Union field with Literal['a', 'b']", + "default": "a", + "optional": True, + "standard": True, + }, + { + "name": "field2", + "type": "int", + "description": "A field with annotated type", + "default": "10", + "optional": True, + "standard": True, + }, + ] + + result = get_provider_field_params(mock_data, "QueryParams") + assert result == expected_result_1 + + result = get_provider_field_params(mock_data, "QueryParams", "provider1") + assert result == expected_result_2 + + result = get_provider_field_params(mock_data, "Data") + assert result == expected_result_3 + + +class TestProviderParameterInfo: + def test_get_provider_parameter_info(self, mock_endpoint_setup): + # Access the mock_endpoint from the fixture + mock_endpoint = mock_endpoint_setup.mock_endpoint + + expected = { + "name": "provider", + "type": "Literal['provider1', 'provider2']", + "description": "The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'provider1' if there is no default.", # noqa: E501 + "default": "provider1", + "optional": True, + "standard": True, + } + + result = get_provider_parameter_info(mock_endpoint) + assert result == expected + + +class TestFunctionParamsDefaultValue: + def test_get_function_params_default_value(self, mock_endpoint): + expected_result = { + "param1": "", + "param2": "default", + } + + result = get_function_params_default_value(mock_endpoint) + assert ( + result == expected_result + ), "Failed to correctly identify default parameter values." + + +class TestPostMethodParametersInfo: + def test_get_post_method_parameters_info(self, monkeypatch): + monkeypatch.setattr( + "generate_platform_v4_markdown.get_field_data_type", + get_field_data_type, + ) + + def mock_endpoint(param1: int, param2: Optional[str] = "default"): + """ + A mock endpoint function. + + Parameters + ---------- + param1 : int + The first parameter. + param2 : Optional[str] + The second parameter, optional. + """ + return + + expected_result = [ + { + "name": "param1", + "type": "int", + "description": "The first parameter.", + "default": "", + "optional": False, + }, + { + "name": "param2", + "type": "str", + "description": "The second parameter, optional.", + "default": "default", + "optional": True, + }, + ] + + result = get_post_method_parameters_info(mock_endpoint) + assert ( + result == expected_result + ), "Failed to correctly extract POST method parameters info." + + +class TestPostMethodReturnsInfo: + def test_get_post_method_returns_info(self, monkeypatch): + monkeypatch.setattr( + "generate_platform_v4_markdown.get_field_data_type", + get_field_data_type, + ) + + class MockReturnModel(BaseModel): + results: List[str] + + def mock_endpoint() -> MockReturnModel: + """ + A mock endpoint function. + + Parameters + ---------- + None + + Returns + ------- + MockReturnModel + A list of strings as results. + """ + return + + expected_result = [ + { + "name": "results", + "type": "List[str]", + "description": "A list of strings as results.", + } + ] + + result = get_post_method_returns_info(mock_endpoint) + assert ( + result == expected_result + ), "Failed to correctly extract POST method returns info."