Skip to main content

openbb-api

The, openbb-platform-api, Python package is for converting FastAPI instances, or openapi.json objects, into OpenBB Workspace Backends and Widget definitions.

It provides drop-in-like functionality for the FastAPI + Uvicorn development stack, and is intended to speed up the development of Workspace Apps and Backends with Minimal Viable Python.

Features

  • Bulk-generate widgets from an OpenAPI JSON schema file.
  • Start and configure Backends for Workspace using arbitrary Python files and FastAPI app factories, or use the environment's installed Open Data Platform extensions.
  • Merges inline widget configurations with automatically generated content for customizing any property of a widget's definition.
  • No separate widgets.json file required.
  • Use an existing widgets.json file, or create a new one, and it can be edited live without requiring restart.
  • Automatically adds endpoints to serve Workspace configuration JSON files.

Installation

Install from PyPI in an environment with a Python version between 3.9 and 3.13:

pip install openbb-platform-api

Usage

Without supplying parameters, the script loads a FastAPI instance generated by the environment's installed Open Data Platform extensions and preferences.

If there are no installed extensions, and no --app parameter is supplied, an API with zero endpoints and widgets will be created.

Defaults are, --host 127.0.0.1 --port 6900, and it will fallback to the next available port if already in use.

Keyword Arguments

--app                           Absolute path to the Python file with the target FastAPI instance. Default is the installed Open Data Platform API.
--name Name of the FastAPI instance in the app file. Default is 'app'.
--factory Flag to indicate if the app name is a factory function. Default is 'false'.
--editable Flag to make widgets.json an editable file that can be modified during runtime. Default is 'false'.
--build If the file already exists, changes prompt action to overwrite/append/ignore. Only valid when --editable true.
--no-build Do not build the widgets.json file. Use this flag to load an existing widgets.json file without checking for updates.
--exclude JSON encoded list of API paths to exclude from widgets.json. Disable entire routes with '*' - e.g. '["/api/v1/*"]'.
--widgets-json Absolute/relative path to use as the widgets.json file. Default is ~/envs/{env}/assets/widgets.json, when --editable is 'true'.
--apps-json Absolute/relative path to use as the apps.json file for the server. Default is ~/OpenBBUserData/workspace_apps.json.
--agents-json Absolute/relative path to use as the agents.json file. Including this will add the /agents endpoint to the API.

Remaining parameters are passed to uvicorn.run

Example Syntax

openbb-api --app ./some_file.py --host 0.0.0.0 --port 8005 --reload

Factory Flag

If the FastAPI instance is served via factory function, set the --factory flag.

Also, declare the name of the factory function.

openbb-api --app some_file.py:main --factory

App or Factory Name

It is assumed that the FastAPI instance within the module is named, app.

openbb-api --app some_file.py --name my_app

Inline Widget Definitions

With FastAPI and Pydantic, widget definitions can be supplied inside the code and openapi.json schema.

OpenAPI Extra

The entrypoint at the function-level provides the same utility as examples demonstrating a @register_widget decorator, with no required imports or boiler-plate code.

Define as many, or few, items as desired here. They will be given the highest priority in the final result, overriding pre-existing definitions within the route.

from fastapi import FastAPI

app = FastAPI()

app.get(
"/some_endpoint",
openapi_extra={
"widget_config": {
# Any key:value object defined on the Widgets JSON Reference page.
"name": "Custom Widget Name",
"description": "Override the function's docstring with a different description",
}
}
)
async def endpoint_func():
"""Description that gets transferred to Widget's description."""
pass

Exclude Endpoint

An API endpoint may not be intended as a widget, like an optionsEndpoint. Exclude it from the widgets by setting {"exclude": True}.

@app.get(
"/some_param_choices",
openapi_extra={
"widget_config": {
"exclude": True
}
}
)
async def some_param_choices():
return [{"label": "Choice 1", "value": "choice1"}]

Dropdown choices are generated automatically from a Literal Type of function parameter.

from typing import Literal

@app.get(
"/some_endpoint_with_dropdown",
)
async def some_endpoint_with_dropdown(
choices: Literal[
"Choice 1",
"Choice 2",
"Choice 3"
] = "Choice 3"
):
pass

The params array in the Widget definition will end up like:

{
"widgetId": "some_endpoint_with_dropdown_custom_obb",
"params": [
{
"paramName": "choices",
"label": "Choices",
"value": "Choice 3",
"type": "text",
"description": "",
"options": [
{"label": "Choice 1", "value": "Choice 1"},
{"label": "Choice 2", "value": "Choice 2"},
{"label": "Choice 3", "value": "Choice 3"},
],
},
],
}

JSON Schema Extra

Annotating parameters and response models extends the capabilities for auto-generation, and is a way to define items unreachable by automation.

Additional settings, compatible with widgets.json, are placed in the json_schema_extra dictionary, under the key, x-widget_config.

from typing import Annotated
from fastapi import FastAPI, Query

my_param: Annotated[
str,
Query(
title="My Title",
description="My custom hovertext with detailed information",
json_schema_extra={
"x-widget_config": {
"optionsEndpoint": "/my_param_choices_endpoint_returning_list_of_dictionaries" #[{"label": "Display Name": "value": "actual_value"}]
}
}
)
]

app = FastAPI()

@app.get(
"/annotated_endpoint",
openapi_extra={
"widget_config": {
"params": [
"paramName": "my_param", # Identify the parameter to match with and update.
"value": "new_default_value" # Tell Workspace to use a different default value.
]
}
}
)
async def annotated_endpoint(my_param = "") -> str: # Defining a `str` here marks the widget as `{"type":"markdown"}`
"""Example creating markdown widget."""
return "Hello world!"

Column definitions for tables are discovered by using a Pydantic model as the return type.

import datetime
from fastapi import FastAPI
from pydantic import BaseModel, Field

class MyData(BaseModel):
"""This is a custom response model."""

# Add fields to the model.
column_1: datetime.date = Field(
description="The date column is a mandatory field.",
title="Some Date",
)
column_2: Optional[str] = Field(
default=None,
description="This is an optional string column.",
title="Some String",
)
column_3: int = Field(
default=-1,
description="This is an integer column.",
title="Some Integer",
)
column_4: float = Field(
default=10.25,
description="This is a float column.",
title="Some Float",
)
column_5: float = Field(
default=10.25,
description="This is a percent column.",
title="Some Percent",
json_schema_extra={"x-widget_config": {"formatterFn": "percent"}},
)
column_6: float = Field(
default=0.1025,
description="This is a normalized percent value adjusted for presentation.",
title="Some Normalized Percent",
json_schema_extra={
"x-widget_config": {
"formatterFn": "normalizedPercent",
"renderFn": "greenRed",
}
},
)


@app.get("/hello_data")
async def hello_data() -> list[MyData]: # Define response as a list of models. This sets {"type":"table"} in the widget definition.
"""Widget description created by docstring."""
# Do something with the parameters and return the result of work.
return [MyData(column_1=datetime.date.today(), column_2="Hello!")]

Convert From OpenAPI JSON

Convert openapi.json to widgets.json with an imported function.

from openbb_platform_api.utils.widgets import build_json

This is the spec file for a basic FastAPI server with only one endpoint that returns a JSON list object:

openapi.json
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"stock_quote": {
"get": {
"summary": "Quote",
"description": "Widget description derived from the endpoint's docstring.",
"operationId": "quotestock_quote_get",
"parameters": [
{
"name": "some_parameter",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"title": "Some Parameter"
}
},
{
"name": "symbol",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "AAPL",
"title": "Symbol"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {},
"title": "Response Quotestock Quote Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
}
}
}
}

Assuming the spec above is an in-memory object, use the syntax below to convert it.

import json
from openbb_platform_api.utils.widgets import build_json

openapi = json.loads(...)
widgets = json.dumps(build_json(openapi), []) # The empty list can be a list of paths to ignore.
widgets.json
{
"stock_quote_custom_obb": {
"name": "Stock Quote",
"description": "Widget description derived from the endpoint's docstring.",
"category": "Stock Quote",
"type": "table",
"searchCategory": "Stock Quote",
"widgetId": "stock_quote_custom_obb",
"params": [
{
"label": "Some Parameter",
"description": "Some Parameter",
"type": "text",
"value": null,
"show": true,
"paramName": "some_parameter"
},
{
"label": "Symbol",
"description": "Symbol",
"type": "text",
"value": "AAPL",
"show": true,
"paramName": "symbol"
}
],
"endpoint": "stock_quote",
"runButton": false,
"gridData": {
"w": 40,
"h": 15
},
"data": {
"dataKey": "",
"table": {
"showAll": true,
"enableAdvanced": true
}
},
"source": [
"Custom"
]
}
}

Examples

The default widget type is table, and all that is required to generate a basic AgGrid widget is to return a list of dictionaries.

note

It should be assumed that app is an instance of FastAPI.

from fastapi import FastAPI

app = FastAPI()
@app.get("/table_widget")
async def table_widget() -> list:
"""Returns a table widget with no column definitions."""
return [{"column1": "Hello", "column2": "World!"}]

Markdown Widget

Define the response output as a string to set the generated widget entry as a Markdown widget.

@app.get("/markdown_widget")
async def markdown_widget() -> str:
"""Returns a markdown widget"""
return "# Markdown Widget"

Markdown Widget

Metric Widget

A Metric Widget is generated by importing a response model type, and returning a dictionary.

from openbb_platform_api.response_models import MetricResponseModel

@app.get("/test_metric", response_model=MetricResponseModel)
async def test_metric():
"""Widget description created by docstring."""
return dict(label="Example Label", value=12345, delta=5.67)

Metric Widget

Generated Widget Entry
{
"test_metric_custom_obb": {
"name": "Test Metric",
"description": "Widget description created by docstring.",
"category": "Metric",
"type": "metric",
"searchCategory": "Metric",
"widgetId": "test_metric_custom_obb",
"params": [],
"endpoint": "/test_metric",
"runButton": false,
"gridData": {
"w": 4,
"h": 5
},
"data": {
"dataKey": "",
"table": {
"showAll": true
}
},
"source": [
"Custom"
]
}
}
info

Return multiple metrics in the same widget by returning a list of MetricResponseModel types.

@app.get("/test_metric", response_model=list[MetricResponseModel])
async def test_metric():
"""Widget description created by docstring."""
return [
dict(label="Example Label", value=12345, delta=5.67),
dict(label="Another Label", value=67890, delta=-2.34),
]

PDF Widget

Generate a PDF widget by importing a response model and returning a dictionary. The model formats the PDF as a base64-encoded string that can be read by Workspace.

from typing import Annotated

from fastapi import Query
from openbb_platform_api.response_models import PdfResponseModel

@app.get("/open_pdf", response_model=PdfResponseModel)
async def open_pdf(
file_path: Annotated[
str,
Query(
description="Local path to the PDF document.",
title="File Path",
),
],
):
"""Open a local PDF document."""
with open(file_path, "rb") as file:
pdf = file.read()

return dict(
content=pdf,
)
Generated Widget Entry
{
"open_pdf_custom_obb": {
"name": "Open PDF",
"description": "Open a local PDF document.",
"category": "File",
"type": "pdf",
"searchCategory": "File",
"widgetId": "open_pdf_custom_obb",
"params": [
{
"label": "File Path",
"description": "Local path to the PDF document.",
"optional": false,
"type": "text",
"value": null,
"show": true,
"paramName": "file_path"
}
],
"endpoint": "/open_pdf",
"runButton": false,
"gridData": {
"w": 20,
"h": 25
},
"data": {
"dataKey": "",
"table": {
"showAll": true
}
},
"source": [
"Custom"
],
"refetchInterval": false,
"subCategory": "PDF"
}
}

Plotly Chart Widget

Plotly Figure objects should be returned as their JSON-serializable, dictionary, representation. At minimum, the openapi_extra dictionary should contain the following:

@app.get(
"/hello_chart",
openapi_extra={"widget_config": {"type": "chart"}},
)
async def hello_chart() -> dict:
"""Widget description created by docstring."""
from plotly.graph_objs import Bar, Layout, Figure

fig = Figure(
data=[Bar(x=["A", "B", "C"], y=[1, 2, 3])],
layout=Layout(title="Hello Chart!", template="plotly_dark"),
)

return fig.to_plotly_json()
Generated Widget Entry
{
"hello_chart_custom_obb": {
"name": "Hello Chart",
"description": "Widget description created by docstring.",
"category": "Hello Chart",
"type": "chart",
"searchCategory": "Hello Chart",
"widgetId": "hello_chart_custom_obb",
"params": [],
"endpoint": "/hello_chart",
"runButton": false,
"gridData": {
"w": 40,
"h": 15
},
"data": {
"dataKey": "",
"table": {
"showAll": true
}
},
"source": [
"Custom"
]
}
}

Omni Widget

An Omni Widget requires a little more structure, it relies on Pydantic models to generate query parameters and validate the output.

  • POST endpoint where all parameters are sent to the request body as a dictionary.
    • A single, positional argument.
  • Inherit from utility class for Input model and parameter definitions.
    • Import: from openbb_platform_api.query_models import OmniWidgetInput
    • Pre-defined prompt parameter. Where required, overwrite the existing in the inheriting model's definition.
  • Use utility class for API response model.
    • Import: from openbb_platform_api.response_models import OmniWidgetResponseModel
    • Simplifies output handling by dynamically setting the data_format object based on the supplied content.
  • Define the response model as, OmniWidgetResponseModel, return a dictionary from the function.
    • {"content": ...}
    • API will validate the content and assign the output based on:
      • "text": For markdown/text content
      • "table": For tabular data (list of dictionaries or dictionary of arrays)
      • "chart": For Plotly Figure objects
Code
from typing import Literal, Optional
from openbb_platform_api.query_models import OmniWidgetInput
from openbb_platform_api.response_models import OmniWidgetResponseModel
from pydantic import Field

class TestOmniWidgetQueryModel(OmniWidgetInput):
"""Test query model for OmniWidget."""
# Here, all parameters are required except `parse_as`.
param1: str = Field(description="A string parameter for testing")
param2: int = Field(description="An integer parameter for testing")
param3: bool = Field(default=False, description="A boolean parameter for testing")
start_date: str = Field(description="The start date for testing")
end_date: str = Field(description="The end date for testing")
parse_as: Optional[Literal["table", "chart", "text"]] = Field( # Here for demonstration and handled by function output validation.
default=None,
description="The format to parse the response as, either 'table', 'chart', or 'text'."
+ " If not defined, the model will try to infer the type based on the content.",
)

@app.post("/omni_widget", response_model=OmniWidgetResponseModel)
async def create_omni_widget(item: TestOmniWidgetQueryModel):
"""This is a test endpoint for generating an OmniWidget in OpenBB Workspace."""
# Here you would process the incoming request and return a response

some_test_data = [
{"prompt": item.prompt, # This is the text area box on the widget.
"param1": item.param1,
"param2": item.param2,
"param3": item.param3,
"start_date": item.start_date,
"end_date": item.end_date,
}]

if item.parse_as == "chart": # This could also be an instance of plotly.graph_objects.Figure
some_test_data = {
"data": [{"type": "bar", "x": ["A", "B", "C"], "y": [1, 2, 3]}],
"layout": {"template": "plotly_dark", "title": {"text": "Hello Chart!"}}
}
elif item.parse_as == "text":
some_test_data = f"""
### This is a test OmniWidget response

- Prompt: {item.prompt}
- Param1: {item.param1}
- Param2: {item.param2}
- Param3: {item.param3}
- Start Date: {item.start_date}
- End Date: {item.end_date}
"""
return {"content": some_test_data}

omni widget

Generated Widget Entry
{
"omni_widget_custom_obb": {
"name": "Omni Widget",
"description": "This is a test endpoint for generating an OmniWidget in OpenBB Workspace.",
"category": "Omni Widget",
"type": "omni",
"searchCategory": "Omni Widget",
"widgetId": "omni_widget_custom_obb",
"params": [
{
"label": "Prompt",
"description": "Input prompt value for the OmniWidget.",
"optional": true,
"type": "text",
"value": "",
"show": false,
"paramName": "prompt"
},
{
"label": "Param 1",
"description": "A string parameter for testing",
"optional": true,
"type": "text",
"value": null,
"paramName": "param_1"
},
{
"label": "Param 2",
"description": "An integer parameter for testing",
"optional": true,
"type": "number",
"value": null,
"paramName": "param_2"
},
{
"label": "Param 3",
"description": "A boolean parameter for testing",
"optional": true,
"type": "boolean",
"value": false,
"paramName": "param_3"
},
{
"label": "Start Date",
"description": "The start date for testing",
"optional": true,
"type": "date",
"value": null,
"paramName": "start_date"
},
{
"label": "End Date",
"description": "The end date for testing",
"optional": true,
"type": "date",
"value": null,
"paramName": "end_date"
},
{
"label": "Parse As",
"description": "The format to parse the response as, either 'table', 'chart', or 'text'. If not defined, the model will try to infer the type based on the content.",
"optional": true,
"type": "text",
"value": null,
"options": [
{
"label": "table",
"value": "table"
},
{
"label": "chart",
"value": "chart"
},
{
"label": "text",
"value": "text"
}
],
"paramName": "parse_as"
}
],
"endpoint": "/omni_widget",
"runButton": false,
"gridData": {
"w": 40,
"h": 15
},
"data": {
"dataKey": "",
"table": {
"showAll": true
}
},
"source": [
"Custom"
]
}
}

Form Widget

An input form can be added as a widget parameter, where a successful submission to a POST endpoint triggers a refresh of the widget data.

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

  • GET request route defines in top-level widget_config:
    • {"form_endpoint": "/path_to/form_post_endpoint"}
  • POST method route takes 1 positional argument, a sub-class of Pydantic BaseModel.
    • Create a model, similar to annotated table fields, defining all inputs to the form.
    • Submit button added automatically if not manually defined.

The following snippet creates a widget with a form as the input, and an output table of all submitted forms, as processed through the IntakeForm model.

Code
import uuid
from datetime import date as dateType
from typing import Literal, Union

from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field

app = FastAPI()

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


class GeneralIntake(BaseModel):
"""Submit a form via POST request."""

model_config = ConfigDict(
extra="ignore", model_title_generator=lambda model: "Submit Form"
)

date_created: dateType = Field(alias="Created On", default_factory=dateType.today)
first_name: str = Field(alias="First Name")
last_name: str = Field(alias="Last Name")
email: str = Field(alias="Contact Email")
dob: dateType = Field(
alias="Date Of Birth",
)
account_types: Union[AccountTypes, list[AccountTypes]] = Field(
alias="Type Of Account",
json_schema_extra={
"x-widget_config": {"multiSelect": True},
},
)


class IntakeForm(BaseModel):
"""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_submit")
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",
openapi_extra= {
"widget_config": {
"form_endpoint": "/general_intake_submit",
},
},
)
async def general_intake() -> list[IntakeForm]:
return INTAKE_FORMS
Form Input Widget
Generated Widget Entry
{
"general_intake_custom_obb": {
"name": "General Intake",
"description": "",
"category": "General Intake",
"type": "table",
"searchCategory": "General Intake",
"widgetId": "general_intake_custom_obb",
"params": [
{
"type": "form",
"paramName": "form",
"label": "Submit Form",
"description": "Submit a form via POST request.",
"endpoint": "/general_intake_submit",
"inputParams": [
{
"label": "Created On",
"description": "Created On",
"type": "date",
"value": null,
"paramName": "Created On"
},
{
"label": "First Name",
"description": "First Name",
"type": "text",
"value": null,
"paramName": "First Name"
},
{
"label": "Last Name",
"description": "Last Name",
"type": "text",
"value": null,
"paramName": "Last Name"
},
{
"label": "Contact Email",
"description": "Contact Email",
"type": "text",
"value": null,
"paramName": "Contact Email"
},
{
"label": "Date Of Birth",
"description": "Date Of Birth",
"type": "date",
"value": null,
"paramName": "Date Of Birth"
},
{
"label": "Type Of Account",
"description": "Type Of Account",
"type": "text",
"value": null,
"multiSelect": true,
"options": [
{
"label": "General Fund",
"value": "General Fund"
},
{
"label": "Separately Managed",
"value": "Separately Managed"
},
{
"label": "Private Equity",
"value": "Private Equity"
},
{
"label": "Family Office",
"value": "Family Office"
}
],
"paramName": "Type Of Account"
},
{
"paramName": "submit",
"label": "Submit",
"value": true,
"type": "button",
"description": "Submit the form."
}
]
}
],
"endpoint": "/general_intake",
"runButton": false,
"gridData": {
"w": 40,
"h": 15
},
"data": {
"dataKey": "",
"table": {
"showAll": true,
"columnsDefs": [
{
"field": "contacted",
"formatterFn": null,
"headerName": "Contacted",
"headerTooltip": "Contacted",
"cellDataType": "text"
},
{
"field": "date_created",
"formatterFn": null,
"headerName": "Created On",
"headerTooltip": "Created On",
"cellDataType": "date"
},
{
"field": "first_name",
"formatterFn": null,
"headerName": "First Name",
"headerTooltip": "First Name",
"cellDataType": "text"
},
{
"field": "last_name",
"formatterFn": null,
"headerName": "Last Name",
"headerTooltip": "Last Name",
"cellDataType": "text"
},
{
"field": "email",
"formatterFn": null,
"headerName": "Contact Email",
"headerTooltip": "Contact Email",
"cellDataType": "text"
},
{
"field": "dob",
"formatterFn": null,
"headerName": "Date Of Birth",
"headerTooltip": "Date Of Birth",
"cellDataType": "date"
},
{
"field": "account_types",
"formatterFn": null,
"headerName": "Account Interest",
"headerTooltip": "Account Interest",
"cellDataType": "text"
},
{
"field": "unique_id",
"formatterFn": null,
"headerName": "Unique ID",
"headerTooltip": "Unique ID",
"cellDataType": "text"
}
],
"enableAdvanced": true
}
},
"source": [
"Custom"
],
"form_endpoint": "/general_intake_submit"
}
}