Skip to content

Commit f84ac76

Browse files
authored
[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
1 parent 2e22d79 commit f84ac76

File tree

10 files changed

+411
-250
lines changed

10 files changed

+411
-250
lines changed

openbb_platform/extensions/economy/openbb_economy/economy_router.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ async def direction_of_trade(
870870
"h": 27,
871871
},
872872
"refetchInterval": False,
873-
"endpoint": "/api/v1/economy/fomc_documents/download",
873+
"endpoint": f"{api_prefix}/economy/fomc_documents/download",
874874
"params": [
875875
{
876876
"type": "endpoint",
@@ -915,7 +915,7 @@ async def fomc_documents(
915915
"""
916916
results = await OBBject.from_query(Query(**locals()))
917917

918-
return results.results.content
918+
return results.results.content # type: ignore
919919

920920

921921
# This endpoint is used to download FOMC documents in Workspace.
@@ -926,11 +926,7 @@ async def fomc_documents(
926926
@router._api_router.post(
927927
"/fomc_documents/download",
928928
include_in_schema=False,
929-
openapi_extra={
930-
"widget_config": {
931-
"exclude": True,
932-
}
933-
},
929+
openapi_extra={},
934930
)
935931
async def fomc_documents_download(params: Annotated[dict, Body()]) -> list:
936932
"""
@@ -947,7 +943,7 @@ async def fomc_documents_download(params: Annotated[dict, Body()]) -> list:
947943

948944
urls = params.get("url", [])
949945

950-
results = []
946+
results: list = []
951947
for url in urls:
952948
try:
953949
response = make_request(url)

openbb_platform/extensions/platform_api/README.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,10 +433,12 @@ For example, the response to submitting a form can be a Markdown widget with a c
433433

434434
The entry in `widgets.json` will be automatically created if the conditions below are met:
435435

436-
- GET and POST methods must share the same API route.
436+
- GET request defines in top-level `widget_config`:
437+
- `{"form_endpoint": /path_to/form_post_endpoint}`
437438
- POST method takes 1 positional argument, a sub-class of Pydantic BaseModel.
438439
- Create a model, like annotated table fields, defining all inputs to the form.
439440

441+
440442
#### Example
441443

442444
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
447449
from typing import Literal, Union
448450

449451
# from fastapi import FastAPI
450-
from openbb_platform_api.query_models import FormData
451452
from openbb_platform_api.response_models import Data
452-
from pydantic import ConfigDict, Field
453+
from pydantic import BaseModel, ConfigDict, Field
453454

454455
# app = FastAPI()
455456

456457
AccountTypes = Literal["General Fund", "Separately Managed", "Private Equity", "Family Office"]
457458

458-
class GeneralIntake(FormData):
459+
class GeneralIntake(BaseModel):
459460
"""Submit a form via POST request."""
460461

461462
date_created: dateType = Field(
@@ -476,7 +477,11 @@ class GeneralIntake(FormData):
476477
submit: bool = Field(
477478
default=True,
478479
title="Submit",
479-
type="button", # This creates a button, when pressed the parameter is sent as True
480+
json_schema_extra={
481+
"x-widget_config": {
482+
"type": "button",
483+
},
484+
}
480485
)
481486

482487

@@ -510,7 +515,7 @@ class IntakeForm(Data):
510515
INTAKE_FORMS: list[IntakeForm] = []
511516

512517

513-
@app.post("/general_intake")
518+
@app.post("/general_intake_submit")
514519
async def general_intake_post(data: GeneralIntake) -> bool:
515520
global INTAKE_FORMS
516521
try:
@@ -520,7 +525,14 @@ async def general_intake_post(data: GeneralIntake) -> bool:
520525
raise e from e
521526

522527

523-
@app.get("/general_intake")
528+
@app.get(
529+
"/general_intake",
530+
openapi_extra= {
531+
"widget_config": {
532+
"form_endpoint": "/general_intake_submit",
533+
},
534+
},
535+
)
524536
async def general_intake() -> list[IntakeForm]:
525537
return INTAKE_FORMS
526538
```
@@ -529,6 +541,12 @@ async def general_intake() -> list[IntakeForm]:
529541

530542
### Omni Widget Example
531543

544+
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").
545+
546+
The returned type can be a list of records (table), a Plotly Figure, or formatted Markdwon.
547+
The model will attempt to assign the correct return type dynamically.
548+
549+
Set the response model as `OmniWidgetResponseModel`, then return `{"content": your_content}` from the endpoint.
532550

533551
```python
534552
from typing import Literal, Optional
@@ -565,7 +583,7 @@ async def create_omni_widget(item: TestOmniWidgetQueryModel):
565583
if item.parse_as == "chart":
566584
some_test_data = {
567585
"data": [{"type": "bar", "x": ["A", "B", "C"], "y": [1, 2, 3]}],
568-
"layout": {"template": "...", "title": {"text": "Hello Chart!"}}
586+
"layout": {"template": "plotly_dark", "title": {"text": "Hello Chart!"}}
569587
}
570588
elif item.parse_as == "text":
571589
some_test_data = f"""

openbb_platform/extensions/platform_api/openbb_platform_api/query_models.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@
77
from pydantic.alias_generators import to_snake
88

99

10-
class FormData(Data):
11-
"""Submit a form via POST request."""
12-
13-
model_config = ConfigDict(
14-
extra="allow",
15-
alias_generator=AliasGenerator(to_snake),
16-
title="Submit Form",
17-
)
18-
19-
2010
class OmniWidgetInput(Data):
2111
"""Input for OmniWidget."""
2212

openbb_platform/extensions/platform_api/openbb_platform_api/utils/openapi.py

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""OpenAPI parsing Utils."""
22

3+
# pylint: disable=C0302,R0912
4+
# flake8: noqa: PLR0912
5+
36
from typing import Optional
47

58
from openbb_core.provider.utils.helpers import to_snake_case
@@ -937,39 +940,93 @@ def set_param(k, v):
937940
):
938941
# Get the reference to the schema for the request body.
939942

940-
param_ref = (
941-
schema["items"].get("$ref")
942-
if "items" in schema
943-
else schema.get("$ref") or schema
944-
)
943+
title = schema.get("title")
944+
providers: list[str] = []
945945

946-
if isinstance(param_ref, dict) and "type" in param_ref:
947-
param_ref = param_ref["type"]
946+
if title and title in schema:
947+
providers = [title]
948+
elif title and "," in title:
949+
providers = title.split(",")
950+
else:
951+
providers = ["Custom"]
952+
953+
if params := _route.get("parameters"):
954+
if isinstance(params, list):
955+
for _param in params:
956+
set_param(_param["name"], _param["schema"])
957+
elif isinstance(params, dict):
958+
for k, v in params.items():
959+
set_param(k, v)
948960

949-
if param_ref and isinstance(param_ref, str):
950-
# Extract the schema name from the reference
951-
schema_name = param_ref.split("/")[-1]
952-
schema = openapi_json["components"]["schemas"].get(schema_name, schema_name)
953-
props = {} if isinstance(schema, str) else schema.get("properties", {})
954-
955-
for k, v in props.items():
956-
if target_schema and target_schema != k:
957-
continue
958-
if nested_schema := v.get("$ref"):
959-
nested_schema_name = nested_schema.split("/")[-1]
960-
nested_schema = openapi_json["components"]["schemas"].get(
961-
nested_schema_name, {}
962-
)
963-
for nested_k, nested_v in nested_schema.get(
964-
"properties", {}
965-
).items():
966-
set_param(nested_k, nested_v)
961+
if "items" in schema or "$ref" in schema:
962+
param_ref = (
963+
schema["items"].get("$ref")
964+
if "items" in schema
965+
else schema.get("$ref") or schema
966+
)
967967

968-
else:
969-
set_param(k, v)
968+
if isinstance(param_ref, dict) and "type" in param_ref:
969+
param_ref = param_ref["type"]
970970

971-
route_params: list[dict] = []
972-
providers = ["custom"]
971+
if param_ref and isinstance(param_ref, str):
972+
# Extract the schema name from the reference
973+
schema_name = param_ref.split("/")[-1]
974+
schema = openapi_json["components"]["schemas"].get(
975+
schema_name, schema_name
976+
)
977+
props = {} if isinstance(schema, str) else schema.get("properties", {})
978+
979+
for k, v in props.items():
980+
if target_schema and target_schema != k:
981+
continue
982+
if nested_schema := v.get("$ref"):
983+
nested_schema_name = nested_schema.split("/")[-1]
984+
nested_schema = openapi_json["components"]["schemas"].get(
985+
nested_schema_name, {}
986+
)
987+
for nested_k, nested_v in nested_schema.get(
988+
"properties", {}
989+
).items():
990+
set_param(nested_k, nested_v)
991+
992+
else:
993+
set_param(k, v)
994+
995+
route_params: list[dict] = []
996+
997+
for new_param_values in new_params.values():
998+
_new_values = new_param_values.copy()
999+
p = process_parameter(_new_values, providers)
1000+
if not p.get("exclude") and not p.get("x-widget_config", {}).get(
1001+
"exclude"
1002+
):
1003+
route_params.append(p)
1004+
1005+
return route_params
1006+
if "anyOf" in _route or "anyOf" in schema:
1007+
any_of_schema = (
1008+
schema.get("anyOf", [])
1009+
if "anyOf" in schema
1010+
else _route.get("anyOf", [])
1011+
)
1012+
for item in any_of_schema:
1013+
# If item is a $ref, resolve it
1014+
if "$ref" in item:
1015+
ref_name = item["$ref"].split("/")[-1]
1016+
ref_schema = openapi_json["components"]["schemas"].get(ref_name, {})
1017+
if "properties" in ref_schema:
1018+
for k, v in ref_schema["properties"].items():
1019+
if target_schema and target_schema != k:
1020+
continue
1021+
set_param(k, v)
1022+
# If item has properties directly
1023+
elif "properties" in item:
1024+
for k, v in item["properties"].items():
1025+
if target_schema and target_schema != k:
1026+
continue
1027+
set_param(k, v)
1028+
1029+
route_params = []
9731030

9741031
for new_param_values in new_params.values():
9751032
_new_values = new_param_values.copy()

0 commit comments

Comments
 (0)