[Feature] openbb-platform-api - Create Entry In widgets.json For Forms (#7055)

* define pattern for automating widget entry for forms

* Add screenshot

* linters

* linters

* linters

* too many locals

* linters

* linters

* difference between date and _date

* more to caps strings

* catch exclude in params config

* skip finding columnsDefs for multi file viewer

* catch edge case

* fix tests

---------

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
This commit is contained in:
Danglewood
2025-03-18 16:15:19 -07:00
committed by GitHub
parent 9d04ede19e
commit 706046ab4a
7 changed files with 904 additions and 217 deletions

View File

@@ -418,6 +418,112 @@ async def hello_chart() -> dict:
return fig.to_plotly_json()
```
### Form Submit Widget
When submitted, Workspace makes a POST request to the endpoint.
If the POST function returns a 200 status code, the widget associated with the GET function is refreshed.
The results of the GET function does not have to correspond with the parameters and results of the POST function.
For example, the response to submitting a form can be a Markdown widget with a custom message.
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.
- 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.
```python
import uuid
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
# app = FastAPI()
AccountTypes = Literal["General Fund", "Separately Managed", "Private Equity", "Family Office"]
class GeneralIntake(FormData):
"""Submit a form via POST request."""
date_created: dateType = Field(
title="Created On", default_factory=dateType.today
)
first_name: str = Field(title="First Name")
last_name: str = Field(title="Last Name")
email: str = Field(title="Contact Email")
dob: dateType = Field(
title="Date Of Birth",
)
account_types: Union[AccountTypes, list[AccountTypes]] = Field(
title="Type Of Account",
json_schema_extra={
"x-widget_config": {"multiSelect": True},
},
)
submit: bool = Field(
default=True,
title="Submit",
type="button", # This creates a button, when pressed the parameter is sent as True
)
class IntakeForm(Data):
"""Submission Records."""
model_config = ConfigDict(extra="ignore")
contacted: bool = Field(
title="Contacted",
default=False,
)
date_created: dateType = Field(
title="Created On",
)
first_name: str = Field(title="First Name")
last_name: str = Field(title="Last Name")
email: str = Field(title="Contact Email")
dob: dateType = Field(
title="Date Of Birth",
)
account_types: Union[AccountTypes, list[AccountTypes]] = Field(
title="Account Interest",
)
unique_id: uuid.UUID = Field(
title="Unique ID",
default_factory=uuid.uuid4,
)
INTAKE_FORMS: list[IntakeForm] = []
@app.post("/general_intake")
async def general_intake_post(data: GeneralIntake) -> bool:
global INTAKE_FORMS
try:
INTAKE_FORMS.append(IntakeForm(**data.model_dump()))
return True
except Exception as e:
raise e from e
@app.get("/general_intake")
async def general_intake() -> list[IntakeForm]:
return INTAKE_FORMS
```
<img width="1552" alt="Screenshot 2025-03-09 at 9 51 47 PM" src="https://github.com/user-attachments/assets/16bb3844-ea43-44c8-ae44-67159b0b70e4" />
## Widget Config
Any value from the [`widgets.json`](https://docs.openbb.co/terminal/custom-backend/widgets-json-reference) structure can be passed into the `@app` decorator by including an `openapi_extra` dictionary with the key, `"widget_config"`.

View File

@@ -0,0 +1,15 @@
"""OpenBB Workspace Query Models."""
from openbb_core.provider.abstract.data import Data
from pydantic import AliasGenerator, ConfigDict
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",
)

View File

@@ -55,6 +55,10 @@ TO_CAPS_STRINGS = [
"Figi",
"Cusip",
"Pdf",
"Otm",
"Atm",
"Itm",
"Fomc",
]
@@ -109,7 +113,7 @@ def set_parameter_type(p: dict, p_schema: dict):
):
p["type"] = "boolean"
if "date" in p["parameter_name"]:
if p["parameter_name"] == "date" or "_date" in p["parameter_name"]:
p["type"] = "date"
if "timeframe" in p["parameter_name"]:
@@ -118,13 +122,15 @@ def set_parameter_type(p: dict, p_schema: dict):
if p["parameter_name"] == "limit":
p["type"] = "number"
if p.get("type") in ("array", "list") or isinstance(p.get("type"), list):
if p.get("type") in ("array", "list") or isinstance(p.get("type"), (list, dict)):
p["type"] = "text"
return p
def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict:
def set_parameter_options( # pylint: disable=too-many-branches
p: dict, p_schema: dict, providers: list[str]
) -> dict:
"""
Set options for the parameter based on the schema.
@@ -142,7 +148,11 @@ def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict
Dict
Updated parameter dictionary with options.
"""
choices: dict[str, list[dict[str, str]]] = {}
choices: dict[str, list[dict[str, str]]] = (
p.get("options", {})
if p.get("options")
else p_schema.get("options", {}) if p_schema.get("options") else {}
)
widget_configs: dict[str, dict] = {}
multiple_items_allowed_dict: dict = {}
is_provider_specific = False
@@ -164,6 +174,7 @@ def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict
elif len(providers) == 1 and "enum" in p_schema:
provider_choices = p_schema["enum"]
p_schema.pop("enum")
if provider_choices:
choices[provider] = [
{"label": str(c), "value": c} for c in provider_choices
@@ -175,7 +186,9 @@ def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict
# Check title for provider-specific information
if "title" in p_schema:
title_providers = set(p_schema["title"].split(","))
title_providers = (
set(p_schema["title"].split(",")) if p_schema.get("title") else set()
)
if title_providers.intersection(providers):
is_provider_specific = True
available_providers.update(title_providers)
@@ -235,10 +248,12 @@ def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict
if provider in p_schema and p_schema[provider].get("x-widget_config"):
widget_configs[provider] = p_schema[provider].get("x-widget_config")
p["options"] = choices
p["multiple_items_allowed"] = multiple_items_allowed_dict
if is_provider_specific:
if choices:
p["options"] = choices
if is_provider_specific and len(available_providers) > 1:
p["available_providers"] = list(available_providers)
p["x-widget_config"] = widget_configs
@@ -273,11 +288,17 @@ def process_parameter(param: dict, providers: list[str]) -> dict:
param_name.replace("_", " ").replace("fixedincome", "fixed income").title()
)
p["description"] = (
(param.get("description", param_name).split(" (provider:")[0].strip())
.split("Multiple comma separated items allowed")[0]
.strip()
(
(param.get("description", param_name).split(" (provider:")[0].strip())
.split("Multiple comma separated items allowed")[0]
.strip()
)
if param.get("description")
else ""
)
p["optional"] = param.get("required", False) is False
p["type"] = param.get("type", "text")
p["value"] = param.get("value")
if param_name == "provider":
p["type"] = "text"
@@ -314,12 +335,31 @@ def process_parameter(param: dict, providers: list[str]) -> dict:
return p
p_schema = param.get("schema", {})
if x_widget_config := param.get("x-widget_config", {}):
p["x-widget_config"] = x_widget_config
p_schema = param.get("schema", {}) or param
p["value"] = p_schema.get("default", None)
custom = (
param.get("options", {}).get("custom", [])
if "options" in param and "custom" in param["options"]
else []
)
if custom:
p["options"] = {"custom": custom}
p = set_parameter_options(p, p_schema, providers)
p = set_parameter_type(p, p_schema)
if "options" not in p:
p["options"] = []
if p_schema.get("options", []):
p["options"] = p_schema["options"]
if title := p_schema.get("title", ""):
p["label"] = (
p.get("parameter_name", "").replace("_", " ").title()
if ("," in title and title.replace(",", "").islower())
@@ -328,6 +368,10 @@ def process_parameter(param: dict, providers: list[str]) -> dict:
)
if _widget_config := p_schema.get("x-widget_config", {}):
for provider in providers:
if provider in _widget_config:
_widget_config = _widget_config[provider]
break
p.update(_widget_config)
return p
@@ -353,8 +397,7 @@ def get_query_schema_for_widget(
"""
has_chart = False
command = openapi_json["paths"][command_route]
schema_method = list(command)[0]
command = command[schema_method]
command = command.get("get", {})
params = command.get("parameters", [])
route_params: list[dict] = []
providers: list[str] = extract_providers(params)
@@ -370,7 +413,9 @@ def get_query_schema_for_widget(
p = process_parameter(param, providers)
if "show" not in p:
p["show"] = True
route_params.append(p)
if not p.get("exclude") and not p.get("x-widget_config", {}).get("exclude"):
route_params.append(p)
return route_params, has_chart
@@ -416,7 +461,7 @@ def get_data_schema_for_widget(openapi_json, operation_id, route: Optional[str]
if isinstance(response_ref, dict) and "type" in response_ref:
response_ref = response_ref["type"]
if response_ref:
if response_ref and isinstance(response_ref, str):
# Extract the schema name from the reference
schema_name = response_ref.split("/")[-1]
# Fetch and return the schema from components
@@ -586,7 +631,7 @@ def data_schema_to_columns_defs( # noqa: PLR0912 # pylint: disable=too-many-br
column_def["headerName"] = " ".join(
[
(word.upper() if word in TO_CAPS_STRINGS else word)
for word in header_name.split(" ")
for word in header_name.replace("_", " ").split(" ")
]
)
column_def["headerTooltip"] = prop.get(
@@ -663,3 +708,150 @@ def data_schema_to_columns_defs( # noqa: PLR0912 # pylint: disable=too-many-br
column_defs.append(column_def)
return column_defs
def post_query_schema_for_widget(
openapi_json,
operation_id,
route: Optional[str] = None,
target_schema: Optional[str] = None,
):
"""
Get the POST query schema for a widget based on its operationId.
Args:
openapi (dict): The OpenAPI specification as a dictionary.
operation_id (str): The operationId of the widget.
route (str): The route of the widget, if any.
target_schema (str): The target schema to extract, if any.
Returns:
list[dict]: The schema dictionary for the widget's data.
"""
new_params: dict = {}
def set_param(k, v):
"""Set the parameter."""
nonlocal new_params
new_params[k] = {}
new_params[k]["name"] = k
new_params[k]["type"] = (
"text"
if v.get("type") == "object"
else "date" if "date" in v.get("format", "") else v.get("type", "text")
)
new_params[k]["title"] = v.get("title")
new_params[k]["description"] = v.get("description")
new_params[k]["default"] = v.get("default")
new_params[k]["x-widget_config"] = v.get("x-widget_config", {})
choices: list = (
[{"label": c, "value": c} for c in v.get("choices", []) if c]
if v.get("choices")
else []
)
if isinstance(v, dict) and "anyOf" in v:
param_types = []
for item in v["anyOf"]:
if "type" in item and item.get("type") != "null":
param_types.append(item["type"])
if "enum" in item:
choices.extend({"label": c, "value": c} for c in item["enum"])
if param_types:
new_params[k]["type"] = (
"number"
if "number" in param_types
or "integer" in param_types
and "string" not in param_types
and "date" not in param_types
else (
"date"
if any(
"date" in sub_prop.get("format", "")
for sub_prop in v["anyOf"]
if isinstance(sub_prop, dict)
)
else "text"
)
)
else:
new_params[k]["type"] = (
"text"
if v.get("type") == "object"
else (
"date"
if "date" in v.get("format", "")
else v.get("type", "text")
)
)
elif isinstance(v, dict) and "enum" in v:
choices.extend({"label": c, "value": c} for c in v["enum"] if c)
if choices:
new_params[k]["options"] = {"custom": choices}
if not route:
for path, methods in openapi_json["paths"].items():
for _method, details in methods.items():
if details.get("operationId") == operation_id:
route = path
break
_route = openapi_json["paths"].get(route, {}).get("post", {})
if (
schema := _route.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
):
# 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
)
if isinstance(param_ref, dict) and "type" in param_ref:
param_ref = param_ref["type"]
if param_ref:
# Extract the schema name from the reference
schema_name = param_ref.split("/")[-1]
schema = openapi_json["components"]["schemas"].get(schema_name, schema_name)
props = 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] = []
providers = ["custom"]
for param in list(new_params):
p = process_parameter(new_params[param], providers)
if not p.get("exclude") and not p.get("x-widget_config", {}).get(
"exclude"
):
route_params.append(p)
return route_params
# Return None if the schema is not found
return None

View File

@@ -99,7 +99,6 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
_item = deepcopy(item)
provider_value_options: dict = {}
provider_value_widget_config: dict = {}
# Exclude provider parameter. Those will be added last.
if "parameter_name" in _item and _item["parameter_name"] == "provider":
continue
@@ -111,17 +110,22 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
):
continue
if provider_value in _item["multiple_items_allowed"] and _item[
"multiple_items_allowed"
].get(provider_value, False):
if (
provider_value
and isinstance(_item, dict)
and provider_value in _item.get("multiple_items_allowed", {})
and _item.get("multiple_items_allowed", {}).get(provider_value, False)
):
_item["description"] = (
_item["description"] + " Multiple comma separated items allowed."
)
_item["type"] = "text"
_item["multiSelect"] = True
if "options" in _item:
provider_value_options = _item.pop("options")
if "options" in _item and _item.get("options"):
provider_value_options = _item.pop("options", None)
if isinstance(provider_value_options, list):
provider_value_options = {provider_value: provider_value_options}
if provider_value in provider_value_options and bool(
provider_value_options[provider_value]
@@ -132,12 +136,12 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
_item["options"] = provider_value_options["other"]
_item["type"] = "text"
_item.pop("multiple_items_allowed")
_ = _item.pop("multiple_items_allowed", None)
if "available_providers" in _item:
_item.pop("available_providers")
_item["paramName"] = _item.pop("parameter_name")
_item["paramName"] = _item.pop("parameter_name", None)
if not _item.get("label") and _item["paramName"] in [
"url",
@@ -158,8 +162,9 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
)
if "x-widget_config" in _item:
provider_value_widget_config = _item.pop("x-widget_config")
_item.update(provider_value_widget_config)
provider_value_widget_config[
provider_value if provider_value else "custom"
] = _item.pop("x-widget_config", {})
if (
provider_value_widget_config
@@ -185,7 +190,7 @@ def modify_query_schema(query_schema: list[dict], provider_value: str):
return modified_query_schema
def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches, too-many-locals
openapi: dict, widget_exclude_filter: list
):
"""Build the widgets.json file."""
@@ -194,6 +199,7 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
TO_CAPS_STRINGS,
data_schema_to_columns_defs,
get_query_schema_for_widget,
post_query_schema_for_widget,
)
if not openapi:
@@ -212,6 +218,12 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
if openapi["paths"].get(p, {}) and "get" 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"]
skip = False
for starred in starred_list:
if route.startswith(
@@ -228,7 +240,6 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
continue
route_api = openapi["paths"][route]
method = list(route_api)[0]
widget_id = (
(
route[1:].replace("/", "_")
@@ -242,7 +253,7 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
if widget_id in widget_exclude_filter:
continue
widget_config_dict = route_api[method].get("widget_config", {})
widget_config_dict = route_api.get("get", {}).get("widget_config", {})
# If the widget is marked as excluded, skip it.
if widget_config_dict.get("exclude") is True:
@@ -250,10 +261,14 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
# Prepare the query schema of the widget
query_schema, has_chart = get_query_schema_for_widget(openapi, route)
response_schema = route_api[method]["responses"]["200"]["content"][
"application/json"
].get("schema", {})
response_schema = (
route_api.get("get", {})
.get("responses", {})
.get("200", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
)
# Extract providers from the query schema
providers: list = []
for item in query_schema:
@@ -264,8 +279,12 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
providers = ["custom"]
for provider in providers:
columns_defs = data_schema_to_columns_defs(
openapi, widget_id, provider, route
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"]
else []
)
_cats = [
r
@@ -294,8 +313,113 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
for word in name.split()
]
)
modified_query_schema = modify_query_schema(query_schema, provider)
if has_form_endpoint:
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)
if param_names:
for _param in param_names:
post_params = post_query_schema_for_widget(
openapi, form_route.get("operationId"), route, _param
)
modified_post_params = modify_query_schema(
post_params, provider
)
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
),
"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:
post_params = post_query_schema_for_widget(
openapi, form_route.get("operationId"), route
)
modified_post_params = modify_query_schema(post_params, provider)
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,
}
# Widget Config at the model level goes first.
if post_config := var_schema.get("x-widget_config", {}):
form_params = deep_merge_configs(
form_params,
post_config,
)
var_key: dict = {}
# 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
provider_map = {
"tmx": "TMX",
"ecb": "ECB",
@@ -374,22 +498,22 @@ def build_json( # noqa: PLR0912 # pylint: disable=too-many-branches
if columns_defs:
widget_config["data"]["table"]["columnsDefs"] = columns_defs
var_key: dict = {}
data_var_key: dict = {}
if data_config := data_schema_to_columns_defs(
openapi, widget_id, provider, route, True
):
for key, value in data_config.copy().items():
if key.startswith("$."):
var_key[key] = value
data_var_key[key] = value
widget_config["data"] = deep_merge_configs(
widget_config["data"],
{k: v for k, v in data_config.items() if not k.startswith("$.")},
)
if var_key:
for key, value in var_key.items():
if data_var_key:
for key, value in data_var_key.items():
if (
key.replace("$.", "") in widget_config_dict
and key != "$.data"

View File

@@ -3500,6 +3500,87 @@
}
]
}
},
"/form_submit": {
"post": {
"summary": "Form Submit",
"operationId": "form_submit_form_submit_post",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Form"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "boolean",
"title": "Response Form Submit Form Submit Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"summary": "All Forms",
"operationId": "all_forms_form_submit_get",
"parameters": [
{
"name": "some_param",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "",
"title": "Some Param"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
},
"title": "Response All Forms Form Submit Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
@@ -6290,6 +6371,90 @@
"title": "SecSymbolMapData",
"description": "SEC symbol map Data."
},
"Form": {
"properties": {
"date": {
"type": "string",
"format": "date",
"title": "Record Opened Date"
},
"record_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "number"
}
],
"title": "Record ID"
},
"first_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "First Name"
},
"last_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Name"
},
"symptoms": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"title": "Symptoms",
"choices": [
"dizzy",
"hungry",
"sleepy"
],
"x-widget_config": {
"multiSelect": true
}
},
"diagnosis": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Diagnosis"
},
"add_record": {
"type": "button",
"title": "Add Record",
"default": true
}
},
"additionalProperties": true,
"type": "object",
"title": "Submit Form",
"description": "Submit a form via POST request."
},
"ValidationError": {
"properties": {
"loc": {

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ from openbb_platform_api.utils.openapi import (
data_schema_to_columns_defs,
get_data_schema_for_widget,
get_query_schema_for_widget,
post_query_schema_for_widget,
)
# pylint: disable=redefined-outer-name
@@ -32,6 +33,7 @@ def mock_openapi_json():
"description": "Source of the data.",
"optional": True,
"type": "text",
"value": None,
"available_providers": ["fred"],
"show": True,
},
@@ -40,7 +42,9 @@ def mock_openapi_json():
"label": "Country",
"description": "The country to get data. Enter as a 3-letter ISO country code, default is USA.",
"optional": True,
"type": "text",
"value": "united_states",
"multiple_items_allowed": {},
"options": {
"fred": [
{"label": "argentina", "value": "argentina"},
@@ -93,37 +97,32 @@ def mock_openapi_json():
{"label": "g20", "value": "g20"},
]
},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "text",
"show": True,
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "start_date",
"label": "Start Date",
"description": "Start date of the data, in YYYY-MM-DD format.",
"optional": True,
"value": None,
"options": {"fred": []},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "date",
"show": True,
"value": None,
"multiple_items_allowed": {},
"options": {"fred": []},
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "end_date",
"label": "End Date",
"description": "End date of the data, in YYYY-MM-DD format.",
"optional": True,
"value": None,
"options": {"fred": []},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "date",
"show": True,
"value": None,
"multiple_items_allowed": {},
"options": {"fred": []},
"x-widget_config": {},
"show": True,
},
],
False,
@@ -138,6 +137,7 @@ def mock_openapi_json():
"description": "Source of the data.",
"optional": True,
"type": "text",
"value": None,
"available_providers": ["fred"],
"show": True,
},
@@ -147,6 +147,7 @@ def mock_openapi_json():
"description": "Symbol to get data for.",
"optional": False,
"type": "text",
"value": None,
"multiple_items_allowed": {"fred": True},
"show": True,
},
@@ -155,71 +156,45 @@ def mock_openapi_json():
"label": "Start Date",
"description": "Start date of the data, in YYYY-MM-DD format.",
"optional": True,
"value": None,
"options": {"fred": []},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "date",
"show": True,
"value": None,
"multiple_items_allowed": {},
"options": {"fred": []},
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "end_date",
"label": "End Date",
"description": "End date of the data, in YYYY-MM-DD format.",
"optional": True,
"value": None,
"options": {"fred": []},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "date",
"show": True,
"value": None,
"multiple_items_allowed": {},
"options": {"fred": []},
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "limit",
"label": "Limit",
"description": "The number of data entries to return.",
"optional": True,
"value": 100000,
"options": {"fred": []},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"type": "number",
"show": True,
"value": 100000,
"multiple_items_allowed": {},
"options": {"fred": []},
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "frequency",
"label": "Frequency",
"description": "Frequency aggregation to convert high frequency data to lower frequency."
+ "\n "
+ "\n None = No change"
+ "\n "
+ "\n a = Annual\n "
+ "\n q = Quarterly\n "
+ "\n m = Monthly\n "
+ "\n w = Weekly\n "
+ "\n d = Daily\n "
+ "\n wef = Weekly, Ending Friday"
+ "\n "
+ "\n weth = Weekly, Ending Thursday"
+ "\n "
+ "\n wew = Weekly, Ending Wednesday"
+ "\n "
+ "\n wetu = Weekly, Ending Tuesday"
+ "\n "
+ "\n wem = Weekly, Ending Monday"
+ "\n "
+ "\n wesu = Weekly, Ending Sunday"
+ "\n "
+ "\n wesa = Weekly, Ending Saturday"
+ "\n "
+ "\n bwew = Biweekly, Ending Wednesday"
+ "\n "
+ "\n bwem = Biweekly, Ending Monday",
"description": "Frequency aggregation to convert high frequency data to lower frequency.\n \n None = No change\n \n a = Annual\n \n q = Quarterly\n \n m = Monthly\n \n w = Weekly\n \n d = Daily\n \n wef = Weekly, Ending Friday\n \n weth = Weekly, Ending Thursday\n \n wew = Weekly, Ending Wednesday\n \n wetu = Weekly, Ending Tuesday\n \n wem = Weekly, Ending Monday\n \n wesu = Weekly, Ending Sunday\n \n wesa = Weekly, Ending Saturday\n \n bwew = Biweekly, Ending Wednesday\n \n bwem = Biweekly, Ending Monday",
"optional": True,
"type": "text",
"value": None,
"multiple_items_allowed": {},
"options": {
"fred": [
{"label": "a", "value": "a"},
@@ -238,24 +213,17 @@ def mock_openapi_json():
{"label": "bwem", "value": "bwem"},
]
},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
"show": True,
},
{
"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",
"multiple_items_allowed": {},
"options": {
"fred": [
{"label": "avg", "value": "avg"},
@@ -263,35 +231,17 @@ def mock_openapi_json():
{"label": "eop", "value": "eop"},
]
},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "transform",
"label": "Transform",
"description": "Transformation type"
+ "\n "
+ "\n None = No transformation"
+ "\n "
+ "\n chg = Change"
+ "\n "
+ "\n ch1 = Change from Year Ago"
+ "\n "
+ "\n pch = Percent Change"
+ "\n "
+ "\n pc1 = Percent Change from Year Ago"
+ "\n "
+ "\n pca = Compounded Annual Rate of Change"
+ "\n "
+ "\n cch = Continuously Compounded Rate of Change"
+ "\n "
+ "\n cca = Continuously Compounded Annual Rate of Change"
+ "\n "
+ "\n log = Natural Log",
"description": "Transformation type\n \n None = No transformation\n \n chg = Change\n \n ch1 = Change from Year Ago\n \n pch = Percent Change\n \n pc1 = Percent Change from Year Ago\n \n pca = Compounded Annual Rate of Change\n \n cch = Continuously Compounded Rate of Change\n \n cca = Continuously Compounded Annual Rate of Change\n \n log = Natural Log",
"optional": True,
"type": "text",
"value": None,
"multiple_items_allowed": {},
"options": {
"fred": [
{"label": "chg", "value": "chg"},
@@ -304,10 +254,8 @@ def mock_openapi_json():
{"label": "log", "value": "log"},
]
},
"multiple_items_allowed": {},
"available_providers": ["fred"],
"show": True,
"x-widget_config": {},
"show": True,
},
],
True,
@@ -322,6 +270,7 @@ def mock_openapi_json():
"description": "Source of the data.",
"optional": True,
"type": "text",
"value": None,
"available_providers": ["sec"],
"show": True,
},
@@ -330,24 +279,22 @@ def mock_openapi_json():
"label": "Query",
"description": "Search query.",
"optional": True,
"value": "",
"options": {"sec": []},
"multiple_items_allowed": {},
"available_providers": ["sec"],
"type": "text",
"show": True,
"value": "",
"multiple_items_allowed": {},
"options": {"sec": []},
"x-widget_config": {},
"show": True,
},
{
"parameter_name": "use_cache",
"label": "Use Cache",
"description": "Whether or not to use cache.",
"optional": True,
"value": True,
"options": {"sec": []},
"multiple_items_allowed": {},
"available_providers": ["sec"],
"type": "boolean",
"value": True,
"multiple_items_allowed": {},
"options": {"sec": []},
"x-widget_config": {},
"show": True,
},
@@ -356,10 +303,10 @@ def mock_openapi_json():
"label": "Url",
"description": "Enter an optional URL path to fetch the next level.",
"optional": True,
"type": "text",
"value": None,
"options": {"sec": []},
"multiple_items_allowed": {},
"available_providers": ["sec"],
"options": {"sec": []},
"x-widget_config": {},
"show": True,
},
@@ -390,6 +337,17 @@ def test_get_data_schema_for_widget(mock_openapi_json, openapi_operation_id):
assert schema is not None
@pytest.mark.parametrize(
"openapi_operation_id",
[
"form_submit_form_submit_post",
],
)
def test_post_query_schema_for_widget(mock_openapi_json, openapi_operation_id):
schema = post_query_schema_for_widget(mock_openapi_json, openapi_operation_id)
assert schema
@pytest.mark.parametrize(
"openapi_operation_id",
[