openbb-platform-api 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbb_platform_api-1.0.0/PKG-INFO +37 -0
- openbb_platform_api-1.0.0/README.md +11 -0
- openbb_platform_api-1.0.0/openbb_platform_api/__init__.py +1 -0
- openbb_platform_api-1.0.0/openbb_platform_api/main.py +133 -0
- openbb_platform_api-1.0.0/openbb_platform_api/utils/__init__.py +1 -0
- openbb_platform_api-1.0.0/openbb_platform_api/utils/api.py +244 -0
- openbb_platform_api-1.0.0/openbb_platform_api/utils/openapi.py +454 -0
- openbb_platform_api-1.0.0/openbb_platform_api/utils/widgets.py +240 -0
- openbb_platform_api-1.0.0/pyproject.toml +27 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: openbb-platform-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenBB Platform API: Launch script and widgets builder for the OpenBB Platform API and Terminal Pro Connector.
|
|
5
|
+
Home-page: https://openbb.co
|
|
6
|
+
License: AGPL-3.0-only
|
|
7
|
+
Author: OpenBB
|
|
8
|
+
Author-email: hello@openbb.co
|
|
9
|
+
Requires-Python: >=3.9,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: black
|
|
17
|
+
Requires-Dist: deepdiff
|
|
18
|
+
Requires-Dist: openbb-core
|
|
19
|
+
Requires-Dist: poetry (>=1.8,<2.0)
|
|
20
|
+
Requires-Dist: ruff
|
|
21
|
+
Requires-Dist: setuptools
|
|
22
|
+
Project-URL: Documentation, https://docs.openbb.co
|
|
23
|
+
Project-URL: Repository, https://github.com/openbb-finance/openbb
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# OpenBB Platform API Meta Package
|
|
27
|
+
|
|
28
|
+
This is a meta package entry point for the OpenBB Platform API, and the OpenBB Terminal Pro data connector widgets build script.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install this package locally by navigating into the directory and entering:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OpenBB Platform API Meta Package
|
|
2
|
+
|
|
3
|
+
This is a meta package entry point for the OpenBB Platform API, and the OpenBB Terminal Pro data connector widgets build script.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install this package locally by navigating into the directory and entering:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install -e .
|
|
11
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OpenBB Platform API Meta Package."""
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""OpenBB Platform API.
|
|
2
|
+
|
|
3
|
+
Launch script and widgets builder for the OpenBB Terminal Custom Backend.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from openbb_core.api.rest_api import app
|
|
12
|
+
|
|
13
|
+
from .utils.api import check_port, get_user_settings, get_widgets_json, parse_args
|
|
14
|
+
|
|
15
|
+
FIRST_RUN = True
|
|
16
|
+
|
|
17
|
+
HOME = os.environ.get("HOME") or os.environ.get("USERPROFILE")
|
|
18
|
+
|
|
19
|
+
if not HOME:
|
|
20
|
+
raise ValueError("HOME or USERPROFILE environment variable not set.")
|
|
21
|
+
|
|
22
|
+
CURRENT_USER_SETTINGS = os.path.join(HOME, ".openbb_platform", "user_settings.json")
|
|
23
|
+
USER_SETTINGS_COPY = os.path.join(HOME, ".openbb_platform", "user_settings_backup.json")
|
|
24
|
+
|
|
25
|
+
# Widget filtering is optional and can be used to exclude widgets from the widgets.json file
|
|
26
|
+
# You can generate this filter on OpenBB Hub: https://my.openbb.co/app/platform/widgets
|
|
27
|
+
WIDGET_SETTINGS = os.path.join(HOME, ".openbb_platform", "widget_settings.json")
|
|
28
|
+
|
|
29
|
+
kwargs = parse_args()
|
|
30
|
+
build = kwargs.pop("build", True)
|
|
31
|
+
build = False if kwargs.pop("no-build", None) else build
|
|
32
|
+
login = kwargs.pop("login", False)
|
|
33
|
+
dont_filter = kwargs.pop("no-filter", False)
|
|
34
|
+
|
|
35
|
+
if not dont_filter and os.path.exists(WIDGET_SETTINGS):
|
|
36
|
+
with open(WIDGET_SETTINGS) as f:
|
|
37
|
+
try:
|
|
38
|
+
widget_exclude_filter = json.load(f)["exclude"]
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
widget_exclude_filter = []
|
|
41
|
+
else:
|
|
42
|
+
widget_exclude_filter = []
|
|
43
|
+
|
|
44
|
+
openapi = app.openapi()
|
|
45
|
+
|
|
46
|
+
# We don't need the current settings,
|
|
47
|
+
# but we need to call the function to update, login, and/or identify the settings file.
|
|
48
|
+
current_settings = get_user_settings(login, CURRENT_USER_SETTINGS, USER_SETTINGS_COPY)
|
|
49
|
+
|
|
50
|
+
widgets_json = get_widgets_json(build, openapi, widget_exclude_filter)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.get("/")
|
|
54
|
+
async def get_root():
|
|
55
|
+
"""Root response and welcome message."""
|
|
56
|
+
return JSONResponse(
|
|
57
|
+
content="Welcome to the OpenBB Platform API."
|
|
58
|
+
+ " Learn how to connect to Pro in docs.openbb.co/pro/data-connectors,"
|
|
59
|
+
+ " or see the API documentation here: /docs"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.get("/widgets.json")
|
|
64
|
+
async def get_widgets():
|
|
65
|
+
"""Widgets configuration file for the OpenBB Terminal Pro."""
|
|
66
|
+
# This allows us to serve an edited widgets.json file without reloading the server.
|
|
67
|
+
global FIRST_RUN # noqa PLW0603 # pylint: disable=global-statement
|
|
68
|
+
if FIRST_RUN is True:
|
|
69
|
+
FIRST_RUN = False
|
|
70
|
+
return JSONResponse(content=widgets_json)
|
|
71
|
+
return JSONResponse(content=get_widgets_json(False, openapi, widget_exclude_filter))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def launch_api(**_kwargs): # noqa PRL0912
|
|
75
|
+
"""Main function."""
|
|
76
|
+
host = _kwargs.pop("host", os.getenv("OPENBB_API_HOST", "127.0.0.1"))
|
|
77
|
+
if not host:
|
|
78
|
+
print( # noqa: T201
|
|
79
|
+
"\n\nOPENBB_API_HOST is set incorrectly. It should be an IP address or hostname."
|
|
80
|
+
)
|
|
81
|
+
host = input("Enter the host IP address or hostname: ")
|
|
82
|
+
if not host:
|
|
83
|
+
host = "127.0.0.1"
|
|
84
|
+
|
|
85
|
+
port = _kwargs.pop("port", os.getenv("OPENBB_API_PORT", "6900"))
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
port = int(port)
|
|
89
|
+
except ValueError:
|
|
90
|
+
print( # noqa: T201
|
|
91
|
+
"\n\nOPENBB_API_PORT is set incorrectly. It should be an port number."
|
|
92
|
+
)
|
|
93
|
+
port = input("Enter the port number: ")
|
|
94
|
+
try:
|
|
95
|
+
port = int(port)
|
|
96
|
+
except ValueError:
|
|
97
|
+
print("\n\nInvalid port number. Defaulting to 6900.") # noqa: T201
|
|
98
|
+
port = 6900
|
|
99
|
+
if port < 1025:
|
|
100
|
+
port = 6900
|
|
101
|
+
print( # noqa: T201
|
|
102
|
+
"\n\nInvalid port number, must be above 1024. Defaulting to 6900."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
free_port = check_port(host, port)
|
|
106
|
+
|
|
107
|
+
if free_port != port:
|
|
108
|
+
print( # noqa: T201
|
|
109
|
+
f"\n\nPort {port} is already in use. Using port {free_port}.\n"
|
|
110
|
+
)
|
|
111
|
+
port = free_port
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
package_name = __package__
|
|
115
|
+
uvicorn.run(f"{package_name}.main:app", host=host, port=port, **_kwargs)
|
|
116
|
+
finally:
|
|
117
|
+
# If user_settings_copy.json exists, then restore the original settings.
|
|
118
|
+
if os.path.exists(USER_SETTINGS_COPY):
|
|
119
|
+
print("\n\nRestoring the original settings.\n") # noqa: T201
|
|
120
|
+
os.replace(USER_SETTINGS_COPY, CURRENT_USER_SETTINGS)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
"""Launch the API."""
|
|
125
|
+
launch_api(**kwargs)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
main()
|
|
132
|
+
except KeyboardInterrupt:
|
|
133
|
+
print("Restoring the original settings.") # noqa: T201
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OpenBB Platform API Utils."""
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""API Utils."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
from deepdiff import DeepDiff
|
|
11
|
+
|
|
12
|
+
from .widgets import build_json
|
|
13
|
+
|
|
14
|
+
LAUNCH_SCRIPT_DESCRIPTION = """
|
|
15
|
+
Serve the OpenBB Platform API.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Launcher specific arguments:
|
|
19
|
+
|
|
20
|
+
--build Build the widgets.json file.
|
|
21
|
+
--no-build Do not build the widgets.json file.
|
|
22
|
+
--login Login to the OpenBB Platform.
|
|
23
|
+
--no-filter Do not filter the widgets.json file.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
All other arguments will be passed to uvicorn. Here are the most common ones:
|
|
27
|
+
|
|
28
|
+
--host TEXT Host IP address or hostname.
|
|
29
|
+
[default: 127.0.0.1]
|
|
30
|
+
--port INTEGER Port number.
|
|
31
|
+
[default: 6900]
|
|
32
|
+
--ssl-keyfile TEXT SSL key file.
|
|
33
|
+
--ssl-certfile TEXT SSL certificate file.
|
|
34
|
+
--ssl-keyfile-password TEXT SSL keyfile password.
|
|
35
|
+
--ssl-version INTEGER SSL version to use.
|
|
36
|
+
(see stdlib ssl module's)
|
|
37
|
+
[default: 17]
|
|
38
|
+
--ssl-cert-reqs INTEGER Whether client certificate is required.
|
|
39
|
+
(see stdlib ssl module's)
|
|
40
|
+
[default: 0]
|
|
41
|
+
--ssl-ca-certs TEXT CA certificates file.
|
|
42
|
+
--ssl-ciphers TEXT Ciphers to use.
|
|
43
|
+
(see stdlib ssl module's)
|
|
44
|
+
[default: TLSv1]
|
|
45
|
+
|
|
46
|
+
Run `uvicorn --help` to get the full list of arguments.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_port(host, port):
|
|
51
|
+
"""Check if the port number is free."""
|
|
52
|
+
port = int(port)
|
|
53
|
+
not_free = True
|
|
54
|
+
while not_free:
|
|
55
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
56
|
+
res = sock.connect_ex((host, port))
|
|
57
|
+
if res != 0:
|
|
58
|
+
not_free = False
|
|
59
|
+
else:
|
|
60
|
+
port += 1
|
|
61
|
+
return port
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_user_settings(
|
|
65
|
+
_login: bool, current_user_settings: str, user_settings_copy: str
|
|
66
|
+
):
|
|
67
|
+
"""Login to the OpenBB Platform."""
|
|
68
|
+
# pylint: disable=import-outside-toplevel
|
|
69
|
+
import getpass
|
|
70
|
+
|
|
71
|
+
if Path(current_user_settings).exists():
|
|
72
|
+
with open(current_user_settings, encoding="utf-8") as f:
|
|
73
|
+
_current_settings = json.load(f)
|
|
74
|
+
else:
|
|
75
|
+
_current_settings = {
|
|
76
|
+
"credentials": {},
|
|
77
|
+
"preferences": {},
|
|
78
|
+
"defaults": {"commands": {}},
|
|
79
|
+
}
|
|
80
|
+
if (isinstance(_login, str) and _login.lower() == "false") or not _login:
|
|
81
|
+
return _current_settings
|
|
82
|
+
|
|
83
|
+
pat = getpass.getpass(
|
|
84
|
+
"\n\nEnter your personal access token (PAT) to authorize the API and update your local settings."
|
|
85
|
+
+ "\nSkip to use a pre-configured 'user_settings.json' file."
|
|
86
|
+
+ "\nPress Enter to skip or copy (entered values are not displayed on screen) your PAT to the command line: "
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if pat:
|
|
90
|
+
from openbb_core.app.service.hub_service import HubService
|
|
91
|
+
|
|
92
|
+
hub_credentials: Dict = {}
|
|
93
|
+
hub_preferences: Dict = {}
|
|
94
|
+
hub_defaults: Dict = {}
|
|
95
|
+
try:
|
|
96
|
+
Hub = HubService()
|
|
97
|
+
_ = Hub.connect(pat=pat)
|
|
98
|
+
hub_settings = Hub.pull()
|
|
99
|
+
hub_credentials = json.loads(
|
|
100
|
+
hub_settings.credentials.model_dump_json() # pylint: disable=no-member
|
|
101
|
+
)
|
|
102
|
+
hub_preferences = json.loads(
|
|
103
|
+
hub_settings.preferences.model_dump_json() # pylint: disable=no-member
|
|
104
|
+
)
|
|
105
|
+
hub_defaults = json.loads(
|
|
106
|
+
hub_settings.defaults.model_dump_json() # pylint: disable=no-member
|
|
107
|
+
)
|
|
108
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
109
|
+
print( # noqa: T201
|
|
110
|
+
f"\n\nError connecting with Hub:\n{e}\n\nUsing the local settings.\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if hub_credentials:
|
|
114
|
+
# Prompt the user to ask if they want to persist the new settings
|
|
115
|
+
persist_input = (
|
|
116
|
+
input(
|
|
117
|
+
"\n\nDo you want to persist the new settings?"
|
|
118
|
+
+ " Not recommended for public machines. (yes/no): "
|
|
119
|
+
)
|
|
120
|
+
.strip()
|
|
121
|
+
.lower()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if persist_input in ["yes", "y"]:
|
|
125
|
+
PERSIST = True
|
|
126
|
+
elif persist_input in ["no", "n"]:
|
|
127
|
+
PERSIST = False
|
|
128
|
+
else:
|
|
129
|
+
print( # noqa: T201
|
|
130
|
+
"\n\nInvalid input. Defaulting to not persisting the new settings."
|
|
131
|
+
)
|
|
132
|
+
PERSIST = False
|
|
133
|
+
|
|
134
|
+
# Save the current settings to restore at the end of the session.
|
|
135
|
+
if PERSIST is False:
|
|
136
|
+
with open(user_settings_copy, "w", encoding="utf-8") as f:
|
|
137
|
+
json.dump(_current_settings, f, indent=4)
|
|
138
|
+
|
|
139
|
+
new_settings = _current_settings.copy()
|
|
140
|
+
new_settings.setdefault("credentials", {})
|
|
141
|
+
new_settings.setdefault("preferences", {})
|
|
142
|
+
new_settings.setdefault("defaults", {"commands": {}})
|
|
143
|
+
|
|
144
|
+
# Update the current settings with the new settings
|
|
145
|
+
if hub_credentials:
|
|
146
|
+
for k, v in hub_credentials.items():
|
|
147
|
+
if v:
|
|
148
|
+
new_settings["credentials"][k] = v.strip('"').strip("'")
|
|
149
|
+
|
|
150
|
+
if hub_preferences:
|
|
151
|
+
for k, v in hub_preferences.items():
|
|
152
|
+
if v:
|
|
153
|
+
new_settings["preferences"][k] = v
|
|
154
|
+
|
|
155
|
+
if hub_defaults:
|
|
156
|
+
for k, v in hub_defaults.items():
|
|
157
|
+
if k == "commands":
|
|
158
|
+
for key, value in hub_defaults["commands"].items():
|
|
159
|
+
if value:
|
|
160
|
+
new_settings["defaults"]["commands"][key] = value
|
|
161
|
+
elif v:
|
|
162
|
+
new_settings["defaults"][k] = v
|
|
163
|
+
else:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Write the new settings to the user_settings.json file
|
|
167
|
+
with open(current_user_settings, "w", encoding="utf-8") as f:
|
|
168
|
+
json.dump(new_settings, f, indent=4)
|
|
169
|
+
|
|
170
|
+
_current_settings = new_settings
|
|
171
|
+
|
|
172
|
+
return _current_settings
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_widgets_json(_build: bool, _openapi, widget_exclude_filter: list):
|
|
176
|
+
"""Generate and serve the widgets.json for the OpenBB Platform API."""
|
|
177
|
+
python_path = Path(sys.executable)
|
|
178
|
+
parent_path = python_path.parent if os.name == "nt" else python_path.parents[1]
|
|
179
|
+
widgets_json_path = parent_path.joinpath("assets", "widgets.json").resolve()
|
|
180
|
+
json_exists = widgets_json_path.exists()
|
|
181
|
+
|
|
182
|
+
if not json_exists:
|
|
183
|
+
widgets_json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
_build = True
|
|
185
|
+
|
|
186
|
+
existing_widgets_json: Dict = {}
|
|
187
|
+
|
|
188
|
+
if json_exists:
|
|
189
|
+
with open(widgets_json_path, encoding="utf-8") as f:
|
|
190
|
+
existing_widgets_json = json.load(f)
|
|
191
|
+
|
|
192
|
+
_widgets_json = (
|
|
193
|
+
existing_widgets_json
|
|
194
|
+
if _build is False
|
|
195
|
+
else build_json(_openapi, widget_exclude_filter)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if _build:
|
|
199
|
+
diff = DeepDiff(existing_widgets_json, _widgets_json, ignore_order=True)
|
|
200
|
+
merge_prompt = None
|
|
201
|
+
if diff and json_exists:
|
|
202
|
+
print("Differences found:", diff) # noqa: T201
|
|
203
|
+
merge_prompt = input(
|
|
204
|
+
"\nDo you want to overwrite the existing widgets.json configuration?"
|
|
205
|
+
"\nEnter 'n' to append existing with only new entries, or 'i' to ignore all changes. (y/n/i): "
|
|
206
|
+
)
|
|
207
|
+
if merge_prompt.lower().startswith("n"):
|
|
208
|
+
_widgets_json.update(existing_widgets_json)
|
|
209
|
+
elif merge_prompt.lower().startswith("i"):
|
|
210
|
+
_widgets_json = existing_widgets_json
|
|
211
|
+
|
|
212
|
+
if merge_prompt is None or not merge_prompt.lower().startswith("i"):
|
|
213
|
+
try:
|
|
214
|
+
with open(widgets_json_path, "w", encoding="utf-8") as f:
|
|
215
|
+
json.dump(_widgets_json, f, ensure_ascii=False, indent=4)
|
|
216
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
217
|
+
print( # noqa: T201
|
|
218
|
+
f"Error writing widgets.json: {e}. Loading from memory instead."
|
|
219
|
+
)
|
|
220
|
+
_widgets_json = (
|
|
221
|
+
existing_widgets_json
|
|
222
|
+
if existing_widgets_json
|
|
223
|
+
else build_json(_openapi, widget_exclude_filter)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return _widgets_json
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def parse_args():
|
|
230
|
+
"""Parse the launch script command line arguments."""
|
|
231
|
+
args = sys.argv[1:].copy()
|
|
232
|
+
_kwargs: Dict = {}
|
|
233
|
+
for i, arg in enumerate(args):
|
|
234
|
+
if arg == "--help":
|
|
235
|
+
print(LAUNCH_SCRIPT_DESCRIPTION) # noqa: T201
|
|
236
|
+
sys.exit(0)
|
|
237
|
+
if arg.startswith("--"):
|
|
238
|
+
key = arg[2:]
|
|
239
|
+
if i + 1 < len(args) and not args[i + 1].startswith("--"):
|
|
240
|
+
value = args[i + 1]
|
|
241
|
+
_kwargs[key] = value
|
|
242
|
+
else:
|
|
243
|
+
_kwargs[key] = True
|
|
244
|
+
return _kwargs
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""OpenAPI parsing Utils."""
|
|
2
|
+
|
|
3
|
+
from openbb_core.provider.utils.helpers import to_snake_case
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def extract_providers(params: list[dict]) -> list[str]:
|
|
7
|
+
"""
|
|
8
|
+
Extract provider options from parameters.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
params : List[Dict]
|
|
13
|
+
List of parameter dictionaries.
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
List[str]
|
|
18
|
+
List of provider options.
|
|
19
|
+
"""
|
|
20
|
+
provider_params = [p for p in params if p["name"] == "provider"]
|
|
21
|
+
if provider_params:
|
|
22
|
+
return provider_params[0].get("schema", {}).get("enum", [])
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_parameter_type(p: dict, p_schema: dict):
|
|
27
|
+
"""
|
|
28
|
+
Determine and set the type for the parameter.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
p : Dict
|
|
33
|
+
Processed parameter dictionary.
|
|
34
|
+
p_schema : Dict
|
|
35
|
+
Schema dictionary for the parameter.
|
|
36
|
+
"""
|
|
37
|
+
p_type = p_schema.get("type") if not p.get("type") else p.get("type")
|
|
38
|
+
|
|
39
|
+
if p_type == "string":
|
|
40
|
+
p["type"] = "text"
|
|
41
|
+
|
|
42
|
+
if p_type in ("float", "integer") or (
|
|
43
|
+
not isinstance(p["value"], bool) and isinstance(p["value"], (int, float))
|
|
44
|
+
):
|
|
45
|
+
p["type"] = "number"
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
p_type == "boolean"
|
|
49
|
+
or p_schema.get("type") == "boolean"
|
|
50
|
+
or ("anyOf" in p_schema and p_schema["anyOf"][0].get("type") == "boolean")
|
|
51
|
+
):
|
|
52
|
+
p["type"] = "boolean"
|
|
53
|
+
|
|
54
|
+
if "date" in p["parameter_name"]:
|
|
55
|
+
p["type"] = "date"
|
|
56
|
+
|
|
57
|
+
if "timeframe" in p["parameter_name"]:
|
|
58
|
+
p["type"] = "text"
|
|
59
|
+
|
|
60
|
+
if p["parameter_name"] == "limit":
|
|
61
|
+
p["type"] = "number"
|
|
62
|
+
|
|
63
|
+
if p.get("type") in ("array", "list") or isinstance(p.get("type"), list):
|
|
64
|
+
p["type"] = "text"
|
|
65
|
+
|
|
66
|
+
return p
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_parameter_options(p: dict, p_schema: dict, providers: list[str]) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
Set options for the parameter based on the schema.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
p : Dict
|
|
76
|
+
Processed parameter dictionary.
|
|
77
|
+
p_schema : Dict
|
|
78
|
+
Schema dictionary for the parameter.
|
|
79
|
+
providers : List[str]
|
|
80
|
+
List of provider options.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
Dict
|
|
85
|
+
Updated parameter dictionary with options.
|
|
86
|
+
"""
|
|
87
|
+
choices: dict[str, list[dict[str, str]]] = {}
|
|
88
|
+
multiple_items_allowed_dict: dict = {}
|
|
89
|
+
is_provider_specific = False
|
|
90
|
+
available_providers: set = set()
|
|
91
|
+
unique_general_choices: list = []
|
|
92
|
+
|
|
93
|
+
# Handle provider-specific choices
|
|
94
|
+
for provider in providers:
|
|
95
|
+
if (provider in p_schema) or (len(providers) == 1):
|
|
96
|
+
is_provider_specific = True
|
|
97
|
+
provider_choices: list = []
|
|
98
|
+
if provider not in available_providers:
|
|
99
|
+
available_providers.add(provider)
|
|
100
|
+
if provider in p_schema:
|
|
101
|
+
provider_choices = p_schema[provider].get("choices", [])
|
|
102
|
+
elif len(providers) == 1 and "enum" in p_schema:
|
|
103
|
+
provider_choices = p_schema["enum"]
|
|
104
|
+
p_schema.pop("enum")
|
|
105
|
+
if provider_choices:
|
|
106
|
+
choices[provider] = [
|
|
107
|
+
{"label": str(c), "value": c} for c in provider_choices
|
|
108
|
+
]
|
|
109
|
+
if provider in p_schema and p_schema[provider].get(
|
|
110
|
+
"multiple_items_allowed", False
|
|
111
|
+
):
|
|
112
|
+
multiple_items_allowed_dict[provider] = True
|
|
113
|
+
|
|
114
|
+
# Check title for provider-specific information
|
|
115
|
+
if "title" in p_schema:
|
|
116
|
+
title_providers = set(p_schema["title"].split(","))
|
|
117
|
+
if title_providers.intersection(providers):
|
|
118
|
+
is_provider_specific = True
|
|
119
|
+
available_providers.update(title_providers)
|
|
120
|
+
|
|
121
|
+
# Handle general choices
|
|
122
|
+
general_choices: list = []
|
|
123
|
+
if "enum" in p_schema:
|
|
124
|
+
general_choices.extend(
|
|
125
|
+
[
|
|
126
|
+
{"label": str(c), "value": c}
|
|
127
|
+
for c in p_schema["enum"]
|
|
128
|
+
if c not in ["null", None]
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
elif "anyOf" in p_schema:
|
|
132
|
+
for sub_schema in p_schema["anyOf"]:
|
|
133
|
+
if "enum" in sub_schema:
|
|
134
|
+
general_choices.extend(
|
|
135
|
+
[
|
|
136
|
+
{"label": str(c), "value": c}
|
|
137
|
+
for c in sub_schema["enum"]
|
|
138
|
+
if c not in ["null", None]
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if general_choices:
|
|
143
|
+
# Remove duplicates by converting list of dicts to a set of tuples and back to list of dicts
|
|
144
|
+
unique_general_choices = [
|
|
145
|
+
dict(t) for t in {tuple(d.items()) for d in general_choices}
|
|
146
|
+
]
|
|
147
|
+
if not is_provider_specific:
|
|
148
|
+
if len(providers) == 1:
|
|
149
|
+
choices[providers[0]] = unique_general_choices
|
|
150
|
+
multiple_items_allowed_dict[providers[0]] = p_schema.get(
|
|
151
|
+
"multiple_items_allowed", False
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
choices["other"] = unique_general_choices
|
|
155
|
+
multiple_items_allowed_dict["other"] = p_schema.get(
|
|
156
|
+
"multiple_items_allowed", False
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Use general choices as fallback for providers without specific options
|
|
160
|
+
for provider in available_providers:
|
|
161
|
+
if provider not in choices:
|
|
162
|
+
if "anyOf" in p_schema and p_schema["anyOf"]:
|
|
163
|
+
fallback_choices = p_schema["anyOf"][0].get("enum", [])
|
|
164
|
+
choices[provider] = [
|
|
165
|
+
{"label": str(c), "value": c}
|
|
166
|
+
for c in fallback_choices
|
|
167
|
+
if c not in ["null", None]
|
|
168
|
+
]
|
|
169
|
+
else:
|
|
170
|
+
choices[provider] = unique_general_choices
|
|
171
|
+
|
|
172
|
+
p["options"] = choices
|
|
173
|
+
p["multiple_items_allowed"] = multiple_items_allowed_dict
|
|
174
|
+
|
|
175
|
+
if is_provider_specific:
|
|
176
|
+
p["available_providers"] = list(available_providers)
|
|
177
|
+
|
|
178
|
+
return p
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def process_parameter(param: dict, providers: list[str]) -> dict:
|
|
182
|
+
"""
|
|
183
|
+
Process a single parameter and return the processed dictionary.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
param : Dict
|
|
188
|
+
Parameter dictionary.
|
|
189
|
+
providers : List[str]
|
|
190
|
+
List of provider options.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
Dict
|
|
195
|
+
Processed parameter dictionary.
|
|
196
|
+
"""
|
|
197
|
+
p: dict = {}
|
|
198
|
+
param_name = param["name"]
|
|
199
|
+
p["parameter_name"] = param_name
|
|
200
|
+
p["label"] = (
|
|
201
|
+
param_name.replace("_", " ").replace("fixedincome", "fixed income").title()
|
|
202
|
+
)
|
|
203
|
+
p["description"] = (
|
|
204
|
+
(param.get("description", param_name).split(" (provider:")[0].strip())
|
|
205
|
+
.split("Multiple comma separated items allowed")[0]
|
|
206
|
+
.strip()
|
|
207
|
+
)
|
|
208
|
+
p["optional"] = param.get("required", False) is False
|
|
209
|
+
|
|
210
|
+
if param_name == "provider":
|
|
211
|
+
p["type"] = "text"
|
|
212
|
+
p["label"] = "Provider"
|
|
213
|
+
p["description"] = "Source of the data."
|
|
214
|
+
p["available_providers"] = providers
|
|
215
|
+
return p
|
|
216
|
+
|
|
217
|
+
if param_name in ["symbol", "series_id", "release_id"]:
|
|
218
|
+
p["type"] = "text"
|
|
219
|
+
p["label"] = param_name.title().replace("_", " ").replace("Id", "ID")
|
|
220
|
+
p["description"] = (
|
|
221
|
+
p["description"]
|
|
222
|
+
.split("Multiple comma separated items allowed for provider(s)")[0]
|
|
223
|
+
.strip()
|
|
224
|
+
)
|
|
225
|
+
multiple_items_allowed_dict: dict = {}
|
|
226
|
+
for _provider in providers:
|
|
227
|
+
if _provider in param["schema"] and param["schema"][_provider].get(
|
|
228
|
+
"multiple_items_allowed", False
|
|
229
|
+
):
|
|
230
|
+
multiple_items_allowed_dict[_provider] = True
|
|
231
|
+
p["multiple_items_allowed"] = multiple_items_allowed_dict
|
|
232
|
+
if "Multiple comma separated items allowed" in p["description"]:
|
|
233
|
+
p["description"] = (
|
|
234
|
+
p["description"]
|
|
235
|
+
.split("Multiple comma separated items allowed")[0]
|
|
236
|
+
.strip()
|
|
237
|
+
)
|
|
238
|
+
return p
|
|
239
|
+
|
|
240
|
+
p_schema = param.get("schema", {})
|
|
241
|
+
p["value"] = p_schema.get("default", None)
|
|
242
|
+
p = set_parameter_options(p, p_schema, providers)
|
|
243
|
+
p = set_parameter_type(p, p_schema)
|
|
244
|
+
|
|
245
|
+
return p
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def get_query_schema_for_widget(
|
|
249
|
+
openapi_json: dict, command_route: str
|
|
250
|
+
) -> tuple[list[dict], bool]:
|
|
251
|
+
"""
|
|
252
|
+
Extract the query schema for a widget.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
openapi_json : dict
|
|
257
|
+
The OpenAPI specification as a dictionary.
|
|
258
|
+
command_route : str
|
|
259
|
+
The route of the command in the OpenAPI specification.
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
Tuple[List[Dict], bool]
|
|
264
|
+
A tuple containing the list of processed parameters and a boolean indicating if a chart is present.
|
|
265
|
+
"""
|
|
266
|
+
has_chart = False
|
|
267
|
+
command = openapi_json["paths"][command_route]
|
|
268
|
+
schema_method = list(command)[0]
|
|
269
|
+
command = command[schema_method]
|
|
270
|
+
params = command.get("parameters", [])
|
|
271
|
+
route_params: list[dict] = []
|
|
272
|
+
providers: list[str] = extract_providers(params)
|
|
273
|
+
|
|
274
|
+
for param in params:
|
|
275
|
+
if param["name"] in ["sort", "order"]:
|
|
276
|
+
continue
|
|
277
|
+
if param["name"] == "chart":
|
|
278
|
+
has_chart = True
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
p = process_parameter(param, providers)
|
|
282
|
+
p["show"] = True
|
|
283
|
+
route_params.append(p)
|
|
284
|
+
|
|
285
|
+
return route_params, has_chart
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_data_schema_for_widget(openapi_json, operation_id):
|
|
289
|
+
"""
|
|
290
|
+
Get the data schema for a widget based on its operationId.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
openapi (dict): The OpenAPI specification as a dictionary.
|
|
294
|
+
operation_id (str): The operationId of the widget.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
dict: The schema dictionary for the widget's data.
|
|
298
|
+
"""
|
|
299
|
+
# Find the route and method for the given operationId
|
|
300
|
+
for _, methods in openapi_json["paths"].items():
|
|
301
|
+
for _, details in methods.items():
|
|
302
|
+
if details.get("operationId") == operation_id:
|
|
303
|
+
# Get the reference to the schema from the successful response
|
|
304
|
+
response_ref = details["responses"]["200"]["content"][
|
|
305
|
+
"application/json"
|
|
306
|
+
]["schema"]["$ref"]
|
|
307
|
+
# Extract the schema name from the reference
|
|
308
|
+
schema_name = response_ref.split("/")[-1]
|
|
309
|
+
# Fetch and return the schema from components
|
|
310
|
+
return (
|
|
311
|
+
openapi_json["components"]["schemas"][schema_name]
|
|
312
|
+
.get("properties", {})
|
|
313
|
+
.get("results", {})
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Return None if the schema is not found
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def data_schema_to_columns_defs(openapi_json, operation_id, provider):
|
|
321
|
+
"""Convert data schema to column definitions for the widget."""
|
|
322
|
+
# Initialize an empty list to hold the schema references
|
|
323
|
+
schema_refs: list = []
|
|
324
|
+
|
|
325
|
+
result_schema_ref = get_data_schema_for_widget(openapi_json, operation_id)
|
|
326
|
+
# Check if 'anyOf' is in the result_schema_ref and handle the nested structure
|
|
327
|
+
if "anyOf" in result_schema_ref:
|
|
328
|
+
for item in result_schema_ref["anyOf"]:
|
|
329
|
+
# When there are multiple providers a 'oneOf' is used
|
|
330
|
+
if "items" in item and "oneOf" in item["items"]:
|
|
331
|
+
# Extract the $ref values
|
|
332
|
+
schema_refs.extend(
|
|
333
|
+
[
|
|
334
|
+
oneOf_item["$ref"].split("/")[-1]
|
|
335
|
+
for oneOf_item in item["items"]["oneOf"]
|
|
336
|
+
if "$ref" in oneOf_item
|
|
337
|
+
]
|
|
338
|
+
)
|
|
339
|
+
# When there's only one model there is no oneOf
|
|
340
|
+
elif "items" in item and "$ref" in item["items"]:
|
|
341
|
+
schema_refs.append(item["items"]["$ref"].split("/")[-1])
|
|
342
|
+
|
|
343
|
+
# Fetch the schemas using the extracted references
|
|
344
|
+
schemas = [
|
|
345
|
+
openapi_json["components"]["schemas"][ref]
|
|
346
|
+
for ref in schema_refs
|
|
347
|
+
if ref in openapi_json["components"]["schemas"]
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
# Proceed with finding common keys and generating column definitions
|
|
351
|
+
if not schemas:
|
|
352
|
+
return []
|
|
353
|
+
|
|
354
|
+
target_schema: dict = {}
|
|
355
|
+
|
|
356
|
+
if len(schemas) == 1:
|
|
357
|
+
target_schema = schemas[0]
|
|
358
|
+
else:
|
|
359
|
+
for schema in schemas:
|
|
360
|
+
if (
|
|
361
|
+
schema.get("description", "")
|
|
362
|
+
.lower()
|
|
363
|
+
.startswith(provider.lower().replace("tradingeconomics", "te"))
|
|
364
|
+
) or (schema.get("description", "").lower().startswith("us government")):
|
|
365
|
+
target_schema = schema
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
keys = list(target_schema.get("properties", {}))
|
|
369
|
+
|
|
370
|
+
column_defs: list = []
|
|
371
|
+
for key in keys:
|
|
372
|
+
cell_data_type = None
|
|
373
|
+
formatterFn = None
|
|
374
|
+
prop = target_schema.get("properties", {}).get(key)
|
|
375
|
+
# Handle prop types for both when there's a single prop type or multiple
|
|
376
|
+
if "anyOf" in prop:
|
|
377
|
+
types = [
|
|
378
|
+
sub_prop.get("type") for sub_prop in prop["anyOf"] if "type" in sub_prop
|
|
379
|
+
]
|
|
380
|
+
if "number" in types or "integer" in types or "float" in types:
|
|
381
|
+
cell_data_type = "number"
|
|
382
|
+
elif "string" in types and any(
|
|
383
|
+
sub_prop.get("format") in ["date", "date-time"]
|
|
384
|
+
for sub_prop in prop["anyOf"]
|
|
385
|
+
if "format" in sub_prop
|
|
386
|
+
):
|
|
387
|
+
cell_data_type = "date"
|
|
388
|
+
else:
|
|
389
|
+
cell_data_type = "text"
|
|
390
|
+
else:
|
|
391
|
+
prop_type = prop.get("type", None)
|
|
392
|
+
if prop_type in ["number", "integer", "float"]:
|
|
393
|
+
cell_data_type = "number"
|
|
394
|
+
if prop_type == "integer":
|
|
395
|
+
formatterFn = "int"
|
|
396
|
+
elif "format" in prop and prop["format"] in ["date", "date-time"]:
|
|
397
|
+
cell_data_type = "date"
|
|
398
|
+
else:
|
|
399
|
+
cell_data_type = "text"
|
|
400
|
+
|
|
401
|
+
column_def: dict = {}
|
|
402
|
+
# OpenAPI changes some of the field names.
|
|
403
|
+
k = to_snake_case(key)
|
|
404
|
+
column_def["field"] = k
|
|
405
|
+
if k in [
|
|
406
|
+
"symbol",
|
|
407
|
+
"symbol_root",
|
|
408
|
+
"series_id",
|
|
409
|
+
"date",
|
|
410
|
+
"published",
|
|
411
|
+
"fiscal_year",
|
|
412
|
+
"period_ending",
|
|
413
|
+
"period_beginning",
|
|
414
|
+
"order",
|
|
415
|
+
"name",
|
|
416
|
+
"title",
|
|
417
|
+
"cusip",
|
|
418
|
+
"isin",
|
|
419
|
+
]:
|
|
420
|
+
column_def["pinned"] = "left"
|
|
421
|
+
|
|
422
|
+
column_def["formatterFn"] = formatterFn
|
|
423
|
+
column_def["headerName"] = prop.get("title", key.title())
|
|
424
|
+
column_def["description"] = prop.get(
|
|
425
|
+
"description", prop.get("title", key.title())
|
|
426
|
+
)
|
|
427
|
+
column_def["cellDataType"] = cell_data_type
|
|
428
|
+
column_def["chartDataType"] = (
|
|
429
|
+
"series"
|
|
430
|
+
if cell_data_type in ["number", "integer", "float"]
|
|
431
|
+
and column_def.get("pinned") != "left"
|
|
432
|
+
else "category"
|
|
433
|
+
)
|
|
434
|
+
measurement = prop.get("x-unit_measurement")
|
|
435
|
+
|
|
436
|
+
if measurement == "percent":
|
|
437
|
+
column_def["formatterFn"] = (
|
|
438
|
+
"normalizedPercent"
|
|
439
|
+
if prop.get("x-frontend_multiply") == 100
|
|
440
|
+
else "percent"
|
|
441
|
+
)
|
|
442
|
+
column_def["renderFn"] = "greenRed"
|
|
443
|
+
elif cell_data_type == "number":
|
|
444
|
+
del column_def["formatterFn"]
|
|
445
|
+
|
|
446
|
+
if k in ["cik", "isin", "figi", "cusip", "sedol", "symbol"]:
|
|
447
|
+
column_def["cellDataType"] = "text"
|
|
448
|
+
column_def["headerName"] = (
|
|
449
|
+
column_def["headerName"].upper() if k != "symbol" else "Symbol"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
column_defs.append(column_def)
|
|
453
|
+
|
|
454
|
+
return column_defs
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Utils for building the widgets.json file."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
TO_CAPS_STRINGS = [
|
|
6
|
+
"Pe",
|
|
7
|
+
"Sloos",
|
|
8
|
+
"Eps",
|
|
9
|
+
"Ebitda",
|
|
10
|
+
"Otc",
|
|
11
|
+
"Cpi",
|
|
12
|
+
"Pce",
|
|
13
|
+
"Gdp",
|
|
14
|
+
"Lbma",
|
|
15
|
+
"Ipo",
|
|
16
|
+
"Nbbo",
|
|
17
|
+
"Ameribor",
|
|
18
|
+
"Sonia",
|
|
19
|
+
"Effr",
|
|
20
|
+
"Sofr",
|
|
21
|
+
"Iorb",
|
|
22
|
+
"Estr",
|
|
23
|
+
"Ecb",
|
|
24
|
+
"Dpcredit",
|
|
25
|
+
"Tcm",
|
|
26
|
+
"Us",
|
|
27
|
+
"Ice",
|
|
28
|
+
"Bofa",
|
|
29
|
+
"Hqm",
|
|
30
|
+
"Sp500",
|
|
31
|
+
"Sec",
|
|
32
|
+
"Cftc",
|
|
33
|
+
"Cot",
|
|
34
|
+
"Etf",
|
|
35
|
+
"Eu",
|
|
36
|
+
"Tips",
|
|
37
|
+
"Rss",
|
|
38
|
+
"Sic",
|
|
39
|
+
"Cik",
|
|
40
|
+
"Bls",
|
|
41
|
+
"Fred",
|
|
42
|
+
"Cusip",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def modify_query_schema(query_schema: list[dict], provider_value: str):
|
|
47
|
+
"""Modify query_schema and the description for the current provider."""
|
|
48
|
+
modified_query_schema: list = []
|
|
49
|
+
for item in query_schema:
|
|
50
|
+
# copy the item
|
|
51
|
+
_item = deepcopy(item)
|
|
52
|
+
provider_value_options: dict = {}
|
|
53
|
+
|
|
54
|
+
# Exclude provider parameter. Those will be added last.
|
|
55
|
+
if "parameter_name" in _item and _item["parameter_name"] == "provider":
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Exclude parameters that are not available for the current provider.
|
|
59
|
+
if (
|
|
60
|
+
"available_providers" in _item
|
|
61
|
+
and provider_value not in _item["available_providers"]
|
|
62
|
+
):
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if provider_value in _item["multiple_items_allowed"] and _item[
|
|
66
|
+
"multiple_items_allowed"
|
|
67
|
+
].get(provider_value, False):
|
|
68
|
+
_item["description"] = (
|
|
69
|
+
_item["description"] + " Multiple comma separated items allowed."
|
|
70
|
+
)
|
|
71
|
+
_item["type"] = "text"
|
|
72
|
+
_item["multiSelect"] = True
|
|
73
|
+
|
|
74
|
+
if "options" in _item:
|
|
75
|
+
provider_value_options = _item.pop("options")
|
|
76
|
+
|
|
77
|
+
if provider_value in provider_value_options and bool(
|
|
78
|
+
provider_value_options[provider_value]
|
|
79
|
+
):
|
|
80
|
+
_item["options"] = provider_value_options[provider_value]
|
|
81
|
+
_item["type"] = "text"
|
|
82
|
+
elif len(provider_value_options) == 1 and "other" in provider_value_options:
|
|
83
|
+
_item["options"] = provider_value_options["other"]
|
|
84
|
+
_item["type"] = "text"
|
|
85
|
+
|
|
86
|
+
_item.pop("multiple_items_allowed")
|
|
87
|
+
|
|
88
|
+
if "available_providers" in _item:
|
|
89
|
+
_item.pop("available_providers")
|
|
90
|
+
|
|
91
|
+
_item["paramName"] = _item.pop("parameter_name")
|
|
92
|
+
|
|
93
|
+
modified_query_schema.append(_item)
|
|
94
|
+
|
|
95
|
+
if provider_value != "custom":
|
|
96
|
+
modified_query_schema.append(
|
|
97
|
+
{"paramName": "provider", "value": provider_value, "show": False}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return modified_query_schema
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_json(openapi: dict, widget_exclude_filter: list):
|
|
104
|
+
"""Build the widgets.json file."""
|
|
105
|
+
# pylint: disable=import-outside-toplevel
|
|
106
|
+
from .openapi import data_schema_to_columns_defs, get_query_schema_for_widget
|
|
107
|
+
|
|
108
|
+
if not openapi:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
widgets_json: dict = {}
|
|
112
|
+
routes = [
|
|
113
|
+
p
|
|
114
|
+
for p in openapi["paths"]
|
|
115
|
+
if p.startswith("/api") and "get" in openapi["paths"][p]
|
|
116
|
+
]
|
|
117
|
+
for route in routes:
|
|
118
|
+
route_api = openapi["paths"][route]
|
|
119
|
+
method = list(route_api)[0]
|
|
120
|
+
widget_id = route_api[method]["operationId"]
|
|
121
|
+
|
|
122
|
+
# Prepare the query schema of the widget
|
|
123
|
+
query_schema, has_chart = get_query_schema_for_widget(openapi, route)
|
|
124
|
+
|
|
125
|
+
# Extract providers from the query schema
|
|
126
|
+
providers: list = []
|
|
127
|
+
for item in query_schema:
|
|
128
|
+
if item["parameter_name"] == "provider":
|
|
129
|
+
providers = item["available_providers"]
|
|
130
|
+
|
|
131
|
+
if not providers:
|
|
132
|
+
providers = ["custom"]
|
|
133
|
+
|
|
134
|
+
for provider in providers:
|
|
135
|
+
columns_defs = data_schema_to_columns_defs(openapi, widget_id, provider)
|
|
136
|
+
_cat = route.split("v1/")[-1]
|
|
137
|
+
_cats = _cat.split("/")
|
|
138
|
+
category = _cats[0].title()
|
|
139
|
+
category = category.replace("Fixedincome", "Fixed Income")
|
|
140
|
+
subcat = _cats[1].title().replace("_", " ") if len(_cats) > 2 else None
|
|
141
|
+
name = (
|
|
142
|
+
widget_id.replace("fixedincome", "fixed income")
|
|
143
|
+
.replace("_", " ")
|
|
144
|
+
.title()
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
name = " ".join(
|
|
148
|
+
[
|
|
149
|
+
(word.upper() if word in TO_CAPS_STRINGS else word)
|
|
150
|
+
for word in name.split()
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
modified_query_schema = modify_query_schema(query_schema, provider)
|
|
155
|
+
|
|
156
|
+
provider_map = {
|
|
157
|
+
"tmx": "TMX",
|
|
158
|
+
"ecb": "ECB",
|
|
159
|
+
"econdb": "EconDB",
|
|
160
|
+
"fmp": "FMP",
|
|
161
|
+
"oecd": "OECD",
|
|
162
|
+
"finra": "FINRA",
|
|
163
|
+
"fred": "FRED",
|
|
164
|
+
"imf": "IMF",
|
|
165
|
+
"bls": "BLS",
|
|
166
|
+
"yfinance": "yFinance",
|
|
167
|
+
"sec": "SEC",
|
|
168
|
+
"cftc": "CFTC",
|
|
169
|
+
"tradingeconomics": "Trading Economics",
|
|
170
|
+
"wsj": "WSJ",
|
|
171
|
+
}
|
|
172
|
+
provider_name = provider_map.get(
|
|
173
|
+
provider.lower(), provider.replace("_", " ").title()
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
widget_config = {
|
|
177
|
+
"name": f"{name} ({provider_name})",
|
|
178
|
+
"description": route_api["get"]["description"],
|
|
179
|
+
"category": category,
|
|
180
|
+
"searchCategory": category,
|
|
181
|
+
"widgetId": f"{widget_id}_{provider}_obb",
|
|
182
|
+
"params": modified_query_schema,
|
|
183
|
+
"endpoint": route.replace("/api", "api"),
|
|
184
|
+
"gridData": {"w": 45, "h": 15},
|
|
185
|
+
"data": {
|
|
186
|
+
"dataKey": "results",
|
|
187
|
+
"table": {
|
|
188
|
+
"showAll": True,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"source": [provider_name],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if subcat:
|
|
195
|
+
subcat = " ".join(
|
|
196
|
+
[
|
|
197
|
+
(word.upper() if word in TO_CAPS_STRINGS else word)
|
|
198
|
+
for word in subcat.split()
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
subcat = (
|
|
202
|
+
subcat.replace("Estimates", "Analyst Estimates")
|
|
203
|
+
.replace("Fundamental", "Fundamental Analysis")
|
|
204
|
+
.replace("Compare", "Comparison Analysis")
|
|
205
|
+
)
|
|
206
|
+
widget_config["subCategory"] = subcat
|
|
207
|
+
|
|
208
|
+
if columns_defs:
|
|
209
|
+
widget_config["data"]["table"]["columnsDefs"] = columns_defs
|
|
210
|
+
|
|
211
|
+
# Add the widget configuration to the widgets.json
|
|
212
|
+
if widget_config["widgetId"] not in widget_exclude_filter:
|
|
213
|
+
widgets_json[widget_config["widgetId"]] = widget_config
|
|
214
|
+
|
|
215
|
+
if has_chart:
|
|
216
|
+
widget_config_chart = deepcopy(widget_config)
|
|
217
|
+
widget_config_chart["name"] = widget_config_chart["name"] + " (Chart)"
|
|
218
|
+
widget_config_chart["widgetId"] = (
|
|
219
|
+
f"{widget_config_chart['widgetId']}_chart"
|
|
220
|
+
)
|
|
221
|
+
widget_config_chart["params"].append(
|
|
222
|
+
{
|
|
223
|
+
"paramName": "chart",
|
|
224
|
+
"label": "Chart",
|
|
225
|
+
"description": "Returns chart",
|
|
226
|
+
"optional": True,
|
|
227
|
+
"value": True,
|
|
228
|
+
"type": "boolean",
|
|
229
|
+
"show": False,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
widget_config_chart["searchCategory"] = "chart"
|
|
233
|
+
widget_config_chart["gridData"]["h"] = 20
|
|
234
|
+
widget_config_chart["gridData"]["w"] = 50
|
|
235
|
+
widget_config_chart["defaultViz"] = "chart"
|
|
236
|
+
widget_config_chart["data"]["dataKey"] = "chart.content"
|
|
237
|
+
if widget_config_chart["widgetId"] not in widget_exclude_filter:
|
|
238
|
+
widgets_json[widget_config_chart["widgetId"]] = widget_config_chart
|
|
239
|
+
|
|
240
|
+
return widgets_json
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "openbb-platform-api"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "OpenBB Platform API: Launch script and widgets builder for the OpenBB Platform API and Terminal Pro Connector."
|
|
5
|
+
authors = ["OpenBB <hello@openbb.co>"]
|
|
6
|
+
license = "AGPL-3.0-only"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://openbb.co"
|
|
9
|
+
repository = "https://github.com/openbb-finance/openbb"
|
|
10
|
+
documentation = "https://docs.openbb.co"
|
|
11
|
+
packages = [{ include = "openbb_platform_api" }]
|
|
12
|
+
|
|
13
|
+
[tool.poetry.scripts]
|
|
14
|
+
openbb-api = "openbb_platform_api.main:main"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dependencies]
|
|
17
|
+
python = ">=3.9,<3.13"
|
|
18
|
+
poetry = "^1.8"
|
|
19
|
+
setuptools = "*"
|
|
20
|
+
openbb-core = "*"
|
|
21
|
+
deepdiff = "*"
|
|
22
|
+
ruff = "*"
|
|
23
|
+
black = "*"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["poetry-core>=1.0.0"]
|
|
27
|
+
build-backend = "poetry.core.masonry.api"
|