[Feature] openbb-platform-api Better POST Params Discovery (#7204)

* better post params discovery

* missing sentence in readme

* nasdaq expected widgets

* codespell

* openapi_extra is expected to be not None by packagebuilder

* lint

* lint
This commit is contained in:
Danglewood
2025-09-04 20:09:40 -07:00
committed by GitHub
parent 2e22d79eb7
commit f84ac7647c
10 changed files with 409 additions and 248 deletions

View File

@@ -870,7 +870,7 @@ async def direction_of_trade(
"h": 27,
},
"refetchInterval": False,
"endpoint": "/api/v1/economy/fomc_documents/download",
"endpoint": f"{api_prefix}/economy/fomc_documents/download",
"params": [
{
"type": "endpoint",
@@ -915,7 +915,7 @@ async def fomc_documents(
"""
results = await OBBject.from_query(Query(**locals()))
return results.results.content
return results.results.content # type: ignore
# This endpoint is used to download FOMC documents in Workspace.
@@ -926,11 +926,7 @@ async def fomc_documents(
@router._api_router.post(
"/fomc_documents/download",
include_in_schema=False,
openapi_extra={
"widget_config": {
"exclude": True,
}
},
openapi_extra={},
)
async def fomc_documents_download(params: Annotated[dict, Body()]) -> list:
"""
@@ -947,7 +943,7 @@ async def fomc_documents_download(params: Annotated[dict, Body()]) -> list:
urls = params.get("url", [])
results = []
results: list = []
for url in urls:
try:
response = make_request(url)

View File

@@ -433,10 +433,12 @@ For example, the response to submitting a form can be a Markdown widget with a c
The entry in `widgets.json` will be automatically created if the conditions below are met:
- GET and POST methods must share the same API route.
- GET request defines in top-level `widget_config`:
- `{"form_endpoint": /path_to/form_post_endpoint}`
- POST method takes 1 positional argument, a sub-class of Pydantic BaseModel.
- Create a model, like annotated table fields, defining all inputs to the form.
#### Example
The code below creates a widget with a form as the input, and an output table of all submitted forms, as processed through the `IntakeForm` model.
@@ -447,15 +449,14 @@ from datetime import date as dateType
from typing import Literal, Union
# from fastapi import FastAPI
from openbb_platform_api.query_models import FormData
from openbb_platform_api.response_models import Data
from pydantic import ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field
# app = FastAPI()
AccountTypes = Literal["General Fund", "Separately Managed", "Private Equity", "Family Office"]
class GeneralIntake(FormData):
class GeneralIntake(BaseModel):
"""Submit a form via POST request."""
date_created: dateType = Field(
@@ -476,7 +477,11 @@ class GeneralIntake(FormData):
submit: bool = Field(
default=True,
title="Submit",
type="button", # This creates a button, when pressed the parameter is sent as True
json_schema_extra={
"x-widget_config": {
"type": "button",
},
}
)
@@ -510,7 +515,7 @@ class IntakeForm(Data):
INTAKE_FORMS: list[IntakeForm] = []
@app.post("/general_intake")
@app.post("/general_intake_submit")
async def general_intake_post(data: GeneralIntake) -> bool:
global INTAKE_FORMS
try:
@@ -520,7 +525,14 @@ async def general_intake_post(data: GeneralIntake) -> bool:
raise e from e
@app.get("/general_intake")
@app.get(
"/general_intake",
openapi_extra= {
"widget_config": {
"form_endpoint": "/general_intake_submit",
},
},
)
async def general_intake() -> list[IntakeForm]:
return INTAKE_FORMS
```
@@ -529,6 +541,12 @@ async def general_intake() -> list[IntakeForm]:
### Omni Widget Example
An Omni Widget is a POST request where all parameters are sent to the request body, along with the text input box (keyed as "prompt").
The returned type can be a list of records (table), a Plotly Figure, or formatted Markdwon.
The model will attempt to assign the correct return type dynamically.
Set the response model as `OmniWidgetResponseModel`, then return `{"content": your_content}` from the endpoint.
```python
from typing import Literal, Optional
@@ -565,7 +583,7 @@ async def create_omni_widget(item: TestOmniWidgetQueryModel):
if item.parse_as == "chart":
some_test_data = {
"data": [{"type": "bar", "x": ["A", "B", "C"], "y": [1, 2, 3]}],
"layout": {"template": "...", "title": {"text": "Hello Chart!"}}
"layout": {"template": "plotly_dark", "title": {"text": "Hello Chart!"}}
}
elif item.parse_as == "text":
some_test_data = f"""

View File

@@ -7,16 +7,6 @@ from pydantic import AliasGenerator, ConfigDict, Field, field_validator
from pydantic.alias_generators import to_snake
class FormData(Data):
"""Submit a form via POST request."""
model_config = ConfigDict(
extra="allow",
alias_generator=AliasGenerator(to_snake),
title="Submit Form",
)
class OmniWidgetInput(Data):
"""Input for OmniWidget."""

View File

@@ -1,5 +1,8 @@
"""OpenAPI parsing Utils."""
# pylint: disable=C0302,R0912
# flake8: noqa: PLR0912
from typing import Optional
from openbb_core.provider.utils.helpers import to_snake_case
@@ -937,39 +940,93 @@ def post_query_schema_for_widget(
):
# Get the reference to the schema for the request body.
param_ref = (
schema["items"].get("$ref")
if "items" in schema
else schema.get("$ref") or schema
)
title = schema.get("title")
providers: list[str] = []
if isinstance(param_ref, dict) and "type" in param_ref:
param_ref = param_ref["type"]
if title and title in schema:
providers = [title]
elif title and "," in title:
providers = title.split(",")
else:
providers = ["Custom"]
if param_ref and isinstance(param_ref, str):
# Extract the schema name from the reference
schema_name = param_ref.split("/")[-1]
schema = openapi_json["components"]["schemas"].get(schema_name, schema_name)
props = {} if isinstance(schema, str) else schema.get("properties", {})
for k, v in props.items():
if target_schema and target_schema != k:
continue
if nested_schema := v.get("$ref"):
nested_schema_name = nested_schema.split("/")[-1]
nested_schema = openapi_json["components"]["schemas"].get(
nested_schema_name, {}
)
for nested_k, nested_v in nested_schema.get(
"properties", {}
).items():
set_param(nested_k, nested_v)
else:
if params := _route.get("parameters"):
if isinstance(params, list):
for _param in params:
set_param(_param["name"], _param["schema"])
elif isinstance(params, dict):
for k, v in params.items():
set_param(k, v)
route_params: list[dict] = []
providers = ["custom"]
if "items" in schema or "$ref" in schema:
param_ref = (
schema["items"].get("$ref")
if "items" in schema
else schema.get("$ref") or schema
)
if isinstance(param_ref, dict) and "type" in param_ref:
param_ref = param_ref["type"]
if param_ref and isinstance(param_ref, str):
# Extract the schema name from the reference
schema_name = param_ref.split("/")[-1]
schema = openapi_json["components"]["schemas"].get(
schema_name, schema_name
)
props = {} if isinstance(schema, str) else schema.get("properties", {})
for k, v in props.items():
if target_schema and target_schema != k:
continue
if nested_schema := v.get("$ref"):
nested_schema_name = nested_schema.split("/")[-1]
nested_schema = openapi_json["components"]["schemas"].get(
nested_schema_name, {}
)
for nested_k, nested_v in nested_schema.get(
"properties", {}
).items():
set_param(nested_k, nested_v)
else:
set_param(k, v)
route_params: list[dict] = []
for new_param_values in new_params.values():
_new_values = new_param_values.copy()
p = process_parameter(_new_values, providers)
if not p.get("exclude") and not p.get("x-widget_config", {}).get(
"exclude"
):
route_params.append(p)
return route_params
if "anyOf" in _route or "anyOf" in schema:
any_of_schema = (
schema.get("anyOf", [])
if "anyOf" in schema
else _route.get("anyOf", [])
)
for item in any_of_schema:
# If item is a $ref, resolve it
if "$ref" in item:
ref_name = item["$ref"].split("/")[-1]
ref_schema = openapi_json["components"]["schemas"].get(ref_name, {})
if "properties" in ref_schema:
for k, v in ref_schema["properties"].items():
if target_schema and target_schema != k:
continue
set_param(k, v)
# If item has properties directly
elif "properties" in item:
for k, v in item["properties"].items():
if target_schema and target_schema != k:
continue
set_param(k, v)
route_params = []
for new_param_values in new_params.values():
_new_values = new_param_values.copy()

View File

@@ -239,6 +239,16 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
return modified_query_schema
def get_form_input_paths(openapi: dict) -> dict:
"""Get a mapping of form input paths, defined by 'widget_config.form_endpoint'."""
return {
path: config.get("form_endpoint")
for path, path_config in openapi["paths"].items()
if (config := path_config.get("get", {}).get("widget_config"))
and config.get("form_endpoint")
}
def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-locals, too-many-statements
openapi: dict, widget_exclude_filter: list
):
@@ -261,6 +271,8 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
starred_list.append(item)
widget_exclude_filter.remove(item)
# Collect all routes that are designated as form endpoints to exclude them from direct widget generation
form_endpoint_paths = get_form_input_paths(openapi)
widgets_json: dict = {}
routes = [
p
@@ -269,13 +281,20 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
and ("get" in openapi["paths"][p] or "post" in openapi["paths"][p])
]
for route in routes:
has_form_endpoint = False
form_route: dict = {}
if "post" in openapi["paths"][route] and "get" in openapi["paths"][route]:
has_form_endpoint = True
form_route = openapi["paths"][route]["post"]
elif "post" in openapi["paths"][route]:
form_route = openapi["paths"][route]["post"]
# Skip routes that are only used as form endpoints for other routes
if route in form_endpoint_paths.values():
continue
route_api = openapi["paths"][route]
has_form_endpoint = route in list(form_endpoint_paths)
form_endpoint_path = form_endpoint_paths.get(route) if has_form_endpoint else ""
form_route: dict = (
openapi["paths"][form_endpoint_path]["post"] if has_form_endpoint else {}
)
# Determine the primary method for the widget
# If a GET exists, it's the primary. Otherwise, it's a POST.
route_method = "get" if "get" in route_api else "post"
skip = False
for starred in starred_list:
@@ -292,10 +311,6 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
if skip is True:
continue
route_api = openapi["paths"][route]
route_method = (
"post" if "post" in route_api and not has_form_endpoint else "get"
)
widget_id = (
(
route[1:].replace("/", "_")
@@ -318,7 +333,7 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
# Prepare the query schema of the widget
if route_method == "get":
query_schema, has_chart = get_query_schema_for_widget(openapi, route)
else:
else: # post
query_schema = (
post_query_schema_for_widget(
openapi, route_api.get("post", {}).get("operationId", ""), route
@@ -349,7 +364,7 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
columns_defs = (
data_schema_to_columns_defs(openapi, widget_id, provider, route)
if widget_config_dict.get("type")
not in ["multi_file_viewer", "pdf", "metric", "omni"]
not in ["multi_file_viewer", "pdf", "metric"]
else []
)
_cats = [
@@ -383,121 +398,182 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
param_names: list = []
var_schema: dict = {}
if (
_schema := route_api.get("post", {})
.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
):
schema_name = _schema.get("$ref", "").split("/")[-1]
var_schema = openapi["components"]["schemas"].get(schema_name, {})
if var_schema:
var_props = var_schema.get("properties", {})
for k, v in var_props.items():
if "$ref" in v:
param_names.append(k)
# Determine the source of the POST body schema
post_body_source = None
if has_form_endpoint:
post_body_source = form_route
elif route_method == "post":
post_body_source = route_api.get("post")
if param_names:
for _param in param_names:
if post_body_source:
if (
_schema := post_body_source.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
):
schema_name = _schema.get("$ref", "").split("/")[-1]
var_schema = openapi["components"]["schemas"].get(schema_name, {})
if var_schema:
var_props = var_schema.get("properties", {})
for k, v in var_props.items():
if "$ref" in v:
param_names.append(k)
if param_names:
for _param in param_names:
post_params = post_query_schema_for_widget(
openapi, post_body_source.get("operationId"), route, _param
)
modified_post_params = modify_query_schema(
post_params, provider # type: ignore
)
if has_form_endpoint:
has_submit = False
for item in modified_post_params:
if item.get("type") == "button":
has_submit = True
break
if not has_submit:
modified_post_params.append(
{
"paramName": "submit",
"label": "Submit",
"type": "button",
"value": True,
"description": "Submit the form.",
}
)
form_params = {
"type": "form",
"paramName": _param,
"label": "Form",
"description": "Form Data",
"endpoint": form_endpoint_path,
"inputParams": modified_post_params,
}
if post_config := var_schema.get("x-widget_config", {}):
form_params = deep_merge_configs(
form_params,
post_config,
)
modified_query_schema.append(form_params)
else:
# For non-form endpoints, extend with the modified params directly
modified_query_schema.extend(modified_post_params)
else: # This handles POST requests with no parameters in the body
post_params = post_query_schema_for_widget(
openapi, form_route.get("operationId"), route, _param
openapi,
post_body_source.get("operationId"),
form_endpoint_path if has_form_endpoint else route,
)
modified_post_params = modify_query_schema(
post_params, provider # type: ignore
)
if has_form_endpoint:
has_submit = False
for item in modified_post_params:
if item.get("type") == "button":
has_submit = True
break
if not has_submit:
modified_post_params.append(
{
"paramName": "submit",
"label": "Submit",
"value": True,
"type": "button",
"description": "Submit the form.",
}
)
form_params = {
"type": "form",
"paramName": _param,
"label": "Form",
"description": "Form Data",
"endpoint": (
route.replace("/api", "api")
if "/api" in route
else route[1:] if route[0] == "/" else route
),
"paramName": "form",
"label": var_schema.get("title", "Form"),
"description": var_schema.get("description", ""),
"endpoint": form_endpoint_path,
"inputParams": modified_post_params,
}
var_key: dict = {}
# Widget Config at the model level goes first.
if post_config := var_schema.get("x-widget_config", {}):
for key, value in post_config.copy().items():
if key.startswith("$."):
var_key[key] = value
else:
form_params[key] = value
form_params = deep_merge_configs(
form_params,
post_config,
)
# Then the widget config at the POST endpoint level takes priority.
if post_config := form_route.get("widget_config", {}):
for key, value in post_config.copy().items():
if key.startswith("$."):
var_key[key] = value
form_params = deep_merge_configs(
form_params,
{
k: v
for k, v in post_config.items()
if not k.startswith("$.")
},
)
modified_query_schema.append(form_params)
else:
# For non-form endpoints, extend with the modified params directly
modified_query_schema.extend(modified_post_params)
else:
post_params = post_query_schema_for_widget(
openapi, form_route.get("operationId"), route
)
modified_post_params = modify_query_schema(post_params, provider) # type: ignore
if has_form_endpoint:
form_params = {
"type": "form",
"paramName": "",
"label": var_schema.get("title", "Form"),
"description": var_schema.get("description", ""),
"endpoint": (
route.replace("/api", "api")
if "/api" in route
else route[1:] if route[0] == "/" else route
),
"inputParams": modified_post_params,
}
if var_key:
for key, value in var_key.items():
if (
key.replace("$.", "") in widget_config_dict
and "params" not in key
and "inputParams" not in key
):
widget_config_dict.update(
{key.replace("$.", ""): value}
)
else:
widget_config_dict[key.replace("$.", "")] = value
# For POST-only routes, extend with the modified params directly
# elif route_method == "post":
# modified_query_schema.extend(modified_post_params)
elif route_method == "post":
var_key: dict = {}
# Widget Config at the model level goes first.
if post_config := var_schema.get("x-widget_config", {}):
for key, value in post_config.copy().items():
if key.startswith("$."):
var_key[key] = value
else:
form_params[key] = value
var_key = {}
# Widget Config at the model level goes first.
if post_config := var_schema.get("x-widget_config", {}):
for key, value in post_config.copy().items():
if key.startswith("$."):
var_key[key] = value
form_params = deep_merge_configs(
form_params,
post_config,
)
if var_key:
for key, value in var_key.items():
if (
key.replace("$.", "") in widget_config_dict
and "params" not in key
and "inputParams" not in key
):
widget_config_dict.update(
{key.replace("$.", ""): value}
)
else:
widget_config_dict[key.replace("$.", "")] = value
# Then the widget config at the POST endpoint level takes priority.
if post_config := form_route.get("widget_config", {}):
for key, value in post_config.copy().items():
if key.startswith("$."):
var_key[key] = value
form_params = deep_merge_configs(
form_params,
{
k: v
for k, v in post_config.items()
if not k.startswith("$.")
},
)
modified_query_schema.append(form_params)
if var_key:
for key, value in var_key.items():
if (
key.replace("$.", "") in widget_config_dict
and "params" not in key
and "inputParams" not in key
):
widget_config_dict.update(
{key.replace("$.", ""): value}
)
else:
widget_config_dict[key.replace("$.", "")] = value
# For non-form endpoints, extend with the modified params directly
# else:
# modified_query_schema.extend(modified_post_params)
provider_map = {
"tmx": "TMX",
"ecb": "ECB",
@@ -528,14 +604,10 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
else ""
)
widget_type = (
(
"markdown"
if isinstance(response_schema, dict)
and response_schema.get("type") == "string"
else "table"
)
if route_method == "get"
else "omni"
"markdown"
if isinstance(response_schema, dict)
and response_schema.get("type") == "string"
else "table"
)
widget_config = {
"name": f"{name}" if name else route_api[route_method].get("summary"),
@@ -545,11 +617,7 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
"searchCategory": category.replace("_", " ").title(),
"widgetId": f"{widget_id}_{provider}_obb",
"params": modified_query_schema,
"endpoint": (
route.replace("/api", "api")
if "/api" in route
else route[1:] if route[0] == "/" else route
),
"endpoint": route,
"runButton": False,
"gridData": {"w": 40, "h": 15},
"data": {
@@ -609,9 +677,9 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
widget_config,
widget_config_dict,
)
# TODO: uncomment when charting is implemented for advanced tables.
# if widget_config.get("type") == "table":
# widget_config["data"]["table"]["enableAdvanced"] = True
if widget_config.get("type") == "table":
widget_config["data"]["table"]["enableAdvanced"] = True
if widget_config.get("type") == "metric":
widget_config["gridData"]["w"] = (
@@ -640,6 +708,16 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-
else widget_config["gridData"].get("h")
)
if source := widget_config_dict.get("source", []):
widget_config["source"] = source
if route_method == "post" and widget_config.get("type", "") not in [
"ssrm_table",
"omni",
"multi_file_viewer",
]:
widget_exclude_filter.append(widget_config["widgetId"])
# Add the widget configuration to the widgets.json
if widget_config["widgetId"] not in widget_exclude_filter:
widgets_json[widget_config["widgetId"]] = widget_config

View File

@@ -3538,10 +3538,15 @@
}
}
}
},
}
},
"/form_widget": {
"get": {
"summary": "All Forms",
"operationId": "all_forms_form_submit_get",
"widget_config": {
"form_endpoint": "/form_submit"
},
"parameters": [
{
"name": "some_param",

View File

@@ -125,7 +125,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/survey/sloos",
"endpoint": "/api/v1/economy/survey/sloos",
"runButton": false,
"gridData": {
"w": 40,
@@ -167,7 +167,8 @@
"headerTooltip": "Survey title.",
"cellDataType": "text"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -292,7 +293,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/survey/university_of_michigan",
"endpoint": "/api/v1/economy/survey/university_of_michigan",
"runButton": false,
"gridData": {
"w": 40,
@@ -326,7 +327,8 @@
"cellDataType": "number",
"renderFn": "greenRed"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -451,7 +453,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/survey/economic_conditions_chicago",
"endpoint": "/api/v1/economy/survey/economic_conditions_chicago",
"runButton": false,
"gridData": {
"w": 40,
@@ -533,7 +535,8 @@
"headerTooltip": "Non-Labor Costs Index.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -726,7 +729,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/survey/manufacturing_outlook_texas",
"endpoint": "/api/v1/economy/survey/manufacturing_outlook_texas",
"runButton": false,
"gridData": {
"w": 40,
@@ -783,7 +786,8 @@
"cellDataType": "number",
"renderFn": "greenRed"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -898,7 +902,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/survey/nonfarm_payrolls",
"endpoint": "/api/v1/economy/survey/nonfarm_payrolls",
"runButton": false,
"gridData": {
"w": 40,
@@ -972,7 +976,8 @@
"headerTooltip": "The indentation level of the element.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -1275,7 +1280,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/cpi",
"endpoint": "/api/v1/economy/cpi",
"runButton": false,
"gridData": {
"w": 40,
@@ -1308,7 +1313,8 @@
"headerTooltip": "CPI index value or period change.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -1551,7 +1557,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/balance_of_payments",
"endpoint": "/api/v1/economy/balance_of_payments",
"runButton": false,
"gridData": {
"w": 40,
@@ -1714,7 +1720,8 @@
"headerTooltip": "Current Account Debits Total Secondary Income (USD)",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -1842,7 +1849,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/fred_search",
"endpoint": "/api/v1/economy/fred_search",
"runButton": false,
"gridData": {
"w": 40,
@@ -1995,7 +2002,8 @@
"headerTooltip": "The series group ID of the series. This value is used to query for regional data.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -2191,7 +2199,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/fred_series",
"endpoint": "/api/v1/economy/fred_series",
"runButton": false,
"gridData": {
"w": 40,
@@ -2210,7 +2218,8 @@
"headerTooltip": "The date of the data.",
"cellDataType": "date"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -2415,7 +2424,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/fred_series",
"endpoint": "/api/v1/economy/fred_series",
"runButton": false,
"gridData": {
"w": 40,
@@ -2434,7 +2443,8 @@
"headerTooltip": "The date of the data.",
"cellDataType": "date"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -2489,7 +2499,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/fred_release_table",
"endpoint": "/api/v1/economy/fred_release_table",
"runButton": false,
"gridData": {
"w": 40,
@@ -2577,7 +2587,8 @@
"headerTooltip": "The reported value of the series.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -2852,7 +2863,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/fred_regional",
"endpoint": "/api/v1/economy/fred_regional",
"runButton": false,
"gridData": {
"w": 40,
@@ -2900,7 +2911,8 @@
"headerTooltip": "The individual series ID for the region.",
"cellDataType": "text"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3055,7 +3067,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/retail_prices",
"endpoint": "/api/v1/economy/retail_prices",
"runButton": false,
"gridData": {
"w": 40,
@@ -3104,7 +3116,8 @@
"headerTooltip": "Price, or change in price, per unit.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3183,7 +3196,7 @@
"show": false
}
],
"endpoint": "api/v1/economy/pce",
"endpoint": "/api/v1/economy/pce",
"runButton": false,
"gridData": {
"w": 40,
@@ -3264,7 +3277,8 @@
"headerTooltip": "The line number of the series in the table.",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3304,7 +3318,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/cik_map",
"endpoint": "/api/v1/regulators/sec/cik_map",
"runButton": false,
"gridData": {
"w": 40,
@@ -3323,7 +3337,8 @@
"cellDataType": "text",
"renderFn": null
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3363,7 +3378,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/institutions_search",
"endpoint": "/api/v1/regulators/sec/institutions_search",
"runButton": false,
"gridData": {
"w": 40,
@@ -3388,7 +3403,8 @@
"headerTooltip": "Central Index Key (CIK)",
"cellDataType": "number"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3437,7 +3453,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/schema_files",
"endpoint": "/api/v1/regulators/sec/schema_files",
"runButton": false,
"gridData": {
"w": 40,
@@ -3455,7 +3471,8 @@
"headerTooltip": "Dictionary of URLs to SEC Schema Files",
"cellDataType": "text"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3495,7 +3512,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/symbol_map",
"endpoint": "/api/v1/regulators/sec/symbol_map",
"runButton": false,
"gridData": {
"w": 40,
@@ -3515,7 +3532,8 @@
"cellDataType": "text",
"renderFn": null
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3537,7 +3555,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/rss_litigation",
"endpoint": "/api/v1/regulators/sec/rss_litigation",
"runButton": false,
"gridData": {
"w": 40,
@@ -3584,7 +3602,8 @@
"headerTooltip": "URL to the release.",
"cellDataType": "text"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3624,7 +3643,7 @@
"show": false
}
],
"endpoint": "api/v1/regulators/sec/sic_search",
"endpoint": "/api/v1/regulators/sec/sic_search",
"runButton": false,
"gridData": {
"w": 40,
@@ -3656,7 +3675,8 @@
"headerTooltip": "Reporting office within the Corporate Finance Office",
"cellDataType": "text"
}
]
],
"enableAdvanced": true
}
},
"source": [
@@ -3664,13 +3684,13 @@
],
"subCategory": "SEC"
},
"form_submit_custom_obb": {
"name": "Form Submit",
"form_widget_custom_obb": {
"name": "Form Widget",
"description": "",
"category": "Form Submit",
"category": "Form Widget",
"type": "table",
"searchCategory": "Form Submit",
"widgetId": "form_submit_custom_obb",
"searchCategory": "Form Widget",
"widgetId": "form_widget_custom_obb",
"params": [
{
"label": "Some Param",
@@ -3683,10 +3703,10 @@
},
{
"type": "form",
"paramName": "",
"paramName": "form",
"label": "Submit Form",
"description": "Submit a form via POST request.",
"endpoint": "form_submit",
"endpoint": "/form_submit",
"inputParams": [
{
"label": "Date",
@@ -3762,7 +3782,7 @@
]
}
],
"endpoint": "form_submit",
"endpoint": "/form_widget",
"runButton": false,
"gridData": {
"w": 40,
@@ -3771,11 +3791,13 @@
"data": {
"dataKey": "",
"table": {
"showAll": true
"showAll": true,
"enableAdvanced": true
}
},
"source": [
"Custom"
]
],
"form_endpoint": "/form_submit"
}
}

View File

@@ -1,3 +1,8 @@
"""Test OpenAPI Utils."""
# pylint: disable=redefined-outer-name,line-too-long
# flake8: noqa: E501
import json
from pathlib import Path
@@ -9,8 +14,6 @@ from openbb_platform_api.utils.openapi import (
post_query_schema_for_widget,
)
# pylint: disable=redefined-outer-name
# Load the mock OpenAPI JSON
@pytest.fixture(scope="module")
@@ -100,7 +103,6 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "start_date",
@@ -114,7 +116,6 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "end_date",
@@ -128,7 +129,6 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
],
False,
@@ -170,7 +170,6 @@ def mock_openapi_json():
"options": {"fred": []},
"x-widget_config": {},
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "end_date",
@@ -183,7 +182,6 @@ def mock_openapi_json():
"options": {"fred": []},
"x-widget_config": {},
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "limit",
@@ -196,7 +194,6 @@ def mock_openapi_json():
"options": {"fred": []},
"x-widget_config": {},
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "frequency",
@@ -227,12 +224,13 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "aggregation_method",
"label": "Aggregation Method",
"description": "A key that indicates the aggregation method used for frequency aggregation.\n This parameter has no affect if the frequency parameter is not set.\n \n avg = Average\n \n sum = Sum\n \n eop = End of Period",
"description": "A key that indicates the aggregation method used for frequency aggregation.\n"
+ " This parameter has no affect if the frequency parameter is not set.\n"
+ " \n avg = Average\n \n sum = Sum\n \n eop = End of Period",
"optional": True,
"type": "text",
"value": "eop",
@@ -247,7 +245,6 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "transform",
@@ -272,7 +269,6 @@ def mock_openapi_json():
"x-widget_config": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
},
],
True,
@@ -302,7 +298,6 @@ def mock_openapi_json():
"options": {"sec": []},
"x-widget_config": {},
"show": True,
"x-widget_config": {},
},
{
"parameter_name": "use_cache",
@@ -327,7 +322,6 @@ def mock_openapi_json():
"options": {"sec": []},
"x-widget_config": {},
"available_providers": ["sec"],
"x-widget_config": {},
"show": True,
},
],

View File

@@ -498,9 +498,9 @@ def test_modify_query_schema(query_schema, provider_value, expected_result):
def test_build_json(mock_openapi_json, mock_widgets_json):
result = build_json(openapi=mock_openapi_json, widget_exclude_filter=[])
assert (
len(result) == len(mock_openapi_json["paths"]) + 1
) # +1 for the duplicate path with a chart.
assert len(result) == len(
mock_openapi_json["paths"]
) # +1 for the duplicate path with a chart. -1 for the form widget over 2 paths.
assert result == mock_widgets_json

View File

@@ -210,7 +210,7 @@
]
}
],
"endpoint": "open_document",
"endpoint": "/open_document",
"runButton": false,
"gridData": {
"w": 40,
@@ -264,7 +264,7 @@
}
}
],
"endpoint": "earnings_calendar",
"endpoint": "/earnings_calendar",
"runButton": false,
"gridData": {
"w": 40,
@@ -414,7 +414,7 @@
}
}
],
"endpoint": "dividend_calendar",
"endpoint": "/dividend_calendar",
"runButton": false,
"gridData": {
"w": 40,
@@ -538,7 +538,7 @@
"paramName": "end_date"
}
],
"endpoint": "historical_dividends",
"endpoint": "/historical_dividends",
"runButton": false,
"gridData": {
"w": 40,
@@ -548,6 +548,7 @@
"dataKey": "",
"table": {
"showAll": true,
"enableAdvanced": true,
"columnsDefs": [
{
"field": "ex_dividend_date",
@@ -679,7 +680,7 @@
"paramName": "is_spo"
}
],
"endpoint": "ipo_calendar",
"endpoint": "/ipo_calendar",
"runButton": false,
"gridData": {
"w": 40,
@@ -888,7 +889,7 @@
]
}
],
"endpoint": "economic_calendar",
"endpoint": "/economic_calendar",
"runButton": false,
"gridData": {
"w": 40,