diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index ff98d8e8f6e..2f068019a35 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -70,6 +70,7 @@ jobs:
if [ -n "${{ env.platform_files }}" ]; then
pylint ${{ env.platform_files }}
mypy ${{ env.platform_files }} --ignore-missing-imports --scripts-are-modules --check-untyped-defs
+ pydocstyle ${{ env.platform_files }}
else
echo "No Python files changed in openbb_platform"
fi
diff --git a/.github/workflows/platform-api-integration-test.yml b/.github/workflows/platform-api-integration-test.yml
index 92b7073045b..11b4c9c5d40 100644
--- a/.github/workflows/platform-api-integration-test.yml
+++ b/.github/workflows/platform-api-integration-test.yml
@@ -2,6 +2,9 @@ name: API Integration Tests
on:
workflow_dispatch:
+ pull_request:
+ branches:
+ - release/*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
diff --git a/.gitignore b/.gitignore
index e92f1407831..bdd736b86dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,3 +85,9 @@ lightning_logs/
# Platform auto generated files
openbb_platform/openbb/package/*
+
+# Reference files
+website/content/excel/reference/*
+website/content/excel/reference/*/*
+website/content/platform/data_models/*
+website/content/platform/reference/*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 51e9141e239..62f9111ca92 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1716,8 +1716,6 @@ The `develop` branch is only merged to `main` right before a new release, but so
3. merge `docs/[my-update]` into `main` -> website deployment triggered
4. merge `docs/[my-update]` into `develop` -> NO website deployment, just to sync branches
-The `docs/docs-name` branch is used to update documentation in-between releases, it will trigger the website deployment to GitHub pages.
-
## Installers
When implementing a new feature or fixing something within the codebase, it is necessary to ensure that it is working
diff --git a/build/README.md b/build/README.md
deleted file mode 100644
index 0d9dcc1b1b2..00000000000
--- a/build/README.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Building Installers
-This guide serves to assist developers construct OpenBB Terminal installers on macOS and Windows.
-
-## Overview
-Building an installer takes source code as input and produces deployable software as an output. For MacOS the output is a DMG while for Windows it is an EXE. This process utilizes two major modules: Pyinstaller & Create-dmg. Windows additionally recruits the service of NSIS. There are two ways to build an installer: locally or through Github.
-
-## Local
-In this section we detail the steps necessary to construct an installer locally on a Windows machine and Mac machine. These steps can be performed on either one of these operating systems so long as they have the OpenBBTerminal repository codebase. This process assumes you already have a working conda environment. Building locally usually takes anywhere from 10-20 minutes.
-
-### MacOS Steps
-1. `brew install create-dmg`
-
- This installs create-dmg and its dependencies onto your system through brew.
-
-1. `poetry install -E installer`
-
- This install pyinstaller and its dependencies onto your environment.
-
-3. `build/pyinstaller/build4mac.sh`
-
- This runs a shell script that auto-builds the DMG and stores it on the root of your repository.
-
-4. (Optional) `/Full/Path/To/OpenBB\ Terminal/.OpenBB/OpenBBTerminal /Full/Path/To/OpenBBTerminal/OpenBBTerminal/scripts/*.openbb -t`
-
- This runs integration tests on your newly created installer.
-
-### Windows Steps
-1. `poetry install -E installer`
-
- This install pyinstaller and its dependencies onto your environment.
-
-2. `pyinstaller build/pyinstaller/terminal.spec`
-
- This runs a shell script that auto-builds the DMG and stores it on the root of your repository.
-
-3. `cp -r .\dist\OpenBBTerminal\ .\build\nsis\app\`
-
- This copies over the contents of the newly dist folder into the app folder so that NSIS can utilize them.
-
-4. Compile NSIS Scripts
-
- Open *NSIS*, click on the "Compile NSI scripts", and then drag the `setup.nsi` file in build\nsis to the window. This creates the EXE installer.
-
-4. (Optional) `Full\Path\To\OpenBBTerminal.exe Full\Path\To\OpenBBTerminal\OpenBBTerminal\scripts -t`
-
- This runs integration tests on your newly created installer.
-
-
-## Github
-In order to utilize the automated build workflow on the OpenBBTerminal repo, the branch in which you would like to build an installer from must already be a branch on the repo. You can also utilize the automated build workflow on a PR that is from a branch on the repo. You cannot run an automated build on a forked branch or even a PR from a forked branch.
-
-If you are using this method to create an installer, there is a limitation where only one build automation can occur at a time per workflow. For example if there is already an installer being created on the ‘Intel MacOS Build’ workflow, any subsequent requests for building will be queued. Additionally, the ‘Windows10 Build’ workflow runs relatively slowly because of the size of the EC2 instance it is currently on. As such, if you are interested in a quick build, then I would suggest building locally. Furthermore, building an installer this way also automatically runs integration tests on the installer.
-
-If you run into a circumstance where a requested build is queued for a long period of time, this might mean that the EC2 instance is not connected to github. If something like this arises, please create an issue.
-
-### Steps
-1. Navigate to `Actions`
-2. Navigate to your desired build system workflow
-3. Choose your desired branch & click Run Workflow
-4. Download your installer artifact
-
- Each build system produces a single installer which can be subsequently downloaded.
-
-5. Investigate the success/failure of your Build
-
- You can click on the build to delve into the steps it takes to construct an installer. This is where you can investigate the success of integration tests, identify if/where the build failed, etc.
-
-
-Note: Intel Mac installers are signed and notarized. During this step you can identify if notarization has succeeded or failed and if all of the binaries have been signed in correctly.
diff --git a/build/docker/api.dockerfile b/build/docker/api.dockerfile
index 430b8511d5b..c384555f9f8 100644
--- a/build/docker/api.dockerfile
+++ b/build/docker/api.dockerfile
@@ -1,5 +1,5 @@
# ---- Base Python ----
-FROM python:3.10-slim-buster AS base
+FROM python:3.11-slim-bullseye AS base
# set work directory
WORKDIR /openbb
@@ -10,19 +10,33 @@ ENV PYTHONUNBUFFERED 1
# install dependencies
RUN apt-get update \
- && apt-get install -y --no-install-recommends build-essential openssh-client \
+ && apt-get install -y --no-install-recommends build-essential openssh-client curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
+# install toml and poetry
+RUN pip install toml poetry
+
# ---- Copy Files/Build ----
FROM base AS builder
WORKDIR /openbb
+# install Rust
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends libwebkit2gtk-4.0-dev \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# add Rust to PATH
+ENV PATH="/root/.cargo/bin:${PATH}"
+
COPY ./openbb_platform ./openbb_platform
# Install the SDK
-RUN pip install /openbb/openbb_platform
+RUN pip install /openbb/openbb_platform[all]
+RUN pip install openbb-devtools
# ---- Copy Files ----
FROM base
diff --git a/build/docker/build.sh b/build/docker/build.sh
deleted file mode 100644
index a61148f313b..00000000000
--- a/build/docker/build.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-
-echo "Building docker release"
-
-if [ -z "$OPENBBTERMINAL_DOCKER_RELEASE_VERSION" ]
-then
- read -p "Version number (X.Y.Z format): " OPENBBTERMINAL_DOCKER_RELEASE_VERSION
-fi
-
-# SET DEFAULT PARAMETERS IF EMPTY
-OPENBBTERMINAL_DOCKER_REGISTRY="${OPENBBTERMINAL_DOCKER_REGISTRY:-ghcr.io}"
-OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY="${OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY:-openbb-finance/openbbterminal}"
-OPENBBTERMINAL_DOCKER_RELEASE_VERSION="${OPENBBTERMINAL_DOCKER_RELEASE_VERSION:-0.0.0}"
-# SET IMAGES NAMES
-OPENBBTERMINAL_DOCKER_POETRY_IMAGE=${OPENBBTERMINAL_DOCKER_REGISTRY,,}/${OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY,,}/openbb:${OPENBBTERMINAL_DOCKER_RELEASE_VERSION,,}
-OPENBBTERMINAL_DOCKER_POETRY_IMAGE_LATEST=${OPENBBTERMINAL_DOCKER_REGISTRY,,}/${OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY,,}/openbb:latest
-
-# DISPLAY PARAMETERS
-echo "OPENBBTERMINAL_DOCKER_REGISTRY = $OPENBBTERMINAL_DOCKER_REGISTRY"
-echo "OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY = $OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY"
-echo "OPENBBTERMINAL_DOCKER_RELEASE_VERSION = $OPENBBTERMINAL_DOCKER_RELEASE_VERSION"
-# DISPLAY IMAGES NAMES
-echo "OPENBBTERMINAL_DOCKER_POETRY_IMAGE = $OPENBBTERMINAL_DOCKER_POETRY_IMAGE"
-
-# Docker command to build image
-docker build -f build/docker/openbb.dockerfile -t "${OPENBBTERMINAL_DOCKER_POETRY_IMAGE}" -t "${OPENBBTERMINAL_DOCKER_POETRY_IMAGE_LATEST}" .
\ No newline at end of file
diff --git a/build/docker/compose.env b/build/docker/compose.env
deleted file mode 100644
index bc8a9bcb697..00000000000
--- a/build/docker/compose.env
+++ /dev/null
@@ -1,2 +0,0 @@
-OPENBBTERMINAL_DOCKER_REGISTRY="ghcr.io"
-OPENBBTERMINAL_DOCKER_RELEASE_VERSION="3.2.5"
diff --git a/build/docker/docker-compose.x11.yaml b/build/docker/docker-compose.x11.yaml
deleted file mode 100644
index a10f5594d5a..00000000000
--- a/build/docker/docker-compose.x11.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-services:
- openbb:
- environment:
- DISPLAY: ${DISPLAY}
- volumes:
- - /tmp/.X11-unix:/tmp/.X11-unix
diff --git a/build/docker/docker-compose.yaml b/build/docker/docker-compose.yaml
deleted file mode 100644
index c4193a96b7c..00000000000
--- a/build/docker/docker-compose.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-version: '3.8'
-services:
- openbb:
- environment:
- DISPLAY: host.docker.internal:0.0
- volumes:
- - ~/OpenBBUserData:/home/python/OpenBBUserData
- - ~/.openbb_terminal:/home/python/.openbb_terminal
- platform: linux/amd64
- image: ghcr.io/openbb-finance/openbbterminal/openbb:latest
- stdin_open: true # docker run -i
- tty: true # docker run -t
diff --git a/build/docker/openbb.dockerfile b/build/docker/openbb.dockerfile
deleted file mode 100644
index 81731b5197a..00000000000
--- a/build/docker/openbb.dockerfile
+++ /dev/null
@@ -1,75 +0,0 @@
-# SETUP PYTHON IMAGE
-FROM --platform=linux/amd64 python:3.10-slim-bullseye as python
-
-LABEL org.opencontainers.image.source https://github.com/OpenBB-finance/OpenBBTerminal
-
-# SETUP DEBIAN IMAGE
-FROM python as debian
-
-RUN apt-get update
-
-RUN apt-get -y install --no-install-recommends \
- gcc \
- g++ \
- make
-
-RUN apt-get -y install --no-install-recommends \
- git
-
-RUN apt-get -y install --no-install-recommends \
- curl \
- wget
-
-RUN apt-get -y install --no-install-recommends \
- libsm6 \
- libxt6 \
- libgl1-mesa-glx \
- libpng16-16 \
- libwebkit2gtk-4.0-dev \
- build-essential \
- libssl-dev \
- librsvg2-dev \
- ffmpeg \
- python3-tk
-
-RUN apt-get clean
-
-RUN useradd --create-home --shell /bin/bash python
-
-USER python
-WORKDIR /home/python
-
-# SETUP POETRY IMAGE
-FROM debian as poetry
-
-ENV PATH="/home/python/.local/bin:${PATH}"
-
-RUN pip install --upgrade pip wheel
-RUN pip install poetry==1.4.0
-
-# SETUP OPENBB IMAGE
-FROM poetry as repository
-
-COPY --chown=python:python pyproject.toml poetry.lock terminal.py ./
-
-RUN mkdir openbb_terminal
-COPY --chown=python:python openbb_terminal openbb_terminal
-
-RUN mkdir -p website/content/sdk
-COPY --chown=python:python ./website/content/sdk/installation.md ./website/content/sdk
-
-RUN mkdir OpenBBUserData
-RUN chown python:python OpenBBUserData
-
-RUN mkdir .openbb_terminal
-RUN chown python:python .openbb_terminal
-
-# SETUP OPENBB IMAGE
-FROM repository as dependencies
-
-RUN poetry install --no-root --no-dev --extras optimization --extras forecast
-
-# SETUP OPENBB IMAGE
-FROM dependencies as openbb
-
-CMD ["poetry", "run", "python", "terminal.py"]
diff --git a/build/nsis/assets/Installer_horizontal.bmp b/build/nsis/assets/Installer_horizontal.bmp
deleted file mode 100644
index 7de27025699..00000000000
Binary files a/build/nsis/assets/Installer_horizontal.bmp and /dev/null differ
diff --git a/build/nsis/assets/Installer_horizontal2.png b/build/nsis/assets/Installer_horizontal2.png
deleted file mode 100644
index 2261c710255..00000000000
Binary files a/build/nsis/assets/Installer_horizontal2.png and /dev/null differ
diff --git a/build/nsis/assets/Installer_horizontall.png b/build/nsis/assets/Installer_horizontall.png
deleted file mode 100644
index bddba4ab947..00000000000
Binary files a/build/nsis/assets/Installer_horizontall.png and /dev/null differ
diff --git a/build/nsis/assets/Installer_vertical#2.png b/build/nsis/assets/Installer_vertical#2.png
deleted file mode 100644
index f722988ffa2..00000000000
Binary files a/build/nsis/assets/Installer_vertical#2.png and /dev/null differ
diff --git a/build/nsis/assets/Installer_vertical.png b/build/nsis/assets/Installer_vertical.png
deleted file mode 100644
index 078721b00d1..00000000000
Binary files a/build/nsis/assets/Installer_vertical.png and /dev/null differ
diff --git a/build/nsis/assets/Installer_vertical2.bmp b/build/nsis/assets/Installer_vertical2.bmp
deleted file mode 100644
index 54f9b618845..00000000000
Binary files a/build/nsis/assets/Installer_vertical2.bmp and /dev/null differ
diff --git a/build/nsis/assets/Splashscreen #2.png b/build/nsis/assets/Splashscreen #2.png
deleted file mode 100644
index 0f95c376fb2..00000000000
Binary files a/build/nsis/assets/Splashscreen #2.png and /dev/null differ
diff --git a/build/nsis/assets/Splashscreen.png b/build/nsis/assets/Splashscreen.png
deleted file mode 100644
index a0c18bd195f..00000000000
Binary files a/build/nsis/assets/Splashscreen.png and /dev/null differ
diff --git a/build/nsis/assets/openbb_icon.ico b/build/nsis/assets/openbb_icon.ico
deleted file mode 100644
index ec1a7d484f4..00000000000
Binary files a/build/nsis/assets/openbb_icon.ico and /dev/null differ
diff --git a/build/nsis/license.txt b/build/nsis/license.txt
deleted file mode 100644
index 5ef8d0820f7..00000000000
--- a/build/nsis/license.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-The information provided by OpenBB Terminal ("application") is for general informational purposes only. All information on the application is provided in good faith, however we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability or completeness of any information on the application.
-
-Under no circumstance shall we have any liability to you for any loss or damage of any kind incurred as a result of the use of the application or reliance on any information provided on the application. Your use of the application and your reliance on any information on it is solely at your own risk.
\ No newline at end of file
diff --git a/build/nsis/setup.nsi b/build/nsis/setup.nsi
deleted file mode 100644
index c3f23890cd6..00000000000
--- a/build/nsis/setup.nsi
+++ /dev/null
@@ -1,277 +0,0 @@
-;--------------------------------
-; Includes
-
- !include "MUI2.nsh"
- !include "logiclib.nsh"
-
-
-;--------------------------------
-; Custom defines
- !define NAME "OpenBB Terminal"
- !define COMPANY "OpenBB"
- !define APPFILE "OpenBBTerminal.exe"
- !define VERSION "3.2.5"
- !define SLUG "${NAME} v${VERSION}"
-
-;--------------------------------
-; Info for Installer.exe
- VIProductVersion 3.2.5.0
- VIAddVersionKey ProductName "OpenBB Terminal"
- VIAddVersionKey Comments "An installer for OpenBB Terminal. For additional details, visit OpenBB.co"
- VIAddVersionKey CompanyName OpenBB.co
- VIAddVersionKey FileDescription "OpenBB Terminal Program"
- VIAddVersionKey FileVersion 3.2.5.0
- VIAddVersionKey ProductVersion 3.2.5.0
- VIAddVersionKey InternalName "OpenBB Terminal"
-
-;--------------------------------
-; General
- Name "${NAME}"
- OutFile "${NAME} Setup.exe"
- InstallDir $PROFILE\OpenBB
- RequestExecutionLevel user
-
-;--------------------------------
-; UI
-
- !define MUI_ICON "assets\openbb_icon.ico"
- !define MUI_UNICON "assets\openbb_icon.ico"
- !define MUI_HEADERIMAGE
- !define MUI_WELCOMEFINISHPAGE_BITMAP "assets\installer_vertical2.bmp"
- !define MUI_HEADERIMAGE_BITMAP "assets\installer_horizontal.bmp"
- !define MUI_ABORTWARNING
- !define MUI_WELCOMEPAGE_TITLE "${SLUG} Setup"
- !define UninstId "OpenBBTerminal" ; You might want to use a GUID here
- !define MUI_FINISHPAGE_RUN
- !define MUI_FINISHPAGE_RUN_NOTCHECKED
- !define MUI_FINISHPAGE_RUN_TEXT "Start OpenBB Terminal"
- !define MUI_FINISHPAGE_RUN_FUNCTION "StartOpenBB"
-
-;--------------------------------
-; Pages
-
- ; Installer pages
- !insertmacro MUI_PAGE_WELCOME
- !insertmacro MUI_PAGE_LICENSE "license.txt"
-; !insertmacro MUI_PAGE_COMPONENTS
- !insertmacro MUI_PAGE_DIRECTORY
- !insertmacro MUI_PAGE_INSTFILES
- !insertmacro MUI_PAGE_FINISH
-
- ; Uninstaller pages
- !insertmacro MUI_UNPAGE_CONFIRM
- !insertmacro MUI_UNPAGE_INSTFILES
-
- ; Set UI language
- !insertmacro MUI_LANGUAGE "English"
-
-
- Var /GLOBAL installerPath
-
-
-
-
-
-; StrContains
-; This function does a case sensitive searches for an occurrence of a substring in a string.
-; It returns the substring if it is found.
-; Otherwise it returns null("").
-; Written by kenglish_hi
-; Adapted from StrReplace written by dandaman32
-
-
-Var STR_HAYSTACK
-Var STR_NEEDLE
-Var STR_CONTAINS_VAR_1
-Var STR_CONTAINS_VAR_2
-Var STR_CONTAINS_VAR_3
-Var STR_CONTAINS_VAR_4
-Var STR_RETURN_VAR
-
-
-Function StartOpenBB
- Exec "$InstDir\OpenBBTerminal.exe"
-FunctionEnd
-
-
-Function StrContains
- Exch $STR_NEEDLE
- Exch 1
- Exch $STR_HAYSTACK
- ; Uncomment to debug
- ;MessageBox MB_OK 'STR_NEEDLE = $STR_NEEDLE STR_HAYSTACK = $STR_HAYSTACK '
- StrCpy $STR_RETURN_VAR ""
- StrCpy $STR_CONTAINS_VAR_1 -1
- StrLen $STR_CONTAINS_VAR_2 $STR_NEEDLE
- StrLen $STR_CONTAINS_VAR_4 $STR_HAYSTACK
- loop:
- IntOp $STR_CONTAINS_VAR_1 $STR_CONTAINS_VAR_1 + 1
- StrCpy $STR_CONTAINS_VAR_3 $STR_HAYSTACK $STR_CONTAINS_VAR_2 $STR_CONTAINS_VAR_1
- StrCmp $STR_CONTAINS_VAR_3 $STR_NEEDLE found
- StrCmp $STR_CONTAINS_VAR_1 $STR_CONTAINS_VAR_4 done
- Goto loop
- found:
- StrCpy $STR_RETURN_VAR $STR_NEEDLE
- Goto done
- done:
- Pop $STR_NEEDLE ;Prevent "invalid opcode" errors and keep the
- Exch $STR_RETURN_VAR
-FunctionEnd
-
-!macro _StrContainsConstructor OUT NEEDLE HAYSTACK
- Push `${HAYSTACK}`
- Push `${NEEDLE}`
- Call StrContains
- Pop `${OUT}`
-!macroend
-
-!define StrContains '!insertmacro "_StrContainsConstructor"'
-
-
-;-------------------------------
-; Uninstall Previous Version if exists
-
-!macro UninstallExisting exitcode uninstcommand
-Push `${uninstcommand}`
-Call UninstallExisting
-Pop ${exitcode}
-!macroend
-Function UninstallExisting
-Exch $1 ; uninstcommand
-Push $2 ; Uninstaller
-Push $3 ; Len
-StrCpy $3 ""
-StrCpy $2 $1 1
-StrCmp $2 '"' qloop sloop
-sloop:
- StrCpy $2 $1 1 $3
- IntOp $3 $3 + 1
- StrCmp $2 "" +2
- StrCmp $2 ' ' 0 sloop
- IntOp $3 $3 - 1
- Goto run
-qloop:
- StrCmp $3 "" 0 +2
- StrCpy $1 $1 "" 1 ; Remove initial quote
- IntOp $3 $3 + 1
- StrCpy $2 $1 1 $3
- StrCmp $2 "" +2
- StrCmp $2 '"' 0 qloop
-run:
- StrCpy $2 $1 $3 ; Path to uninstaller
- StrCpy $1 161 ; ERROR_BAD_PATHNAME
- GetFullPathName $3 "$2\.." ; $InstDir
- IfFileExists "$2" 0 +4
- ExecWait '"$2" /S _?=$3' $1 ; This assumes the existing uninstaller is a NSIS uninstaller, other uninstallers don't support /S nor _?=
- IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted
- Delete "$2" ; Delete the uninstaller
- RMDir "$3" ; Try to delete $InstDir
- RMDir "$3\.." ; (Optional) Try to delete the parent of $InstDir
-Pop $3
-Pop $2
-Exch $1 ; exitcode
-FunctionEnd
-
-
-Function .onInit
-ReadRegStr $0 HKCU "Software\Software\Microsoft\Windows\CurrentVersion\Uninstall\${UninstId}" "UninstallString"
-${If} $0 != ""
-${AndIf} ${Cmd} `MessageBox MB_YESNO|MB_ICONQUESTION "It is highly recommended to uninstall the previous version of OpenBB Terminal - Please click Yes to proceed (Note - You will not lose your custom settings) - Or you can uninstall manually. " /SD IDYES IDYES`
- !insertmacro UninstallExisting $0 $0
- ${If} $0 <> 0
- MessageBox MB_YESNO|MB_ICONSTOP "Failed to uninstall, continue anyway?" /SD IDYES IDYES +2
- Abort
- ${EndIf}
-${EndIf}
-FunctionEnd
-
-;--------------------------------
-; Section - Install App
-
- Section "-hidden app"
- SectionIn RO
- ${StrContains} $0 "\OpenBB" "$INSTDIR"
- ; Making sure here if user manually removes \openbb from their path that it still installs there
- ; so we dont have issues with uninstaller later.
- StrCmp $0 "" notfound
- ; MessageBox MB_OK 'Found string $0 $INSTDIR'
- SetOutPath "$INSTDIR"
- Goto done
- notfound:
- ; MessageBox MB_OK "$INSTDIR is 'bla'"
- SetOutPath "$INSTDIR\OpenBB"
- StrCpy $InstDir "$INSTDIR\OpenBB"
- ; MessageBox MB_OK 'Did not find string "$INSTDIR\OpenBB" "$installerPath"'
- done:
-
- File /r "app\*.*"
- WriteRegStr HKCU "Software\${NAME}" "" $INSTDIR
- WriteUninstaller "$INSTDIR\Uninstall.exe"
- WriteRegStr HKCU "Software\Software\Microsoft\Windows\CurrentVersion\Uninstall\${UninstId}" "OpenBBTerminal" "OpenBB"
- WriteRegStr HKCU "Software\Software\Microsoft\Windows\CurrentVersion\Uninstall\${UninstId}" "UninstallString" '"$InstDir\Uninstall.exe"'
- WriteRegStr HKCU "Software\Software\Microsoft\Windows\CurrentVersion\Uninstall\${UninstId}" "QuietUninstallString" '"$InstDir\Uninstall.exe" /S'
- SectionEnd
-
-
-;--------------------------------
-; Section - Shortcut
-
- Section "Desktop Shortcut" DeskShort
- CreateShortCut "$DESKTOP\${NAME}.lnk" "$INSTDIR\${APPFILE}"
- CreateDirectory '$SMPROGRAMS\${Company}\${NAME}'
- CreateShortCut '$SMPROGRAMS\${Company}\${NAME}\${NAME}.lnk' '$INSTDIR\${APPFILE}' "" '$INSTDIR\${APPFILE}' 0
- CreateShortCut '$SMPROGRAMS\${Company}\${NAME}\Uninstall ${NAME}.lnk' '$INSTDIR\Uninstall.exe' "" '$INSTDIR\Uninstall.exe' 0
- SectionEnd
-
-
-;--------------------------------
-; Remove empty parent directories
-
- Function un.RMDirUP
- !define RMDirUP '!insertmacro RMDirUPCall'
-
- !macro RMDirUPCall _PATH
- push '${_PATH}'
- Call un.RMDirUP
- !macroend
-
- ; $0 - current folder
- ClearErrors
-
- Exch $0
- ;DetailPrint "ASDF - $0\.."
- RMDir "$0\.."
-
- IfErrors Skip
- ${RMDirUP} "$0\.."
- Skip:
-
- Pop $0
-
- FunctionEnd
-
-;--------------------------------
-; Section - Uninstaller
-
-Section "Uninstall"
-
- ;Delete Shortcut
- Delete "$DESKTOP\${NAME}.lnk"
-
- ;Delete Directory
- Delete '$SMPROGRAMS\${Company}\${NAME}'
-
- ;Delete Reg Key
- DeleteRegKey HKCU "Software\Software\Microsoft\Windows\CurrentVersion\Uninstall\${UninstId}"
-
- ;Delete Uninstall
- Delete "$INSTDIR\Uninstall.exe"
-
-
- ;Delete Folder
- RMDir /r "$INSTDIR"
- ${RMDirUP} "$INSTDIR"
-
- DeleteRegKey /ifempty HKCU "Software\${NAME}"
-
-SectionEnd
diff --git a/build/pyinstaller/.env b/build/pyinstaller/.env
deleted file mode 100644
index 58e3640f467..00000000000
--- a/build/pyinstaller/.env
+++ /dev/null
@@ -1,26 +0,0 @@
-OPENBB_LOGGING_APP_NAME=gst_packaged
-OPENBB_LOGGING_COMMIT_HASH=sha:a7c3d29
-OPENBB_ENABLE_PREDICT=false
-OPENBB_ENABLE_CHECK_API=true
-OPENBB_ENABLE_THOUGHTS_DAY=false
-OPENBB_REMEMBER_CONTEXTS=true
-OPENBB_USE_FLAIR=:openbb
-OPENBB_USE_WATERMARK=true
-OPENBB_USE_CMD_LOCATION_FIGURE=true
-OPENBB_USE_PROMPT_TOOLKIT=true
-OPENBB_USE_COLOR=true
-OPENBB_USE_ION=true
-OPENBB_USE_PLOT_AUTOSCALING=false
-OPENBB_ENABLE_RICH=true
-OPENBB_ENABLE_RICH_PANEL=true
-OPENBB_MPLSTYLE=dark
-OPENBB_MPFSTYLE=dark
-OPENBB_RICHSTYLE=dark
-OPENBB_DEBUG_MODE=false
-OPENBB_USE_TABULATE_DF=true
-OPENBB_USE_INTERACTIVE_DF=true
-OPENBB_USE_CLEAR_AFTER_CMD=false
-OPENBB_USE_DATETIME=true
-OPENBB_OPEN_REPORT_AS_HTML=true
-OPENBB_ENABLE_QUICK_EXIT=false
-OPENBB_ENABLE_EXIT_AUTO_HELP=false
diff --git a/build/pyinstaller/build4mac.sh b/build/pyinstaller/build4mac.sh
deleted file mode 100755
index 5f6758be10f..00000000000
--- a/build/pyinstaller/build4mac.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-
-DISK_IMAGE_NAME="OpenBB Terminal"
-SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
-BUNDLER_PATH="$SCRIPTPATH/macOS/build-macos.sh"
-
-# exit when any command fails
-set -e
-
-# Clean Up artifacts from previous builds
-rm -rf build/terminal && rm -rf dist && rm -rf DMG
-
-# Clean up local logging id
-rm -rf openbb_terminal/logs
-
-# Running build
-pyinstaller build/pyinstaller/terminal.spec # --clean
-
-# Assign icons to the built folder and launcher
-osascript build/pyinstaller/setup_icons.applescript
-
-# Create the folder that is used for packaging
-mkdir DMG
-
-# Copy relevant artifacts to the packaging folder
-cp -r build/pyinstaller/macOS_package_assets/* DMG/
-mv dist/OpenBBTerminal DMG/"$DISK_IMAGE_NAME"/.OpenBB
diff --git a/build/pyinstaller/entitlements.plist b/build/pyinstaller/entitlements.plist
deleted file mode 100644
index 1d765f1a827..00000000000
--- a/build/pyinstaller/entitlements.plist
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- com.apple.security.cs.disable-library-validation
-
- com.apple.security.cs.allow-dyld-environment-variables
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.cs.allow-unsigned-executable-memory
-
- com.apple.security.cs.disable-executable-page-protection
-
- com.apple.security.cs.debugger
-
-
-
diff --git a/build/pyinstaller/hooks/hook-debugpy.py b/build/pyinstaller/hooks/hook-debugpy.py
deleted file mode 100644
index 42438ae0300..00000000000
--- a/build/pyinstaller/hooks/hook-debugpy.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import os
-
-# This environment variable suppresses the warning:
-# 0.00s - Debugger warning: The os.path.realpath.__code__.co_filename (posixpath.py)
-# 0.00s - is not absolute, which may make the debugger miss breakpoints.
-# 0.00s - Note: Debugging will proceed. Set PYDEVD_DISABLE_FILE_VALIDATION=1 to disable this validation.
-
-os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
diff --git a/build/pyinstaller/hooks/hook-pytrends.py b/build/pyinstaller/hooks/hook-pytrends.py
deleted file mode 100644
index 7209e9b024a..00000000000
--- a/build/pyinstaller/hooks/hook-pytrends.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_all
-
-datas, binaries, hiddenimports = collect_all("pytrends")
diff --git a/build/pyinstaller/hooks/hook-rich.py b/build/pyinstaller/hooks/hook-rich.py
deleted file mode 100644
index 392b324ac0f..00000000000
--- a/build/pyinstaller/hooks/hook-rich.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_data_files, copy_metadata
-
-datas = copy_metadata("rich") + collect_data_files("rich")
diff --git a/build/pyinstaller/hooks/hook-streamlit.py b/build/pyinstaller/hooks/hook-streamlit.py
deleted file mode 100644
index 05080f9a1fe..00000000000
--- a/build/pyinstaller/hooks/hook-streamlit.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import copy_metadata
-
-datas = copy_metadata("streamlit")
diff --git a/build/pyinstaller/hooks/hook-whisper.py b/build/pyinstaller/hooks/hook-whisper.py
deleted file mode 100644
index 184d234339e..00000000000
--- a/build/pyinstaller/hooks/hook-whisper.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import importlib
-
-import importlib_metadata
-from PyInstaller.utils.hooks import copy_metadata
-
-datas = copy_metadata("transformers")
-datas += copy_metadata("tokenizers")
-datas += copy_metadata("tqdm")
-datas += copy_metadata("regex")
-datas += copy_metadata("requests")
-datas += copy_metadata("packaging")
-datas += copy_metadata("filelock")
-datas += copy_metadata("numpy")
-datas += copy_metadata("torch")
-
-candidates = [
- "tensorflow",
- "tensorflow-cpu",
- "tensorflow-gpu",
- "tf-nightly",
- "tf-nightly-cpu",
- "tf-nightly-gpu",
- "intel-tensorflow",
- "intel-tensorflow-avx512",
- "tensorflow-rocm",
- "tensorflow-macos",
- "tensorflow-aarch64",
-]
-for candidate in candidates:
- try:
- if importlib.util.find_spec(candidate):
- datas += copy_metadata(candidate)
- break
- except importlib_metadata.PackageNotFoundError:
- pass
diff --git a/build/pyinstaller/hooks/hook-xgboost.py b/build/pyinstaller/hooks/hook-xgboost.py
deleted file mode 100644
index 4274e6e0aa7..00000000000
--- a/build/pyinstaller/hooks/hook-xgboost.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_all
-
-datas, binaries, hiddenimports = collect_all("xgboost")
diff --git a/build/pyinstaller/hooks/pyi_rth_inspect.py b/build/pyinstaller/hooks/pyi_rth_inspect.py
deleted file mode 100644
index 718972baf50..00000000000
--- a/build/pyinstaller/hooks/pyi_rth_inspect.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Runtime hook copied from pyinstaller
-
-
-import inspect
-import os
-import sys
-
-# pylint:disable=W0622,W0212,E1101
-
-_orig_inspect_getsourcefile = inspect.getsourcefile
-
-
-# Provide custom implementation of inspect.getsourcefile() for frozen applications that properly resolves relative
-# filenames obtained from object (e.g., inspect stack-frames). See #5963.
-def _pyi_getsourcefile(object):
- filename = inspect.getfile(object)
- if not os.path.isabs(filename):
- # Check if given filename matches the basename of __main__'s __file__.
- if hasattr(sys.modules["__main__"], "__file__"):
- main_file = sys.modules["__main__"].__file__
- if filename == os.path.basename(main_file):
- return main_file
-
- # If filename ends with .py suffix and does not correspond to frozen entry-point script, convert it to
- # corresponding .pyc in sys._MEIPASS.
- if filename.endswith(".py"):
- filename = os.path.normpath(os.path.join(sys._MEIPASS, filename + "c"))
- # Ensure the relative path did not try to jump out of sys._MEIPASS, just in case...
- if filename.startswith(sys._MEIPASS):
- return filename
- elif filename.startswith(sys._MEIPASS) and filename.endswith(".pyc"):
- # If filename is already PyInstaller-compatible, prevent any further processing (i.e., with original
- # implementation).
- return filename
- # Use original implementation as a fallback.
- return _orig_inspect_getsourcefile(object)
-
-
-inspect.getsourcefile = _pyi_getsourcefile
diff --git a/build/pyinstaller/macOS/.gitignore b/build/pyinstaller/macOS/.gitignore
deleted file mode 100644
index 2f7896d1d13..00000000000
--- a/build/pyinstaller/macOS/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-target/
diff --git a/build/pyinstaller/macOS/build-macos.sh b/build/pyinstaller/macOS/build-macos.sh
deleted file mode 100755
index b187ee58fe7..00000000000
--- a/build/pyinstaller/macOS/build-macos.sh
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/bin/bash
-
-#Parameters
-SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
-TARGET_DIRECTORY="$SCRIPTPATH/target"
-BASE_DIRECTORY="$( cd -- "$(dirname "$1")" >/dev/null 2>&1 ; pwd -P )"
-APP_DIRECTORY="$BASE_DIRECTORY/DMG/OpenBB Terminal"
-BINARIES="$BASE_DIRECTORY/DMG/OpenBB\ Terminal/.OpenBB"
-ENTITLEMENTS="$BASE_DIRECTORY/build/pyinstaller/entitlements.plist"
-PRODUCT=${1}
-VERSION=${2}
-DATE=`date +%Y-%m-%d`
-TIME=`date +%H:%M:%S`
-LOG_PREFIX="[$DATE $TIME]"
-
-
-function printUsage() {
- echo -e "\033[1mUsage:\033[0m"
- echo "$0 [APPLICATION_NAME] [APPLICATION_VERSION]"
- echo
- echo -e "\033[1mOptions:\033[0m"
- echo " -h (--help)"
- echo
- echo -e "\033[1mExample::\033[0m"
- echo "$0 openbbpro 0.0.1"
-
-}
-
-#Argument validation
-if [ -z "$APPLE_DEVELOPER_CERTIFICATE_ID" ]; then
- echo "APPLE_DEVELOPER_CERTIFICATE_ID environment variable is not set"
- echo "Please run 'security find-identity -v', and then use the value for"
- echo "the developer ID Installer (and NOT the Developer ID Application)"
- exit 1
-fi
-if [[ "$1" == "-h" || "$1" == "--help" ]]; then
- printUsage
- exit 1
-fi
-if [ -z "$1" ]; then
- echo "Please enter a valid application name for your application"
- echo
- printUsage
- exit 1
-else
- echo "Application Name : $1"
-fi
-if [[ "$2" =~ [0-9]+.[0-9]+.[0-9]+ ]]; then
- echo "Application Version : $2"
-else
- echo "Please enter a valid version for your application (format [0-9].[0-9].[0-9])"
- echo
- printUsage
- exit 1
-fi
-if [ -z "$APPLE_SIGNING_IDENTITY" ]; then
- echo "Please set the APPLE_SIGNING_IDENTITY environment variable"
- exit 1
-fi
-
-#Functions
-go_to_dir() {
- pushd $1 >/dev/null 2>&1
-}
-
-log_info() {
- echo "${LOG_PREFIX}[INFO]" $1
-}
-
-log_warn() {
- echo "${LOG_PREFIX}[WARN]" $1
-}
-
-log_error() {
- echo "${LOG_PREFIX}[ERROR]" $1
-}
-
-deleteInstallationDirectory() {
- log_info "Cleaning $TARGET_DIRECTORY directory."
- rm -rf "$TARGET_DIRECTORY"
-
- if [[ $? != 0 ]]; then
- log_error "Failed to clean $TARGET_DIRECTORY directory" $?
- exit 1
- fi
-}
-
-signFiles() {
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*.so
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*.so
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*.so
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*/*.so
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*/*/*.so
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*/*/*/*.so
- echo "Code Signing DYLIB Files"
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*/*/*/*/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/pyarrow/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/.dylibs/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/scipy/.dylibs/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/xgboost/.dylibs/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/cmdstan-2.26.1/stan/lib/stan_math/lib/tbb/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/torch/.dylibs/*.dylib
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/sklearn/.dylibs/*.dylib
- echo "Coce Signing LIB Files"
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/*.lib
- echo "Code Signing Other Files"
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/prophet_model.bin
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/cmdstan-2.26.1/bin/diagnose
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/cmdstan-2.26.1/bin/print
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/cmdstan-2.26.1/bin/stanc
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/prophet/stan_model/cmdstan-2.26.1/bin/stansummary
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/pyarrow/plasma-store-server
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/torch/bin/protoc
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/torch/bin/protoc-3.13.0.0
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/torch/bin/torch_shm_manager
- echo "Code Sign OpenBB Executable File"
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/OpenBBTerminal
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/OpenBBPlotsBackend
- codesign --deep --force --verify --verbose --options runtime --entitlements "$ENTITLEMENTS" -s "$APPLE_SIGNING_IDENTITY" build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/pywry
-}
-
-createInstallationDirectory() {
- if [ -d "${TARGET_DIRECTORY}" ]; then
- deleteInstallationDirectory
- fi
- mkdir -pv "$TARGET_DIRECTORY"
-
- if [[ $? != 0 ]]; then
- log_error "Failed to create $TARGET_DIRECTORY directory" $?
- exit 1
- fi
-}
-
-copyDarwinDirectory(){
- createInstallationDirectory
- cp -r "$SCRIPTPATH/darwin" "${TARGET_DIRECTORY}/"
- chmod -R 755 "${TARGET_DIRECTORY}/darwin/scripts"
- chmod -R 755 "${TARGET_DIRECTORY}/darwin/Resources"
- chmod 755 "${TARGET_DIRECTORY}/darwin/Distribution"
-}
-
-# This creates the target/darwin folder which contains the contents for formatting the installer
-# It also created the target/darkwinpkg folder which has the .app to install
-copyBuildDirectory() {
- sed -i '' -e 's/__VERSION__/'${VERSION}'/g' "${TARGET_DIRECTORY}/darwin/scripts/postinstall"
- sed -i '' -e 's/__PRODUCT__/'${PRODUCT}'/g' "${TARGET_DIRECTORY}/darwin/scripts/postinstall"
- chmod -R 755 "${TARGET_DIRECTORY}/darwin/scripts/postinstall"
-
- sed -i '' -e 's/__VERSION__/'${VERSION}'/g' "${TARGET_DIRECTORY}/darwin/Distribution"
- sed -i '' -e 's/__PRODUCT__/'${PRODUCT}'/g' "${TARGET_DIRECTORY}/darwin/Distribution"
- chmod -R 755 "${TARGET_DIRECTORY}/darwin/Distribution"
-
- sed -i '' -e 's/__VERSION__/'${VERSION}'/g' "${TARGET_DIRECTORY}"/darwin/Resources/*.html
- sed -i '' -e 's/__PRODUCT__/'${PRODUCT}'/g' "${TARGET_DIRECTORY}"/darwin/Resources/*.html
- chmod -R 755 "${TARGET_DIRECTORY}/darwin/Resources/"
-
- rm -rf "${TARGET_DIRECTORY}/darwinpkg"
- mkdir -p "${TARGET_DIRECTORY}/darwinpkg"
-
- mkdir -p "${TARGET_DIRECTORY}"/darwinpkg/Applications
- cp -a "${APP_DIRECTORY}" "${TARGET_DIRECTORY}"/darwinpkg/Applications
-
- rm -rf "${TARGET_DIRECTORY}/package"
- mkdir -p "${TARGET_DIRECTORY}/package"
- chmod -R 755 "${TARGET_DIRECTORY}/package"
-
- rm -rf "${TARGET_DIRECTORY}/pkg"
- mkdir -p "${TARGET_DIRECTORY}/pkg"
- chmod -R 755 "${TARGET_DIRECTORY}/pkg"
-}
-
-# Creates the target/package folder. This includes a basic.pkg file with no formatting
-function buildPackage() {
- log_info "Application installer package building started.(1/3)"
- # For now I removed the --identifier
- pkgbuild --version "${VERSION}" \
- --scripts "${TARGET_DIRECTORY}/darwin/scripts" \
- --root "${TARGET_DIRECTORY}/darwinpkg" \
- --identifier "OpenBB Terminal" \
- --ownership preserve \
- "${TARGET_DIRECTORY}/package/${PRODUCT}.pkg"
-}
-
-# Creates the target/pkg folder. This includes the finished pkg file to let users download
-function buildProduct() {
- log_info "Application installer product building started.(2/3)"
- productbuild --distribution "${TARGET_DIRECTORY}/darwin/Distribution" \
- --resources "${TARGET_DIRECTORY}/darwin/Resources" \
- --package-path "${TARGET_DIRECTORY}/package" \
- --sign "${APPLE_DEVELOPER_CERTIFICATE_ID}" \
- "${TARGET_DIRECTORY}/pkg/$1"
-}
-
-function createInstaller() {
- log_info "Application installer generation process started.(3 Steps)"
- buildPackage
- chmod -R 754 "${TARGET_DIRECTORY}/darwinpkg"
- buildProduct OpenBBTerminalM1.pkg
- log_info "Application installer generation steps finished."
-}
-
-#Main script
-log_info "Installer generating process started."
-
-copyDarwinDirectory
-copyBuildDirectory
-signFiles
-chmod 755 build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/.env
-# chmod -R 755 build/pyinstaller/macOS/target/darwinpkg/Applications/OpenBB\ Terminal/.OpenBB/
-createInstaller
-
-log_info "Installer generating process finished"
-exit 0
diff --git a/build/pyinstaller/macOS/darwin/Distribution b/build/pyinstaller/macOS/darwin/Distribution
deleted file mode 100644
index 9e328d8662c..00000000000
--- a/build/pyinstaller/macOS/darwin/Distribution
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
- __PRODUCT__
-
-
-
-
-
-
-
-
-
-
-
-
-
- __PRODUCT__.pkg
-
- __PRODUCT__.pkg
-
diff --git a/build/pyinstaller/macOS/darwin/Resources/LICENSE.txt b/build/pyinstaller/macOS/darwin/Resources/LICENSE.txt
deleted file mode 100644
index b86fa3e85d2..00000000000
--- a/build/pyinstaller/macOS/darwin/Resources/LICENSE.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2021-2024 OpenBB Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/build/pyinstaller/macOS/darwin/Resources/background-light.png b/build/pyinstaller/macOS/darwin/Resources/background-light.png
deleted file mode 100644
index 29b0d7c410a..00000000000
Binary files a/build/pyinstaller/macOS/darwin/Resources/background-light.png and /dev/null differ
diff --git a/build/pyinstaller/macOS/darwin/Resources/background.png b/build/pyinstaller/macOS/darwin/Resources/background.png
deleted file mode 100644
index 46a9ac8e48b..00000000000
Binary files a/build/pyinstaller/macOS/darwin/Resources/background.png and /dev/null differ
diff --git a/build/pyinstaller/macOS/darwin/Resources/banner.png b/build/pyinstaller/macOS/darwin/Resources/banner.png
deleted file mode 100644
index 3290b6063ea..00000000000
Binary files a/build/pyinstaller/macOS/darwin/Resources/banner.png and /dev/null differ
diff --git a/build/pyinstaller/macOS/darwin/Resources/conclusion.html b/build/pyinstaller/macOS/darwin/Resources/conclusion.html
deleted file mode 100644
index b8e90473469..00000000000
--- a/build/pyinstaller/macOS/darwin/Resources/conclusion.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
WSO2 API Manager
-
Thank you for installing WSO2 API MAnager.
-
Run API Manager
-
Open a new terminal and run the following command to get started with APIM server:
-
$ wso2am-2.6.0 [command]
-
-
-
-
Resources
-
Go through following links for additional information.
-
-
-
-
-
Uninstall WSO2 API Manager
-
Run the following command to uninstall WSO2 API Manager.
-
$ sudo bash /Library/__PRODUCT__/__VERSION__/uninstall.sh
-
-
-
-
Copyright © 2019 WSO2 Inc. All rights reserved. API Manager and its use subject to a license agreement and are also subject to copyright, trademark patent and/or other laws.
-
-
-
diff --git a/build/pyinstaller/macOS/darwin/Resources/uninstall.sh b/build/pyinstaller/macOS/darwin/Resources/uninstall.sh
deleted file mode 100644
index 09e364e9019..00000000000
--- a/build/pyinstaller/macOS/darwin/Resources/uninstall.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/bash
-
-#Generate application uninstallers for macOS.
-
-#Parameters
-DATE=`date +%Y-%m-%d`
-TIME=`date +%H:%M:%S`
-LOG_PREFIX="[$DATE $TIME]"
-
-#Functions
-log_info() {
- echo "${LOG_PREFIX}[INFO]" $1
-}
-
-log_warn() {
- echo "${LOG_PREFIX}[WARN]" $1
-}
-
-log_error() {
- echo "${LOG_PREFIX}[ERROR]" $1
-}
-
-#Check running user
-if (( $EUID != 0 )); then
- echo "Please run as root."
- exit
-fi
-
-echo "Welcome to Application Uninstaller"
-echo "The following packages will be REMOVED:"
-echo " __PRODUCT__-__VERSION__"
-while true; do
- read -p "Do you wish to continue [Y/n]?" answer
- [[ $answer == "y" || $answer == "Y" || $answer == "" ]] && break
- [[ $answer == "n" || $answer == "N" ]] && exit 0
- echo "Please answer with 'y' or 'n'"
-done
-
-
-#Need to replace these with install preparation script
-VERSION=__VERSION__
-PRODUCT=__PRODUCT__
-
-echo "Application uninstalling process started"
-# remove link to shortcut file
-find "/usr/local/bin/" -name "__PRODUCT__-__VERSION__" | xargs rm
-if [ $? -eq 0 ]
-then
- echo "[1/3] [DONE] Successfully deleted shortcut links"
-else
- echo "[1/3] [ERROR] Could not delete shortcut links" >&2
-fi
-
-#forget from pkgutil
-pkgutil --forget "org.$PRODUCT.$VERSION" > /dev/null 2>&1
-if [ $? -eq 0 ]
-then
- echo "[2/3] [DONE] Successfully deleted application informations"
-else
- echo "[2/3] [ERROR] Could not delete application informations" >&2
-fi
-
-#remove application source distribution
-[ -e "/Library/${PRODUCT}/${VERSION}" ] && rm -rf "/Library/${PRODUCT}/${VERSION}"
-if [ $? -eq 0 ]
-then
- echo "[3/3] [DONE] Successfully deleted application"
-else
- echo "[3/3] [ERROR] Could not delete application" >&2
-fi
-
-echo "Application uninstall process finished"
-exit 0
diff --git a/build/pyinstaller/macOS/darwin/Resources/welcome.html b/build/pyinstaller/macOS/darwin/Resources/welcome.html
deleted file mode 100644
index 393d21d1799..00000000000
--- a/build/pyinstaller/macOS/darwin/Resources/welcome.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
OpenBB is a leading open source investment research software platform for accessing and analyzing financial market data. We represent millions of investors who want to leverage state-of-the-art data science and machine learning technologies to make sense of raw unrefined data. Our mission is to make investment research effective, powerful and accessible to everyone.
-
This installation procedure will install the OpenBB Terminal.
-
-
-
-
diff --git a/build/pyinstaller/macOS/darwin/scripts/postinstall b/build/pyinstaller/macOS/darwin/scripts/postinstall
deleted file mode 100644
index 8c3cbfc39d9..00000000000
--- a/build/pyinstaller/macOS/darwin/scripts/postinstall
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-exit 0
diff --git a/build/pyinstaller/macOS_package_assets/OpenBB Terminal/OpenBB Terminal b/build/pyinstaller/macOS_package_assets/OpenBB Terminal/OpenBB Terminal
deleted file mode 100755
index 455e34ab2fb..00000000000
--- a/build/pyinstaller/macOS_package_assets/OpenBB Terminal/OpenBB Terminal
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-clear
-
-SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-
-echo " _ _ _ "
-echo " | | | (_) "
-echo " | | ___ __ _ __| |_ _ __ __ _ "
-echo " | | / _ \ / _\` |/ _\` | | '_ \ / _\` | "
-echo " | |___| (_) | (_| | (_| | | | | | (_| |_ _ _ "
-echo " |______\___/ \__,_|\__,_|_|_| |_|\__, (_|_|_)"
-echo " __/ | "
-echo " |___/ "
-
-BOOTUP_MESSAGE="
-The boot up process might take a while. Please be patient...
-
-OpenBB Terminal v3.2.5 will be the last version of this Terminal.
-
-To continue our mission of democratizing investment research, we are working on a new open-source CLI tool built on top of the OpenBB Platform that will soon be available to everyone for free on my.openbb.co.
-
-Join us on social media to be among the first to hear the news.
-
-Discord: openbb.co/discord
-Twitter: openbb.co/twitter
-LinkedIn: openbb.co/linkedin
-"
-echo "$BOOTUP_MESSAGE"
-
-"$SCRIPTDIR"/.OpenBB/OpenBBTerminal
diff --git a/build/pyinstaller/macOS_package_assets/README.TXT b/build/pyinstaller/macOS_package_assets/README.TXT
deleted file mode 100644
index a4911975f0c..00000000000
--- a/build/pyinstaller/macOS_package_assets/README.TXT
+++ /dev/null
@@ -1 +0,0 @@
-MOVE THE OpenBB Terminal FOLDER TO THE APPLICATIONS FOLDER
diff --git a/build/pyinstaller/setup_icons.applescript b/build/pyinstaller/setup_icons.applescript
deleted file mode 100644
index 8bbce2b5471..00000000000
--- a/build/pyinstaller/setup_icons.applescript
+++ /dev/null
@@ -1,20 +0,0 @@
-use framework "Foundation"
-use framework "AppKit"
-use scripting additions
-
--- Get script location
-tell application "Finder"
- set current_path to POSIX path of (container of (path to me) as alias)
-end tell
-
--- Set OpenBB Terminal folder icon relative to script location
-set sourcePath to current_path & "../../images/openbb_folder_icon.icns"
-set destPath to current_path & "macOS_package_assets/OpenBB Terminal"
-set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath)
-(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)
-
--- Set OpenBB Terminal launcher icon relative to script location
-set sourcePath to current_path & "../../images/openbb.icns"
-set destPath to current_path & "macOS_package_assets/OpenBB Terminal/OpenBB Terminal"
-set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath)
-(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)
diff --git a/build/pyinstaller/setup_icons_post.applescript b/build/pyinstaller/setup_icons_post.applescript
deleted file mode 100644
index 73587aca012..00000000000
--- a/build/pyinstaller/setup_icons_post.applescript
+++ /dev/null
@@ -1,20 +0,0 @@
-use framework "Foundation"
-use framework "AppKit"
-use scripting additions
-
--- Get script location
-tell application "Finder"
- set current_path to POSIX path of (container of (path to me) as alias)
-end tell
-
--- Set OpenBB Terminal launcher icon in folder relative to script location
-set sourcePath to current_path & "../../images/openbb.icns"
-set destPath to current_path & "../../DMG/OpenBB Terminal/.OpenBB/OpenBBTerminal"
-set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath)
-(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)
-
--- Set OpenBB pywry icon relative to script location
-set sourcePath to current_path & "../../images/openbb.icns"
-set destPath to current_path & "../../DMG/OpenBB Terminal/.OpenBB/pywry"
-set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath)
-(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)
diff --git a/build/pyinstaller/terminal.spec b/build/pyinstaller/terminal.spec
deleted file mode 100644
index 35d0b300995..00000000000
--- a/build/pyinstaller/terminal.spec
+++ /dev/null
@@ -1,217 +0,0 @@
-# -*- mode: python ; coding: utf-8 -*- # noqa
-import os
-from shutil import which
-import subprocess
-import sys
-from pathlib import Path
-import matplotlib
-
-import scipy
-from dotenv import set_key
-from PyInstaller.building.api import COLLECT, EXE, PYZ
-from PyInstaller.building.build_main import Analysis
-from PyInstaller.building.splash import Splash
-from PyInstaller.compat import is_darwin, is_win
-
-from openbb_terminal.loggers import get_commit_hash
-
-NAME = "OpenBBTerminal"
-
-build_type = (
- os.getenv("OPENBB_BUILD_TYPE") if bool(os.getenv("OPENBB_BUILD_TYPE")) else "folder"
-)
-repo_path = Path(os.getcwd()).resolve()
-
-# Local python environment packages folder
-venv_path = Path(sys.executable).parent.parent.resolve()
-
-# Check if we are running in a conda environment
-if is_darwin:
- pathex = os.path.join(os.path.dirname(os.__file__), "site-packages")
-elif "site-packages" in list(venv_path.iterdir()):
- pathex = str(venv_path / "site-packages")
-else:
- pathex = str(venv_path / "lib" / "site-packages")
-
-pathex = Path(pathex).resolve()
-
-# Removing unused ARM64 binary
-binary_to_remove = pathex / "_scs_direct.cpython-39-darwin.so"
-print("Removing ARM64 Binary: _scs_direct.cpython-39-darwin.so")
-binary_to_remove.unlink(missing_ok=True)
-build_assets_folder = repo_path / "build/pyinstaller"
-
-# Removing inspect hook
-destination = pathex / "pyinstaller/hooks/rthooks/pyi_rth_inspect.py"
-print("Replacing Pyinstaller Hook: pyi_rth_inspect.py")
-source = build_assets_folder / "hooks/pyi_rth_inspect.py"
-subprocess.run(["cp", str(source), str(destination)], check=True)
-
-
-# Get latest commit
-commit_hash = get_commit_hash()
-default_env_file = build_assets_folder / ".env"
-set_key(default_env_file, "OPENBB_LOGGING_COMMIT_HASH", str(commit_hash))
-
-# Files that are explicitly pulled into the bundle
-added_files = [
- (str(repo_path / "openbb_terminal"), "openbb_terminal"),
- (str(repo_path / "openbb_terminal/core/plots"), "openbb_terminal/core/plots"),
- (str(pathex / "property_cached"), "property_cached"),
- (str(pathex / "user_agent"), "user_agent"),
- (str(pathex / "vaderSentiment"), "vaderSentiment"),
- (str(pathex / "prophet"), "prophet"),
- (str(pathex / "whisper"), "whisper"),
- (str(pathex / "transformers"), "transformers"),
- (str(pathex / "linearmodels/datasets"), "./linearmodels/datasets"),
- (str(pathex / "statsmodels/datasets"), "./statsmodels/datasets"),
- (str(pathex / "debugpy/_vendored"), "./debugpy/_vendored"),
- (".env", "."),
- (str(pathex / "blib2to3/Grammar.txt"), "blib2to3"),
- (str(pathex / "blib2to3/PatternGrammar.txt"), "blib2to3"),
- (str(pathex / "streamlit"), "streamlit"),
- (str(pathex / "altair"), "altair"),
- (str(pathex / "pyarrow"), "pyarrow"),
- (which("pywry"), "."),
-]
-
-if is_win:
- added_files.extend(
- [
- (os.path.join(f"{os.path.dirname(scipy.__file__)}.libs"), "scipy.libs/"),
- (str(pathex / "frozendict/version.py"), "frozendict"),
- (
- os.path.join(f"{os.path.dirname(matplotlib.__file__)}.libs"),
- "matplotlib.libs/",
- ),
- ]
- )
-
-
-# Python libraries that are explicitly pulled into the bundle
-hidden_imports = [
- "sklearn.utils._cython_blas",
- "sklearn.utils._typedefs",
- "sklearn.utils._heap",
- "sklearn.utils._sorting",
- "sklearn.utils._vector_sentinel",
- "sklearn.neighbors.quad_tree",
- "sklearn.tree._utils",
- "sklearn.neighbors._partition_nodes",
- "sklearn.metrics._pairwise_distances_reduction._datasets_pair",
- "sklearn.metrics._pairwise_distances_reduction._middle_term_computer",
- "squarify",
- "linearmodels",
- "statsmodels",
- "user_agent",
- "vaderSentiment",
- "feedparser",
- "_sysconfigdata__darwin_darwin",
- "prophet",
- "debugpy",
- "pywry",
- "scipy.sparse.linalg._isolve._iterative",
- "whisper",
- "transformers",
- "yt_dlp",
- "textwrap3",
- "streamlit",
- "pytrends",
- "pytrends.request",
- "pyarrow",
-]
-
-
-if is_win:
- hidden_imports.append("frozendict")
-
-analysis_kwargs = dict(
- scripts=[str(repo_path / "terminal.py")],
- pathex=[str(pathex), "."],
- binaries=[],
- datas=added_files,
- hiddenimports=hidden_imports,
- hookspath=["build/pyinstaller/hooks"],
- hooksconfig={},
- runtime_hooks=["build/pyinstaller/hooks/hook-debugpy.py"],
- excludes=[],
- win_no_prefer_redirects=False,
- win_private_assemblies=False,
- cipher=None,
- noarchive=False,
-)
-
-a = Analysis(**analysis_kwargs)
-pyz = PYZ(a.pure, a.zipped_data, cipher=analysis_kwargs["cipher"])
-
-# Executable icon
-if is_win:
- exe_icon = (str(repo_path / "images/openbb_icon.ico"),)
-if is_darwin:
- exe_icon = (str(repo_path / "images/openbb.icns"),)
-
-
-exe_args = [
- pyz,
- a.scripts,
- [],
-]
-
-exe_kwargs = dict(
- name=NAME,
- debug=False,
- bootloader_ignore_signals=False,
- strip=False,
- upx=False,
- upx_exclude=[],
- console=True,
- disable_windowed_traceback=False,
- target_arch="x86_64",
- codesign_identity=None,
- entitlements_file=None,
- icon=exe_icon,
- version="version.rc",
-)
-
-
-# Packaging settings
-if build_type == "onefile":
- exe_args += [a.binaries, a.zipfiles, a.datas]
- exe_kwargs["runtime_tmpdir"] = None
-elif build_type == "folder":
- exe_kwargs["exclude_binaries"] = True
- collect_args = [
- a.binaries,
- a.zipfiles,
- a.datas,
- ]
- collect_kwargs = dict(
- strip=False,
- upx=True,
- upx_exclude=[],
- name=NAME,
- )
-
-
-# Platform specific settings
-if is_win:
- splash = Splash(
- str(repo_path / "images/openbb_splashscreen.png"),
- binaries=a.binaries,
- datas=a.datas,
- text_pos=(200, 400),
- text_size=12,
- text_color="white",
- )
- exe_args += [splash, splash.binaries]
-
-if is_darwin:
- exe_kwargs["argv_emulation"] = True
-
-exe = EXE(*exe_args, **exe_kwargs)
-
-if build_type == "folder":
- coll = COLLECT(
- *([exe] + collect_args),
- **collect_kwargs,
- )
diff --git a/build/pyinstaller/version.rc b/build/pyinstaller/version.rc
deleted file mode 100644
index 8a232ed040d..00000000000
--- a/build/pyinstaller/version.rc
+++ /dev/null
@@ -1,42 +0,0 @@
-# UTF-8
-#
-# For more details about fixed file info 'ffi' see:
-# http://msdn.microsoft.com/en-us/library/ms646997.aspx
-VSVersionInfo(
- ffi=FixedFileInfo(
- # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
- # Set not needed items to zero 0.
- filevers=(3, 2, 4, 0),
- prodvers=(3, 2, 4, 0),
- # Contains a bitmask that specifies the valid bits 'flags'r
- mask=0x0,
- # Contains a bitmask that specifies the Boolean attributes of the file.
- flags=0x0,
- # The operating system for which this file was designed.
- # 0x4 - NT and there is no need to change it.
- OS=0x4,
- # The general type of file.
- # 0x1 - the file is an application.
- fileType=0x1,
- # The function of the file.
- # 0x0 - the function is not defined for this fileType
- subtype=0x0,
- # Creation date and time stamp.
- date=(0, 0)
- ),
- kids=[
- StringFileInfo(
- [
- StringTable(
- '040904b0',
- [StringStruct('Comments', 'The OpenBB Terminal. For additional details, visit OpenBB.co'),
- StringStruct('CompanyName', 'OpenBB'),
- StringStruct('FileDescription', 'OpenBB Terminal Program'),
- StringStruct('FileVersion', '5.0'),
- StringStruct('InternalName', 'OpenBB Terminal'),
- StringStruct('ProductName', 'OpenBB Terminal'),
- StringStruct('ProductVersion', '3.2.5.0')])
- ]),
- VarFileInfo([VarStruct('Translation', [1033, 1200])])
- ]
-)
diff --git a/build/pypi/openbb_terminal/build.sh b/build/pypi/openbb_terminal/build.sh
deleted file mode 100755
index b02d361e436..00000000000
--- a/build/pypi/openbb_terminal/build.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-# BUILD `openbb_terminal/.env`
-OPENBB_LOGGING_COMMIT_HASH="sha:$(git rev-parse --short=8 HEAD)"
-OPENBB_LOGGING_APP_NAME="gst_packaged_pypi"
-echo "OPENBB_LOGGING_APP_NAME='${OPENBB_LOGGING_APP_NAME}'" > openbb_terminal/.env
-echo "OPENBB_LOGGING_COMMIT_HASH=${OPENBB_LOGGING_COMMIT_HASH}" >> openbb_terminal/.env
-
-poetry publish --build
\ No newline at end of file
diff --git a/build/pypi/openbb_terminal/test_publish.sh b/build/pypi/openbb_terminal/test_publish.sh
deleted file mode 100755
index ef5c2e1a348..00000000000
--- a/build/pypi/openbb_terminal/test_publish.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-# BUILD `openbb_terminal/.env`
-OPENBB_LOGGING_COMMIT_HASH="sha:$(git rev-parse --short=8 HEAD)"
-OPENBB_LOGGING_APP_NAME="gst_packaged_pypi"
-echo "OPENBB_LOGGING_APP_NAME='${OPENBB_LOGGING_APP_NAME}'" > openbb_terminal/.env
-echo "OPENBB_LOGGING_COMMIT_HASH=${OPENBB_LOGGING_COMMIT_HASH}" >> openbb_terminal/.env
-
-poetry publish -r testpypi --build
\ No newline at end of file
diff --git a/examples/usdLiquidityIndex.ipynb b/examples/usdLiquidityIndex.ipynb
index 7681e0c9488..4f177babebb 100644
--- a/examples/usdLiquidityIndex.ipynb
+++ b/examples/usdLiquidityIndex.ipynb
@@ -20,12 +20,10 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
- "import json\n",
- "\n",
"from openbb import obb\n",
"from pandas import DataFrame\n"
]
@@ -42,7 +40,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
@@ -50,15 +48,15 @@
"text/plain": [
"OBBject\n",
"\n",
- "id: 06580cab-5bdc-7e28-8000-632a5d8dc92f\n",
- "results: [{'date': datetime.date(2002, 12, 18), 'WALCL': 719542.0, 'WLRRAL': 21905....\n",
+ "id: 0660e135-3511-73d7-8000-b792f7e418bb\n",
+ "results: [{'date': datetime.date(2002, 12, 18), 'provider': 'fred', 'WALCL': 719542...\n",
"provider: fred\n",
- "warnings: [{'category': 'UserWarning', 'message': '{\"WALCL\": {\"title\": \"Assets: Tot...\n",
+ "warnings: None\n",
"chart: None\n",
- "extra: {'metadata': {'arguments': {'provider_choices': {'provider': None}, 'standar..."
+ "extra: {'results_metadata': {'WALCL': {'title': 'Assets: Total Assets: Total Assets..."
]
},
- "execution_count": 4,
+ "execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
@@ -78,7 +76,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -110,7 +108,7 @@
}
],
"source": [
- "metadata = json.loads(data.warnings[0].message)\n",
+ "metadata = data.extra[\"results_metadata\"]\n",
"\n",
"display(metadata.keys())\n",
"display(metadata[\"WALCL\"].get(\"title\"))\n",
@@ -573,7 +571,7 @@
},
{
"cell_type": "code",
- "execution_count": 33,
+ "execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -10197,7 +10195,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.4"
+ "version": "3.11.7"
},
"orig_nbformat": 4
},
diff --git a/openbb_platform/assets/providers.json b/openbb_platform/assets/providers.json
new file mode 100644
index 00000000000..c40e8cece34
--- /dev/null
+++ b/openbb_platform/assets/providers.json
@@ -0,0 +1,58 @@
+[
+ {
+ "name": "Alpha Vantage",
+ "credentials": ["alpha_vantage_api_key"],
+ "v3_credentials": ["API_KEY_ALPHAVANTAGE"],
+ "link": "https://www.alphavantage.co/support/#api-key",
+ "instructions": "Go to: https://www.alphavantage.co/support/#api-key\n\n\n\nFill out the form, pass Captcha, and click on, \"GET FREE API KEY\"."
+ },
+ {
+ "name": "BizToc",
+ "credentials": ["biztoc_api_key"],
+ "v3_credentials": ["API_BIZTOC_TOKEN"],
+ "link": "https://biztoc.com",
+ "instructions": "The BizToc API is hosted on RapidAPI. To set up, go to: https://rapidapi.com/thma/api/biztoc.\n\n\n\nIn the top right, select 'Sign Up'. After answering some questions, you will be prompted to select one of their plans.\n\n\n\nAfter signing up, navigate back to https://rapidapi.com/thma/api/biztoc. If you are logged in, you will see a header called X-RapidAPI-Key.\n\n"
+ },
+ {
+ "name": "FRED",
+ "credentials": ["fred_api_key"],
+ "v3_credentials": ["API_FRED_KEY"],
+ "link": "https://fred.stlouisfed.org/docs/api/api_key.html",
+ "instructions": "Go to: https://fred.stlouisfed.org\n\n\n\nClick on, \"My Account\", create a new account or sign in with Google:\n\n\n\nAfter completing the sign-up, go to \"My Account\", and select \"API Keys\". Then, click on, \"Request API Key\".\n\n\n\nFill in the box for information about the use-case for FRED, and by clicking, \"Request API key\", at the bottom of the page, the API key will be issued.\n\n"
+ },
+ {
+ "name": "Financial Modeling Prep",
+ "credentials": ["fmp_api_key"],
+ "v3_credentials": ["API_KEY_FINANCIALMODELINGPREP"],
+ "link": "https://financialmodelingprep.com/developer",
+ "instructions": "Go to: https://site.financialmodelingprep.com/developer/docs\n\n\n\nClick on, \"Get my API KEY here\", and sign up for a free account.\n\n\n\nWith an account created, sign in and navigate to the Dashboard, which shows the assigned token. by pressing the \"Dashboard\" button which will show the API key.\n\n"
+ },
+ {
+ "name": "Intrinio",
+ "credentials": ["intrinio_api_key"],
+ "v3_credentials": ["API_INTRINIO_KEY"],
+ "link": "https://intrinio.com/",
+ "instructions": "Go to: https://intrinio.com/starter-plan\n\n\n\nAn API key will be issued with a subscription. Find the token value within the account dashboard."
+ },
+ {
+ "name": "Polygon",
+ "credentials": ["polygon_api_key"],
+ "v3_credentials": ["API_POLYGON_KEY"],
+ "link": "https://polygon.io",
+ "instructions": "Go to: https://polygon.io\n\n\n\nClick on, \"Get your Free API Key\".\n\n\n\nAfter signing up, the API Key is found at the bottom of the account dashboard page.\n\n"
+ },
+ {
+ "name": "Nasdaq",
+ "credentials": ["nasdaq_api_key"],
+ "v3_credentials": ["API_KEY_QUANDL"],
+ "link": "https://www.quandl.com/tools/api",
+ "instructions": "Go to: https://www.quandl.com\n\n\n\nClick on, \"Sign Up\", and register a new account.\n\n\n\nFollow the sign-up instructions, and upon completion the API key will be assigned.\n\n"
+ },
+ {
+ "name": "Tradier",
+ "credentials": ["tradier_api_key"],
+ "v3_credentials": ["API_TRADIER_TOKEN"],
+ "link": "https://tradier.com/products/market-data-api",
+ "instructions": "Go to: https://documentation.tradier.com\n\n\n\nClick on, \"Open Account\", to start the sign-up process. After the account has been setup, navigate to [Tradier Broker Dash](https://dash.tradier.com/login?redirect=settings.api) and create the application. Request a sandbox access token."
+ }
+]
diff --git a/openbb_platform/core/integration/test_obbject.py b/openbb_platform/core/integration/test_obbject.py
index 735dc520969..4a2d1ad339f 100644
--- a/openbb_platform/core/integration/test_obbject.py
+++ b/openbb_platform/core/integration/test_obbject.py
@@ -1,3 +1,5 @@
+"""Test the OBBject."""
+
import contextlib
import sys
diff --git a/openbb_platform/core/openbb_core/__init__.py b/openbb_platform/core/openbb_core/__init__.py
index e69de29bb2d..13627db21b0 100644
--- a/openbb_platform/core/openbb_core/__init__.py
+++ b/openbb_platform/core/openbb_core/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core."""
diff --git a/openbb_platform/core/openbb_core/api/app_loader.py b/openbb_platform/core/openbb_core/api/app_loader.py
index 9c8330af10b..d6ff19878ca 100644
--- a/openbb_platform/core/openbb_core/api/app_loader.py
+++ b/openbb_platform/core/openbb_core/api/app_loader.py
@@ -1,11 +1,29 @@
+"""App loader module."""
+
from typing import List, Optional
from fastapi import APIRouter, FastAPI
+from openbb_core.app.router import RouterLoader
class AppLoader:
"""App loader."""
+ @staticmethod
+ def get_openapi_tags() -> List[dict]:
+ """Get openapi tags."""
+ main_router = RouterLoader.from_extensions()
+ openapi_tags = []
+ # Add tag data for each router in the main router
+ for r in main_router.routers:
+ openapi_tags.append(
+ {
+ "name": r,
+ "description": main_router.get_attr(r, "description"),
+ }
+ )
+ return openapi_tags
+
@staticmethod
def from_routers(
app: FastAPI, routers: List[Optional[APIRouter]], prefix: str
diff --git a/openbb_platform/core/openbb_core/api/auth/user.py b/openbb_platform/core/openbb_core/api/auth/user.py
index dab13cca5a9..c61aa5303cf 100644
--- a/openbb_platform/core/openbb_core/api/auth/user.py
+++ b/openbb_platform/core/openbb_core/api/auth/user.py
@@ -1,3 +1,5 @@
+"""User authentication."""
+
import secrets
from typing import Optional
@@ -15,7 +17,6 @@ async def authenticate_user(
credentials: Annotated[Optional[HTTPBasicCredentials], Depends(security)]
):
"""Authenticate the user."""
-
if credentials:
username = Env().API_USERNAME
password = Env().API_PASSWORD
diff --git a/openbb_platform/core/openbb_core/api/dependency/__init__.py b/openbb_platform/core/openbb_core/api/dependency/__init__.py
index e69de29bb2d..4abe38b227a 100644
--- a/openbb_platform/core/openbb_core/api/dependency/__init__.py
+++ b/openbb_platform/core/openbb_core/api/dependency/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core API Dependency."""
diff --git a/openbb_platform/core/openbb_core/api/rest_api.py b/openbb_platform/core/openbb_core/api/rest_api.py
index f166a91be4e..0ad92f613b6 100644
--- a/openbb_platform/core/openbb_core/api/rest_api.py
+++ b/openbb_platform/core/openbb_core/api/rest_api.py
@@ -67,13 +67,13 @@ app = FastAPI(
],
lifespan=lifespan,
)
-
app.add_middleware(
CORSMiddleware,
allow_origins=system.api_settings.cors.allow_origins,
allow_methods=system.api_settings.cors.allow_methods,
allow_headers=system.api_settings.cors.allow_headers,
)
+app.openapi_tags = AppLoader.get_openapi_tags()
AppLoader.from_routers(
app=app,
routers=(
diff --git a/openbb_platform/core/openbb_core/api/router/__init__.py b/openbb_platform/core/openbb_core/api/router/__init__.py
index e69de29bb2d..93fabc683c4 100644
--- a/openbb_platform/core/openbb_core/api/router/__init__.py
+++ b/openbb_platform/core/openbb_core/api/router/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core API Router."""
diff --git a/openbb_platform/core/openbb_core/api/router/commands.py b/openbb_platform/core/openbb_core/api/router/commands.py
index d524156ddbd..ce1cab484a8 100644
--- a/openbb_platform/core/openbb_core/api/router/commands.py
+++ b/openbb_platform/core/openbb_core/api/router/commands.py
@@ -132,8 +132,8 @@ def validate_output(c_out: OBBject) -> OBBject:
Returns
-------
- OBBject
- Validated OBBject object.
+ Dict
+ Serialized OBBject.
"""
def is_model(type_):
@@ -188,7 +188,7 @@ def build_api_wrapper(
func.__annotations__ = new_annotations_map
@wraps(wrapped=func)
- async def wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]):
+ async def wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]) -> OBBject:
user_settings: UserSettings = UserSettings.model_validate(
kwargs.pop(
"__authenticated_user_settings",
@@ -198,8 +198,7 @@ def build_api_wrapper(
execute = partial(command_runner.run, path, user_settings)
output: OBBject = await execute(*args, **kwargs)
- output = validate_output(output)
- return output
+ return validate_output(output)
return wrapper
diff --git a/openbb_platform/core/openbb_core/app/__init__.py b/openbb_platform/core/openbb_core/app/__init__.py
index e69de29bb2d..6b3de9cda26 100644
--- a/openbb_platform/core/openbb_core/app/__init__.py
+++ b/openbb_platform/core/openbb_core/app/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core App Module."""
diff --git a/openbb_platform/core/openbb_core/app/command_runner.py b/openbb_platform/core/openbb_core/app/command_runner.py
index 3a43aba3e8a..4d9985a2172 100644
--- a/openbb_platform/core/openbb_core/app/command_runner.py
+++ b/openbb_platform/core/openbb_core/app/command_runner.py
@@ -1,5 +1,7 @@
"""Command runner module."""
+# pylint: disable=R0903
+
from copy import deepcopy
from dataclasses import asdict, is_dataclass
from datetime import datetime
@@ -37,6 +39,7 @@ class ExecutionContext:
system_settings: SystemSettings,
user_settings: UserSettings,
) -> None:
+ """Initialize the execution context."""
self.command_map = command_map
self.route = route
self.system_settings = system_settings
@@ -192,6 +195,8 @@ class ParametersBuilder:
):
valid = asdict(annotation()) # type: ignore
for p in extra_params:
+ if "chart_params" in p:
+ continue
if p not in valid:
warn(
message=f"Parameter '{p}' not found.",
@@ -224,7 +229,7 @@ class ParametersBuilder:
}
# We allow extra fields to return with model with 'cc: CommandContext'
config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
- ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore
+ ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore # pylint: disable=C0103
# Validate and coerce
model = ValidationModel(**kwargs)
ParametersBuilder._warn_kwargs(
@@ -233,6 +238,7 @@ class ParametersBuilder:
)
return dict(model)
+ # pylint: disable=R0913
@classmethod
def build(
cls,
@@ -273,6 +279,7 @@ class ParametersBuilder:
return kwargs
+# pylint: disable=too-few-public-methods
class StaticCommandRunner:
"""Static Command Runner."""
@@ -281,6 +288,7 @@ class StaticCommandRunner:
cls,
func: Callable,
kwargs: Dict[str, Any],
+ show_warnings: bool = True, # pylint: disable=unused-argument # type: ignore
) -> OBBject:
"""Run a command and return the output."""
obbject = await maybe_coroutine(func, **kwargs)
@@ -300,8 +308,29 @@ class StaticCommandRunner:
raise OpenBBError(
"Charting is not installed. Please install `openbb-charting`."
)
- obbject.charting.show(render=False, **kwargs) # type: ignore
+ chart_params = {}
+ extra_params = kwargs.get("extra_params", {})
+ if hasattr(extra_params, "__dict__") and hasattr(extra_params, "chart_params"):
+ chart_params = kwargs["extra_params"].__dict__.get("chart_params", {})
+ elif isinstance(extra_params, dict) and "chart_params" in extra_params:
+ chart_params = kwargs["extra_params"].get("chart_params", {})
+
+ if "chart_params" in kwargs and kwargs["chart_params"] is not None:
+ chart_params.update(kwargs.pop("chart_params", {}))
+
+ if (
+ "kwargs" in kwargs
+ and "chart_params" in kwargs["kwargs"]
+ and kwargs["kwargs"].get("chart_params") is not None
+ ):
+ chart_params.update(kwargs.pop("kwargs", {}).get("chart_params", {}))
+
+ if chart_params:
+ kwargs.update(chart_params)
+ obbject.charting.show(render=False, **kwargs)
+
+ # pylint: disable=R0913, R0914
@classmethod
async def _execute_func(
cls,
@@ -348,7 +377,6 @@ class StaticCommandRunner:
# pylint: disable=protected-access
obbject._route = route
obbject._standard_params = kwargs.get("standard_params", None)
-
if chart and obbject.results:
cls._chart(obbject, **kwargs)
@@ -381,6 +409,7 @@ class StaticCommandRunner:
)
return obbject
+ # pylint: disable=W0718
@classmethod
async def run(
cls,
diff --git a/openbb_platform/core/openbb_core/app/deprecation.py b/openbb_platform/core/openbb_core/app/deprecation.py
index 5dd38c1ae54..a771589913a 100644
--- a/openbb_platform/core/openbb_core/app/deprecation.py
+++ b/openbb_platform/core/openbb_core/app/deprecation.py
@@ -50,6 +50,7 @@ class OpenBBDeprecationWarning(DeprecationWarning):
since: Optional[Tuple[int, int]] = None,
expected_removal: Optional[Tuple[int, int]] = None,
) -> None:
+ """Initialize the warning."""
super().__init__(message, *args)
self.message = message.rstrip(".")
self.since = since or get_major_minor(VERSION)
diff --git a/openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py b/openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py
index 17ccd72ff64..f3f8c8f9066 100644
--- a/openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py
+++ b/openbb_platform/core/openbb_core/app/logs/formatters/formatter_with_exceptions.py
@@ -1,16 +1,15 @@
-# IMPORTATION STANDARD
+"""Logging Formatter that includes formatting of Exceptions."""
+
import logging
import re
from copy import deepcopy
from pathlib import Path
-# IMPORTATION THIRDPARTY
-# IMPORTATION INTERNAL
from openbb_core.app.logs.models.logging_settings import LoggingSettings
class FormatterWithExceptions(logging.Formatter):
- """Logging Formatter that includes formatting of Exceptions"""
+ """Logging Formatter that includes formatting of Exceptions."""
DATEFORMAT = "%Y-%m-%dT%H:%M:%S%z"
LOGFORMAT = "%(asctime)s|%(name)s|%(funcName)s|%(lineno)s|%(message)s"
@@ -20,6 +19,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def calculate_level_name(record: logging.LogRecord) -> str:
+ """Calculate the level name of the log record."""
if record.exc_text:
level_name = "X"
elif record.levelname:
@@ -31,6 +31,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def extract_log_extra(record: logging.LogRecord):
+ """Extract extra log information from the record."""
log_extra = dict()
if hasattr(record, "func_name_override"):
@@ -44,6 +45,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def mock_ipv4(text: str) -> str:
+ """Mock IPv4 addresses in the text."""
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
replacement = " FILTERED_IP "
text_mocked = re.sub(pattern, replacement, text)
@@ -52,6 +54,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def mock_email(text: str) -> str:
+ """Mock email addresses in the text."""
pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
replacement = " FILTERED_EMAIL "
text_mocked = re.sub(pattern, replacement, text)
@@ -60,6 +63,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def mock_password(text: str) -> str:
+ """Mock passwords in the text."""
pattern = r'("password": ")[^"]+'
replacement = r"\1 FILTERED_PASSWORD "
text_mocked = re.sub(pattern, replacement, text)
@@ -67,6 +71,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def mock_flair(text: str) -> str:
+ """Mock flair in the text."""
pattern = r'("FLAIR": "\[)(.*?)\]'
replacement = r"\1 FILTERED_FLAIR ]"
text_mocked = re.sub(pattern, replacement, text)
@@ -75,6 +80,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def mock_home_directory(text: str) -> str:
+ """Mock home directory in the text."""
user_home_directory = str(Path.home().as_posix())
text_mocked = text.replace("\\", "/").replace(
user_home_directory, "MOCKING_USER_PATH"
@@ -84,6 +90,7 @@ class FormatterWithExceptions(logging.Formatter):
@staticmethod
def filter_special_tags(text: str) -> str:
+ """Filter special tags in the text."""
text_filtered = text.replace("\n", " MOCKING_BREAKLINE ")
text_filtered = text_filtered.replace("'Traceback", "Traceback")
@@ -91,6 +98,7 @@ class FormatterWithExceptions(logging.Formatter):
@classmethod
def filter_piis(cls, text: str) -> str:
+ """Filter Personally Identifiable Information in the text."""
text_filtered = cls.mock_ipv4(text=text)
text_filtered = cls.mock_email(text=text_filtered)
text_filtered = cls.mock_password(text=text_filtered)
@@ -101,6 +109,7 @@ class FormatterWithExceptions(logging.Formatter):
@classmethod
def filter_log_line(cls, text: str):
+ """Filter log line."""
text_filtered = cls.filter_special_tags(text=text)
text_filtered = cls.filter_piis(text=text_filtered)
@@ -113,6 +122,7 @@ class FormatterWithExceptions(logging.Formatter):
style="%",
validate=True,
) -> None:
+ """Initialize the FormatterWithExceptions."""
super().__init__(
fmt=self.LOGFORMAT,
datefmt=self.DATEFORMAT,
@@ -123,15 +133,18 @@ class FormatterWithExceptions(logging.Formatter):
@property
def settings(self) -> LoggingSettings:
+ """Get the settings."""
return deepcopy(self.__settings)
@settings.setter
def settings(self, settings: LoggingSettings) -> None:
+ """Set the settings."""
self.__settings = settings
# OVERRIDE
def formatException(self, ei) -> str:
- """Exception formatting handler
+ """Define the Exception formatting handler.
+
Parameters
----------
ei : logging._SysExcInfoType
@@ -141,13 +154,13 @@ class FormatterWithExceptions(logging.Formatter):
str
Formatted exception
"""
-
result = super().formatException(ei)
return repr(result)
# OVERRIDE
def format(self, record: logging.LogRecord) -> str:
- """Log formatter
+ """Define the Log formatter.
+
Parameters
----------
record : logging.LogRecord
@@ -157,7 +170,6 @@ class FormatterWithExceptions(logging.Formatter):
str
Formatted_log message
"""
-
level_name = self.calculate_level_name(record=record)
log_prefix_content = {
"appName": self.settings.app_name,
diff --git a/openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py b/openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py
index d8b65afaa22..acf8a50d5cc 100644
--- a/openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py
+++ b/openbb_platform/core/openbb_core/app/logs/handlers/path_tracking_file_handler.py
@@ -1,3 +1,5 @@
+"""Path Tracking File Handler."""
+
# IMPORTATION STANDARD
from copy import deepcopy
from logging.handlers import TimedRotatingFileHandler
@@ -17,8 +19,11 @@ TMP_FOLDER_NAME = "tmp"
class PathTrackingFileHandler(TimedRotatingFileHandler):
+ """Path Tracking File Handler."""
+
@staticmethod
def build_log_file_path(settings: LoggingSettings) -> Path:
+ """Build the log file path."""
app_name = settings.app_name
directory = settings.user_logs_directory
session_id = settings.session_id
@@ -27,8 +32,7 @@ class PathTrackingFileHandler(TimedRotatingFileHandler):
return path
def clean_expired_files(self, before_timestamp: float):
- """Remove expired files from logs directory"""
-
+ """Remove expired files from logs directory."""
logs_dir = self.settings.user_logs_directory
archives_directory = logs_dir / ARCHIVES_FOLDER_NAME
tmp_directory = logs_dir / TMP_FOLDER_NAME
@@ -51,10 +55,12 @@ class PathTrackingFileHandler(TimedRotatingFileHandler):
@property
def settings(self) -> LoggingSettings:
+ """Get the settings."""
return deepcopy(self.__settings)
@settings.setter
def settings(self, settings: LoggingSettings) -> None:
+ """Set the settings."""
self.__settings = settings
# OVERRIDE
@@ -64,6 +70,7 @@ class PathTrackingFileHandler(TimedRotatingFileHandler):
*args,
**kwargs,
) -> None:
+ """Initialize the PathTrackingFileHandler."""
# SETUP PARENT CLASS
filename = str(self.build_log_file_path(settings=settings))
frequency = settings.frequency
diff --git a/openbb_platform/core/openbb_core/app/logs/handlers_manager.py b/openbb_platform/core/openbb_core/app/logs/handlers_manager.py
index 3f2229b13c0..2d9a0efb927 100644
--- a/openbb_platform/core/openbb_core/app/logs/handlers_manager.py
+++ b/openbb_platform/core/openbb_core/app/logs/handlers_manager.py
@@ -1,9 +1,8 @@
-# IMPORT STANDARD
+"""Handlers Manager."""
+
import logging
import sys
-# IMPORT THIRD-PARTY
-# IMPORT INTERNAL
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
@@ -15,7 +14,10 @@ from openbb_core.app.logs.models.logging_settings import LoggingSettings
class HandlersManager:
+ """Handlers Manager."""
+
def __init__(self, settings: LoggingSettings):
+ """Initialize the HandlersManager."""
self._handlers = settings.handler_list
self._settings = settings
@@ -34,36 +36,42 @@ class HandlersManager:
logging.getLogger().debug("Unknown log handler.")
def _add_posthog_handler(self):
+ """Add a Posthog handler."""
handler = PosthogHandler(settings=self._settings)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_stdout_handler(self):
+ """Add a stdout handler."""
handler = logging.StreamHandler(sys.stdout)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_stderr_handler(self):
+ """Add a stderr handler."""
handler = logging.StreamHandler(sys.stderr)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_noop_handler(self):
+ """Add a null handler."""
handler = logging.NullHandler()
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_file_handler(self):
+ """Add a file handler."""
handler = PathTrackingFileHandler(settings=self._settings)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def update_handlers(self, settings: LoggingSettings):
+ """Update the handlers with new settings."""
logger = logging.getLogger()
for hdlr in logger.handlers:
if isinstance(hdlr, (PathTrackingFileHandler, PosthogHandler)):
diff --git a/openbb_platform/core/openbb_core/app/logs/logging_service.py b/openbb_platform/core/openbb_core/app/logs/logging_service.py
index 28067def685..abc605ca7d8 100644
--- a/openbb_platform/core/openbb_core/app/logs/logging_service.py
+++ b/openbb_platform/core/openbb_core/app/logs/logging_service.py
@@ -19,8 +19,7 @@ from pydantic_core import to_jsonable_python
class LoggingService(metaclass=SingletonMeta):
- """
- Logging Manager class responsible for managing logging settings and handling logs.
+ """Logging Manager class responsible for managing logging settings and handling logs.
Attributes
----------
@@ -59,8 +58,7 @@ class LoggingService(metaclass=SingletonMeta):
system_settings: SystemSettings,
user_settings: UserSettings,
) -> None:
- """
- Logging Service Constructor.
+ """Define the Logging Service Constructor.
Sets up the logging settings and handlers and then logs the startup information.
@@ -82,8 +80,7 @@ class LoggingService(metaclass=SingletonMeta):
@property
def logging_settings(self) -> LoggingSettings:
- """
- Current logging settings.
+ """Define the Current logging settings.
Returns
-------
@@ -94,8 +91,7 @@ class LoggingService(metaclass=SingletonMeta):
@logging_settings.setter
def logging_settings(self, value: Tuple[SystemSettings, UserSettings]):
- """
- Setter for updating the logging settings.
+ """Define the Setter for updating the logging settings.
Parameters
----------
@@ -109,8 +105,7 @@ class LoggingService(metaclass=SingletonMeta):
)
def _setup_handlers(self) -> HandlersManager:
- """
- Setup Logging Handlers.
+ """Set up Logging Handlers.
Returns
-------
diff --git a/openbb_platform/core/openbb_core/app/logs/models/logging_settings.py b/openbb_platform/core/openbb_core/app/logs/models/logging_settings.py
index 1cf14d5240f..708246f9e9a 100644
--- a/openbb_platform/core/openbb_core/app/logs/models/logging_settings.py
+++ b/openbb_platform/core/openbb_core/app/logs/models/logging_settings.py
@@ -1,3 +1,5 @@
+"""Logging settings."""
+
from pathlib import Path
from typing import List, Optional
@@ -8,11 +10,14 @@ from openbb_core.app.model.user_settings import UserSettings
# pylint: disable=too-many-instance-attributes
class LoggingSettings:
+ """Logging settings."""
+
def __init__(
self,
user_settings: Optional[UserSettings] = None,
system_settings: Optional[SystemSettings] = None,
):
+ """Initialize the logging settings."""
user_settings = user_settings or UserSettings()
system_settings = system_settings or SystemSettings()
diff --git a/openbb_platform/core/openbb_core/app/logs/utils/expired_files.py b/openbb_platform/core/openbb_core/app/logs/utils/expired_files.py
index 4f101d23db8..feb933c9a69 100644
--- a/openbb_platform/core/openbb_core/app/logs/utils/expired_files.py
+++ b/openbb_platform/core/openbb_core/app/logs/utils/expired_files.py
@@ -1,20 +1,19 @@
-# IMPORTATION STANDARD
+"""Expired files management utilities."""
+
import contextlib
from datetime import datetime
from pathlib import Path
from typing import List
-# IMPORTATION THIRDPARTY
-
-# IMPORTATION INTERNAL
-
def get_timestamp_from_x_days(x: int) -> float:
+ """Get the timestamp from x days ago."""
timestamp_from_x_days = datetime.now().timestamp() - x * 86400
return timestamp_from_x_days
def get_expired_file_list(directory: Path, before_timestamp: float) -> List[Path]:
+ """Get the list of expired files from a directory."""
expired_files = []
if directory.is_dir(): # Check if the directory exists and is a directory
for file in directory.iterdir():
@@ -25,6 +24,7 @@ def get_expired_file_list(directory: Path, before_timestamp: float) -> List[Path
def remove_file_list(file_list: List[Path]):
+ """Remove a list of files."""
for file in file_list:
with contextlib.suppress(PermissionError):
file.unlink(missing_ok=True)
diff --git a/openbb_platform/core/openbb_core/app/logs/utils/utils.py b/openbb_platform/core/openbb_core/app/logs/utils/utils.py
index a94efd331cb..e150627b147 100644
--- a/openbb_platform/core/openbb_core/app/logs/utils/utils.py
+++ b/openbb_platform/core/openbb_core/app/logs/utils/utils.py
@@ -1,12 +1,10 @@
-# IMPORT STANDARD
+"""Utility functions for logging."""
+
import time
import uuid
import warnings
from pathlib import Path, PosixPath
-# IMPORT THIRD-PARTY
-# IMPORT INTERNAL
-
def get_session_id() -> str:
"""UUID of the current session."""
@@ -41,6 +39,7 @@ def get_log_dir(contextual_user_data_directory: str) -> PosixPath:
def create_log_dir_if_not_exists(contextual_user_data_directory: str) -> Path:
+ """Create a log directory for the current installation."""
log_dir = Path(contextual_user_data_directory).joinpath("logs").absolute()
if not log_dir.is_dir():
log_dir.mkdir(parents=True, exist_ok=True)
@@ -49,6 +48,7 @@ def create_log_dir_if_not_exists(contextual_user_data_directory: str) -> Path:
def create_log_uuid_if_not_exists(log_dir: Path) -> str:
+ """Create a log id file for the current logging session."""
log_id = get_log_id(log_dir)
if not log_id.is_file():
logging_id = f"{uuid.uuid4()}"
@@ -60,10 +60,12 @@ def create_log_uuid_if_not_exists(log_dir: Path) -> str:
def get_log_id(log_dir):
+ """Get the log id file."""
return (log_dir / ".logid").absolute()
def create_uuid_dir_if_not_exists(log_dir, logging_id) -> PosixPath:
+ """Create a directory for the current logging session."""
uuid_log_dir = (log_dir / logging_id).absolute()
if not uuid_log_dir.is_dir():
diff --git a/openbb_platform/core/openbb_core/app/model/__init__.py b/openbb_platform/core/openbb_core/app/model/__init__.py
index e69de29bb2d..44adb9707fe 100644
--- a/openbb_platform/core/openbb_core/app/model/__init__.py
+++ b/openbb_platform/core/openbb_core/app/model/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core App Model."""
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/__init__.py b/openbb_platform/core/openbb_core/app/model/abstract/__init__.py
index e69de29bb2d..65daa6e5650 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/__init__.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core App Abstract Model."""
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/error.py b/openbb_platform/core/openbb_core/app/model/abstract/error.py
index 95cc53b434e..91c67d9aa24 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/error.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/error.py
@@ -1,3 +1,5 @@
+"""OpenBB Error."""
+
from typing import Optional, Union
@@ -5,5 +7,6 @@ class OpenBBError(Exception):
"""OpenBB Error."""
def __init__(self, original: Optional[Union[str, Exception]] = None):
+ """Initialize the OpenBBError."""
self.original = original
super().__init__(str(original))
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/results.py b/openbb_platform/core/openbb_core/app/model/abstract/results.py
index 5152f0fad58..a9456b99189 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/results.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/results.py
@@ -1,3 +1,5 @@
+"""OpenBB Core App Model Abstract Results."""
+
from pydantic import BaseModel
Results = BaseModel
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/singleton.py b/openbb_platform/core/openbb_core/app/model/abstract/singleton.py
index 3575186ee22..0ecccafc446 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/singleton.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/singleton.py
@@ -1,13 +1,18 @@
+"""Singleton metaclass implementation."""
+
from typing import Dict, Generic, TypeVar
T = TypeVar("T")
class SingletonMeta(type, Generic[T]):
+ """Singleton metaclass."""
+
# TODO : check if we want to update this to be thread safe
_instances: Dict[T, T] = {}
def __call__(cls: "SingletonMeta", *args, **kwargs):
+ """Singleton pattern implementation."""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/tagged.py b/openbb_platform/core/openbb_core/app/model/abstract/tagged.py
index 134a2605567..80cfe1678e0 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/tagged.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/tagged.py
@@ -1,6 +1,10 @@
+"""OpenBB Core App Abstract Model Tagged."""
+
from pydantic import BaseModel, Field
from uuid_extensions import uuid7str # type: ignore
class Tagged(BaseModel):
+ """Model for Tagged."""
+
id: str = Field(default_factory=uuid7str, alias="_id")
diff --git a/openbb_platform/core/openbb_core/app/model/abstract/warning.py b/openbb_platform/core/openbb_core/app/model/abstract/warning.py
index 2e2f778c397..7914b6af7f7 100644
--- a/openbb_platform/core/openbb_core/app/model/abstract/warning.py
+++ b/openbb_platform/core/openbb_core/app/model/abstract/warning.py
@@ -1,14 +1,19 @@
+"""Module for warnings."""
+
from warnings import WarningMessage
from pydantic import BaseModel
class Warning_(BaseModel):
+ """Model for Warning."""
+
category: str
message: str
def cast_warning(w: WarningMessage) -> Warning_:
+ """Cast a warning to a pydantic model."""
return Warning_(
category=w.category.__name__,
message=str(w.message),
@@ -16,4 +21,4 @@ def cast_warning(w: WarningMessage) -> Warning_:
class OpenBBWarning(Warning):
- pass
+ """Base class for OpenBB warnings."""
diff --git a/openbb_platform/core/openbb_core/app/model/fast_api_settings.py b/openbb_platform/core/openbb_core/app/model/api_settings.py
similarity index 98%
rename from openbb_platform/core/openbb_core/app/model/fast_api_settings.py
rename to openbb_platform/core/openbb_core/app/model/api_settings.py
index 24ad76a3916..1eccb73eaef 100644
--- a/openbb_platform/core/openbb_core/app/model/fast_api_settings.py
+++ b/openbb_platform/core/openbb_core/app/model/api_settings.py
@@ -24,7 +24,7 @@ class Servers(BaseModel):
description: str = "Local OpenBB development server"
-class FastAPISettings(BaseModel):
+class APISettings(BaseModel):
"""Settings model for FastAPI configuration."""
model_config = ConfigDict(frozen=True)
diff --git a/openbb_platform/core/openbb_core/app/model/charts/chart.py b/openbb_platform/core/openbb_core/app/model/charts/chart.py
index a825be1e9fe..38d062d7966 100644
--- a/openbb_platform/core/openbb_core/app/model/charts/chart.py
+++ b/openbb_platform/core/openbb_core/app/model/charts/chart.py
@@ -1,3 +1,5 @@
+"""OpenBB Core Chart model."""
+
from enum import Enum
from typing import Any, Dict, Optional
@@ -5,10 +7,14 @@ from pydantic import BaseModel, ConfigDict, Field
class ChartFormat(str, Enum):
+ """Chart format."""
+
plotly = "plotly"
class Chart(BaseModel):
+ """Model for Chart."""
+
content: Optional[Dict[str, Any]] = Field(
default=None,
description="Raw textual representation of the chart.",
@@ -25,6 +31,7 @@ class Chart(BaseModel):
model_config = ConfigDict(validate_assignment=True)
def __repr__(self) -> str:
+ """Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
diff --git a/openbb_platform/core/openbb_core/app/model/command_context.py b/openbb_platform/core/openbb_core/app/model/command_context.py
index 052fb5bc780..977df7a485b 100644
--- a/openbb_platform/core/openbb_core/app/model/command_context.py
+++ b/openbb_platform/core/openbb_core/app/model/command_context.py
@@ -1,3 +1,5 @@
+"""Command Context."""
+
from pydantic import BaseModel, Field
from openbb_core.app.model.system_settings import SystemSettings
@@ -5,5 +7,7 @@ from openbb_core.app.model.user_settings import UserSettings
class CommandContext(BaseModel):
+ """Command Context."""
+
user_settings: UserSettings = Field(default_factory=UserSettings)
system_settings: SystemSettings = Field(default_factory=SystemSettings)
diff --git a/openbb_platform/core/openbb_core/app/model/credentials.py b/openbb_platform/core/openbb_core/app/model/credentials.py
index 30cae535ef7..5ed7684e351 100644
--- a/openbb_platform/core/openbb_core/app/model/credentials.py
+++ b/openbb_platform/core/openbb_core/app/model/credentials.py
@@ -100,7 +100,7 @@ class Credentials(_Credentials): # type: ignore
"""Credentials model used to store provider credentials."""
def __repr__(self) -> str:
- """String representation of the credentials."""
+ """Define the string representation of the credentials."""
return (
self.__class__.__name__
+ "\n\n"
diff --git a/openbb_platform/core/openbb_core/app/model/custom_parameter.py b/openbb_platform/core/openbb_core/app/model/custom_parameter.py
deleted file mode 100644
index 06b947e0c55..00000000000
--- a/openbb_platform/core/openbb_core/app/model/custom_parameter.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import sys
-from dataclasses import dataclass
-from typing import Dict, Optional
-
-from typing_extensions import LiteralString
-
-# `slots` is available on Python >= 3.10
-if sys.version_info >= (3, 10):
- slots_true = {"slots": True}
-else:
- slots_true: Dict[str, bool] = {}
-
-
-class BaseMetadata:
- """Base class for all metadata.
-
- This exists mainly so that implementers
- can do `isinstance(..., BaseMetadata)` while traversing field annotations.
- """
-
- __slots__ = ()
-
-
-@dataclass(frozen=True, **slots_true)
-class OpenBBCustomParameter(BaseMetadata):
- """Custom parameter for OpenBB."""
-
- description: Optional[str] = None
-
-
-@dataclass(frozen=True, **slots_true)
-class OpenBBCustomChoices(BaseMetadata):
- """Custom choices for OpenBB."""
-
- choices: Optional[LiteralString] = None
diff --git a/openbb_platform/core/openbb_core/app/model/defaults.py b/openbb_platform/core/openbb_core/app/model/defaults.py
index ad41b1dc228..52007edf5de 100644
--- a/openbb_platform/core/openbb_core/app/model/defaults.py
+++ b/openbb_platform/core/openbb_core/app/model/defaults.py
@@ -1,3 +1,5 @@
+"""Defaults model."""
+
from typing import Dict, Optional
from pydantic import BaseModel, ConfigDict, Field
@@ -11,6 +13,7 @@ class Defaults(BaseModel):
routes: Dict[str, Dict[str, Optional[str]]] = Field(default_factory=dict)
def __repr__(self) -> str:
+ """Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
diff --git a/openbb_platform/core/openbb_core/app/model/field.py b/openbb_platform/core/openbb_core/app/model/field.py
new file mode 100644
index 00000000000..d791cb08f08
--- /dev/null
+++ b/openbb_platform/core/openbb_core/app/model/field.py
@@ -0,0 +1,28 @@
+"""Custom field for OpenBB."""
+
+from typing import Any, List, Optional
+
+from pydantic.fields import FieldInfo
+
+
+class OpenBBField(FieldInfo):
+ """Custom field for OpenBB."""
+
+ def __repr__(self):
+ """Override FieldInfo __repr__."""
+ # We use repr() to avoid decoding special characters like \n
+ if self.choices:
+ return f"OpenBBField(description={repr(self.description)}, choices={repr(self.choices)})"
+ return f"OpenBBField(description={repr(self.description)})"
+
+ def __init__(self, description: str, choices: Optional[List[Any]] = None):
+ """Initialize OpenBBField."""
+ json_schema_extra = {"choices": choices} if choices else None
+ super().__init__(description=description, json_schema_extra=json_schema_extra) # type: ignore[arg-type]
+
+ @property
+ def choices(self) -> Optional[List[Any]]:
+ """Custom choices."""
+ if self.json_schema_extra:
+ return self.json_schema_extra.get("choices") # type: ignore[union-attr,return-value]
+ return None
diff --git a/openbb_platform/core/openbb_core/app/model/hub/features_keys.py b/openbb_platform/core/openbb_core/app/model/hub/features_keys.py
deleted file mode 100644
index c0653ff443c..00000000000
--- a/openbb_platform/core/openbb_core/app/model/hub/features_keys.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from typing import Optional
-
-from pydantic import BaseModel, Field
-
-
-class FeaturesKeys(BaseModel):
- API_BITQUERY_KEY: Optional[str] = Field(default=None)
- API_BIZTOC_TOKEN: Optional[str] = Field(default=None)
- API_CMC_KEY: Optional[str] = Field(default=None)
- API_COINGLASS_KEY: Optional[str] = Field(default=None)
- API_CRYPTO_PANIC_KEY: Optional[str] = Field(default=None)
- API_DAPPRADAR_KEY: Optional[str] = Field(default=None)
- API_DATABENTO_KEY: Optional[str] = Field(default=None)
- API_EODHD_KEY: Optional[str] = Field(default=None)
- API_ETHPLORER_KEY: Optional[str] = Field(default=None)
- API_FINNHUB_KEY: Optional[str] = Field(default=None)
- API_FRED_KEY: Optional[str] = Field(default=None)
- API_GITHUB_KEY: Optional[str] = Field(default=None)
- API_GLASSNODE_KEY: Optional[str] = Field(default=None)
- API_INTRINIO_KEY: Optional[str] = Field(default=None)
- API_KEY_ALPHAVANTAGE: Optional[str] = Field(default=None)
- API_KEY_FINANCIALMODELINGPREP: Optional[str] = Field(default=None)
- API_KEY_QUANDL: Optional[str] = Field(default=None)
- API_MESSARI_KEY: Optional[str] = Field(default=None)
- API_NEWS_TOKEN: Optional[str] = Field(default=None)
- API_POLYGON_KEY: Optional[str] = Field(default=None)
- API_REDDIT_CLIENT_ID: Optional[str] = Field(default=None)
- API_REDDIT_CLIENT_SECRET: Optional[str] = Field(default=None)
- API_REDDIT_PASSWORD: Optional[str] = Field(default=None)
- API_REDDIT_USERNAME: Optional[str] = Field(default=None)
- API_REDDIT_USER_AGENT: Optional[str] = Field(default=None)
- API_SANTIMENT_KEY: Optional[str] = Field(default=None)
- API_SHROOM_KEY: Optional[str] = Field(default=None)
- API_SMARTSTAKE_KEY: Optional[str] = Field(default=None)
- API_SMARTSTAKE_TOKEN: Optional[str] = Field(default=None)
- API_TOKEN_TERMINAL_KEY: Optional[str] = Field(default=None)
- API_WHALE_ALERT_KEY: Optional[str] = Field(default=None)
diff --git a/openbb_platform/core/openbb_core/app/model/hub/hub_session.py b/openbb_platform/core/openbb_core/app/model/hub/hub_session.py
index fb0142eafc1..7a9cf615991 100644
--- a/openbb_platform/core/openbb_core/app/model/hub/hub_session.py
+++ b/openbb_platform/core/openbb_core/app/model/hub/hub_session.py
@@ -1,9 +1,13 @@
+"""Model for HubSession."""
+
from typing import Optional
from pydantic import BaseModel, SecretStr, field_serializer
class HubSession(BaseModel):
+ """Model for HubSession."""
+
username: Optional[str] = None
email: str
primary_usage: str
@@ -12,10 +16,12 @@ class HubSession(BaseModel):
access_token: SecretStr
def __repr__(self) -> str:
+ """Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
@field_serializer("access_token", when_used="json-unless-none")
def _dump_secret(self, v):
+ """Dump secret."""
return v.get_secret_value()
diff --git a/openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py b/openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py
index aebcf6afa31..16ea348319e 100644
--- a/openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py
+++ b/openbb_platform/core/openbb_core/app/model/hub/hub_user_settings.py
@@ -1,12 +1,15 @@
-# from typing import Dict, List, Union
+"""Hub user settings model."""
+
+from typing import Dict, Optional
-from openbb_core.app.model.hub.features_keys import FeaturesKeys
from pydantic import BaseModel, ConfigDict, Field
class HubUserSettings(BaseModel):
+ """Hub user settings model."""
+
# features_settings: Dict[str, str]
- features_keys: FeaturesKeys = Field(default_factory=FeaturesKeys)
+ features_keys: Dict[str, Optional[str]] = Field(default_factory=dict)
# features_sources: Dict[str, List[str]]
# features_terminal_style: Dict[str, Union[str, Dict[str, str]]]
diff --git a/openbb_platform/core/openbb_core/app/model/metadata.py b/openbb_platform/core/openbb_core/app/model/metadata.py
index cf421fca7e0..9162bfa0bea 100644
--- a/openbb_platform/core/openbb_core/app/model/metadata.py
+++ b/openbb_platform/core/openbb_core/app/model/metadata.py
@@ -1,3 +1,5 @@
+"""Metadata model."""
+
from datetime import datetime
from inspect import isclass
from typing import Any, Dict, Optional, Sequence, Union
@@ -10,6 +12,8 @@ from openbb_core.provider.abstract.data import Data
class Metadata(BaseModel):
+ """Metadata of a command execution."""
+
arguments: Dict[str, Any] = Field(
default_factory=dict,
description="Arguments of the command.",
@@ -21,6 +25,7 @@ class Metadata(BaseModel):
timestamp: datetime = Field(description="Execution starting timestamp.")
def __repr__(self) -> str:
+ """Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
@@ -29,96 +34,108 @@ class Metadata(BaseModel):
@classmethod
def scale_arguments(cls, v):
"""Scale arguments.
+
This function is meant to limit the size of the input arguments of a command.
If the type is one of the following: `Data`, `List[Data]`, `DataFrame`, `List[DataFrame]`,
`Series`, `List[Series]` or `ndarray`, the value of the argument is swapped by a dictionary
containing the type and the columns. If the type is not one of the previous, the
value is kept or trimmed to 80 characters.
"""
- for arg, arg_val in v.items():
- new_arg_val: Optional[Union[str, dict[str, Sequence[Any]]]] = None
+ arguments: Dict[str, Any] = {}
+ for item in ["provider_choices", "standard_params", "extra_params"]:
+ arguments[item] = {}
+ # The item could be class or it could a dictionary.
+ v_item = (
+ v.__dict__.get(item, {}) if not isinstance(v, dict) else v.get(item, {})
+ )
+ # The item might not be a dictionary yet.
+ v_item = v_item if isinstance(v_item, dict) else v_item.__dict__
+ for arg, arg_val in v_item.items():
+ new_arg_val: Optional[Union[str, dict[str, Sequence[Any]]]] = None
- # Data
- if isclass(type(arg_val)) and issubclass(type(arg_val), Data):
- new_arg_val = {
- "type": f"{type(arg_val).__name__}",
- "columns": list(arg_val.model_dump().keys()),
- }
+ # Data
+ if isclass(type(arg_val)) and issubclass(type(arg_val), Data):
+ new_arg_val = {
+ "type": f"{type(arg_val).__name__}",
+ "columns": list(arg_val.model_dump().keys()),
+ }
- # List[Data]
- if isinstance(arg_val, list) and issubclass(type(arg_val[0]), Data):
- _columns = [list(d.model_dump().keys()) for d in arg_val]
- ld_columns = (
- item for sublist in _columns for item in sublist
- ) # flatten
- new_arg_val = {
- "type": f"List[{type(arg_val[0]).__name__}]",
- "columns": list(set(ld_columns)),
- }
+ # List[Data]
+ if isinstance(arg_val, list) and issubclass(type(arg_val[0]), Data):
+ _columns = [list(d.model_dump().keys()) for d in arg_val]
+ ld_columns = (
+ item for sublist in _columns for item in sublist
+ ) # flatten
+ new_arg_val = {
+ "type": f"List[{type(arg_val[0]).__name__}]",
+ "columns": list(set(ld_columns)),
+ }
- # DataFrame
- elif isinstance(arg_val, pd.DataFrame):
- df_columns = (
- list(arg_val.index.names) + arg_val.columns.tolist()
- if any(index is not None for index in list(arg_val.index.names))
- else arg_val.columns.tolist()
- )
- new_arg_val = {
- "type": f"{type(arg_val).__name__}",
- "columns": df_columns,
- }
-
- # List[DataFrame]
- elif isinstance(arg_val, list) and issubclass(
- type(arg_val[0]), pd.DataFrame
- ):
- ldf_columns = [
- (
- list(df.index.names) + df.columns.tolist()
- if any(index is not None for index in list(df.index.names))
- else df.columns.tolist()
+ # DataFrame
+ elif isinstance(arg_val, pd.DataFrame):
+ df_columns = (
+ list(arg_val.index.names) + arg_val.columns.tolist()
+ if any(index is not None for index in list(arg_val.index.names))
+ else arg_val.columns.tolist()
)
- for df in arg_val
- ]
- new_arg_val = {
- "type": f"List[{type(arg_val[0]).__name__}]",
- "columns": ldf_columns,
- }
+ new_arg_val = {
+ "type": f"{type(arg_val).__name__}",
+ "columns": df_columns,
+ }
- # Series
- elif isinstance(arg_val, pd.Series):
- new_arg_val = {
- "type": f"{type(arg_val).__name__}",
- "columns": list(arg_val.index.names) + [arg_val.name],
- }
+ # List[DataFrame]
+ elif isinstance(arg_val, list) and issubclass(
+ type(arg_val[0]), pd.DataFrame
+ ):
+ ldf_columns = [
+ (
+ list(df.index.names) + df.columns.tolist()
+ if any(index is not None for index in list(df.index.names))
+ else df.columns.tolist()
+ )
+ for df in arg_val
+ ]
+ new_arg_val = {
+ "type": f"List[{type(arg_val[0]).__name__}]",
+ "columns": ldf_columns,
+ }
- # List[Series]
- elif isinstance(arg_val, list) and isinstance(arg_val[0], pd.Series):
- ls_columns = [
- (
- list(series.index.names) + [series.name]
- if any(index is not None for index in list(series.index.names))
- else series.name
- )
- for series in arg_val
- ]
- new_arg_val = {
- "type": f"List[{type(arg_val[0]).__name__}]",
- "columns": ls_columns,
- }
+ # Series
+ elif isinstance(arg_val, pd.Series):
+ new_arg_val = {
+ "type": f"{type(arg_val).__name__}",
+ "columns": list(arg_val.index.names) + [arg_val.name],
+ }
- # ndarray
- elif isinstance(arg_val, np.ndarray):
- new_arg_val = {
- "type": f"{type(arg_val).__name__}",
- "columns": list(arg_val.dtype.names or []),
- }
+ # List[Series]
+ elif isinstance(arg_val, list) and isinstance(arg_val[0], pd.Series):
+ ls_columns = [
+ (
+ list(series.index.names) + [series.name]
+ if any(
+ index is not None for index in list(series.index.names)
+ )
+ else series.name
+ )
+ for series in arg_val
+ ]
+ new_arg_val = {
+ "type": f"List[{type(arg_val[0]).__name__}]",
+ "columns": ls_columns,
+ }
- else:
- str_repr_arg_val = str(arg_val)
- if len(str_repr_arg_val) > 80:
- new_arg_val = str_repr_arg_val[:80]
+ # ndarray
+ elif isinstance(arg_val, np.ndarray):
+ new_arg_val = {
+ "type": f"{type(arg_val).__name__}",
+ "columns": list(arg_val.dtype.names or []),
+ }
- v[arg] = new_arg_val or arg_val
+ else:
+ str_repr_arg_val = str(arg_val)
+ if len(str_repr_arg_val) > 80:
+ new_arg_val = str_repr_arg_val[:80]
- return v
+ arguments[item][arg] = new_arg_val or arg_val
+
+ return arguments
diff --git a/openbb_platform/core/openbb_core/app/model/obbject.py b/openbb_platform/core/openbb_core/app/model/obbject.py
index 89232702405..2db09e0cd00 100644
--- a/openbb_platform/core/openbb_core/app/model/obbject.py
+++ b/openbb_platform/core/openbb_core/app/model/obbject.py
@@ -1,6 +1,5 @@
"""The OBBject."""
-from re import sub
from typing import (
TYPE_CHECKING,
Any,
@@ -26,6 +25,7 @@ from openbb_core.app.model.abstract.tagged import Tagged
from openbb_core.app.model.abstract.warning import Warning_
from openbb_core.app.model.charts.chart import Chart
from openbb_core.app.utils import basemodel_to_df
+from openbb_core.provider.abstract.annotated_result import AnnotatedResult
from openbb_core.provider.abstract.data import Data
if TYPE_CHECKING:
@@ -81,40 +81,6 @@ class OBBject(Tagged, Generic[T]):
]
return f"{self.__class__.__name__}\n\n" + "\n".join(items)
- @classmethod
- def results_type_repr(cls, params: Optional[Any] = None) -> str:
- """Return the results type representation."""
- results_field = cls.model_fields.get("results")
- type_repr = "Any"
- if results_field:
- type_ = params[0] if params else results_field.annotation
- type_repr = getattr(type_, "__name__", str(type_))
-
- if json_schema_extra := getattr(results_field, "json_schema_extra", {}):
- model = json_schema_extra.get("model", "Any")
-
- if json_schema_extra.get("is_union"):
- return f"Union[List[{model}], {model}]"
- if json_schema_extra.get("has_list"):
- return f"List[{model}]"
-
- return model
-
- if "typing." in str(type_):
- unpack_optional = sub(r"Optional\[(.*)\]", r"\1", str(type_))
- type_repr = sub(
- r"(\w+\.)*(\w+)?(\, NoneType)?",
- r"\2",
- unpack_optional,
- )
-
- return type_repr
-
- @classmethod
- def model_parametrized_name(cls, params: Any) -> str:
- """Return the model name with the parameters."""
- return f"OBBject[{cls.results_type_repr(params)}]"
-
def to_df(
self, index: Optional[Union[str, None]] = "date", sort_by: Optional[str] = None
) -> pd.DataFrame:
@@ -277,6 +243,24 @@ class OBBject(Tagged, Generic[T]):
del results["index"]
return results
+ def to_llm(self) -> Union[Dict[Hashable, Any], List[Dict[Hashable, Any]]]:
+ """Convert results field to an LLM compatible output.
+
+ Returns
+ -------
+ Union[Dict[Hashable, Any], List[Dict[Hashable, Any]]]
+ Dictionary of lists or list of dictionaries if orient is "records".
+ """
+ df = self.to_dataframe(index=None)
+
+ results = df.to_json(
+ orient="records",
+ date_format="iso",
+ date_unit="s",
+ )
+
+ return results
+
def show(self, **kwargs: Any) -> None:
"""Display chart."""
# pylint: disable=no-member
@@ -299,4 +283,9 @@ class OBBject(Tagged, Generic[T]):
OBBject[ResultsType]
OBBject with results.
"""
- return cls(results=await query.execute())
+ results = await query.execute()
+ if isinstance(results, AnnotatedResult):
+ return cls(
+ results=results.result, extra={"results_metadata": results.metadata}
+ )
+ return cls(results=results)
diff --git a/openbb_platform/core/openbb_core/app/model/preferences.py b/openbb_platform/core/openbb_core/app/model/preferences.py
index 57987020b17..19c2d7f745d 100644
--- a/openbb_platform/core/openbb_core/app/model/preferences.py
+++ b/openbb_platform/core/openbb_core/app/model/preferences.py
@@ -9,24 +9,28 @@ from pydantic import BaseModel, ConfigDict, Field, PositiveInt
class Preferences(BaseModel):
"""Preferences for the OpenBB platform."""
- data_directory: str = str(Path.home() / "OpenBBUserData")
- export_directory: str = str(Path.home() / "OpenBBUserData" / "exports")
- user_styles_directory: str = str(Path.home() / "OpenBBUserData" / "styles" / "user")
cache_directory: str = str(Path.home() / "OpenBBUserData" / "cache")
chart_style: Literal["dark", "light"] = "dark"
+ data_directory: str = str(Path.home() / "OpenBBUserData")
+ export_directory: str = str(Path.home() / "OpenBBUserData" / "exports")
+ metadata: bool = True
+ output_type: Literal[
+ "OBBject", "dataframe", "polars", "numpy", "dict", "chart", "llm"
+ ] = Field(
+ default="OBBject",
+ description="Python default output type.",
+ validate_default=True,
+ )
plot_enable_pywry: bool = True
- plot_pywry_width: PositiveInt = 1400
- plot_pywry_height: PositiveInt = 762
plot_open_export: bool = (
False # Whether to open plot image exports after they are created
)
- table_style: Literal["dark", "light"] = "dark"
+ plot_pywry_height: PositiveInt = 762
+ plot_pywry_width: PositiveInt = 1400
request_timeout: PositiveInt = 15
- metadata: bool = True
- output_type: Literal["OBBject", "dataframe", "polars", "numpy", "dict", "chart"] = (
- Field(default="OBBject", description="Python default output type.")
- )
show_warnings: bool = True
+ table_style: Literal["dark", "light"] = "dark"
+ user_styles_directory: str = str(Path.home() / "OpenBBUserData" / "styles" / "user")
model_config = ConfigDict(validate_assignment=True)
diff --git a/openbb_platform/core/openbb_core/app/model/profile.py b/openbb_platform/core/openbb_core/app/model/profile.py
index 64959e5c798..f1e62db0393 100644
--- a/openbb_platform/core/openbb_core/app/model/profile.py
+++ b/openbb_platform/core/openbb_core/app/model/profile.py
@@ -1,3 +1,5 @@
+"""Profile model."""
+
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
@@ -6,10 +8,13 @@ from openbb_core.app.model.hub.hub_session import HubSession
class Profile(BaseModel):
+ """Profile."""
+
hub_session: Optional[HubSession] = Field(default=None)
model_config = ConfigDict(validate_assignment=True)
def __repr__(self) -> str:
+ """Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)
diff --git a/openbb_platform/core/openbb_core/app/model/python_settings.py b/openbb_platform/core/openbb_core/app/model/python_settings.py
new file mode 100644
index 00000000000..f03648c92b4
--- /dev/null
+++ b/openbb_platform/core/openbb_core/app/model/python_settings.py
@@ -0,0 +1,23 @@
+"""Python configuration settings model."""
+
+from typing import List, Optional
+
+from pydantic import BaseModel, Field, PositiveInt
+
+
+class PythonSettings(BaseModel):
+ """Settings model for Python interface configuration."""
+
+ docstring_sections: List[str] = Field(
+ default_factory=lambda: ["description", "parameters", "returns", "examples"],
+ description="Sections to include in autogenerated docstrings.",
+ )
+ docstring_max_length: Optional[PositiveInt] = Field(
+ default=None, description="Maximum length of autogenerated docstrings."
+ )
+
+ def __repr__(self) -> str:
+ """Return a string representation of the model."""
+ return f"{self.__class__.__name__}\n\n" + "\n".join(
+ f"{k}: {v}" for k, v in self.model_dump().items()
+ )
diff --git a/openbb_platform/core/openbb_core/app/model/results/__init__.py b/openbb_platform/core/openbb_core/app/model/results/__init__.py
index e69de29bb2d..a87e3e8784f 100644
--- a/openbb_platform/core/openbb_core/app/model/results/__init__.py
+++ b/openbb_platform/core/openbb_core/app/model/results/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core App Model Results."""
diff --git a/openbb_platform/core/openbb_core/app/model/results/empty.py b/openbb_platform/core/openbb_core/app/model/results/empty.py
index 4b23c903033..0f9779a681d 100644
--- a/openbb_platform/core/openbb_core/app/model/results/empty.py
+++ b/openbb_platform/core/openbb_core/app/model/results/empty.py
@@ -1,5 +1,7 @@
+"""Empty results."""
+
from openbb_core.app.model.abstract.results import Results
class Empty(Results):
- pass
+ """Empty results."""
diff --git a/openbb_platform/core/openbb_core/app/model/system_settings.py b/openbb_platform/core/openbb_core/app/model/system_settings.py
index 39ceebbc813..c5439fb760d 100644
--- a/openbb_platform/core/openbb_core/app/model/system_settings.py
+++ b/openbb_platform/core/openbb_core/app/model/system_settings.py
@@ -14,8 +14,9 @@ from openbb_core.app.constants import (
USER_SETTINGS_PATH,
)
from openbb_core.app.model.abstract.tagged import Tagged
-from openbb_core.app.model.fast_api_settings import FastAPISettings
-from openbb_core.app.version import VERSION
+from openbb_core.app.model.api_settings import APISettings
+from openbb_core.app.model.python_settings import PythonSettings
+from openbb_core.app.version import CORE_VERSION, VERSION
class SystemSettings(Tagged):
@@ -28,6 +29,7 @@ class SystemSettings(Tagged):
# OpenBB section
version: str = VERSION
+ core: str = CORE_VERSION
home_directory: str = str(HOME_DIRECTORY)
openbb_directory: str = str(OPENBB_DIRECTORY)
user_settings_path: str = str(USER_SETTINGS_PATH)
@@ -45,7 +47,10 @@ class SystemSettings(Tagged):
log_collect: bool = True
# API section
- api_settings: FastAPISettings = Field(default_factory=FastAPISettings)
+ api_settings: APISettings = Field(default_factory=APISettings)
+
+ # Python section
+ python_settings: PythonSettings = Field(default_factory=PythonSettings)
# Others
debug_mode: bool = False
diff --git a/openbb_platform/core/openbb_core/app/provider_interface.py b/openbb_platform/core/openbb_core/app/provider_interface.py
index ac43a50a16a..0932704cb96 100644
--- a/openbb_platform/core/openbb_core/app/provider_interface.py
+++ b/openbb_platform/core/openbb_core/app/provider_interface.py
@@ -2,18 +2,33 @@
from dataclasses import dataclass, make_dataclass
from difflib import SequenceMatcher
-from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, Union
+from typing import (
+ Annotated,
+ Any,
+ Callable,
+ Dict,
+ List,
+ Literal,
+ Optional,
+ Tuple,
+ Type,
+ Union,
+)
from fastapi import Query
from pydantic import (
BaseModel,
ConfigDict,
+ Discriminator,
Field,
+ SerializeAsAny,
+ Tag,
create_model,
)
from pydantic.fields import FieldInfo
from openbb_core.app.model.abstract.singleton import SingletonMeta
+from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.query_executor import QueryExecutor
from openbb_core.provider.registry_map import MapType, RegistryMap
from openbb_core.provider.utils.helpers import to_snake_case
@@ -92,12 +107,15 @@ class ProviderInterface(metaclass=SingletonMeta):
self._registry_map = registry_map or RegistryMap()
self._query_executor = query_executor or QueryExecutor
- self._map = self._registry_map.map
+ self._map = self._registry_map.standard_extra
# TODO: Try these 4 methods in a single iteration
self._model_providers_map = self._generate_model_providers_dc(self._map)
self._params = self._generate_params_dc(self._map)
self._data = self._generate_data_dc(self._map)
self._return_schema = self._generate_return_schema(self._data)
+ self._return_annotations = self._generate_return_annotations(
+ self._registry_map.original_models
+ )
self._available_providers = self._registry_map.available_providers
self._provider_choices = self._get_provider_choices(self._available_providers)
@@ -148,9 +166,9 @@ class ProviderInterface(metaclass=SingletonMeta):
return self._registry_map.models
@property
- def return_map(self) -> Dict[str, Dict[str, Any]]:
+ def return_annotations(self) -> Dict[str, Type[OBBject]]:
"""Return map."""
- return self._registry_map.return_map
+ return self._return_annotations
def create_executor(self) -> QueryExecutor:
"""Get query executor."""
@@ -164,9 +182,11 @@ class ProviderInterface(metaclass=SingletonMeta):
curr_name = current.name
curr_type: Optional[Type] = current.annotation
curr_desc = getattr(current.default, "description", "")
+ curr_json_schema_extra = getattr(current.default, "json_schema_extra", {})
inc_type: Optional[Type] = incoming.annotation
inc_desc = getattr(incoming.default, "description", "")
+ inc_json_schema_extra = getattr(incoming.default, "json_schema_extra", {})
def split_desc(desc: str) -> str:
"""Split field description."""
@@ -174,6 +194,24 @@ class ProviderInterface(metaclass=SingletonMeta):
detail = item[0] if item else ""
return detail
+ def merge_json_schema_extra(curr: dict, inc: dict) -> dict:
+ """Merge json schema extra."""
+ for key in curr.keys() & inc.keys():
+ # Merge keys that are in both dictionaries if both are lists
+ curr_value = curr[key]
+ inc_value = inc[key]
+ if isinstance(curr_value, list) and isinstance(inc_value, list):
+ curr[key] = list(set(curr.get(key, []) + inc.get(key, [])))
+ inc.pop(key)
+
+ # Add any remaining keys from inc to curr
+ curr.update(inc)
+ return curr
+
+ json_schema_extra: dict = merge_json_schema_extra(
+ curr=curr_json_schema_extra or {}, inc=inc_json_schema_extra or {}
+ )
+
curr_detail = split_desc(curr_desc)
inc_detail = split_desc(inc_desc)
@@ -192,6 +230,7 @@ class ProviderInterface(metaclass=SingletonMeta):
default=getattr(current.default, "default", None),
title=providers,
description=new_desc,
+ json_schema_extra=json_schema_extra,
)
merged_type: Optional[Type] = (
@@ -221,7 +260,9 @@ class ProviderInterface(metaclass=SingletonMeta):
additional_description += " Multiple comma separated items allowed."
else:
additional_description += (
- " Multiple comma separated items allowed for provider(s): " + ", ".join(multiple) + "." # type: ignore
+ " Multiple comma separated items allowed for provider(s): "
+ + ", ".join(multiple) # type: ignore[arg-type]
+ + "."
)
provider_field = (
@@ -375,7 +416,7 @@ class ProviderInterface(metaclass=SingletonMeta):
This creates a dictionary of dataclasses that can be injected as a FastAPI
dependency.
- Example:
+ Example
-------
@dataclass
class CompanyNews(StandardParams):
@@ -416,7 +457,7 @@ class ProviderInterface(metaclass=SingletonMeta):
This creates a dictionary that maps model names to dataclasses that can be
injected as a FastAPI dependency.
- Example:
+ Example
-------
@dataclass
class CompanyNews(ProviderChoices):
@@ -450,7 +491,7 @@ class ProviderInterface(metaclass=SingletonMeta):
This creates a dictionary of dataclasses.
- Example:
+ Example
-------
class EquityHistoricalData(StandardData):
date: date
@@ -525,3 +566,60 @@ class ProviderInterface(metaclass=SingletonMeta):
fields=[("provider", Literal[tuple(available_providers)])], # type: ignore
bases=(ProviderChoices,),
)
+
+ def _generate_return_annotations(
+ self, original_models: Dict[str, Dict[str, Any]]
+ ) -> Dict[str, Type[OBBject]]:
+ """Generate return annotations for FastAPI.
+
+ Example
+ -------
+ class Data(BaseModel):
+ ...
+
+ class EquityData(Data):
+ price: float
+
+ class YFEquityData(EquityData):
+ yf_field: str
+
+ class AVEquityData(EquityData):
+ av_field: str
+
+ class OBBject(BaseModel):
+ results: List[
+ SerializeAsAny[
+ Annotated[
+ Union[
+ Annotated[YFEquityData, Tag("yf")],
+ Annotated[AVEquityData, Tag("av")],
+ ],
+ Discriminator(get_provider),
+ ]
+ ]
+ ]
+ """
+
+ def get_provider(v: Type[BaseModel]):
+ """Callable to discriminate which BaseModel to use."""
+ return getattr(v, "_provider", None)
+
+ annotations = {}
+ for name, models in original_models.items():
+ outer = set()
+ args = set()
+ for provider, model in models.items():
+ data = model["data"]
+ outer.add(model["results_type"])
+ args.add(Annotated[data, Tag(provider)])
+ # We set the provider to use it in discriminator function
+ setattr(data, "_provider", provider)
+ meta = Discriminator(get_provider) if len(args) > 1 else None
+ inner = SerializeAsAny[Annotated[Union[tuple(args)], meta]] # type: ignore[misc,valid-type]
+ full = Union[tuple((o[inner] if o else inner) for o in outer)] # type: ignore[valid-type]
+ annotations[name] = create_model(
+ f"OBBject_{name}",
+ __base__=OBBject[full], # type: ignore[valid-type]
+ __doc__=f"OBBject with results of type {name}",
+ )
+ return annotations
diff --git a/openbb_platform/core/openbb_core/app/router.py b/openbb_platform/core/openbb_core/app/router.py
index 218629be8ba..c9d196216aa 100644
--- a/openbb_platform/core/openbb_core/app/router.py
+++ b/openbb_platform/core/openbb_core/app/router.py
@@ -12,7 +12,6 @@ from typing import (
Mapping,
Optional,
Type,
- Union,
get_args,
get_origin,
get_type_hints,
@@ -20,7 +19,7 @@ from typing import (
)
from fastapi import APIRouter, Depends
-from pydantic import BaseModel, Field, SerializeAsAny, Tag, create_model
+from pydantic import BaseModel
from pydantic.v1.validators import find_validators
from typing_extensions import Annotated, ParamSpec, _AnnotatedAlias
@@ -203,15 +202,33 @@ class Router:
"""API Router."""
return self._api_router
+ @property
+ def prefix(self) -> str:
+ """Prefix."""
+ return self._api_router.prefix
+
+ @property
+ def description(self) -> Optional[str]:
+ """Description."""
+ return self._description
+
+ @property
+ def routers(self) -> Dict[str, "Router"]:
+ """Routers nested within the Router, i.e. sub-routers."""
+ return self._routers
+
def __init__(
self,
prefix: str = "",
+ description: Optional[str] = None,
) -> None:
"""Initialize Router."""
self._api_router = APIRouter(
prefix=prefix,
responses={404: {"description": "Not found"}},
)
+ self._description = description
+ self._routers: Dict[str, Router] = {}
@overload
def command(self, func: Optional[Callable[P, OBBject]]) -> Callable[P, OBBject]:
@@ -290,10 +307,41 @@ class Router:
prefix: str = "",
):
"""Include router."""
- tags = [prefix[1:]] if prefix else None
+ tags = [prefix.strip("/")] if prefix else None
self._api_router.include_router(
router=router.api_router, prefix=prefix, tags=tags # type: ignore
)
+ name = prefix if prefix else router.prefix
+ self._routers[name.strip("/")] = router
+
+ def get_attr(self, path: str, attr: str) -> Any:
+ """Get router attribute from path.
+
+ Parameters
+ ----------
+ path : str
+ Path to the router or nested router.
+ E.g. "/equity" or "/equity/price".
+ attr : str
+ Attribute to get.
+
+ Returns
+ -------
+ Any
+ Attribute value.
+ """
+ return self._search_attr(self, path, attr)
+
+ @staticmethod
+ def _search_attr(router: "Router", path: str, attr: str) -> Any:
+ """Recursively search router attribute from path."""
+ path = path.strip("/")
+ first = path.split("/")[0]
+ if first in router.routers:
+ return Router._search_attr(
+ router.routers[first], "/".join(path.split("/")[1:]), attr
+ )
+ return getattr(router, attr, None)
class SignatureInspector:
@@ -348,10 +396,9 @@ class SignatureInspector:
callable_=provider_interface.params[model]["extra"],
)
- func = cls.inject_return_type(
+ func = cls.inject_return_annotation(
func=func,
- return_map=provider_interface.return_map.get(model, {}),
- model=model,
+ annotation=provider_interface.return_annotations[model],
)
else:
@@ -368,60 +415,6 @@ class SignatureInspector:
return func
- @staticmethod
- def inject_return_type(
- func: Callable[P, OBBject],
- return_map: Dict[str, dict],
- model: str,
- ) -> Callable[P, OBBject]:
- """Inject full return model into the function. Also updates __name__ and __doc__ for API schemas."""
- results: Dict[str, Any] = {"list_type": [], "dict_type": []}
-
- for provider, return_data in return_map.items():
- if return_data["is_list"]:
- results["list_type"].append(
- Annotated[return_data["model"], Tag(provider)]
- )
- continue
-
- results["dict_type"].append(Annotated[return_data["model"], Tag(provider)])
-
- list_models, union_models = results.values()
-
- return_types = []
- for t, v in results.items():
- if not v:
- continue
-
- inner_type: Any = SerializeAsAny[ # type: ignore[misc,valid-type]
- Annotated[
- Union[tuple(v)], # type: ignore
- Field(discriminator="provider"),
- ]
- ]
- return_types.append(List[inner_type] if t == "list_type" else inner_type)
-
- return_type = create_model(
- f"OBBject_{model}",
- __base__=OBBject,
- __doc__=f"OBBject with results of type {model}",
- results=(
- Optional[Union[tuple(return_types)]], # type: ignore
- Field(
- None,
- description="Serializable results.",
- json_schema_extra={
- "model": model,
- "has_list": bool(len(list_models) > 0),
- "is_union": bool(list_models and union_models),
- },
- ),
- ),
- )
-
- func.__annotations__["return"] = return_type
- return func
-
@staticmethod
def polish_return_schema(func: Callable[P, OBBject]) -> Callable[P, OBBject]:
"""Polish API schemas by filling `__doc__` and `__name__`."""
@@ -468,6 +461,14 @@ class SignatureInspector:
func.__annotations__[arg] = Annotated[callable_, Depends()] # type: ignore
return func
+ @staticmethod
+ def inject_return_annotation(
+ func: Callable[P, OBBject], annotation: Type[OBBject]
+ ) -> Callable[P, OBBject]:
+ """Annotate function with return annotation."""
+ func.__annotations__["return"] = annotation
+ return func
+
@staticmethod
def get_description(func: Callable) -> str:
"""Get description from docstring."""
diff --git a/openbb_platform/core/openbb_core/app/service/auth_service.py b/openbb_platform/core/openbb_core/app/service/auth_service.py
index 6ce4ecbb221..3bbe97c92dc 100644
--- a/openbb_platform/core/openbb_core/app/service/auth_service.py
+++ b/openbb_platform/core/openbb_core/app/service/auth_service.py
@@ -29,7 +29,7 @@ class AuthService(metaclass=SingletonMeta):
"""Auth service."""
def __init__(self, ext_name: Optional[str] = EXT_NAME) -> None:
- """Initializes AuthService."""
+ """Initialize AuthService."""
if not self._load_extension(ext_name):
self._router = default_router
self._auth_hook = default_auth_hook
@@ -37,22 +37,22 @@ class AuthService(metaclass=SingletonMeta):
@property
def router(self) -> APIRouter:
- """Gets router."""
+ """Get router."""
return self._router
@property
def auth_hook(self) -> Callable[..., Awaitable[None]]:
- """Gets general authentication hook."""
+ """Get general authentication hook."""
return self._auth_hook
@property
def user_settings_hook(self) -> Callable[..., Awaitable[UserSettings]]:
- """Gets user settings hook."""
+ """Get user settings hook."""
return self._user_settings_hook
@staticmethod
def _is_installed(ext_name: str) -> bool:
- """Checks if auth_extension is installed."""
+ """Check if auth_extension is installed."""
extension = ExtensionLoader().get_core_entry_point(ext_name) or False
return extension and ext_name == extension.name # type: ignore
diff --git a/openbb_platform/core/openbb_core/app/service/hub_service.py b/openbb_platform/core/openbb_core/app/service/hub_service.py
index 22e6330bd76..bc932699ed5 100644
--- a/openbb_platform/core/openbb_core/app/service/hub_service.py
+++ b/openbb_platform/core/openbb_core/app/service/hub_service.py
@@ -1,6 +1,7 @@
"""Hub manager class."""
from typing import Optional
+from warnings import warn
from fastapi import HTTPException
from jose import JWTError
@@ -8,13 +9,11 @@ from jose.exceptions import ExpiredSignatureError
from jose.jwt import decode, get_unverified_header
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.credentials import Credentials
-from openbb_core.app.model.hub.features_keys import FeaturesKeys
from openbb_core.app.model.hub.hub_session import HubSession
from openbb_core.app.model.hub.hub_user_settings import HubUserSettings
from openbb_core.app.model.profile import Profile
from openbb_core.app.model.user_settings import UserSettings
from openbb_core.env import Env
-from pydantic import SecretStr
from requests import get, post, put
@@ -22,14 +21,28 @@ class HubService:
"""Hub service class."""
TIMEOUT = 10
+ # Mapping of V3 keys to V4 keys for backward compatibility
+ V3TOV4 = {
+ "API_KEY_ALPHAVANTAGE": "alpha_vantage_api_key",
+ "API_BIZTOC_TOKEN": "biztoc_api_key",
+ "API_FRED_KEY": "fred_api_key",
+ "API_KEY_FINANCIALMODELINGPREP": "fmp_api_key",
+ "API_INTRINIO_KEY": "intrinio_api_key",
+ "API_POLYGON_KEY": "polygon_api_key",
+ "API_KEY_QUANDL": "nasdaq_api_key",
+ "API_TRADIER_TOKEN": "tradier_api_key",
+ }
+ V4TOV3 = {v: k for k, v in V3TOV4.items()}
def __init__(
self,
session: Optional[HubSession] = None,
base_url: Optional[str] = None,
):
+ """Initialize Hub service."""
self._base_url = base_url or Env().HUB_BACKEND
self._session = session
+ self._hub_user_settings: Optional[HubUserSettings] = None
@property
def base_url(self) -> str:
@@ -80,13 +93,10 @@ class HubService:
def pull(self) -> UserSettings:
"""Pull user settings from Hub."""
if self._session:
- hub_user_settings = self._get_user_settings(self._session)
- if hub_user_settings:
- profile = Profile(
- hub_session=self._session,
- )
- credentials = self.hub2platform(hub_user_settings)
- return UserSettings(profile=profile, credentials=credentials)
+ self._hub_user_settings = self._get_user_settings(self._session)
+ profile = Profile(hub_session=self._session)
+ credentials = self.hub2platform(self._hub_user_settings)
+ return UserSettings(profile=profile, credentials=credentials)
raise OpenBBError(
"No session found. Login or provide a 'HubSession' on initialization."
)
@@ -215,42 +225,38 @@ class HubService:
detail = response.json().get("detail", None)
raise HTTPException(status_code, detail)
- @classmethod
- def hub2platform(cls, settings: HubUserSettings) -> Credentials:
+ def hub2platform(self, settings: HubUserSettings) -> Credentials:
"""Convert Hub user settings to Platform models."""
- # TODO: Hub is getting credentials from `all_api_keys.json`, which uses
- # the terminal names conventions. We need to update it to use a file that
- # lives here in the platform and maps the credential names between the two.
- credentials = Credentials(
- alpha_vantage_api_key=settings.features_keys.API_KEY_ALPHAVANTAGE,
- biztoc_api_key=settings.features_keys.API_BIZTOC_TOKEN,
- fred_api_key=settings.features_keys.API_FRED_KEY,
- fmp_api_key=settings.features_keys.API_KEY_FINANCIALMODELINGPREP,
- intrinio_api_key=settings.features_keys.API_INTRINIO_KEY,
- polygon_api_key=settings.features_keys.API_POLYGON_KEY,
- nasdaq_api_key=settings.features_keys.API_KEY_QUANDL,
- )
- return credentials
+ if any(k in settings.features_keys for k in self.V3TOV4):
+ deprecated = {
+ k: v for k, v in self.V3TOV4.items() if k in settings.features_keys
+ }
+ msg = ""
+ for k, v in deprecated.items():
+ msg += f"\n'{k}' -> '{v}', "
+ msg = msg.strip(", ")
+ warn(
+ message=f"\nDeprecated v3 credentials found.\n{msg}"
+ "\n\nYou can update them at https://my.openbb.co/app/platform/credentials.",
+ )
+ # We give priority to v4 keys over v3 keys if both are present
+ hub_credentials = {
+ self.V3TOV4.get(k, k): settings.features_keys.get(self.V3TOV4.get(k, k), v)
+ for k, v in settings.features_keys.items()
+ }
+ return Credentials(**hub_credentials)
- @classmethod
- def platform2hub(cls, credentials: Credentials) -> HubUserSettings:
+ def platform2hub(self, credentials: Credentials) -> HubUserSettings:
"""Convert Platform models to Hub user settings."""
-
- def get_cred(cred: str) -> Optional[str]:
- secret_str: Optional[SecretStr] = getattr(credentials, cred, None)
- return secret_str.get_secret_value() if secret_str else None
-
- features_keys = FeaturesKeys(
- API_BIZTOC_TOKEN=get_cred("biztoc_api_key"),
- API_FRED_KEY=get_cred("fred_api_key"),
- API_INTRINIO_KEY=get_cred("intrinio_api_key"),
- API_KEY_ALPHAVANTAGE=get_cred("alpha_vantage_api_key"),
- API_KEY_FINANCIALMODELINGPREP=get_cred("fmp_api_key"),
- API_POLYGON_KEY=get_cred("polygon_api_key"),
- API_KEY_QUANDL=get_cred("nasdaq_api_key"),
- )
- hub_user_settings = HubUserSettings(features_keys=features_keys)
- return hub_user_settings
+ # Dump mode json ensures SecretStr values are serialized as strings
+ credentials = credentials.model_dump(mode="json", exclude_none=True)
+ settings = self._hub_user_settings or HubUserSettings()
+ for v4_k, v in sorted(credentials.items()):
+ v3_k = self.V4TOV3.get(v4_k, None)
+ # If v3 key was there, we keep it
+ k = v3_k if v3_k in settings.features_keys else v4_k
+ settings.features_keys[k] = v
+ return settings
@staticmethod
def check_token_expiration(token: str) -> None:
diff --git a/openbb_platform/core/openbb_core/app/service/system_service.py b/openbb_platform/core/openbb_core/app/service/system_service.py
index c18bc1354ef..906ff6808ec 100644
--- a/openbb_platform/core/openbb_core/app/service/system_service.py
+++ b/openbb_platform/core/openbb_core/app/service/system_service.py
@@ -20,6 +20,7 @@ class SystemService(metaclass=SingletonMeta):
"headless",
"logging_sub_app",
"api_settings",
+ "python_settings",
"debug_mode",
}
@@ -29,6 +30,7 @@ class SystemService(metaclass=SingletonMeta):
self,
**kwargs,
):
+ """Initialize system service."""
self._system_settings = self._read_default_system_settings(
path=self.SYSTEM_SETTINGS_PATH, **kwargs
)
diff --git a/openbb_platform/core/openbb_core/app/service/user_service.py b/openbb_platform/core/openbb_core/app/service/user_service.py
index 9b6fcff50bc..8ab438782eb 100644
--- a/openbb_platform/core/openbb_core/app/service/user_service.py
+++ b/openbb_platform/core/openbb_core/app/service/user_service.py
@@ -20,6 +20,7 @@ class UserService(metaclass=SingletonMeta):
self,
default_user_settings: Optional[UserSettings] = None,
):
+ """Initialize user service."""
self._default_user_settings = (
default_user_settings or self.read_default_user_settings()
)
diff --git a/openbb_platform/core/openbb_core/app/static/__init__.py b/openbb_platform/core/openbb_core/app/static/__init__.py
index e69de29bb2d..bbfce45ce15 100644
--- a/openbb_platform/core/openbb_core/app/static/__init__.py
+++ b/openbb_platform/core/openbb_core/app/static/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Core App Static."""
diff --git a/openbb_platform/core/openbb_core/app/static/account.py b/openbb_platform/core/openbb_core/app/static/account.py
index d589c265663..b17eef7c713 100644
--- a/openbb_platform/core/openbb_core/app/static/account.py
+++ b/openbb_platform/core/openbb_core/app/static/account.py
@@ -34,6 +34,7 @@ class Account:
self._openbb_directory = (
base_app._command_runner.system_settings.openbb_directory
)
+ self._hub_service: Optional[HubService] = None
def __repr__(self) -> str:
"""Human readable representation of the object."""
@@ -120,18 +121,20 @@ class Account:
Optional[UserSettings]
User settings: profile, credentials, preferences
"""
- hs = self._create_hub_service(email, password, pat)
- incoming = hs.pull()
+ self._hub_service = self._create_hub_service(email, password, pat)
+ incoming = self._hub_service.pull()
updated: UserSettings = UserService.update_default(incoming)
self._base_app._command_runner.user_settings = updated
if remember_me:
Path(self._openbb_directory).mkdir(parents=False, exist_ok=True)
session_file = Path(self._openbb_directory, self.SESSION_FILE)
with open(session_file, "w") as f:
- if not hs.session:
+ if not self._hub_service.session:
raise OpenBBError("Not connected to hub.")
- json.dump(hs.session.model_dump(mode="json"), f, indent=4)
+ json.dump(
+ self._hub_service.session.model_dump(mode="json"), f, indent=4
+ )
if return_settings:
return self._base_app._command_runner.user_settings
@@ -151,14 +154,12 @@ class Account:
Optional[UserSettings]
User settings: profile, credentials, preferences
"""
- hub_session = self._base_app._command_runner.user_settings.profile.hub_session
- if not hub_session:
+ if not self._hub_service:
UserService.write_default_user_settings(
self._base_app._command_runner.user_settings
)
else:
- hs = HubService(hub_session)
- hs.push(self._base_app._command_runner.user_settings)
+ self._hub_service.push(self._base_app._command_runner.user_settings)
if return_settings:
return self._base_app._command_runner.user_settings
@@ -178,14 +179,12 @@ class Account:
Optional[UserSettings]
User settings: profile, credentials, preferences
"""
- hub_session = self._base_app._command_runner.user_settings.profile.hub_session
- if not hub_session:
+ if not self._hub_service:
self._base_app._command_runner.user_settings = (
UserService.read_default_user_settings()
)
else:
- hs = HubService(hub_session)
- incoming = hs.pull()
+ incoming = self._hub_service.pull()
updated: UserSettings = UserService.update_default(incoming)
updated.id = self._base_app._command_runner.user_settings.id
self._base_app._command_runner.user_settings = updated
@@ -208,12 +207,10 @@ class Account:
Optional[UserSettings]
User settings: profile, credentials, preferences
"""
- hub_session = self._base_app._command_runner.user_settings.profile.hub_session
- if not hub_session:
+ if not self._hub_service:
raise OpenBBError("Not connected to hub.")
- hs = HubService(hub_session)
- hs.disconnect()
+ self._hub_service.disconnect()
session_file = Path(self._openbb_directory, self.SESSION_FILE)
if session_file.exists():
diff --git a/openbb_platform/core/openbb_core/app/static/app_factory.py b/openbb_platform/core/openbb_core/app/static/app_factory.py
index 5686194c467..7141e573400 100644
--- a/openbb_platform/core/openbb_core/app/static/app_factory.py
+++ b/openbb_platform/core/openbb_core/app/static/app_factory.py
@@ -1,6 +1,6 @@
"""App factory."""
-from typing import Optional, Type, TypeVar
+from typing import Dict, Optional, Type, TypeVar
from openbb_core.app.command_runner import CommandRunner
from openbb_core.app.model.system_settings import SystemSettings
@@ -8,6 +8,7 @@ from openbb_core.app.model.user_settings import UserSettings
from openbb_core.app.static.account import Account
from openbb_core.app.static.container import Container
from openbb_core.app.static.coverage import Coverage
+from openbb_core.app.static.reference_loader import ReferenceLoader
from openbb_core.app.version import VERSION
E = TypeVar("E", bound=Type[Container])
@@ -25,10 +26,12 @@ class BaseApp:
"""Base app."""
def __init__(self, command_runner: CommandRunner):
+ """Initialize the app."""
command_runner.init_logging_service()
self._command_runner = command_runner
self._account = Account(self)
self._coverage = Coverage(self)
+ self._reference = ReferenceLoader().reference
@property
def account(self) -> Account:
@@ -50,14 +53,16 @@ class BaseApp:
"""Coverage menu."""
return self._coverage
+ @property
+ def reference(self) -> Dict[str, Dict]:
+ """Return reference data."""
+ return self._reference
+
def create_app(extensions: Optional[E] = None) -> Type[BaseApp]:
"""Create the app."""
- class EmptyClass:
- pass
-
- class App(BaseApp, extensions or EmptyClass): # type: ignore
+ class App(BaseApp, extensions or object): # type: ignore[misc]
def __repr__(self) -> str:
# pylint: disable=E1101
ext_doc = extensions.__doc__ if extensions else ""
diff --git a/openbb_platform/core/openbb_core/app/static/container.py b/openbb_platform/core/openbb_core/app/static/container.py
index a8a38b62963..60fc1def9bb 100644
--- a/openbb_platform/core/openbb_core/app/static/container.py
+++ b/openbb_platform/core/openbb_core/app/static/container.py
@@ -11,6 +11,7 @@ class Container:
"""Container class for the command runner session."""
def __init__(self, command_runner: CommandRunner) -> None:
+ """Initialize the container."""
self._command_runner = command_runner
OBBject._user_settings = command_runner.user_settings
OBBject._system_settings = command_runner.system_settings
diff --git a/openbb_platform/core/openbb_core/app/static/package_builder.py b/openbb_platform/core/openbb_core/app/static/package_builder.py
index 663fa7e4340..8a2a86a6619 100644
--- a/openbb_platform/core/openbb_core/app/static/package_builder.py
+++ b/openbb_platform/core/openbb_core/app/static/package_builder.py
@@ -6,7 +6,8 @@ import inspect
import re
import shutil
import sys
-from dataclasses import Field
+from dataclasses import Field as DCField
+from functools import partial
from inspect import Parameter, _empty, isclass, signature
from json import dumps, load
from pathlib import Path
@@ -24,6 +25,7 @@ from typing import (
TypeVar,
Union,
get_args,
+ get_origin,
get_type_hints,
)
@@ -36,15 +38,15 @@ from starlette.routing import BaseRoute
from typing_extensions import Annotated, _AnnotatedAlias
from openbb_core.app.extension_loader import ExtensionLoader, OpenBBGroups
-from openbb_core.app.model.custom_parameter import (
- OpenBBCustomChoices,
- OpenBBCustomParameter,
-)
from openbb_core.app.model.example import Example
+from openbb_core.app.model.field import OpenBBField
+from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import ProviderInterface
from openbb_core.app.router import RouterLoader
+from openbb_core.app.service.system_service import SystemService
from openbb_core.app.static.utils.console import Console
from openbb_core.app.static.utils.linters import Linters
+from openbb_core.app.version import CORE_VERSION, VERSION
from openbb_core.env import Env
from openbb_core.provider.abstract.data import Data
@@ -86,13 +88,17 @@ class PackageBuilder:
self.lint = lint
self.verbose = verbose
self.console = Console(verbose)
+ self.route_map = PathHandler.build_route_map()
+ self.path_list = PathHandler.build_path_list(route_map=self.route_map)
def auto_build(self) -> None:
"""Trigger build if there are differences between built and installed extensions."""
if Env().AUTO_BUILD:
- add, remove = PackageBuilder._diff(
- self.directory / "assets" / "extension_map.json"
+ reference = PackageBuilder._read(
+ self.directory / "assets" / "reference.json"
)
+ ext_map = reference.get("info", {}).get("extensions", {})
+ add, remove = PackageBuilder._diff(ext_map)
if add:
a = ", ".join(sorted(add))
print(f"Extensions to add: {a}") # noqa: T201
@@ -113,10 +119,9 @@ class PackageBuilder:
self.console.log("\nBuilding extensions package...\n")
self._clean(modules)
ext_map = self._get_extension_map()
- self._save_extension_map(ext_map)
self._save_modules(modules, ext_map)
self._save_package()
- self._save_reference_file()
+ self._save_reference_file(ext_map)
if self.lint:
self._run_linters()
@@ -143,12 +148,6 @@ class PackageBuilder:
]
return ext_map
- def _save_extension_map(self, ext_map: Dict[str, List[str]]) -> None:
- """Save the map of extensions available at build time."""
- code = dumps(obj=dict(sorted(ext_map.items())), indent=4)
- self.console.log("Writing extension map...")
- self._write(code=code, name="extension_map", extension="json", folder="assets")
-
def _save_modules(
self,
modules: Optional[Union[str, List[str]]] = None,
@@ -156,28 +155,26 @@ class PackageBuilder:
):
"""Save the modules."""
self.console.log("\nWriting modules...")
- route_map = PathHandler.build_route_map()
- path_list = PathHandler.build_path_list(route_map=route_map)
- if not path_list:
+ if not self.path_list:
self.console.log("\nThere is nothing to write.")
return
- MAX_LEN = max([len(path) for path in path_list if path != "/"])
+ MAX_LEN = max([len(path) for path in self.path_list if path != "/"])
- if modules:
- path_list = [path for path in path_list if path in modules]
+ _path_list = (
+ [path for path in self.path_list if path in modules]
+ if modules
+ else self.path_list
+ )
- for path in path_list:
- route = PathHandler.get_route(path=path, route_map=route_map)
+ for path in _path_list:
+ route = PathHandler.get_route(path, self.route_map)
if route is None:
- module_code = ModuleBuilder.build(
- path=path,
- ext_map=ext_map,
- )
- module_name = PathHandler.build_module_name(path=path)
+ code = ModuleBuilder.build(path, ext_map)
+ name = PathHandler.build_module_name(path)
self.console.log(f"({path})", end=" " * (MAX_LEN - len(path)))
- self._write(code=module_code, name=module_name)
+ self._write(code, name)
def _save_package(self):
"""Save the package."""
@@ -185,11 +182,23 @@ class PackageBuilder:
code = "### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###\n"
self._write(code=code, name="__init__")
- def _save_reference_file(self):
+ def _save_reference_file(self, ext_map: Optional[Dict[str, List[str]]] = None):
"""Save the reference.json file."""
self.console.log("\nWriting reference file...")
- data = ReferenceGenerator.get_reference_data()
- code = dumps(obj=data, indent=4)
+ code = dumps(
+ obj={
+ "openbb": VERSION.replace("dev", ""),
+ "info": {
+ "title": "OpenBB Platform (Python)",
+ "description": "This is the OpenBB Platform (Python).",
+ "core": CORE_VERSION.replace("dev", ""),
+ "extensions": ext_map,
+ },
+ "paths": ReferenceGenerator.get_paths(self.route_map),
+ "routers": ReferenceGenerator.get_routers(self.route_map),
+ },
+ indent=4,
+ )
self._write(code=code, name="reference", extension="json", folder="assets")
def _run_linters(self):
@@ -224,13 +233,28 @@ class PackageBuilder:
return content
@staticmethod
- def _diff(path: Path) -> Tuple[Set[str], Set[str]]:
+ def _diff(ext_map: Dict[str, List[str]]) -> Tuple[Set[str], Set[str]]:
"""Check differences between built and installed extensions.
Parameters
----------
- path: Path
- The path to the folder where the extension map is stored.
+ ext_map: Dict[str, List[str]]
+ Dictionary containing the extensions.
+ Example:
+ {
+ "openbb_core_extension": [
+ "commodity@1.0.1",
+ ...
+ ],
+ "openbb_provider_extension": [
+ "benzinga@1.1.3",
+ ...
+ ],
+ "openbb_obbject_extension": [
+ "openbb_charting@1.0.0",
+ ...
+ ]
+ }
Returns
-------
@@ -238,8 +262,6 @@ class PackageBuilder:
First element: set of installed extensions that are not in the package.
Second element: set of extensions in the package that are not installed.
"""
- ext_map = PackageBuilder._read(path)
-
add: Set[str] = set()
remove: Set[str] = set()
groups = OpenBBGroups.groups()
@@ -263,7 +285,7 @@ class ModuleBuilder:
def build(path: str, ext_map: Optional[Dict[str, List[str]]] = None) -> str:
"""Build the module."""
code = "### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###\n\n"
- code += ImportDefinition.build(path=path)
+ code += ImportDefinition.build(path)
code += ClassDefinition.build(path, ext_map)
return code
@@ -300,7 +322,7 @@ class ImportDefinition:
hint_type = get_args(get_type_hints(return_type)["results"])[0]
hint_type_list.append(hint_type)
- hint_type_list = cls.filter_hint_type_list(hint_type_list=hint_type_list)
+ hint_type_list = cls.filter_hint_type_list(hint_type_list)
return hint_type_list
@@ -330,7 +352,6 @@ class ImportDefinition:
hint_type_list = cls.get_path_hint_type_list(path=path)
code = "from openbb_core.app.static.container import Container"
code += "\nfrom openbb_core.app.model.obbject import OBBject"
- code += "\nfrom openbb_core.app.model.custom_parameter import OpenBBCustomParameter, OpenBBCustomChoices"
# These imports were not detected before build, so we add them manually and
# ruff --fix the resulting code to remove unused imports.
@@ -339,6 +360,7 @@ class ImportDefinition:
code += "\nimport pandas"
code += "\nimport numpy"
code += "\nimport datetime"
+ code += "\nfrom datetime import date"
code += "\nimport pydantic"
code += "\nfrom pydantic import BaseModel"
code += "\nfrom inspect import Parameter"
@@ -355,6 +377,7 @@ class ImportDefinition:
code += "\nfrom openbb_core.app.static.utils.filters import filter_inputs\n"
code += "\nfrom openbb_core.provider.abstract.data import Data"
code += "\nfrom openbb_core.app.deprecation import OpenBBDeprecationWarning\n"
+ code += "\nfrom openbb_core.app.model.field import OpenBBField"
if path.startswith("/quantitative"):
code += "\nfrom openbb_quantitative.models import "
code += "(CAPMModel,NormalityModel,OmegaModel,SummaryModel,UnitRootModel)"
@@ -380,7 +403,7 @@ class ClassDefinition:
code = f"class {class_name}(Container):\n"
route_map = PathHandler.build_route_map()
- path_list = PathHandler.build_path_list(route_map=route_map)
+ path_list = PathHandler.build_path_list(route_map)
child_path_list = sorted(
PathHandler.get_child_path_list(
path=path,
@@ -391,7 +414,7 @@ class ClassDefinition:
doc = f' """{path}\n' if path else ' # fmt: off\n """\nRouters:\n'
methods = ""
for c in child_path_list:
- route = PathHandler.get_route(path=c, route_map=route_map)
+ route = PathHandler.get_route(c, route_map)
if route:
doc += f" {route.name}\n"
methods += MethodDefinition.build_command_method(
@@ -557,8 +580,8 @@ class MethodDefinition:
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=Annotated[
bool,
- OpenBBCustomParameter(
- description="Whether to create a chart or not, by default False."
+ OpenBBField(
+ description="Whether to create a chart or not, by default False.",
),
],
default=False,
@@ -580,14 +603,14 @@ class MethodDefinition:
name="provider",
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=Annotated[
- Union[MethodDefinition.get_type(field), None],
- OpenBBCustomParameter(
+ Optional[MethodDefinition.get_type(field)],
+ OpenBBField(
description=(
"The provider to use for the query, by default None.\n"
f" If None, the provider specified in defaults is selected or '{first}' if there is\n"
" no default."
""
- )
+ ),
),
],
default=None,
@@ -638,7 +661,7 @@ class MethodDefinition:
):
"""Add the field custom description and choices to the param signature as annotations."""
if model_name:
- available_fields: Dict[str, Field] = (
+ available_fields: Dict[str, DCField] = (
ProviderInterface().params[model_name]["standard"].__dataclass_fields__
)
@@ -647,27 +670,26 @@ class MethodDefinition:
continue
field_default = available_fields[param].default
-
choices = getattr(field_default, "json_schema_extra", {}).get(
"choices", []
)
description = getattr(field_default, "description", "")
- if choices:
- new_value = value.replace(
- annotation=Annotated[
- value.annotation,
- OpenBBCustomParameter(description=description),
- OpenBBCustomChoices(choices=choices),
- ],
- )
- else:
- new_value = value.replace(
- annotation=Annotated[
- value.annotation,
- OpenBBCustomParameter(description=description),
- ],
- )
+ PartialParameter = partial(
+ OpenBBField,
+ description=description,
+ )
+
+ new_value = value.replace(
+ annotation=Annotated[
+ value.annotation,
+ (
+ PartialParameter(choices=choices)
+ if choices
+ else PartialParameter()
+ ),
+ ],
+ )
od[param] = new_value
@@ -901,17 +923,17 @@ class DocstringGenerator:
Parameters
----------
- field_type (Any):
- Typing object containing the field type.
- is_required (bool):
- Flag to indicate if the field is required.
- target (Literal["docstring", "website"], optional):
- Target to return type for. Defaults to "docstring".
+ field_type : Any
+ Typing object containing the field type.
+ is_required : bool
+ Flag to indicate if the field is required.
+ target : Literal["docstring", "website"]
+ Target to return type for. Defaults to "docstring".
Returns
-------
- str:
- String representation of the field type.
+ str
+ String representation of the field type.
"""
is_optional = not is_required
@@ -1020,6 +1042,7 @@ class DocstringGenerator:
kwarg_params: dict,
returns: Dict[str, FieldInfo],
results_type: str,
+ sections: List[str],
) -> str:
"""Create the docstring for model."""
@@ -1053,50 +1076,56 @@ class DocstringGenerator:
description = getattr(metadata[0], "description", "") if metadata else ""
return type_, description
- docstring = summary.strip("\n").replace("\n ", f"\n{create_indent(2)}")
- docstring += "\n\n"
- docstring += f"{create_indent(2)}Parameters\n"
- docstring += f"{create_indent(2)}----------\n"
+ # Description summary
+ if "description" in sections:
+ docstring = summary.strip("\n").replace("\n ", f"\n{create_indent(2)}")
+ docstring += "\n\n"
+ if "parameters" in sections:
+ docstring += f"{create_indent(2)}Parameters\n"
+ docstring += f"{create_indent(2)}----------\n"
- # Explicit parameters
- for param_name, param in explicit_params.items():
- type_, description = get_param_info(param)
- type_str = format_type(str(type_), char_limit=79)
- docstring += f"{create_indent(2)}{param_name} : {type_str}\n"
- docstring += f"{create_indent(3)}{format_description(description)}\n"
+ # Explicit parameters
+ for param_name, param in explicit_params.items():
+ type_, description = get_param_info(param)
+ type_str = format_type(str(type_), char_limit=79)
+ docstring += f"{create_indent(2)}{param_name} : {type_str}\n"
+ docstring += f"{create_indent(3)}{format_description(description)}\n"
- # Kwargs
- for param_name, param in kwarg_params.items():
- p_type = getattr(param, "type", "")
- type_ = (
- getattr(p_type, "__name__", "") if inspect.isclass(p_type) else p_type
- )
+ # Kwargs
+ for param_name, param in kwarg_params.items():
+ p_type = getattr(param, "type", "")
+ type_ = (
+ getattr(p_type, "__name__", "")
+ if inspect.isclass(p_type)
+ else p_type
+ )
- if "NoneType" in str(type_):
- type_ = f"Optional[{type_}]".replace(", NoneType", "")
+ if "NoneType" in str(type_):
+ type_ = f"Optional[{type_}]".replace(", NoneType", "")
- default = getattr(param, "default", "")
- description = getattr(default, "description", "")
- docstring += f"{create_indent(2)}{param_name} : {type_}\n"
- docstring += f"{create_indent(3)}{format_description(description)}\n"
+ default = getattr(param, "default", "")
+ description = getattr(default, "description", "")
+ docstring += f"{create_indent(2)}{param_name} : {type_}\n"
+ docstring += f"{create_indent(3)}{format_description(description)}\n"
- # Returns
- docstring += "\n"
- docstring += f"{create_indent(2)}Returns\n"
- docstring += f"{create_indent(2)}-------\n"
- providers, _ = get_param_info(explicit_params.get("provider", None))
- docstring += cls.get_OBBject_description(results_type, providers)
+ if "returns" in sections:
+ # Returns
+ docstring += "\n"
+ docstring += f"{create_indent(2)}Returns\n"
+ docstring += f"{create_indent(2)}-------\n"
+ providers, _ = get_param_info(explicit_params.get("provider", None))
+ docstring += cls.get_OBBject_description(results_type, providers)
- # Schema
- underline = "-" * len(model_name)
- docstring += f"\n{create_indent(2)}{model_name}\n"
- docstring += f"{create_indent(2)}{underline}\n"
+ # Schema
+ underline = "-" * len(model_name)
+ docstring += f"\n{create_indent(2)}{model_name}\n"
+ docstring += f"{create_indent(2)}{underline}\n"
- for name, field in returns.items():
- field_type = cls.get_field_type(field.annotation, field.is_required())
- description = getattr(field, "description", "")
- docstring += f"{create_indent(2)}{field.alias or name} : {field_type}\n"
- docstring += f"{create_indent(3)}{format_description(description)}\n"
+ for name, field in returns.items():
+ field_type = cls.get_field_type(field.annotation, field.is_required())
+ description = getattr(field, "description", "")
+ docstring += f"{create_indent(2)}{field.alias or name} : {field_type}\n"
+ docstring += f"{create_indent(3)}{format_description(description)}\n"
return docstring
@classmethod
@@ -1112,6 +1141,11 @@ class DocstringGenerator:
doc = func.__doc__ or ""
param_types = {}
+ sections = SystemService().system_settings.python_settings.docstring_sections
+ max_length = (
+ SystemService().system_settings.python_settings.docstring_max_length
+ )
+
# Parameters explicit in the function signature
explicit_params = dict(formatted_params)
explicit_params.pop("extra_params", None)
@@ -1125,32 +1159,105 @@ class DocstringGenerator:
# Parameters passed as **kwargs
kwarg_params = params["extra"].__dataclass_fields__
param_types.update({k: v.type for k, v in kwarg_params.items()})
-
- returns = return_schema.model_fields
- results_type = func.__annotations__.get("return", model_name)
- if hasattr(results_type, "results_type_repr"):
- results_type = results_type.results_type_repr()
-
+ # Format the annotation to hide the metadata, tags, etc.
+ annotation = func.__annotations__.get("return")
+ results_type = (
+ cls._get_repr(
+ cls._get_generic_types(
+ annotation.model_fields["results"].annotation, # type: ignore[union-attr,arg-type]
+ [],
+ ),
+ model_name,
+ )
+ if isclass(annotation) and issubclass(annotation, OBBject) # type: ignore[arg-type]
+ else model_name
+ )
doc = cls.generate_model_docstring(
model_name=model_name,
summary=func.__doc__ or "",
explicit_params=explicit_params,
kwarg_params=kwarg_params,
- returns=returns,
+ returns=return_schema.model_fields,
results_type=results_type,
+ sections=sections,
)
else:
doc = doc.replace("\n ", f"\n{create_indent(2)}")
- if doc and examples:
+ if doc and examples and "examples" in sections:
doc += cls.build_examples(
path.replace("/", "."),
param_types,
examples,
)
+ if (
+ max_length # pylint: disable=chained-comparison
+ and len(doc) > max_length
+ and max_length > 3
+ ):
+ doc = doc[: max_length - 3] + "..."
return doc
+ @classmethod
+ def _get_generic_types(cls, type_: type, items: list) -> List[str]:
+ """Unpack generic types recursively.
+
+ Parameters
+ ----------
+ type_ : type
+ Type to unpack.
+ items : list
+ List to store the unpacked types.
+
+ Returns
+ -------
+ List[str]
+ List of unpacked type names.
+
+ Examples
+ --------
+ Union[List[str], Dict[str, str], Tuple[str]] -> ["List", "Dict", "Tuple"]
+ """
+ if hasattr(type_, "__args__"):
+ origin = get_origin(type_)
+ # pylint: disable=unidiomatic-typecheck
+ if (
+ type(origin) is type
+ and origin is not Annotated
+ and (name := getattr(type_, "_name", getattr(type_, "__name__", None)))
+ ):
+ items.append(name.title())
+ func = partial(cls._get_generic_types, items=items)
+ set().union(*map(func, type_.__args__), items)
+ return items
+
+ @staticmethod
+ def _get_repr(items: List[str], model: str) -> str:
+ """Get the string representation of the types list with the model name.
+
+ Parameters
+ ----------
+ items : List[str]
+ List of type names.
+ model : str
+ Model name to access the model providers.
+
+ Returns
+ -------
+ str
+ String representation of the unpacked types list.
+
+ Examples
+ --------
+ [List, Dict, Tuple], M -> "Union[List[M], Dict[str, M], Tuple[M]]"
+ """
+ if s := [
+ f"Dict[str, {model}]" if i == "Dict" else f"{i}[{model}]" for i in items
+ ]:
+ return f"Union[{', '.join(s)}]" if len(s) > 1 else s[0]
+ return model
+
class PathHandler:
"""Handle the paths for the Platform."""
@@ -1235,7 +1342,7 @@ class ReferenceGenerator:
pi = DocstringGenerator.provider_interface
@classmethod
- def get_endpoint_examples(
+ def _get_endpoint_examples(
cls,
path: str,
func: Callable,
@@ -1248,18 +1355,18 @@ class ReferenceGenerator:
Parameters
----------
- path (str):
- Path of the router.
- func (Callable):
- Router endpoint function.
- examples (Optional[List[Example]]):
- List of Examples (APIEx or PythonEx type)
- for the endpoint.
+ path : str
+ Path of the router.
+ func : Callable
+ Router endpoint function.
+ examples : Optional[List[Example]]
+ List of Examples (APIEx or PythonEx type)
+ for the endpoint.
Returns
-------
- str:
- Formatted string containing the examples for the endpoint.
+ str:
+ Formatted string containing the examples for the endpoint.
"""
sig = signature(func)
parameter_map = dict(sig.parameters)
@@ -1278,18 +1385,18 @@ class ReferenceGenerator:
)
@classmethod
- def get_provider_parameter_info(cls, model: str) -> Dict[str, str]:
+ def _get_provider_parameter_info(cls, model: str) -> Dict[str, str]:
"""Get the name, type, description, default value and optionality information for the provider parameter.
Parameters
----------
- model (str):
- Standard model to access the model providers.
+ model : str
+ Standard model to access the model providers.
Returns
-------
- Dict[str, str]:
- Dictionary of the provider parameter information
+ Dict[str, str]
+ Dictionary of the provider parameter information
"""
pi_model_provider = cls.pi.model_providers[model]
provider_params_field = pi_model_provider.__dataclass_fields__["provider"]
@@ -1316,7 +1423,7 @@ class ReferenceGenerator:
return provider_parameter_info
@classmethod
- def get_provider_field_params(
+ def _get_provider_field_params(
cls,
model: str,
params_type: str,
@@ -1326,22 +1433,22 @@ class ReferenceGenerator:
Parameters
----------
- model (str):
- Model name to access the provider interface
- params_type (str):
- Parameters to fetch data for (QueryParams or Data)
- provider (str, optional):
- Provider name. Defaults to "openbb".
+ model : str
+ Model name to access the provider interface
+ params_type : str
+ Parameters to fetch data for (QueryParams or Data)
+ provider : str
+ Provider name. Defaults to "openbb".
Returns
-------
- List[Dict[str, str]]:
- List of dictionaries containing the field name, type, description, default,
- optional flag and standard flag for each provider.
+ List[Dict[str, str]]
+ List of dictionaries containing the field name, type, description, default,
+ optional flag and standard flag for each provider.
"""
provider_field_params = []
expanded_types = MethodDefinition.TYPE_EXPANSION
- model_map = cls.pi._map[model] # pylint: disable=protected-access
+ model_map = cls.pi.map[model]
for field, field_info in model_map[provider][params_type]["fields"].items():
# Determine the field type, expanding it if necessary and if params_type is "Parameters"
@@ -1391,24 +1498,24 @@ class ReferenceGenerator:
return provider_field_params
@staticmethod
- def get_obbject_returns_fields(
+ def _get_obbject_returns_fields(
model: str,
providers: str,
) -> List[Dict[str, str]]:
"""Get the fields of the OBBject returns object for the given standard_model.
- Args
- ----
- model (str):
- Standard model of the returned object.
- providers (str):
- Available providers for the model.
+ Parameters
+ ----------
+ model : str
+ Standard model of the returned object.
+ providers : str
+ Available providers for the model.
Returns
-------
- List[Dict[str, str]]:
- List of dictionaries containing the field name, type, description, default
- and optionality of each field.
+ List[Dict[str, str]]
+ List of dictionaries containing the field name, type, description, default
+ and optionality of each field.
"""
obbject_list = [
{
@@ -1441,21 +1548,21 @@ class ReferenceGenerator:
return obbject_list
@staticmethod
- def get_post_method_parameters_info(
+ def _get_post_method_parameters_info(
docstring: str,
) -> List[Dict[str, Union[bool, str]]]:
"""Get the parameters for the POST method endpoints.
Parameters
----------
- docstring (str):
- Router endpoint function's docstring
+ docstring : str
+ Router endpoint function's docstring
Returns
-------
- List[Dict[str, str]]:
- List of dictionaries containing the name,type, description, default
- and optionality of each parameter.
+ List[Dict[str, str]]
+ List of dictionaries containing the name,type, description, default
+ and optionality of each parameter.
"""
parameters_list = []
@@ -1497,19 +1604,19 @@ class ReferenceGenerator:
return parameters_list
@staticmethod
- def get_post_method_returns_info(docstring: str) -> List[Dict[str, str]]:
+ def _get_post_method_returns_info(docstring: str) -> List[Dict[str, str]]:
"""Get the returns information for the POST method endpoints.
Parameters
----------
- docstring (str):
- Router endpoint function's docstring
+ docstring: str
+ Router endpoint function's docstring
Returns
-------
- List[Dict[str, str]]:
- Single element list having a dictionary containing the name, type,
- description of the return value
+ List[Dict[str, str]]
+ Single element list having a dictionary containing the name, type,
+ description of the return value
"""
returns_list = []
@@ -1538,8 +1645,8 @@ class ReferenceGenerator:
return returns_list
@classmethod
- def get_reference_data(cls) -> Dict[str, Dict[str, Any]]:
- """Get the reference data for the Platform.
+ def get_paths(cls, route_map: Dict[str, BaseRoute]) -> Dict[str, Dict[str, Any]]:
+ """Get path reference data.
The reference data is a dictionary containing the description, parameters,
returns and examples for each endpoint. This is currently useful for
@@ -1547,12 +1654,11 @@ class ReferenceGenerator:
Returns
-------
- Dict[str, Dict[str, Any]]:
- Dictionary containing the description, parameters, returns and
- examples for each endpoint.
+ Dict[str, Dict[str, Any]]
+ Dictionary containing the description, parameters, returns and
+ examples for each endpoint.
"""
reference: Dict[str, Dict] = {}
- route_map = PathHandler.build_route_map()
for path, route in route_map.items():
# Initialize the reference fields as empty dictionaries
@@ -1574,7 +1680,7 @@ class ReferenceGenerator:
}
# Add endpoint examples
examples = openapi_extra.get("examples", [])
- reference[path]["examples"] = cls.get_endpoint_examples(
+ reference[path]["examples"] = cls._get_endpoint_examples(
path,
route_func,
examples, # type: ignore
@@ -1584,19 +1690,21 @@ class ReferenceGenerator:
reference[path]["description"] = getattr(
route, "description", "No description available."
)
+
+ # TODO: The reference is not getting populated when a command does not use a standard model
# Access model map from the ProviderInterface
- model_map = cls.pi._map[
- standard_model
- ] # pylint: disable=protected-access
+ model_map = cls.pi.map.get(standard_model, {})
for provider in model_map:
if provider == "openbb":
# openbb provider is always present hence its the standard field
reference[path]["parameters"]["standard"] = (
- cls.get_provider_field_params(standard_model, "QueryParams")
+ cls._get_provider_field_params(
+ standard_model, "QueryParams"
+ )
)
# Add `provider` parameter fields to the openbb provider
- provider_parameter_fields = cls.get_provider_parameter_info(
+ provider_parameter_fields = cls._get_provider_parameter_info(
standard_model
)
reference[path]["parameters"]["standard"].append(
@@ -1605,23 +1713,23 @@ class ReferenceGenerator:
# Add endpoint data fields for standard provider
reference[path]["data"]["standard"] = (
- cls.get_provider_field_params(standard_model, "Data")
+ cls._get_provider_field_params(standard_model, "Data")
)
continue
# Adds provider specific parameter fields to the reference
reference[path]["parameters"][provider] = (
- cls.get_provider_field_params(
+ cls._get_provider_field_params(
standard_model, "QueryParams", provider
)
)
# Adds provider specific data fields to the reference
- reference[path]["data"][provider] = cls.get_provider_field_params(
+ reference[path]["data"][provider] = cls._get_provider_field_params(
standard_model, "Data", provider
)
# Add endpoint returns data
# Currently only OBBject object is returned
providers = provider_parameter_fields["type"]
- reference[path]["returns"]["OBBject"] = cls.get_obbject_returns_fields(
+ reference[path]["returns"]["OBBject"] = cls._get_obbject_returns_fields(
standard_model, providers
)
# Add data for the endpoints without a standard model (data processing endpoints)
@@ -1635,12 +1743,43 @@ class ReferenceGenerator:
reference[path]["description"] = re.sub(" +", " ", description)
# Add endpoint parameters fields for POST methods
reference[path]["parameters"]["standard"] = (
- ReferenceGenerator.get_post_method_parameters_info(docstring)
+ cls._get_post_method_parameters_info(docstring)
)
# Add endpoint returns data
# Currently only OBBject object is returned
reference[path]["returns"]["OBBject"] = (
- cls.get_post_method_returns_info(docstring)
+ cls._get_post_method_returns_info(docstring)
)
return reference
+
+ @classmethod
+ def get_routers(cls, route_map: Dict[str, BaseRoute]) -> Dict[str, Dict[str, Any]]:
+ """Get router reference data.
+
+ Parameters
+ ----------
+ route_map : Dict[str, BaseRoute]
+ Dictionary containing the path and route object for the router.
+
+ Returns
+ -------
+ Dict[str, Dict[str, Any]]
+ Dictionary containing the description for each router.
+ """
+ main_router = RouterLoader.from_extensions()
+ routers = {}
+ for path in route_map:
+ path_parts = path.split("/")
+ # We start at 2: ["/", "some_router"] "/some_router"
+ i = 2
+ p = "/".join(path_parts[:i])
+ while p != path:
+ if p not in routers:
+ description = main_router.get_attr(p, "description")
+ if description is not None:
+ routers[p] = {"description": description}
+ # We go down the path to include sub-routers
+ i += 1
+ p = "/".join(path_parts[:i])
+ return routers
diff --git a/openbb_platform/core/openbb_core/app/static/utils/decorators.py b/openbb_platform/core/openbb_core/app/static/utils/decorators.py
index 61d9b46578b..a8fbf1f0d59 100644
--- a/openbb_platform/core/openbb_core/app/static/utils/decorators.py
+++ b/openbb_platform/core/openbb_core/app/static/utils/decorators.py
@@ -29,7 +29,7 @@ def validate(
"""Validate function calls."""
def decorated(f: Callable[P, R]):
- """Decorated function."""
+ """Use for decorating functions."""
@wraps(f)
def wrapper(*f_args, **f_kwargs):
diff --git a/openbb_platform/core/openbb_core/app/static/utils/filters.py b/openbb_platform/core/openbb_core/app/static/utils/filters.py
index 32a92138586..b2262a1e8c8 100644
--- a/openbb_platform/core/openbb_core/app/static/utils/filters.py
+++ b/openbb_platform/core/openbb_core/app/static/utils/filters.py
@@ -42,5 +42,16 @@ def filter_inputs(
kwargs[p][field] = new
break
+ else:
+ provider = kwargs.get("provider_choices", {}).get("provider")
+ for param_category in ("standard_params", "extra_params"):
+ if param_category in kwargs:
+ for field, value in kwargs[param_category].items():
+ if isinstance(value, list):
+ kwargs[param_category][field] = ",".join(map(str, value))
+ check_single_item(
+ kwargs[param_category][field],
+ f"{field} -> multiple items not allowed for '{provider}'",
+ )
return kwargs
diff --git a/openbb_platform/core/openbb_core/app/utils.py b/openbb_platform/core/openbb_core/app/utils.py
index f7c6c1ca7dc..452300c4334 100644
--- a/openbb_platform/core/openbb_core/app/utils.py
+++ b/openbb_platform/core/openbb_core/app/utils.py
@@ -166,6 +166,6 @@ def check_single_item(
value: Optional[str], message: Optional[str] = None
) -> Optional[str]:
"""Check that string contains a single item."""
- if value and ("," in value or ";" in value):
+ if value and isinstance(value, str) and ("," in value or ";" in value):
raise OpenBBError(message if message else "multiple items not allowed")
return value
diff --git a/openbb_platform/core/openbb_core/app/version.py b/openbb_platform/core/openbb_core/app/version.py
index a022eb1c790..39872d98a1c 100644
--- a/openbb_platform/core/openbb_core/app/version.py
+++ b/openbb_platform/core/openbb_core/app/version.py
@@ -58,3 +58,8 @@ try:
VERSION = get_package_version(PACKAGE)
except pkg_resources.DistributionNotFound:
VERSION = "unknown"
+
+try:
+ CORE_VERSION = get_package_version("openbb-core")
+except pkg_resources.DistributionNotFound:
+ CORE_VERSION = "unknown"
diff --git a/openbb_platform/core/openbb_core/env.py b/openbb_platform/core/openbb_core/env.py
index e4e4c1df461..9249658ffbc 100644
--- a/openbb_platform/core/openbb_core/env.py
+++ b/openbb_platform/core/openbb_core/env.py
@@ -1,3 +1,5 @@
+"""Environment variables."""
+
import os
from pathlib import Path
from typing import Dict, Optional
@@ -14,47 +16,48 @@ class Env(metaclass=SingletonMeta):
_environ: Dict[str, str]
def __init__(self) -> None:
+ """Initialize the environment."""
dotenv.load_dotenv(Path(OPENBB_DIRECTORY, ".env"))
self._environ = os.environ.copy()
@property
def API_AUTH(self) -> bool:
- """API authentication: enables API endpoint authentication"""
+ """API authentication: enables API endpoint authentication."""
return self.str2bool(self._environ.get("OPENBB_API_AUTH", False))
@property
def API_USERNAME(self) -> Optional[str]:
- """API username: sets API username"""
+ """API username: sets API username."""
return self._environ.get("OPENBB_API_USERNAME", None)
@property
def API_PASSWORD(self) -> Optional[str]:
- """API password: sets API password"""
+ """API password: sets API password."""
return self._environ.get("OPENBB_API_PASSWORD", None)
@property
def API_AUTH_EXTENSION(self) -> Optional[str]:
- """Auth extension: specifies which authentication extension to use"""
+ """Auth extension: specifies which authentication extension to use."""
return self._environ.get("OPENBB_API_AUTH_EXTENSION", None)
@property
def AUTO_BUILD(self) -> bool:
- """Automatic build: enables automatic package build on import"""
+ """Automatic build: enables automatic package build on import."""
return self.str2bool(self._environ.get("OPENBB_AUTO_BUILD", True))
@property
def DEBUG_MODE(self) -> bool:
- """Debug mode: enables debug mode"""
+ """Debug mode: enables debug mode."""
return self.str2bool(self._environ.get("OPENBB_DEBUG_MODE", False))
@property
def DEV_MODE(self) -> bool:
- """Dev mode: enables development mode"""
+ """Dev mode: enables development mode."""
return self.str2bool(self._environ.get("OPENBB_DEV_MODE", False))
@property
def HUB_BACKEND(self) -> str:
- """Hub backend: sets the backend for the OpenBB Hub"""
+ """Hub backend: sets the backend for the OpenBB Hub."""
return self._environ.get("OPENBB_HUB_BACKEND", "https://payments.openbb.co")
@staticmethod
diff --git a/openbb_platform/core/openbb_core/provider/abstract/annotated_result.py b/openbb_platform/core/openbb_core/provider/abstract/annotated_result.py
new file mode 100644
index 00000000000..8d51086017b
--- /dev/null
+++ b/openbb_platform/core/openbb_core/provider/abstract/annotated_result.py
@@ -0,0 +1,20 @@
+"""Annotated result."""
+
+from typing import Generic, Optional, TypeVar
+
+from pydantic import BaseModel, Field
+
+T = TypeVar("T")
+
+
+class AnnotatedResult(BaseModel, Generic[T]):
+ """Annotated result allows fetchers to return metadata along with the data."""
+
+ result: Optional[T] = Field(
+ default=None,
+ description="Serializable results.",
+ )
+ metadata: Optional[dict] = Field(
+ default=None,
+ description="Metadata.",
+ )
diff --git a/openbb_platform/core/openbb_core/provider/abstract/fetcher.py b/openbb_platform/core/openbb_core/provider/abstract/fetcher.py
index 011f03f197b..f2cc759821a 100644
--- a/openbb_platform/core/openbb_core/provider/abstract/fetcher.py
+++ b/openbb_platform/core/openbb_core/provider/abstract/fetcher.py
@@ -9,12 +9,14 @@ from typing import (
Generic,
Optional,
TypeVar,
+ Union,
get_args,
get_origin,
)
from pandas import DataFrame
+from openbb_core.provider.abstract.annotated_result import AnnotatedResult
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.helpers import maybe_coroutine, run_async
@@ -56,7 +58,7 @@ class Fetcher(Generic[Q, R]):
"""Extract the data from the provider."""
@staticmethod
- def transform_data(query: Q, data: Any, **kwargs) -> R:
+ def transform_data(query: Q, data: Any, **kwargs) -> Union[R, AnnotatedResult[R]]:
"""Transform the provider-specific data."""
raise NotImplementedError
@@ -79,7 +81,7 @@ class Fetcher(Generic[Q, R]):
params: Dict[str, Any],
credentials: Optional[Dict[str, str]] = None,
**kwargs,
- ) -> R:
+ ) -> Union[R, AnnotatedResult[R]]:
"""Fetch data from a provider."""
query = cls.transform_query(params=params)
data = await maybe_coroutine(
@@ -139,7 +141,7 @@ class Fetcher(Generic[Q, R]):
data = run_async(
cls.extract_data, query=query, credentials=credentials, **kwargs
)
- transformed_data = cls.transform_data(query=query, data=data, **kwargs)
+ result = cls.transform_data(query=query, data=data, **kwargs)
# Class Assertions
assert isinstance(
@@ -184,10 +186,13 @@ class Fetcher(Generic[Q, R]):
assert len(data) > 0, "Data must not be empty."
# Transformed Data Assertions
+ transformed_data = (
+ result.result if isinstance(result, AnnotatedResult) else result
+ )
+
assert transformed_data, "Transformed data must not be None."
- is_list = isinstance(transformed_data, list)
- if is_list:
+ if isinstance(transformed_data, list):
return_type_args = cls.return_type.__args__[0]
return_type_is_dict = (
hasattr(return_type_args, "__origin__")
diff --git a/openbb_platform/core/openbb_core/provider/registry_map.py b/openbb_platform/core/openbb_core/provider/registry_map.py
index eb4573a0efb..05dff9ef4f6 100644
--- a/openbb_platform/core/openbb_core/provider/registry_map.py
+++ b/openbb_platform/core/openbb_core/provider/registry_map.py
@@ -1,12 +1,11 @@
"""Provider registry map."""
-import sys
from copy import deepcopy
from inspect import getfile, isclass
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Tuple, get_origin
-from pydantic import BaseModel, Field, create_model
+from pydantic import BaseModel
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.fetcher import Fetcher
@@ -27,8 +26,8 @@ class RegistryMap:
self._registry = registry or RegistryLoader.from_extensions()
self._credentials = self._get_credentials(self._registry)
self._available_providers = self._get_available_providers(self._registry)
- self._map, self._return_map = self._get_map(self._registry)
- self._models = self._get_models(self._map)
+ self._standard_extra, self._original_models = self._get_maps(self._registry)
+ self._models = self._get_models(self._standard_extra)
@property
def registry(self) -> Registry:
@@ -46,14 +45,14 @@ class RegistryMap:
return self._credentials
@property
- def map(self) -> MapType:
- """Get provider registry map."""
- return self._map
+ def standard_extra(self) -> MapType:
+ """Get standard extra map."""
+ return self._standard_extra
@property
- def return_map(self) -> MapType:
- """Get provider registry return map."""
- return self._return_map
+ def original_models(self) -> MapType:
+ """Get original models."""
+ return self._original_models
@property
def models(self) -> List[str]:
@@ -72,37 +71,42 @@ class RegistryMap:
"""Get list of available providers."""
return sorted(list(registry.providers.keys()))
- def _get_map(self, registry: Registry) -> Tuple[MapType, Dict[str, Dict]]:
+ def _get_maps(self, registry: Registry) -> Tuple[MapType, Dict[str, Dict]]:
"""Generate map for the provider package."""
- map_: MapType = {}
- return_schemas: Dict[str, Dict] = {}
+ standard_extra: MapType = {}
+ original_models: Dict[str, Dict] = {}
for p in registry.providers:
for model_name, fetcher in registry.providers[p].fetcher_dict.items():
- standard_query, extra_query = self.extract_info(fetcher, "query_params")
- standard_data, extra_data = self.extract_info(fetcher, "data")
- if model_name not in map_:
- map_[model_name] = {}
+ standard_query, extra_query = self._extract_info(
+ fetcher, "query_params"
+ )
+ standard_data, extra_data = self._extract_info(fetcher, "data")
+ if model_name not in standard_extra:
+ standard_extra[model_name] = {}
# The deepcopy avoids modifications from one model to affect another
- map_[model_name]["openbb"] = {
+ standard_extra[model_name]["openbb"] = {
"QueryParams": deepcopy(standard_query),
"Data": deepcopy(standard_data),
}
- map_[model_name][p] = {
+ standard_extra[model_name][p] = {
"QueryParams": extra_query,
"Data": extra_data,
}
- if provider_model := self.extract_data_model(fetcher, p):
- is_list = get_origin(self.extract_return_type(fetcher)) == list
+ original_models.setdefault(model_name, {}).update(
+ {
+ p: {
+ "query": self._get_model(fetcher, "query_params"),
+ "data": self._get_model(fetcher, "data"),
+ "results_type": self._get_results_type(fetcher),
+ }
+ }
+ )
- return_schemas.setdefault(model_name, {}).update(
- {p: {"model": provider_model, "is_list": is_list}}
- )
+ self._merge_json_schema_extra(p, fetcher, standard_extra[model_name])
- self._merge_json_schema_extra(p, fetcher, map_[model_name])
-
- return map_, return_schemas
+ return standard_extra, original_models
def _merge_json_schema_extra(
self,
@@ -110,7 +114,7 @@ class RegistryMap:
fetcher: Fetcher,
model_map: dict,
):
- """Merge json schema extra for different providers"""
+ """Merge json schema extra for different providers."""
model: BaseModel = RegistryMap._get_model(fetcher, "query_params")
std_fields = model_map["openbb"]["QueryParams"]["fields"]
extra_fields = model_map[provider]["QueryParams"]["fields"]
@@ -138,49 +142,14 @@ class RegistryMap:
return list(map_.keys())
@staticmethod
- def extract_return_type(fetcher: Fetcher):
+ def _get_results_type(fetcher: Fetcher) -> Any:
"""Extract return info from fetcher."""
- return getattr(fetcher, "return_type", None)
+ return get_origin(getattr(fetcher, "return_type", None))
@staticmethod
- def extract_data_model(fetcher: Fetcher, provider_str: str) -> BaseModel:
- """Extract info (fields and docstring) from fetcher query params or data."""
- model: BaseModel = RegistryMap._get_model(fetcher, "data")
- model_name = getattr(model, "__name__", "")
- fields = {}
- for field_name, field in model.model_fields.items():
- field.serialization_alias = field_name
- fields[field_name] = (field.annotation, field)
-
- fields["provider"] = (
- Literal[provider_str], # type: ignore
- Field(
- default=provider_str,
- description="The data provider for the data.",
- exclude=True,
- ),
- )
-
- provider_model = create_model( # type: ignore[call-overload]
- model_name.replace("Data", ""),
- __base__=model,
- __doc__=model.__doc__,
- __module__=model.__module__,
- **fields,
- )
-
- # Replace the provider models in the modules with the new models we created
- # To make sure provider field is defined to be the provider string
- # This is hacky, but we need to have `provider: Literal['provider_name']`
- # in the model to serve as union discriminator for the API validation
- # the alternative would be to specify it manually in all the models
- if model_name:
- setattr(sys.modules[model.__module__], model_name, provider_model)
-
- return provider_model
-
- @staticmethod
- def extract_info(fetcher: Fetcher, type_: Literal["query_params", "data"]) -> tuple:
+ def _extract_info(
+ fetcher: Fetcher, type_: Literal["query_params", "data"]
+ ) -> tuple:
"""Extract info (fields and docstring) from fetcher query params or data."""
model: BaseModel = RegistryMap._get_model(fetcher, type_)
all_fields = {}
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/etf_holdings.py b/openbb_platform/core/openbb_core/provider/standard_models/etf_holdings.py
index 8d1c4a59038..0ad30366cd9 100644
--- a/openbb_platform/core/openbb_core/provider/standard_models/etf_holdings.py
+++ b/openbb_platform/core/openbb_core/provider/standard_models/etf_holdings.py
@@ -2,7 +2,7 @@
from typing import Optional
-from pydantic import Field
+from pydantic import Field, field_validator
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
@@ -17,6 +17,12 @@ class EtfHoldingsQueryParams(QueryParams):
symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", "") + " (ETF)")
+ @field_validator("symbol")
+ @classmethod
+ def to_upper(cls, v: str) -> str:
+ """Convert field to uppercase."""
+ return v.upper()
+
class EtfHoldingsData(Data):
"""ETF Holdings Data."""
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/forward_eps_estimates.py b/openbb_platform/core/openbb_core/provider/standard_models/forward_eps_estimates.py
new file mode 100644
index 00000000000..e0f95d9c8a1
--- /dev/null
+++ b/openbb_platform/core/openbb_core/provider/standard_models/forward_eps_estimates.py
@@ -0,0 +1,67 @@
+"""Forward EPS Estimates Standard Model."""
+
+from datetime import date as dateType
+from typing import Optional
+
+from pydantic import Field, field_validator
+
+from openbb_core.provider.abstract.data import Data
+from openbb_core.provider.abstract.query_params import QueryParams
+from openbb_core.provider.utils.descriptions import (
+ DATA_DESCRIPTIONS,
+ QUERY_DESCRIPTIONS,
+)
+
+
+class ForwardEpsEstimatesQueryParams(QueryParams):
+ """Forward EPS Estimates Query Parameters."""
+
+ symbol: Optional[str] = Field(
+ default=None,
+ description=QUERY_DESCRIPTIONS["symbol"],
+ )
+
+ @field_validator("symbol", mode="before", check_fields=False)
+ @classmethod
+ def to_upper(cls, v):
+ """Convert field to uppercase."""
+ return v.upper() if v else None
+
+
+class ForwardEpsEstimatesData(Data):
+ """Forward EPS Estimates Data."""
+
+ symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
+ name: Optional[str] = Field(default=None, description="Name of the entity.")
+ date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
+ fiscal_year: Optional[int] = Field(
+ default=None, description="Fiscal year for the estimate."
+ )
+ fiscal_period: Optional[str] = Field(
+ default=None, description="Fiscal quarter for the estimate."
+ )
+ calendar_year: Optional[int] = Field(
+ default=None, description="Calendar year for the estimate."
+ )
+ calendar_period: Optional[str] = Field(
+ default=None, description="Calendar quarter for the estimate."
+ )
+ low_estimate: Optional[float] = Field(
+ default=None, description="Estimated EPS low for the period."
+ )
+ high_estimate: Optional[float] = Field(
+ default=None, description="Estimated EPS high for the period."
+ )
+ mean: Optional[float] = Field(
+ default=None, description="Estimated EPS mean for the period."
+ )
+ median: Optional[float] = Field(
+ default=None, description="Estimated EPS median for the period."
+ )
+ standard_deviation: Optional[float] = Field(
+ default=None, description="Estimated EPS standard deviation for the period."
+ )
+ number_of_analysts: Optional[int] = Field(
+ default=None,
+ description="Number of analysts providing estimates for the period.",
+ )
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/forward_sales_estimates.py b/openbb_platform/core/openbb_core/provider/standard_models/forward_sales_estimates.py
new file mode 100644
index 00000000000..8d3e1217d58
--- /dev/null
+++ b/openbb_platform/core/openbb_core/provider/standard_models/forward_sales_estimates.py
@@ -0,0 +1,68 @@
+"""Forward Sales Estimates Standard Model."""
+
+from datetime import date as dateType
+from typing import Optional
+
+from pydantic import Field, field_validator
+
+from openbb_core.provider.abstract.data import Data, ForceInt
+from openbb_core.provider.abstract.query_params import QueryParams
+from openbb_core.provider.utils.descriptions import (
+ DATA_DESCRIPTIONS,
+ QUERY_DESCRIPTIONS,
+)
+
+
+class ForwardSalesEstimatesQueryParams(QueryParams):
+ """Forward Sales Estimates Query Parameters."""
+
+ symbol: Optional[str] = Field(
+ default=None,
+ description=QUERY_DESCRIPTIONS["symbol"],
+ )
+
+ @field_validator("symbol", mode="before", check_fields=False)
+ @classmethod
+ def to_upper(cls, v):
+ """Convert field to uppercase."""
+ return v.upper() if v else None
+
+
+class ForwardSalesEstimatesData(Data):
+ """Forward Sales Estimates Data."""
+
+ symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
+ name: Optional[str] = Field(default=None, description="Name of the entity.")
+ date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
+ fiscal_year: Optional[int] = Field(
+ default=None, description="Fiscal year for the estimate."
+ )
+ fiscal_period: Optional[str] = Field(
+ default=None, description="Fiscal quarter for the estimate."
+ )
+ calendar_year: Optional[int] = Field(
+ default=None, description="Calendar year for the estimate."
+ )
+ calendar_period: Optional[str] = Field(
+ default=None, description="Calendar quarter for the estimate."
+ )
+ low_estimate: Optional[ForceInt] = Field(
+ default=None, description="The sales estimate low for the period."
+ )
+ high_estimate: Optional[ForceInt] = Field(
+ default=None, description="The sales estimate high for the period."
+ )
+ mean: Optional[ForceInt] = Field(
+ default=None, description="The sales estimate mean for the period."
+ )
+ median: Optional[ForceInt] = Field(
+ default=None, description="The sales estimate median for the period."
+ )
+ standard_deviation: Optional[ForceInt] = Field(
+ default=None,
+ description="The sales estimate standard deviation for the period.",
+ )
+ number_of_analysts: Optional[int] = Field(
+ default=None,
+ description="Number of analysts providing estimates for the period.",
+ )
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py
index ddde23dc45c..231945419ed 100644
--- a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py
+++ b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py
@@ -24,6 +24,7 @@ class OptionsChainsQueryParams(QueryParams):
@classmethod
@field_validator("symbol", mode="before", check_fields=False)
def to_upper(cls, v: str) -> str:
+ """Return the symbol in uppercase."""
return v.upper()
@@ -151,7 +152,7 @@ class OptionsChainsData(Data):
@field_validator("expiration", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
- """Return the datetime object from the date string"""
+ """Return the datetime object from the date string."""
if isinstance(v, datetime):
return datetime.strftime(v, "%Y-%m-%d")
if isinstance(v, str):
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/price_target_consensus.py b/openbb_platform/core/openbb_core/provider/standard_models/price_target_consensus.py
index f6949cb166a..106879a884f 100644
--- a/openbb_platform/core/openbb_core/provider/standard_models/price_target_consensus.py
+++ b/openbb_platform/core/openbb_core/provider/standard_models/price_target_consensus.py
@@ -15,19 +15,22 @@ from openbb_core.provider.utils.descriptions import (
class PriceTargetConsensusQueryParams(QueryParams):
"""Price Target Consensus Query."""
- symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", ""))
+ symbol: Optional[str] = Field(
+ default=None, description=QUERY_DESCRIPTIONS.get("symbol", "")
+ )
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
- def to_upper(cls, v: str) -> str:
+ def to_upper(cls, v):
"""Convert field to uppercase."""
- return v.upper()
+ return v.upper() if v else None
class PriceTargetConsensusData(Data):
"""Price Target Consensus Data."""
symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
+ name: Optional[str] = Field(default=None, description="The company name")
target_high: Optional[float] = Field(
default=None, description="High target of the price target consensus."
)
diff --git a/openbb_platform/core/openbb_core/provider/utils/client.py b/openbb_platform/core/openbb_core/provider/utils/client.py
index 0bec922d9ac..f093a90d4b1 100644
--- a/openbb_platform/core/openbb_core/provider/utils/client.py
+++ b/openbb_platform/core/openbb_core/provider/utils/client.py
@@ -41,6 +41,7 @@ class ClientResponse(aiohttp.ClientResponse):
"""Client response class."""
def __init__(self, *args, **kwargs):
+ """Initialize the response."""
kwargs["request_info"] = self.obfuscate_request_info(kwargs["request_info"])
super().__init__(*args, **kwargs)
@@ -61,10 +62,13 @@ class ClientResponse(aiohttp.ClientResponse):
class ClientSession(aiohttp.ClientSession):
+ """Client session."""
+
_response_class: Type[ClientResponse]
_session: "ClientSession"
def __init__(self, *args, **kwargs):
+ """Initialize the session."""
kwargs["connector"] = kwargs.get(
"connector", aiohttp.TCPConnector(ttl_dns_cache=300)
)
diff --git a/openbb_platform/core/openbb_core/provider/utils/helpers.py b/openbb_platform/core/openbb_core/provider/utils/helpers.py
index 3a5c557341f..604323e9703 100644
--- a/openbb_platform/core/openbb_core/provider/utils/helpers.py
+++ b/openbb_platform/core/openbb_core/provider/utils/helpers.py
@@ -1,8 +1,9 @@
"""Provider helpers."""
import asyncio
+import os
import re
-from datetime import date, datetime
+from datetime import date, datetime, timedelta, timezone
from difflib import SequenceMatcher
from functools import partial
from inspect import iscoroutinefunction
@@ -313,3 +314,12 @@ def filter_by_dates(
return False
return list(filter(_filter, data))
+
+
+def safe_fromtimestamp(
+ timestamp: Union[float, int], tz: Optional[timezone] = None
+) -> datetime:
+ """datetime.fromtimestamp alternative which supports negative timestamps on Windows platform."""
+ if os.name == "nt" and timestamp < 0:
+ return datetime(1970, 1, 1, tzinfo=tz) + timedelta(seconds=timestamp)
+ return datetime.fromtimestamp(timestamp, tz)
diff --git a/openbb_platform/core/pyproject.toml b/openbb_platform/core/pyproject.toml
index 623a8fd1c34..e82caac0f43 100644
--- a/openbb_platform/core/pyproject.toml
+++ b/openbb_platform/core/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
authors = ["OpenBB Team "]
readme = "README.md"
diff --git a/openbb_platform/core/tests/api/test_dependency/__init__.py b/openbb_platform/core/tests/api/test_dependency/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/api/test_dependency/__init__.py
+++ b/openbb_platform/core/tests/api/test_dependency/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/api/test_router/__init__.py b/openbb_platform/core/tests/api/test_router/__init__.py
index e69de29bb2d..21bb0e0cc06 100644
--- a/openbb_platform/core/tests/api/test_router/__init__.py
+++ b/openbb_platform/core/tests/api/test_router/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core API router tests."""
diff --git a/openbb_platform/core/tests/app/__init__.py b/openbb_platform/core/tests/app/__init__.py
index e69de29bb2d..3f4c8aabdda 100644
--- a/openbb_platform/core/tests/app/__init__.py
+++ b/openbb_platform/core/tests/app/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform core tests."""
diff --git a/openbb_platform/core/tests/app/logs/__init__.py b/openbb_platform/core/tests/app/logs/__init__.py
index e69de29bb2d..673bc2dec2f 100644
--- a/openbb_platform/core/tests/app/logs/__init__.py
+++ b/openbb_platform/core/tests/app/logs/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core."""
diff --git a/openbb_platform/core/tests/app/logs/formatters/__init__.py b/openbb_platform/core/tests/app/logs/formatters/__init__.py
index e69de29bb2d..3f4c8aabdda 100644
--- a/openbb_platform/core/tests/app/logs/formatters/__init__.py
+++ b/openbb_platform/core/tests/app/logs/formatters/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform core tests."""
diff --git a/openbb_platform/core/tests/app/logs/formatters/test_formatter_with_exceptions.py b/openbb_platform/core/tests/app/logs/formatters/test_formatter_with_exceptions.py
index 90a2cae1b37..625c0bc87cc 100644
--- a/openbb_platform/core/tests/app/logs/formatters/test_formatter_with_exceptions.py
+++ b/openbb_platform/core/tests/app/logs/formatters/test_formatter_with_exceptions.py
@@ -1,3 +1,5 @@
+"""Tests for the FormatterWithExceptions class."""
+
import logging
import os
from unittest.mock import Mock
@@ -7,6 +9,8 @@ from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
+# pylint: disable=W0621
+
logging_settings = Mock()
logging_settings.app_name = "test_app_name"
logging_settings.app_id = "test_app_id"
@@ -16,11 +20,13 @@ logging_settings.user_id = "test_user_id"
@pytest.fixture
def formatter():
+ """Return a FormatterWithExceptions instance."""
return FormatterWithExceptions(logging_settings)
# Test when exc_text is not empty (should return "X")
def test_level_name_with_exc_text(formatter):
+ """Test the calculate_level_name method."""
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
@@ -37,6 +43,7 @@ def test_level_name_with_exc_text(formatter):
# Test when levelname is not empty (should return the first character of levelname)
def test_level_name_with_levelname(formatter):
+ """Test the calculate_level_name method."""
record = logging.LogRecord(
name="test_logger",
level=logging.WARNING,
@@ -53,6 +60,7 @@ def test_level_name_with_levelname(formatter):
# Test when func_name_override and session_id are present in the record
def test_extract_log_extra_with_override_and_session_id(formatter):
+ """Test the extract_log_extra method."""
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
@@ -75,6 +83,7 @@ def test_extract_log_extra_with_override_and_session_id(formatter):
# Test when only func_name_override is present in the record
def test_extract_log_extra_with_override(formatter):
+ """Test the extract_log_extra method."""
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
@@ -96,6 +105,7 @@ def test_extract_log_extra_with_override(formatter):
# Test when only session_id is present in the record
def test_extract_log_extra_with_session_id(formatter):
+ """Test the extract_log_extra method."""
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
@@ -117,6 +127,7 @@ def test_extract_log_extra_with_session_id(formatter):
# Test when neither func_name_override nor session_id are present in the record
def test_extract_log_extra_with_no_override_or_session_id(formatter):
+ """Test the extract_log_extra method."""
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
@@ -144,6 +155,7 @@ def test_extract_log_extra_with_no_override_or_session_id(formatter):
],
)
def test_mock_ipv4(input_text, expected_output, formatter):
+ """Test the mock_ipv4 method."""
assert formatter.mock_ipv4(input_text) == expected_output
@@ -158,6 +170,7 @@ def test_mock_ipv4(input_text, expected_output, formatter):
],
)
def test_mock_email(input_text, expected_output, formatter):
+ """Test the mock_email method."""
assert formatter.mock_email(input_text) == expected_output
@@ -175,6 +188,7 @@ def test_mock_email(input_text, expected_output, formatter):
],
)
def test_mock_password(input_text, expected_output, formatter):
+ """Test the mock_password method."""
assert formatter.mock_password(input_text) == expected_output
@@ -190,6 +204,7 @@ def test_mock_password(input_text, expected_output, formatter):
],
)
def test_mock_flair(input_text, expected_output, formatter):
+ """Test the mock_flair method."""
assert formatter.mock_flair(input_text) == expected_output
@@ -212,6 +227,7 @@ def test_mock_flair(input_text, expected_output, formatter):
],
)
def test_mock_home_directory(input_text, expected_output, formatter):
+ """Test the mock_home_directory method."""
assert formatter.mock_home_directory(input_text) == expected_output
@@ -232,6 +248,7 @@ def test_mock_home_directory(input_text, expected_output, formatter):
],
)
def test_filter_special_tags(input_text, expected_output, formatter):
+ """Test the filter_special_tags method."""
assert formatter.filter_special_tags(input_text) == expected_output
@@ -249,6 +266,7 @@ def test_filter_special_tags(input_text, expected_output, formatter):
],
)
def test_filter_piis(input_text, expected_output, formatter):
+ """Test the filter_piis method."""
assert formatter.filter_piis(input_text) == expected_output
@@ -267,15 +285,18 @@ def test_filter_piis(input_text, expected_output, formatter):
],
)
def test_filter_log_line(input_text, expected_output, formatter):
+ """Test the filter_log_line method."""
assert formatter.filter_log_line(input_text) == expected_output
def test_formatException_invalid():
+ """Test the formatException method with an invalid exception."""
with pytest.raises(Exception):
formatter.formatException(Exception("Big bad error")) # type: ignore[attr-defined]
def test_format(formatter):
+ """Test the format method."""
# Prepare the log record
log_record = logging.LogRecord(
name="test_logger",
diff --git a/openbb_platform/core/tests/app/logs/handlers/__init__.py b/openbb_platform/core/tests/app/logs/handlers/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/logs/handlers/__init__.py
+++ b/openbb_platform/core/tests/app/logs/handlers/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/logs/handlers/test_path_tracking_file_handler.py b/openbb_platform/core/tests/app/logs/handlers/test_path_tracking_file_handler.py
index 40dae4a702f..ba81d15e1ab 100644
--- a/openbb_platform/core/tests/app/logs/handlers/test_path_tracking_file_handler.py
+++ b/openbb_platform/core/tests/app/logs/handlers/test_path_tracking_file_handler.py
@@ -12,7 +12,10 @@ from openbb_core.app.logs.handlers.path_tracking_file_handler import (
class MockLoggingSettings:
+ """Mock logging settings."""
+
def __init__(self, app_name, user_logs_directory, session_id, frequency):
+ """Initialize the mock logging settings."""
self.app_name = app_name
self.user_logs_directory = Path(user_logs_directory)
self.session_id = session_id
@@ -31,17 +34,20 @@ logging_settings.frequency = "H"
@pytest.fixture(scope="module")
def mocked_path(tmp_path_factory):
+ """Fixture to create a mocked file path."""
return tmp_path_factory.mktemp("mocked_path") / "mocked_file.log"
@pytest.fixture(scope="module")
def handler(mocked_path):
+ """Fixture to create a PathTrackingFileHandler instance."""
# patch `pathlib.Path.joinpath` to return a string containing the joined path
with patch.object(Path, "joinpath", return_value=mocked_path):
return PathTrackingFileHandler(logging_settings)
def test_build_log_file_path(handler, mocked_path):
+ """Test build_log_file_path method."""
# Define a sample LoggingSettings object with mock attributes
settings = MagicMock(spec=MockLoggingSettings)
settings.app_name = "my_app"
@@ -60,6 +66,7 @@ def test_build_log_file_path(handler, mocked_path):
def test_clean_expired_files(handler):
+ """Test clean_expired_files method."""
with patch(
"openbb_core.app.logs.handlers.path_tracking_file_handler.get_expired_file_list"
) as mock_get_expired_file_list, patch(
diff --git a/openbb_platform/core/tests/app/logs/handlers/test_posthog_handler.py b/openbb_platform/core/tests/app/logs/handlers/test_posthog_handler.py
index 5038f3ec016..5e795a4bf5f 100644
--- a/openbb_platform/core/tests/app/logs/handlers/test_posthog_handler.py
+++ b/openbb_platform/core/tests/app/logs/handlers/test_posthog_handler.py
@@ -1,3 +1,5 @@
+"""Tests for the PosthogHandler class."""
+
import logging
from pathlib import Path
from unittest.mock import MagicMock
@@ -8,7 +10,10 @@ from openbb_core.app.logs.handlers.posthog_handler import (
)
+# pylint: disable=W0621, R0913
class MockLoggingSettings:
+ """Mock logging settings."""
+
def __init__(
self,
app_name,
@@ -22,6 +27,7 @@ class MockLoggingSettings:
platform_version,
userid,
):
+ """Initialize the mock logging settings."""
self.app_name = app_name
self.sub_app_name = sub_app_name
self.user_logs_directory = Path(user_logs_directory)
@@ -52,10 +58,12 @@ logging_settings.user_id = "user123"
@pytest.fixture
def handler():
+ """Fixture to create a PosthogHandler instance."""
return PosthogHandler(logging_settings)
def test_emit_calls_send(handler):
+ """Test the emit method."""
# Arrange
record = logging.LogRecord(
name="test_logger",
@@ -78,6 +86,7 @@ def test_emit_calls_send(handler):
def test_emit_calls_handleError_when_send_raises_exception(handler):
+ """Test the emit method."""
# Arrange
record = logging.LogRecord(
name="test_logger",
@@ -104,6 +113,7 @@ def test_emit_calls_handleError_when_send_raises_exception(handler):
def test_emit_calls_handleError_when_send_raises_exception_of_specific_type(handler):
+ """Test the emit method."""
# Arrange
record = logging.LogRecord(
name="test_logger",
@@ -130,6 +140,7 @@ def test_emit_calls_handleError_when_send_raises_exception_of_specific_type(hand
def test_emit_calls_handleError_when_send_raises_exception_of_another_type(handler):
+ """Test the emit method."""
# Arrange
record = logging.LogRecord(
name="test_logger",
@@ -176,6 +187,7 @@ def test_emit_calls_handleError_when_send_raises_exception_of_another_type(handl
],
)
def test_log_to_dict(handler, log_info, expected_dict):
+ """Test the log_to_dict method."""
# Act
result = handler.log_to_dict(log_info)
@@ -204,6 +216,7 @@ def test_log_to_dict(handler, log_info, expected_dict):
],
)
def test_extract_log_extra(handler, record, expected_extra):
+ """Test the extract_log_extra method."""
# Act
result = handler.extract_log_extra(record)
diff --git a/openbb_platform/core/tests/app/logs/test_handlers_manager.py b/openbb_platform/core/tests/app/logs/test_handlers_manager.py
index 43c23214e88..d46451c6314 100644
--- a/openbb_platform/core/tests/app/logs/test_handlers_manager.py
+++ b/openbb_platform/core/tests/app/logs/test_handlers_manager.py
@@ -1,3 +1,5 @@
+"""Tests for the handlers manager."""
+
import logging
from unittest.mock import Mock, patch
@@ -7,25 +9,37 @@ from openbb_core.app.logs.handlers_manager import (
PosthogHandler,
)
+# pylint: disable=W0231
+
class MockPosthogHandler(logging.NullHandler):
+ """Mock posthog handler."""
+
def __init__(self, settings):
+ """Initialize the handler."""
self.settings = settings
self.level = logging.DEBUG
class MockPathTrackingFileHandler(logging.NullHandler):
+ """Mock path tracking file handler."""
+
def __init__(self, settings):
+ """Initialize the handler."""
self.settings = settings
self.level = logging.DEBUG
class MockFormatterWithExceptions(logging.Formatter):
+ """Mock formatter with exceptions."""
+
def __init__(self, settings):
+ """Initialize the formatter."""
self.settings = settings
def test_handlers_added_correctly():
+ """Test if the handlers are added correctly."""
with patch(
"openbb_core.app.logs.handlers_manager.PosthogHandler",
MockPosthogHandler,
@@ -60,6 +74,7 @@ def test_handlers_added_correctly():
def test_update_handlers():
+ """Test if the handlers are updated correctly."""
with patch(
"openbb_core.app.logs.handlers_manager.PosthogHandler",
MockPosthogHandler,
diff --git a/openbb_platform/core/tests/app/logs/test_logging_service.py b/openbb_platform/core/tests/app/logs/test_logging_service.py
index 316b603588c..8eef4b90137 100644
--- a/openbb_platform/core/tests/app/logs/test_logging_service.py
+++ b/openbb_platform/core/tests/app/logs/test_logging_service.py
@@ -1,3 +1,5 @@
+"""Test LoggingService class."""
+
import json
from typing import Optional
from unittest.mock import MagicMock, Mock, patch
@@ -12,18 +14,24 @@ from pydantic import BaseModel
class MockLoggingSettings:
+ """Mock logging settings."""
+
def __init__(self, system_settings, user_settings):
+ """Initialize the mock logging settings."""
self.system_settings = system_settings
self.user_settings = user_settings
class MockOBBject(BaseModel):
+ """Mock object for testing."""
+
output: Optional[str] = None
error: Optional[str] = None
@pytest.fixture(scope="function")
def logging_service():
+ """Return a LoggingService instance."""
mock_system_settings = Mock()
mock_user_settings = Mock()
mock_setup_handlers = Mock()
@@ -48,6 +56,7 @@ def logging_service():
def test_correctly_initialized():
+ """Test the LoggingService is correctly initialized."""
mock_system_settings = Mock()
mock_user_settings = Mock()
mock_setup_handlers = Mock()
@@ -73,6 +82,7 @@ def test_correctly_initialized():
def test_logging_settings_setter(logging_service):
+ """Test the logging_settings setter."""
custom_user_settings = "custom_user_settings"
custom_system_settings = "custom_system_settings"
@@ -90,6 +100,7 @@ def test_logging_settings_setter(logging_service):
def test_log_startup(logging_service):
+ """Test the log_startup method."""
with patch("logging.getLogger") as mock_get_logger:
mock_info = mock_get_logger.return_value.info
@@ -166,6 +177,7 @@ def test_log(
exec_info,
custom_headers,
):
+ """Test the log method."""
with patch(
"openbb_core.app.logs.logging_service.LoggingSettings",
MockLoggingSettings,
diff --git a/openbb_platform/core/tests/app/logs/utils/__init__.py b/openbb_platform/core/tests/app/logs/utils/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/logs/utils/__init__.py
+++ b/openbb_platform/core/tests/app/logs/utils/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/logs/utils/test_expired_files.py b/openbb_platform/core/tests/app/logs/utils/test_expired_files.py
index 7e2721b1058..1cb7897eb25 100644
--- a/openbb_platform/core/tests/app/logs/utils/test_expired_files.py
+++ b/openbb_platform/core/tests/app/logs/utils/test_expired_files.py
@@ -1,3 +1,5 @@
+"""Tests for the expired_files module."""
+
import os
import tempfile
from pathlib import Path
@@ -12,9 +14,12 @@ from openbb_core.app.logs.utils.expired_files import (
remove_file_list,
)
+# pylint: disable=W0621
+
@pytest.fixture
def temp_test_files():
+ """Create temporary files for testing."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_files = [
Path(temp_dir) / "file1.txt",
@@ -30,12 +35,14 @@ def temp_test_files():
def test_get_timestamp_from_x_days():
+ """Test get_timestamp_from_x_days."""
result = get_timestamp_from_x_days(0)
assert isinstance(result, float)
# Test case when all files are expired
def test_all_files_expired(temp_test_files):
+ """Test get_expired_file_list when all files are expired."""
temp_dir, temp_files = temp_test_files
before_timestamp = time() + 3 * 86400 # timestamp 3 days from now
expired_files = get_expired_file_list(Path(temp_dir), before_timestamp)
@@ -44,14 +51,16 @@ def test_all_files_expired(temp_test_files):
# Test case when no files are expired
def test_no_files_expired(temp_test_files):
+ """Test get_expired_file_list when no files are expired."""
temp_dir, _ = temp_test_files
before_timestamp = 0
expired_files = get_expired_file_list(Path(temp_dir), before_timestamp)
- assert expired_files == []
+ assert not expired_files
# Test case when some files are expired and some are not
def test_some_files_expired(temp_test_files):
+ """Test get_expired_file_list when some files are expired and some are not."""
temp_dir, _ = temp_test_files
# add temp file to temp_dir with timestamp in the future
@@ -70,27 +79,31 @@ def test_some_files_expired(temp_test_files):
# Test case when the directory does not exist
def test_directory_not_exists():
+ """Test get_expired_file_list when the directory does not exist."""
directory = Path("/path/that/does/not/exist")
before_timestamp = time()
expired_files = get_expired_file_list(directory, before_timestamp)
- assert expired_files == []
+ assert not expired_files
# Test case when the directory is not a directory
def test_directory_not_dir(temp_test_files):
+ """Test get_expired_file_list when the directory is not a directory."""
_, temp_files = temp_test_files
before_timestamp = time()
expired_files = get_expired_file_list(temp_files[0], before_timestamp)
- assert expired_files == []
+ assert not expired_files
@pytest.fixture
def mock_path():
+ """Return a MagicMock for the Path class."""
# Create a MagicMock for the Path class to represent a file path
return MagicMock()
def test_remove_file_list_no_files(mock_path):
+ """Test remove_file_list when there are no files to remove."""
# Arrange
# Let's assume the file list is empty, meaning there are no files to remove
file_list: List[Path] = []
@@ -104,6 +117,7 @@ def test_remove_file_list_no_files(mock_path):
def test_remove_file_list_remove_files_successfully(mock_path):
+ """Test remove_file_list when unlink is successful."""
# Arrange
# Let's assume we have three files in the file list
file_list = [mock_path, mock_path, mock_path]
@@ -120,6 +134,7 @@ def test_remove_file_list_remove_files_successfully(mock_path):
def test_remove_file_list_ignore_permission_error(mock_path):
+ """Test remove_file_list when unlink raises a PermissionError."""
# Arrange
# Let's assume we have three files in the file list
file_list = [mock_path, mock_path, mock_path]
@@ -136,6 +151,7 @@ def test_remove_file_list_ignore_permission_error(mock_path):
def test_remove_file_list_other_exception(mock_path):
+ """Test remove_file_list when unlink raises an exception other than PermissionError."""
# Arrange
# Let's assume we have three files in the file list
file_list = [mock_path, mock_path, mock_path]
diff --git a/openbb_platform/core/tests/app/logs/utils/test_utils.py b/openbb_platform/core/tests/app/logs/utils/test_utils.py
index f3715abc434..9cda5662e2f 100644
--- a/openbb_platform/core/tests/app/logs/utils/test_utils.py
+++ b/openbb_platform/core/tests/app/logs/utils/test_utils.py
@@ -1,3 +1,5 @@
+"""OpenBB Platform Core tests."""
+
import uuid
from pathlib import Path
from unittest.mock import patch
@@ -9,12 +11,14 @@ from openbb_core.app.logs.utils.utils import get_app_id, get_log_dir, get_sessio
def test_get_session_id_return_type():
+ """Test if the returned value is a string."""
# Test if the returned value is a string
session_id = get_session_id()
assert isinstance(session_id, str)
def test_get_session_id_format():
+ """Test if the returned string has the format "UUID-current_time."""
# Test if the returned string has the format "UUID-current_time"
session_id = get_session_id()
@@ -33,6 +37,7 @@ def test_get_session_id_format():
def test_get_session_id_uniqueness():
+ """Test if subsequent calls return different session IDs."""
# Test if subsequent calls return different session IDs
session_id1 = get_session_id()
session_id2 = get_session_id()
@@ -43,9 +48,10 @@ def test_get_session_id_uniqueness():
def test_get_app_id_success():
+ """Test get_app_id function."""
# Mock the return value of get_log_dir to simulate a successful scenario
with patch("openbb_core.app.logs.utils.utils.get_log_dir") as mock_get_log_dir:
- mock_get_log_dir
+ mock_get_log_dir # pylint: disable=pointless-statement
mock_get_log_dir.return_value = Path(
"/path/to/contextual_user_data_directory/app_id.log"
)
@@ -54,6 +60,7 @@ def test_get_app_id_success():
def test_get_app_id_os_error():
+ """Test handling of OSError with errno 30."""
# Test handling of OSError with errno 30 (Read-only file system)
with patch("openbb_core.app.logs.utils.utils.get_log_dir") as mock_get_log_dir:
mock_get_log_dir.side_effect = OSError(30, "Read-only file system")
@@ -62,6 +69,7 @@ def test_get_app_id_os_error():
def test_get_app_id_other_exception():
+ """Test handling of other exceptions."""
# Test handling of other exceptions
with patch("openbb_core.app.logs.utils.utils.get_log_dir") as mock_get_log_dir:
mock_get_log_dir.side_effect = Exception("Some other error")
@@ -73,6 +81,7 @@ def test_get_app_id_other_exception():
def test_get_log_dir():
+ """Test get_log_dir function."""
with patch(
"openbb_core.app.logs.utils.utils.create_log_dir_if_not_exists",
return_value="/test_dir",
diff --git a/openbb_platform/core/tests/app/model/__init__.py b/openbb_platform/core/tests/app/model/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/model/__init__.py
+++ b/openbb_platform/core/tests/app/model/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/model/abstract/__init__.py b/openbb_platform/core/tests/app/model/abstract/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/model/abstract/__init__.py
+++ b/openbb_platform/core/tests/app/model/abstract/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/model/abstract/test_results.py b/openbb_platform/core/tests/app/model/abstract/test_results.py
index c3dd548ea3a..ccdd5b8a036 100644
--- a/openbb_platform/core/tests/app/model/abstract/test_results.py
+++ b/openbb_platform/core/tests/app/model/abstract/test_results.py
@@ -1,12 +1,15 @@
+"""Tests for the Results model."""
+
from openbb_core.app.model.abstract.results import Results
from pydantic import BaseModel
class MockResults(Results):
- pass
+ """Mock Results class."""
def test_results_model():
+ """Test the Results model."""
res = MockResults()
assert isinstance(res, BaseModel)
diff --git a/openbb_platform/core/tests/app/model/abstract/test_singleton.py b/openbb_platform/core/tests/app/model/abstract/test_singleton.py
index d87502e2f67..e586fb00e9d 100644
--- a/openbb_platform/core/tests/app/model/abstract/test_singleton.py
+++ b/openbb_platform/core/tests/app/model/abstract/test_singleton.py
@@ -1,12 +1,18 @@
+"""Tests for the SingletonMeta metaclass."""
+
from openbb_core.app.model.abstract.singleton import SingletonMeta
class MyClass(metaclass=SingletonMeta):
+ """A simple class."""
+
def __init__(self, value):
+ """Initialize the class."""
self.value = value
def test_singleton_instance_creation():
+ """Test the SingletonMeta metaclass with instance creation."""
# Arrange
instance1 = MyClass(42)
instance2 = MyClass(100)
@@ -18,6 +24,8 @@ def test_singleton_instance_creation():
def test_singleton_multiple_classes():
+ """Test the SingletonMeta metaclass with multiple classes."""
+
# Arrange
class AnotherClass(metaclass=SingletonMeta):
def __init__(self, data):
diff --git a/openbb_platform/core/tests/app/model/abstract/test_tagged.py b/openbb_platform/core/tests/app/model/abstract/test_tagged.py
index 33df298ad58..7c40d15b675 100644
--- a/openbb_platform/core/tests/app/model/abstract/test_tagged.py
+++ b/openbb_platform/core/tests/app/model/abstract/test_tagged.py
@@ -1,13 +1,17 @@
+"""Test the Tagged model."""
+
from openbb_core.app.model.abstract.tagged import Tagged
def test_tagged_model():
+ """Test the Tagged model."""
tagged = Tagged()
assert hasattr(tagged, "id")
def test_fields():
+ """Test the Tagged fields."""
fields = Tagged.model_fields
fields_keys = fields.keys()
diff --git a/openbb_platform/core/tests/app/model/abstract/test_warning.py b/openbb_platform/core/tests/app/model/abstract/test_warning.py
index 46692077cdd..3a506e6d658 100644
--- a/openbb_platform/core/tests/app/model/abstract/test_warning.py
+++ b/openbb_platform/core/tests/app/model/abstract/test_warning.py
@@ -1,3 +1,5 @@
+"""Test the Warnings model."""
+
from unittest.mock import Mock
import pytest
@@ -12,6 +14,7 @@ from openbb_core.app.model.abstract.warning import Warning_, cast_warning
],
)
def test_warn_model(category, message):
+ """Test the Warning_ model."""
war = Warning_(category=category, message=message)
assert war.category == category
@@ -19,6 +22,7 @@ def test_warn_model(category, message):
def test_fields():
+ """Test the Warning_ fields."""
fields = Warning_.model_fields
fields_keys = fields.keys()
@@ -27,6 +31,7 @@ def test_fields():
def test_cast_warning():
+ """Test the cast_warning function."""
mock_warning_message = Mock()
mock_warning_message.category.__name__ = "test"
mock_warning_message.message = "test"
diff --git a/openbb_platform/core/tests/app/model/charts/__init__.py b/openbb_platform/core/tests/app/model/charts/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/model/charts/__init__.py
+++ b/openbb_platform/core/tests/app/model/charts/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/model/charts/test_chart.py b/openbb_platform/core/tests/app/model/charts/test_chart.py
index 3d3ff9960bd..4f0eb140f42 100644
--- a/openbb_platform/core/tests/app/model/charts/test_chart.py
+++ b/openbb_platform/core/tests/app/model/charts/test_chart.py
@@ -1,8 +1,11 @@
+"""Test the chart model."""
+
import pytest
from openbb_core.app.model.charts.chart import Chart, ChartFormat
def test_charting_default_values():
+ """Test the charting default values."""
# Arrange & Act
chart = Chart()
@@ -12,6 +15,7 @@ def test_charting_default_values():
def test_charting_custom_values():
+ """Test the charting custom values."""
# Arrange
content = {"data": [1, 2, 3]}
chart_format = ChartFormat.plotly
@@ -25,6 +29,7 @@ def test_charting_custom_values():
def test_charting_assignment_validation():
+ """Test the charting assignment validation."""
# Arrange
chart = Chart()
@@ -34,6 +39,7 @@ def test_charting_assignment_validation():
def test_charting_config_validation():
+ """Test the charting config validation."""
# Arrange
content = {"data": [1, 2, 3]}
chart_format = ChartFormat.plotly
@@ -48,5 +54,5 @@ def test_charting_config_validation():
def test_show():
+ """Test the show method."""
# TODO : add test after the function is properly refactored
- pass
diff --git a/openbb_platform/core/tests/app/model/hub/__init__.py b/openbb_platform/core/tests/app/model/hub/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/model/hub/__init__.py
+++ b/openbb_platform/core/tests/app/model/hub/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/model/hub/test_features_keys.py b/openbb_platform/core/tests/app/model/hub/test_features_keys.py
deleted file mode 100644
index 4f3a85246e0..00000000000
--- a/openbb_platform/core/tests/app/model/hub/test_features_keys.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from openbb_core.app.model.hub.features_keys import FeaturesKeys
-
-# ruff: noqa: S105 S106
-
-
-def test_features_keys():
- fk = FeaturesKeys(API_BITQUERY_KEY="test", API_BIZTOC_TOKEN="test2")
-
- assert fk.API_BITQUERY_KEY == "test"
- assert fk.API_BIZTOC_TOKEN == "test2"
diff --git a/openbb_platform/core/tests/app/model/hub/test_hub_session.py b/openbb_platform/core/tests/app/model/hub/test_hub_session.py
index 645a097ac1f..19638f22cb9 100644
--- a/openbb_platform/core/tests/app/model/hub/test_hub_session.py
+++ b/openbb_platform/core/tests/app/model/hub/test_hub_session.py
@@ -1,3 +1,5 @@
+"""Test the HubSession class."""
+
from openbb_core.app.model.hub.hub_session import HubSession
from pydantic import SecretStr
@@ -5,6 +7,7 @@ from pydantic import SecretStr
def test_hub_session():
+ """Test the HubSession class."""
session = HubSession(
access_token=SecretStr("mock_access_token"),
token_type="mock_token_type",
@@ -22,6 +25,7 @@ def test_hub_session():
def test_fields():
+ """Test the HubSession fields."""
fields = HubSession.model_fields
fields_keys = fields.keys()
diff --git a/openbb_platform/core/tests/app/model/hub/test_hub_user_settings.py b/openbb_platform/core/tests/app/model/hub/test_hub_user_settings.py
index ca2f78ff9a8..5fdcde411fa 100644
--- a/openbb_platform/core/tests/app/model/hub/test_hub_user_settings.py
+++ b/openbb_platform/core/tests/app/model/hub/test_hub_user_settings.py
@@ -1,7 +1,10 @@
+"""Test the HubUserSettings class."""
+
from openbb_core.app.model.hub.hub_user_settings import HubUserSettings
def test_hub_user_settings():
+ """Test the HubUserSettings class."""
hub_settings = HubUserSettings()
assert isinstance(hub_settings, HubUserSettings)
diff --git a/openbb_platform/core/tests/app/model/test_command_context.py b/openbb_platform/core/tests/app/model/test_command_context.py
index 76423c8b9c5..57d982f58c2 100644
--- a/openbb_platform/core/tests/app/model/test_command_context.py
+++ b/openbb_platform/core/tests/app/model/test_command_context.py
@@ -1,3 +1,5 @@
+"""Test the CommandContext model."""
+
from openbb_core.app.model.command_context import (
CommandContext,
SystemSettings,
@@ -6,6 +8,7 @@ from openbb_core.app.model.command_context import (
def test_command_context():
+ """Test the CommandContext model."""
cc = CommandContext()
assert isinstance(cc, CommandContext)
assert isinstance(cc.user_settings, UserSettings)
@@ -13,6 +16,7 @@ def test_command_context():
def test_fields():
+ """Test the CommandContext fields."""
fields = CommandContext.model_fields
fields_keys = fields.keys()
diff --git a/openbb_platform/core/tests/app/model/test_credentials.py b/openbb_platform/core/tests/app/model/test_credentials.py
index 041ca5f16e6..52f0d25126c 100644
--- a/openbb_platform/core/tests/app/model/test_credentials.py
+++ b/openbb_platform/core/tests/app/model/test_credentials.py
@@ -1,8 +1,11 @@
+"""Test the Credentials model."""
+
import typing
from unittest.mock import patch
def test_credentials():
+ """Test the Credentials model."""
with patch(
target="openbb_core.app.model.credentials.ProviderInterface"
) as mock_provider_interface:
@@ -10,7 +13,9 @@ def test_credentials():
"benzinga_api_key": (typing.Optional[str], None),
"polygon_api_key": (typing.Optional[str], None),
}
- from openbb_core.app.model.credentials import Credentials
+ from openbb_core.app.model.credentials import ( # pylint: disable=import-outside-toplevel
+ Credentials,
+ )
creds = Credentials(
benzinga_api_key="mock_benzinga_api_key",
diff --git a/openbb_platform/core/tests/app/model/test_defaults.py b/openbb_platform/core/tests/app/model/test_defaults.py
index af87fa52e24..e4a3df6dd21 100644
--- a/openbb_platform/core/tests/app/model/test_defaults.py
+++ b/openbb_platform/core/tests/app/model/test_defaults.py
@@ -1,13 +1,17 @@
+"""Test the Defaults class."""
+
from openbb_core.app.model.defaults import Defaults
def test_defaults():
+ """Test the Defaults class."""
cc = Defaults(routes={"test": {"test": "test"}})
assert isinstance(cc, Defaults)
assert cc.routes == {"test": {"test": "test"}}
def test_fields():
+ """Test the Defaults fields."""
fields = Defaults.model_fields
fields_keys = fields.keys()
diff --git a/openbb_platform/core/tests/app/model/test_metadata.py b/openbb_platform/core/tests/app/model/test_metadata.py
index 60c4f95c583..37bf7377984 100644
--- a/openbb_platform/core/tests/app/model/test_metadata.py
+++ b/openbb_platform/core/tests/app/model/test_metadata.py
@@ -1,3 +1,5 @@
+"""Test the Metadata model."""
+
from datetime import datetime
import numpy as np
@@ -8,9 +10,13 @@ from openbb_core.provider.abstract.data import Data
def test_Metadata():
- "Smoke test"
+ """Run Smoke test."""
m = Metadata(
- arguments={"test": "test"},
+ arguments={
+ "provider_choices": {},
+ "standard_params": {},
+ "extra_params": {},
+ },
route="test",
timestamp=datetime.now(),
duration=0,
@@ -20,7 +26,7 @@ def test_Metadata():
def test_fields():
- "Smoke test"
+ """Run Smoke test."""
fields = Metadata.model_fields.keys()
assert "arguments" in fields
assert "duration" in fields
@@ -42,7 +48,7 @@ def test_fields():
{
"data_list": {
"type": "List[Data]",
- "columns": ["open", "close", "volume"],
+ "columns": ["open", "volume", "close"],
}
},
),
@@ -98,20 +104,41 @@ def test_fields():
],
)
def test_scale_arguments(input_data, expected_output):
+ """Test the scale_arguments method."""
+ kwargs = {
+ "provider_choices": {},
+ "standard_params": {},
+ "extra_params": input_data,
+ }
m = Metadata(
- arguments=input_data,
+ arguments=kwargs,
route="test",
timestamp=datetime.now(),
duration=0,
)
arguments = m.arguments
- for arg in arguments:
+ for arg in arguments: # pylint: disable=E1133
if "columns" in arguments[arg]:
# compare the column names disregarding the order with the expected output
- assert sorted(arguments[arg]["columns"]) == sorted(
+ assert sorted(arguments["extra_params"][arg]["columns"]) == sorted(
expected_output[arg]["columns"]
)
assert arguments[arg]["type"] == expected_output[arg]["type"]
else:
- assert m.arguments == expected_output
+ # assert m.arguments["extra_params"] == expected_output
+ keys = list(arguments["extra_params"].keys())
+ expected_keys = list(expected_output.keys())
+ assert sorted(keys) == sorted(expected_keys)
+
+ for key in keys:
+ if "type" in arguments["extra_params"][key]:
+ assert (
+ arguments["extra_params"][key]["type"]
+ == expected_output[key]["type"]
+ )
+ assert sorted(arguments["extra_params"][key]["columns"]) == sorted(
+ expected_output[key]["columns"]
+ )
+ else:
+ assert arguments["extra_params"][key] == expected_output[key]
diff --git a/openbb_platform/core/tests/app/model/test_preferences.py b/openbb_platform/core/tests/app/model/test_preferences.py
index ca3bae14af0..dacfc708ed9 100644
--- a/openbb_platform/core/tests/app/model/test_preferences.py
+++ b/openbb_platform/core/tests/app/model/test_preferences.py
@@ -1,6 +1,9 @@
+"""Test the preferences class."""
+
from openbb_core.app.model.preferences import Preferences
def test_preferences():
+ """Test the preferences class."""
preferences = Preferences()
assert isinstance(preferences, Preferences)
diff --git a/openbb_platform/core/tests/app/model/test_profile.py b/openbb_platform/core/tests/app/model/test_profile.py
index 5c22d7ee73d..7f8a4795327 100644
--- a/openbb_platform/core/tests/app/model/test_profile.py
+++ b/openbb_platform/core/tests/app/model/test_profile.py
@@ -1,6 +1,9 @@
+"""Test the profile class."""
+
from openbb_core.app.model.profile import Profile
def test_preferences():
+ """Test the preferences class."""
preferences = Profile()
assert isinstance(preferences, Profile)
diff --git a/openbb_platform/core/tests/app/model/test_system_settings.py b/openbb_platform/core/tests/app/model/test_system_settings.py
index 90a1c63778f..1ead135bfe4 100644
--- a/openbb_platform/core/tests/app/model/test_system_settings.py
+++ b/openbb_platform/core/tests/app/model/test_system_settings.py
@@ -1,3 +1,5 @@
+"""Tests for the SystemSettings class."""
+
import os
from pathlib import Path
@@ -13,11 +15,13 @@ class MockSystemSettings(BaseModel):
def test_system_settings():
+ """Test the SystemSettings class."""
sys = SystemSettings()
assert isinstance(sys, SystemSettings)
def test_create_openbb_directory_directory_and_files_not_exist(tmpdir):
+ """Test the create_openbb_directory method."""
# Arrange
values = MockSystemSettings(
**{
@@ -37,6 +41,7 @@ def test_create_openbb_directory_directory_and_files_not_exist(tmpdir):
def test_create_openbb_directory_directory_exists_user_settings_missing(tmpdir):
+ """Test the create_openbb_directory method."""
# Arrange
values = MockSystemSettings(
**{
@@ -59,6 +64,7 @@ def test_create_openbb_directory_directory_exists_user_settings_missing(tmpdir):
def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir):
+ """Test the create_openbb_directory method."""
# Arrange
values = MockSystemSettings(
**{
@@ -136,6 +142,7 @@ def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir
],
)
def test_validate_posthog_handler(values, expected_handlers):
+ """Test the validate_posthog_handler method."""
values = MockSystemSettings(**values)
# Act
result = SystemSettings.validate_posthog_handler(values) # type: ignore[operator]
@@ -158,6 +165,7 @@ def test_validate_posthog_handler(values, expected_handlers):
],
)
def test_validate_logging_handlers(handlers, valid):
+ """Test the validate_logging_handlers method."""
# Act and Assert
if valid:
assert SystemSettings.validate_logging_handlers(handlers) == handlers # type: ignore[call-arg]
diff --git a/openbb_platform/core/tests/app/model/test_user_settings.py b/openbb_platform/core/tests/app/model/test_user_settings.py
index b24fb23cae1..ebc78faf4d9 100644
--- a/openbb_platform/core/tests/app/model/test_user_settings.py
+++ b/openbb_platform/core/tests/app/model/test_user_settings.py
@@ -1,3 +1,5 @@
+"""Test the UserSettings model."""
+
from openbb_core.app.model.credentials import Credentials
from openbb_core.app.model.defaults import Defaults
from openbb_core.app.model.preferences import Preferences
@@ -6,6 +8,7 @@ from openbb_core.app.model.user_settings import UserSettings
def test_user_settings():
+ """Test the UserSettings model."""
settings = UserSettings(
credentials=Credentials(),
profile=Profile(),
diff --git a/openbb_platform/core/tests/app/results/__init__.py b/openbb_platform/core/tests/app/results/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/results/__init__.py
+++ b/openbb_platform/core/tests/app/results/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/results/test_empty.py b/openbb_platform/core/tests/app/results/test_empty.py
index 23a73ff369e..5ce63f4a5c5 100644
--- a/openbb_platform/core/tests/app/results/test_empty.py
+++ b/openbb_platform/core/tests/app/results/test_empty.py
@@ -1,8 +1,11 @@
+"""Test the Empty model."""
+
from openbb_core.app.model.results.empty import Empty
from pydantic import BaseModel
def test_empty_model():
+ """Test the Empty model."""
empty = Empty()
assert isinstance(empty, Empty)
diff --git a/openbb_platform/core/tests/app/service/__init__.py b/openbb_platform/core/tests/app/service/__init__.py
index e69de29bb2d..55cb40ff9e8 100644
--- a/openbb_platform/core/tests/app/service/__init__.py
+++ b/openbb_platform/core/tests/app/service/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Core tests."""
diff --git a/openbb_platform/core/tests/app/service/test_hub_service.py b/openbb_platform/core/tests/app/service/test_hub_service.py
index 4b6c702c12f..db2030d561e 100644
--- a/openbb_platform/core/tests/app/service/test_hub_service.py
+++ b/openbb_platform/core/tests/app/service/test_hub_service.py
@@ -4,10 +4,10 @@
# ruff: noqa: S105 S106
+from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
-from openbb_core.app.model.hub.features_keys import FeaturesKeys
from openbb_core.app.service.hub_service import (
Credentials,
HubService,
@@ -25,6 +25,66 @@ def mocker():
yield mock
+def test_v3tov4_map():
+ """Test v3 to v4 map."""
+
+ v3_keys = {
+ "databento": "API_DATABENTO_KEY",
+ "alpha_vantage": "API_KEY_ALPHAVANTAGE",
+ "fmp": "API_KEY_FINANCIALMODELINGPREP",
+ "nasdaq": "API_KEY_QUANDL",
+ "polygon": "API_POLYGON_KEY",
+ "fred": "API_FRED_KEY",
+ "news_api": "API_NEWS_TOKEN",
+ "biztoc": "API_BIZTOC_TOKEN",
+ "cmc": "API_CMC_KEY",
+ "finnhub": "API_FINNHUB_KEY",
+ "whale_alert": "API_WHALE_ALERT_KEY",
+ "glassnode": "API_GLASSNODE_KEY",
+ "coinglass": "API_COINGLASS_KEY",
+ "ethplorer": "API_ETHPLORER_KEY",
+ "cryptopanic": "API_CRYPTO_PANIC_KEY",
+ "crypto_panic": "API_CRYPTO_PANIC_KEY", # If dev choses to use this name
+ "bitquery": "API_BITQUERY_KEY",
+ "smartstake": ["API_SMARTSTAKE_KEY", "API_SMARTSTAKE_TOKEN"],
+ "messari": "API_MESSARI_KEY",
+ "shroom": "API_SHROOM_KEY",
+ "santiment": "API_SANTIMENT_KEY",
+ "eodhd": "API_EODHD_KEY",
+ "tokenterminal": "API_TOKEN_TERMINAL_KEY",
+ "token_terminal": "API_TOKEN_TERMINAL_KEY", # If dev choses to use this name
+ "intrinio": "API_INTRINIO_KEY",
+ "github": "API_GITHUB_KEY",
+ "reddit": [
+ "API_REDDIT_CLIENT_ID",
+ "API_REDDIT_CLIENT_SECRET",
+ "API_REDDIT_USERNAME",
+ "API_REDDIT_USER_AGENT",
+ "API_REDDIT_PASSWORD",
+ ],
+ "companies_house": "API_COMPANIESHOUSE_KEY",
+ "companieshouse": "API_COMPANIESHOUSE_KEY", # If dev choses to use this name
+ "dappradar": "API_DAPPRADAR_KEY",
+ "nixtla": "API_KEY_NIXTLA",
+ }
+
+ providers = sorted(
+ [
+ p.stem
+ for p in Path("openbb_platform", "providers").glob("*")
+ if p.is_dir() and p.name not in ("__pycache__", "tests")
+ ]
+ )
+
+ for provider in providers:
+ if provider in v3_keys:
+ keys = v3_keys[provider]
+ if not isinstance(keys, list):
+ keys = [keys]
+ for k in keys:
+ assert k in HubService.V3TOV4
+
+
def test_connect_with_email_password():
"""Test connect with email and password."""
mock_hub_session = MagicMock(spec=HubSession)
@@ -173,31 +233,78 @@ def test_put_user_settings():
)
-def test_hub2platform():
+def test_hub2platform_v4_only():
"""Test hub2platform."""
mock_user_settings = MagicMock(spec=HubUserSettings)
- mock_user_settings.features_keys = FeaturesKeys(
- API_KEY_FINANCIALMODELINGPREP="fmp",
- API_POLYGON_KEY="polygon",
- API_FRED_KEY="fred",
- )
+ mock_user_settings.features_keys = {
+ "fmp_api_key": "abc",
+ "polygon_api_key": "def",
+ "fred_api_key": "ghi",
+ }
- credentials = HubService.hub2platform(mock_user_settings)
+ credentials = HubService().hub2platform(mock_user_settings)
assert isinstance(credentials, Credentials)
- assert credentials.fmp_api_key.get_secret_value() == "fmp"
- assert credentials.polygon_api_key.get_secret_value() == "polygon"
- assert credentials.fred_api_key.get_secret_value() == "fred"
+ assert credentials.fmp_api_key.get_secret_value() == "abc"
+ assert credentials.polygon_api_key.get_secret_value() == "def"
+ assert credentials.fred_api_key.get_secret_value() == "ghi"
+
+
+def test_hub2platform_v3_only():
+ """Test hub2platform."""
+ mock_user_settings = MagicMock(spec=HubUserSettings)
+ mock_user_settings.features_keys = {
+ "API_KEY_FINANCIALMODELINGPREP": "abc",
+ "API_POLYGON_KEY": "def",
+ "API_FRED_KEY": "ghi",
+ }
+
+ credentials = HubService().hub2platform(mock_user_settings)
+ assert isinstance(credentials, Credentials)
+ assert credentials.fmp_api_key.get_secret_value() == "abc"
+ assert credentials.polygon_api_key.get_secret_value() == "def"
+ assert credentials.fred_api_key.get_secret_value() == "ghi"
+
+
+def test_hub2platform_v3v4():
+ """Test hub2platform."""
+ mock_user_settings = MagicMock(spec=HubUserSettings)
+ mock_user_settings.features_keys = {
+ "API_KEY_FINANCIALMODELINGPREP": "abc",
+ "fmp_api_key": "other_key",
+ "API_POLYGON_KEY": "def",
+ "API_FRED_KEY": "ghi",
+ }
+
+ credentials = HubService().hub2platform(mock_user_settings)
+ assert isinstance(credentials, Credentials)
+ assert credentials.fmp_api_key.get_secret_value() == "other_key"
+ assert credentials.polygon_api_key.get_secret_value() == "def"
+ assert credentials.fred_api_key.get_secret_value() == "ghi"
def test_platform2hub():
"""Test platform2hub."""
- mock_credentials = MagicMock(spec=Credentials)
- mock_credentials.fmp_api_key = SecretStr("fmp")
- mock_credentials.polygon_api_key = SecretStr("polygon")
- mock_credentials.fred_api_key = SecretStr("fred")
+ mock_user_settings = MagicMock(spec=HubUserSettings)
+ mock_user_settings.features_keys = { # Received from Hub
+ "API_KEY_FINANCIALMODELINGPREP": "abc",
+ "fmp_api_key": "other_key",
+ "API_FRED_KEY": "ghi",
+ }
+ mock_hub_service = HubService()
+ mock_hub_service._hub_user_settings = mock_user_settings
+ mock_credentials = Credentials( # Current credentials
+ fmp_api_key=SecretStr("fmp"),
+ polygon_api_key=SecretStr("polygon"),
+ fred_api_key=SecretStr("fred"),
+ benzinga_api_key=SecretStr("benzinga"),
+ some_api_key=SecretStr("some"),
+ )
+ user_settings = mock_hub_service.platform2hub(mock_credentials)
- user_settings = HubService.platform2hub(mock_credentials)
assert isinstance(user_settings, HubUserSettings)
- assert user_settings.features_keys.API_KEY_FINANCIALMODELINGPREP == "fmp"
- assert user_settings.features_keys.API_POLYGON_KEY == "polygon"
- assert user_settings.features_keys.API_FRED_KEY == "fred"
+ assert user_settings.features_keys["API_KEY_FINANCIALMODELINGPREP"] == "fmp"
+ assert user_settings.features_keys["fmp_api_key"] == "other_key"
+ assert user_settings.features_keys["polygon_api_key"] == "polygon"
+ assert user_settings.features_keys["API_FRED_KEY"] == "fred"
+ assert user_settings.features_keys["benzinga_api_key"] == "benzinga"
+ assert "some_api_key" not in user_settings.features_keys
diff --git a/openbb_platform/core/tests/app/static/__init__.py b/openbb_platform/core/tests/app/static/__init__.py
index e69de29bb2d..3f4c8aabdda 100644
--- a/openbb_platform/core/tests/app/static/__init__.py
+++ b/openbb_platform/core/tests/app/static/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform core tests."""
diff --git a/openbb_platform/core/tests/app/static/test_app_factory.py b/openbb_platform/core/tests/app/static/test_app_factory.py
index efb79faabff..9a8799be503 100644
--- a/openbb_platform/core/tests/app/static/test_app_factory.py
+++ b/openbb_platform/core/tests/app/static/test_app_factory.py
@@ -47,3 +47,10 @@ def test_app_coverage(app_factory):
coverage = app_factory.coverage
assert coverage
assert isinstance(coverage, Coverage)
+
+
+def test_app_reference(app_factory):
+ """Test app reference."""
+ reference = app_factory.reference
+ assert reference
+ assert isinstance(reference, dict)
diff --git a/openbb_platform/core/tests/app/static/test_filters.py b/openbb_platform/core/tests/app/static/test_filters.py
index 1f06dc0955d..9532867fe4b 100644
--- a/openbb_platform/core/tests/app/static/test_filters.py
+++ b/openbb_platform/core/tests/app/static/test_filters.py
@@ -49,6 +49,7 @@ test_data = [
def test_filter_inputs(
input_data,
):
+ """Test the filter_inputs function."""
result = filter_inputs(data=input_data, data_processing=True)
# Assert that the result is a dictionary
diff --git a/openbb_platform/core/tests/app/static/test_package_builder.py b/openbb_platform/core/tests/app/static/test_package_builder.py
index 029c200fa45..fe738d69deb 100644
--- a/openbb_platform/core/tests/app/static/test_package_builder.py
+++ b/openbb_platform/core/tests/app/static/test_package_builder.py
@@ -5,6 +5,7 @@
from dataclasses import dataclass
from inspect import _empty
from pathlib import Path
+from typing import Any, Dict, List, Tuple, Union
from unittest.mock import PropertyMock, mock_open, patch
import pandas
@@ -27,6 +28,7 @@ from typing_extensions import Annotated
@pytest.fixture(scope="module")
def tmp_openbb_dir(tmp_path_factory):
+ """Return a temporary openbb directory."""
return tmp_path_factory.mktemp("openbb")
@@ -532,6 +534,7 @@ def test_generate_model_docstring(docstring_generator):
docstring = ""
model_name = "WorldNews"
summary = "This is a summary."
+ sections = ["description", "parameters", "returns", "examples"]
pi = docstring_generator.provider_interface
kwarg_params = pi.params[model_name]["extra"].__dataclass_fields__
@@ -551,6 +554,7 @@ def test_generate_model_docstring(docstring_generator):
kwarg_params=kwarg_params,
returns=returns,
results_type="List[WorldNews]",
+ sections=sections,
)
assert docstring
@@ -560,11 +564,46 @@ def test_generate_model_docstring(docstring_generator):
assert "WorldNews" in docstring
+@pytest.mark.parametrize(
+ "type_, expected",
+ [
+ (Any, []),
+ (List[str], ["List"]),
+ (Dict[str, str], ["Dict"]),
+ (Tuple[str], ["Tuple"]),
+ (Union[List[str], Dict[str, str], Tuple[str]], ["List", "Dict", "Tuple"]),
+ ],
+)
+def test__get_generic_types(docstring_generator, type_, expected):
+ """Test get generic types."""
+ output = docstring_generator._get_generic_types(type_, [])
+ assert output == expected
+
+
+@pytest.mark.parametrize(
+ "items, model, expected",
+ [
+ ([], "test_model", "test_model"),
+ (["List"], "test_model", "List[test_model]"),
+ (["Dict"], "test_model", "Dict[str, test_model]"),
+ (["Tuple"], "test_model", "Tuple[test_model]"),
+ (
+ ["List", "Dict", "Tuple"],
+ "test_model",
+ "Union[List[test_model], Dict[str, test_model], Tuple[test_model]]",
+ ),
+ ],
+)
+def test__get_repr(docstring_generator, items, model, expected):
+ output = docstring_generator._get_repr(items, model)
+ assert output == expected
+
+
def test_generate(docstring_generator):
"""Test generate docstring."""
def some_func():
- """Some func docstring."""
+ """Define Some func docstring."""
formatted_params = {
"param1": Parameter("NoneType", kind=Parameter.POSITIONAL_OR_KEYWORD),
@@ -582,15 +621,15 @@ def test_generate(docstring_generator):
assert "Returns" in doc
-def test_read_extension_map(package_builder, tmp_openbb_dir):
- """Test read extension map."""
+def test__read(package_builder, tmp_openbb_dir):
+ """Test read."""
PATH = "openbb_core.app.static.package_builder."
open_mock = mock_open()
with patch(PATH + "open", open_mock), patch(PATH + "load") as mock_load:
- package_builder._read(Path(tmp_openbb_dir / "assets" / "extension_map.json"))
+ package_builder._read(Path(tmp_openbb_dir / "assets" / "reference.json"))
open_mock.assert_called_once_with(
- Path(tmp_openbb_dir / "assets" / "extension_map.json")
+ Path(tmp_openbb_dir / "assets" / "reference.json")
)
mock_load.assert_called_once()
@@ -646,7 +685,6 @@ def test_read_extension_map(package_builder, tmp_openbb_dir):
)
def test_package_diff(
package_builder,
- tmp_openbb_dir,
ext_built,
ext_installed,
ext_inst_version,
@@ -656,22 +694,20 @@ def test_package_diff(
"""Test package differences."""
def mock_entry_points(group):
+ """Mock entry points."""
return ext_installed.select(**{"group": group})
PATH = "openbb_core.app.static.package_builder."
- with patch.object(PackageBuilder, "_read") as mock_read, patch(
- PATH + "entry_points", mock_entry_points
- ), patch.object(EntryPoint, "dist", new_callable=PropertyMock) as mock_obj:
+ with patch(PATH + "entry_points", mock_entry_points), patch.object(
+ EntryPoint, "dist", new_callable=PropertyMock
+ ) as mock_obj:
class MockPathDistribution:
version = ext_inst_version
mock_obj.return_value = MockPathDistribution()
- mock_read.return_value = ext_built
- add, remove = package_builder._diff(
- Path(tmp_openbb_dir, "assets", "extension_map.json")
- )
+ add, remove = package_builder._diff(ext_built)
# We add whatever is not built, but is installed
assert add == expected_add
@@ -689,7 +725,7 @@ def test_package_diff(
({"this"}, {"that"}, False),
],
)
-def test_auto_build(package_builder, tmp_openbb_dir, add, remove, openbb_auto_build):
+def test_auto_build(package_builder, add, remove, openbb_auto_build):
"""Test auto build."""
with patch.object(PackageBuilder, "_diff") as mock_assets_diff, patch.object(
@@ -700,9 +736,6 @@ def test_auto_build(package_builder, tmp_openbb_dir, add, remove, openbb_auto_bu
package_builder.auto_build()
if openbb_auto_build:
- mock_assets_diff.assert_called_once_with(
- Path(tmp_openbb_dir, "assets", "extension_map.json")
- )
if add or remove:
mock_build.assert_called_once()
else:
diff --git a/openbb_platform/core/tests/app/static/test_reference_loader.py b/openbb_platform/core/tests/app/static/test_reference_loader.py
index 1980ac4611a..20ef1fabf96 100644
--- a/openbb_platform/core/tests/app/static/test_reference_loader.py
+++ b/openbb_platform/core/tests/app/static/test_reference_loader.py
@@ -1,12 +1,17 @@
+"""Tests for the ReferenceLoader class."""
+
import json
from pathlib import Path
import pytest
from openbb_core.app.static.reference_loader import ReferenceLoader
+# pylint: disable=W0212, W0621
+
@pytest.fixture(scope="function")
def reference_loader():
+ """Fixture to create a ReferenceLoader instance."""
ReferenceLoader._instances = {}
yield ReferenceLoader
ReferenceLoader._instances = {}
diff --git a/openbb_platform/core/tests/app/static/utils/test_linters.py b/openbb_platform/core/tests/app/static/utils/test_linters.py
index 37f57e39592..1ebd54b66d0 100644
--- a/openbb_platform/core/tests/app/static/utils/test_linters.py
+++ b/openbb_platform/core/tests/app/static/utils/test_linters.py
@@ -10,6 +10,7 @@ from openbb_core.app.static.package_builder import (
@pytest.fixture(scope="module")
def tmp_package_dir(tmp_path_factory):
+ """Return a temporary package directory."""
return tmp_path_factory.mktemp("package")
diff --git a/openbb_platform/core/tests/app/test_charting_manager.py b/openbb_platform/core/tests/app/test_charting_manager.py
index e69de29bb2d..06b5fb09691 100644
--- a/openbb_platform/core/tests/app/test_charting_manager.py
+++ b/openbb_platform/core/tests/app/test_charting_manager.py
@@ -0,0 +1 @@
+"""Test ChartingManager."""
diff --git a/openbb_platform/core/tests/app/test_command_runner.py b/openbb_platform/core/tests/app/test_command_runner.py
index 4b1b35da92d..d56acfca82c 100644
--- a/openbb_platform/core/tests/app/test_command_runner.py
+++ b/openbb_platform/core/tests/app/test_command_runner.py
@@ -1,3 +1,5 @@
+"""Test command runner."""
+
from dataclasses import dataclass
from inspect import Parameter
from typing import Dict, List
@@ -20,6 +22,8 @@ from openbb_core.app.provider_interface import ExtraParams
from openbb_core.app.router import CommandMap
from pydantic import BaseModel, ConfigDict
+# pylint: disable=W0613, W0621, W0102, W0212
+
@pytest.fixture()
def execution_context():
@@ -37,7 +41,7 @@ def mock_func():
def mock_func(
a: int, b: int, c: float = 10.0, d: int = 5, provider_choices: Dict = {}
) -> None:
- pass
+ """Mock function."""
return mock_func
@@ -155,12 +159,14 @@ def test_parameters_builder_merge_args_and_kwargs(
def test_parameters_builder_update_command_context(
kwargs, system_settings, user_settings, expected_result
):
+ """Test update_command_context."""
+
def other_mock_func(
cc: CommandContext,
a: int,
b: int,
) -> None:
- pass
+ """Mock function."""
result = ParametersBuilder.update_command_context(
other_mock_func, kwargs, system_settings, user_settings
@@ -207,6 +213,7 @@ def test_parameters_builder_update_command_context(
def test_parameters_builder_update_provider_choices(
command_coverage, route, kwargs, route_default, expected_result
):
+ """Test update_provider_choices."""
with patch("openbb_core.app.command_runner.ProviderInterface") as mock_pi:
mock_pi.available_providers = ["provider1", "provider2"]
result = ParametersBuilder.update_provider_choices(
@@ -218,7 +225,6 @@ def test_parameters_builder_update_provider_choices(
def test_parameters_builder_validate_kwargs(mock_func):
"""Test validate_kwargs."""
-
# TODO: add more test cases with @pytest.mark.parametrize
result = ParametersBuilder.validate_kwargs(
@@ -268,7 +274,6 @@ def test_parameters_builder__warn_kwargs(extra_params, base, expect):
def test_parameters_builder_build(mock_func, execution_context):
"""Test build."""
-
# TODO: add more test cases with @pytest.mark.parametrize
with patch("openbb_core.app.command_runner.ProviderInterface") as mock_pi:
@@ -298,14 +303,12 @@ def test_parameters_builder_build(mock_func, execution_context):
@patch("openbb_core.app.command_runner.LoggingService")
def test_command_runner(_):
"""Test command runner."""
-
assert CommandRunner()
@patch("openbb_core.app.command_runner.LoggingService")
def test_command_runner_properties(mock_logging_service):
"""Test properties."""
-
sys = SystemSettings()
user = UserSettings()
cmd_map = CommandMap()
@@ -320,6 +323,7 @@ def test_command_runner_properties(mock_logging_service):
@patch("openbb_core.app.command_runner.LoggingService")
def test_command_runner_run(_):
+ """Test run."""
runner = CommandRunner()
with patch(
@@ -338,14 +342,17 @@ async def test_static_command_runner_run(
"""Test static command runner run."""
def other_mock_func(a: int, b: int, c: int, d: int) -> List[int]:
+ """Mock function."""
return [a, b, c, d]
class MockOBBject:
"""Mock OBBject"""
def __init__(self, results):
+ """Initialize the mock object."""
self.results = results
self.extra = {}
+ self.extra["metadata"] = {"test": "test"}
mock_get_command.return_value = other_mock_func
mock_execute_func.return_value = MockOBBject(results=[1, 2, 3, 4])
diff --git a/openbb_platform/core/tests/app/test_deprecation.py b/openbb_platform/core/tests/app/test_deprecation.py
index 926fe991a7b..bca6c5381ae 100644
--- a/openbb_platform/core/tests/app/test_deprecation.py
+++ b/openbb_platform/core/tests/app/test_deprecation.py
@@ -1,3 +1,5 @@
+"""Test deprecated commands."""
+
import unittest
from openbb_core.app.static.package_builder import PathHandler
diff --git a/openbb_platform/core/tests/app/test_utils.py b/openbb_platform/core/tests/app/test_utils.py
index 4b2bcfc5fb2..ab07be91597 100644
--- a/openbb_platform/core/tests/app/test_utils.py
+++ b/openbb_platform/core/tests/app/test_utils.py
@@ -1,3 +1,5 @@
+"""OpenBB Platform Core app utils tests."""
+
import numpy as np
import pandas as pd
import pytest
@@ -14,6 +16,8 @@ from openbb_core.app.utils import (
)
from openbb_core.provider.abstract.data import Data
+# pylint: disable=W0621
+
df = pd.DataFrame(
{
"x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
@@ -37,36 +41,42 @@ multi_index_base_model = [
def test_df_to_basemodel():
+ """Test the df_to_basemodel helper."""
base_model = df_to_basemodel(df)
assert isinstance(base_model, list)
assert base_model[0].x == 1 # type: ignore[attr-defined]
def test_df_to_basemodel_multiindex():
+ """Test the df_to_basemodel helper with a multi-index DataFrame."""
base_model = df_to_basemodel(df_multiindex)
assert isinstance(base_model, list)
assert hasattr(base_model[0], "is_multiindex")
def test_basemodel_to_df():
+ """Test the basemodel_to_df helper."""
df = basemodel_to_df(simple_base_model)
assert isinstance(df, pd.DataFrame)
assert df.shape == (8, 3)
def test_basemodel_to_multiindex_df():
+ """Test the basemodel_to_df helper with a multi-index DataFrame."""
df = basemodel_to_df(multi_index_base_model)
assert isinstance(df, pd.DataFrame)
assert isinstance(df.index, pd.MultiIndex)
def test_get_target_column():
+ """Test the get_target_column helper."""
target = get_target_column(df, "x")
assert isinstance(target, pd.Series)
assert target[0] == 1
def test_get_target_columns():
+ """Test the get_target_columns helper."""
targets = get_target_columns(df, ["x", "y"])
assert isinstance(targets, pd.DataFrame)
assert targets.shape == (10, 2)
@@ -84,6 +94,7 @@ def test_get_target_columns():
],
)
def test_list_to_basemodel(data_list, expected):
+ """Test the list_to_basemodel helper."""
result = list_to_basemodel(data_list)
for r, e in zip(result, expected):
assert r.model_dump() == e.model_dump()
@@ -101,6 +112,7 @@ def test_list_to_basemodel(data_list, expected):
],
)
def test_dict_to_basemodel(data_dict, expected):
+ """Test the dict_to_basemodel helper."""
result = dict_to_basemodel(data_dict)
assert result.model_dump() == expected.model_dump()
@@ -123,6 +135,7 @@ def test_dict_to_basemodel(data_dict, expected):
],
)
def test_ndarray_to_basemodel(array, expected):
+ """Test the ndarray_to_basemodel helper."""
result = ndarray_to_basemodel(array)
for r, e in zip(result, expected):
assert r.model_dump() == e.model_dump()
@@ -139,6 +152,7 @@ def test_ndarray_to_basemodel(array, expected):
],
)
def test_check_single_item(item, expected):
+ """Test the check_single_item helper."""
if expected is OpenBBError:
with pytest.raises(OpenBBError):
check_single_item(item)
diff --git a/openbb_platform/core/tests/provider/test_registry_map.py b/openbb_platform/core/tests/provider/test_registry_map.py
index 5e2e9e9f061..67c905a8821 100644
--- a/openbb_platform/core/tests/provider/test_registry_map.py
+++ b/openbb_platform/core/tests/provider/test_registry_map.py
@@ -9,8 +9,7 @@ from openbb_core.provider.registry_map import RegistryMap
@pytest.fixture
def load_registry_map():
"""Mock the registry map."""
- registry_map = RegistryMap()
- return registry_map
+ return RegistryMap()
def test_get_credentials(load_registry_map):
@@ -30,9 +29,12 @@ def test_get_available_providers(load_registry_map):
def test_map_and_models(load_registry_map):
"""Test if the _get_map method behaves as expected."""
- map_, return_map = load_registry_map.map, load_registry_map.return_map
+ standard_extra, original_models = (
+ load_registry_map.standard_extra,
+ load_registry_map.original_models,
+ )
models = load_registry_map.models
- assert "EquityHistorical" in map_
- assert "EquityHistorical" in return_map
+ assert "EquityHistorical" in standard_extra
+ assert "EquityHistorical" in original_models
assert "EquityHistorical" in models
diff --git a/openbb_platform/core/tests/provider/utils/test_client.py b/openbb_platform/core/tests/provider/utils/test_client.py
index 1c88c958853..5b018994d9b 100644
--- a/openbb_platform/core/tests/provider/utils/test_client.py
+++ b/openbb_platform/core/tests/provider/utils/test_client.py
@@ -1,3 +1,5 @@
+"""Test the client helper."""
+
import gzip
import json
import zlib
@@ -80,6 +82,7 @@ class MockClientSession(client.ClientSession):
async def request( # type: ignore
self, *args, raise_for_status: bool = False, **kwargs
) -> client.ClientResponse:
+ """Mock the request method."""
response = MockResponse(*args, **kwargs)
if raise_for_status:
diff --git a/openbb_platform/core/tests/provider/utils/test_helpers.py b/openbb_platform/core/tests/provider/utils/test_helpers.py
index 4d37084e4e3..741ce6ee914 100644
--- a/openbb_platform/core/tests/provider/utils/test_helpers.py
+++ b/openbb_platform/core/tests/provider/utils/test_helpers.py
@@ -11,21 +11,31 @@ from openbb_core.provider.utils.helpers import (
to_snake_case,
)
+# pylint: disable=unused-argument
+
class MockResponse:
+ """Mock the response."""
+
def __init__(self):
+ """Initialize the mock response."""
self.status_code = 200
self.status = 200
async def json(self):
+ """Return the json response."""
return {"test": "test"}
class MockSession:
+ """Mock the ClientSession."""
+
def __init__(self):
+ """Initialize the mock session."""
self.response = MockResponse()
- async def request(self, *args, **kwargs):
+ async def request(self, *args, **kwargs): # pylint: disable=unused-argument
+ """Mock the ClientSession.request method."""
if kwargs.get("raise_for_status", False):
raise Exception("Test")
diff --git a/openbb_platform/extensions/__init__.py b/openbb_platform/extensions/__init__.py
index e69de29bb2d..a2fa55684fc 100644
--- a/openbb_platform/extensions/__init__.py
+++ b/openbb_platform/extensions/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Platform Extensions."""
diff --git a/openbb_platform/extensions/commodity/integration/test_commodity_api.py b/openbb_platform/extensions/commodity/integration/test_commodity_api.py
index 89f22935c9c..56e06076e4d 100644
--- a/openbb_platform/extensions/commodity/integration/test_commodity_api.py
+++ b/openbb_platform/extensions/commodity/integration/test_commodity_api.py
@@ -12,6 +12,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -46,6 +47,7 @@ def headers():
)
@pytest.mark.integration
def test_commodity_lbma_fixing(params, headers):
+ """Test the LBMA fixing endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/commodity/integration/test_commodity_python.py b/openbb_platform/extensions/commodity/integration/test_commodity_python.py
index c805ffcb269..192dac81a19 100644
--- a/openbb_platform/extensions/commodity/integration/test_commodity_python.py
+++ b/openbb_platform/extensions/commodity/integration/test_commodity_python.py
@@ -42,6 +42,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_commodity_lbma_fixing(params, obb):
+ """Test the LBMA fixing endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.commodity.lbma_fixing(**params)
diff --git a/openbb_platform/extensions/commodity/openbb_commodity/__init__.py b/openbb_platform/extensions/commodity/openbb_commodity/__init__.py
index e69de29bb2d..c38801bad61 100644
--- a/openbb_platform/extensions/commodity/openbb_commodity/__init__.py
+++ b/openbb_platform/extensions/commodity/openbb_commodity/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Commodity Extension."""
diff --git a/openbb_platform/extensions/commodity/openbb_commodity/commodity_router.py b/openbb_platform/extensions/commodity/openbb_commodity/commodity_router.py
index ebf5401c28f..3d8482d6852 100644
--- a/openbb_platform/extensions/commodity/openbb_commodity/commodity_router.py
+++ b/openbb_platform/extensions/commodity/openbb_commodity/commodity_router.py
@@ -12,7 +12,7 @@ from openbb_core.app.query import Query
from openbb_core.app.router import Router
from pydantic import BaseModel
-router = Router(prefix="")
+router = Router(prefix="", description="Commodity market data.")
# pylint: disable=unused-argument
diff --git a/openbb_platform/extensions/commodity/poetry.lock b/openbb_platform/extensions/commodity/poetry.lock
index a5af39a01ec..c3aaff6a76a 100644
--- a/openbb_platform/extensions/commodity/poetry.lock
+++ b/openbb_platform/extensions/commodity/poetry.lock
@@ -2,87 +2,87 @@
[[package]]
name = "aiohttp"
-version = "3.9.3"
+version = "3.9.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
- {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"},
- {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"},
- {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"},
- {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"},
- {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"},
- {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"},
- {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"},
- {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"},
- {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"},
- {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"},
- {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"},
- {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"},
- {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"},
- {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"},
- {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"},
- {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"},
- {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"},
- {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"},
- {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"},
- {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"},
- {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"},
- {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"},
- {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"},
- {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"},
- {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"},
- {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"},
- {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"},
- {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"},
- {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"},
- {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"},
- {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"},
- {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"},
- {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"},
- {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"},
- {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"},
- {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"},
- {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"},
- {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"},
- {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"},
- {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"},
- {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"},
- {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"},
- {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"},
- {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"},
- {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"},
- {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"},
- {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"},
- {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"},
- {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"},
- {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"},
- {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"},
- {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"},
- {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"},
- {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"},
- {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"},
- {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"},
- {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"},
- {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"},
- {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"},
- {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"},
- {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"},
- {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"},
- {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"},
- {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"},
- {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"},
- {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"},
- {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"},
- {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"},
- {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"},
- {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"},
- {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"},
- {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"},
- {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"},
- {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"},
- {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"},
- {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"},
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
+ {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
+ {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
+ {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
+ {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
+ {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
+ {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
+ {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
+ {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
+ {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
+ {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
+ {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
]
[package.dependencies]
@@ -323,13 +323,13 @@ files = [
[[package]]
name = "ecdsa"
-version = "0.18.0"
+version = "0.19.0"
description = "ECDSA cryptographic signature library (pure python)"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6"
files = [
- {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
- {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},
+ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"},
+ {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"},
]
[package.dependencies]
@@ -341,13 +341,13 @@ gmpy2 = ["gmpy2"]
[[package]]
name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
- {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
- {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+ {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
]
[package.extras]
@@ -493,13 +493,13 @@ lxml = ["lxml"]
[[package]]
name = "idna"
-version = "3.6"
+version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
- {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
- {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
@@ -670,58 +670,58 @@ files = [
[[package]]
name = "numpy"
-version = "1.26.3"
+version = "1.26.4"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
files = [
- {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"},
- {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"},
- {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"},
- {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"},
- {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"},
- {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"},
- {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"},
- {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"},
- {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"},
- {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"},
- {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"},
- {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"},
- {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"},
- {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"},
- {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"},
- {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"},
- {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"},
- {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"},
- {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"},
- {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"},
- {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"},
- {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"},
- {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"},
- {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"},
- {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"},
- {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"},
- {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"},
- {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"},
- {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"},
- {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"},
- {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"},
- {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"},
- {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"},
- {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"},
- {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"},
- {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"},
+ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
+ {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
+ {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
+ {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
+ {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
+ {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
+ {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
+ {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
+ {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
+ {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
+ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
]
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -778,8 +778,8 @@ files = [
[package.dependencies]
numpy = [
{version = ">=1.20.3", markers = "python_version < \"3.10\""},
- {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
{version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
+ {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
]
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
@@ -810,13 +810,13 @@ xml = ["lxml (>=4.6.3)"]
[[package]]
name = "posthog"
-version = "3.3.4"
+version = "3.5.0"
description = "Integrate PostHog into any python application."
optional = false
python-versions = "*"
files = [
- {file = "posthog-3.3.4-py2.py3-none-any.whl", hash = "sha256:2fec5112c6df1d6a214a899e409659ed354511236537e861f1556a0c88e3fd26"},
- {file = "posthog-3.3.4.tar.gz", hash = "sha256:23a891639bc0a4f6fe4d04864d02410c60b7ee5d523de79becbc7325c983dba9"},
+ {file = "posthog-3.5.0-py2.py3-none-any.whl", hash = "sha256:3c672be7ba6f95d555ea207d4486c171d06657eb34b3ce25eb043bfe7b6b5b76"},
+ {file = "posthog-3.5.0.tar.gz", hash = "sha256:8f7e3b2c6e8714d0c0c542a2109b83a7549f63b7113a133ab2763a89245ef2ef"},
]
[package.dependencies]
@@ -833,29 +833,29 @@ test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint"
[[package]]
name = "pyasn1"
-version = "0.5.1"
+version = "0.6.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
+python-versions = ">=3.8"
files = [
- {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"},
- {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"},
+ {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
+ {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
]
[[package]]
name = "pydantic"
-version = "2.6.0"
+version = "2.7.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"},
- {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"},
+ {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"},
+ {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
-pydantic-core = "2.16.1"
+pydantic-core = "2.18.1"
typing-extensions = ">=4.6.1"
[package.extras]
@@ -863,90 +863,90 @@ email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
-version = "2.16.1"
-description = ""
+version = "2.18.1"
+description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"},
- {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"},
- {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"},
- {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"},
- {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"},
- {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"},
- {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"},
- {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"},
- {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"},
- {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"},
- {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"},
- {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"},
- {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"},
- {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"},
- {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"},
- {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"},
- {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"},
- {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"},
- {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"},
- {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"},
- {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"},
- {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"},
- {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"},
- {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"},
- {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"},
- {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"},
- {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"},
- {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"},
- {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"},
- {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"},
- {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"},
- {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"},
- {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"},
- {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"},
- {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"},
- {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"},
- {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"},
- {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"},
- {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"},
- {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"},
+ {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"},
+ {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"},
+ {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"},
+ {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"},
+ {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"},
+ {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"},
+ {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"},
+ {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"},
+ {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"},
+ {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"},
+ {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"},
+ {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"},
+ {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"},
]
[package.dependencies]
@@ -954,13 +954,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "python-dateutil"
-version = "2.8.2"
+version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
- {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
- {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
@@ -1100,13 +1100,13 @@ files = [
[[package]]
name = "sniffio"
-version = "1.3.0"
+version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
files = [
- {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
- {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
@@ -1129,35 +1129,35 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
[[package]]
name = "typing-extensions"
-version = "4.9.0"
+version = "4.11.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
- {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
- {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
]
[[package]]
name = "tzdata"
-version = "2023.4"
+version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
- {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
- {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[[package]]
name = "urllib3"
-version = "2.2.0"
+version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
- {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"},
- {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"},
+ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+ {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
]
[package.extras]
@@ -1393,20 +1393,20 @@ multidict = ">=4.0"
[[package]]
name = "zipp"
-version = "3.17.0"
+version = "3.18.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
- {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
- {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
+ {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
+ {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/commodity/pyproject.toml b/openbb_platform/extensions/commodity/pyproject.toml
index 269d2ed093f..44dcb993f70 100644
--- a/openbb_platform/extensions/commodity/pyproject.toml
+++ b/openbb_platform/extensions/commodity/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-commodity"
-version = "1.0.2"
+version = "1.0.4"
description = "Commodity extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_commodity" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/crypto/integration/test_crypto_api.py b/openbb_platform/extensions/crypto/integration/test_crypto_api.py
index 6d38f1681a5..96a2fec3806 100644
--- a/openbb_platform/extensions/crypto/integration/test_crypto_api.py
+++ b/openbb_platform/extensions/crypto/integration/test_crypto_api.py
@@ -13,6 +13,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -29,6 +30,7 @@ def headers():
)
@pytest.mark.integration
def test_crypto_search(params, headers):
+ """Test the crypto search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -114,6 +116,7 @@ def test_crypto_search(params, headers):
)
@pytest.mark.integration
def test_crypto_price_historical(params, headers):
+ """Test the crypto historical price endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/crypto/integration/test_crypto_python.py b/openbb_platform/extensions/crypto/integration/test_crypto_python.py
index b22c29d4dae..7a3073620ef 100644
--- a/openbb_platform/extensions/crypto/integration/test_crypto_python.py
+++ b/openbb_platform/extensions/crypto/integration/test_crypto_python.py
@@ -25,6 +25,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_crypto_search(params, obb):
+ """Test the crypto search endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.crypto.search(**params)
@@ -109,6 +110,7 @@ def test_crypto_search(params, obb):
)
@pytest.mark.integration
def test_crypto_price_historical(params, obb):
+ """Test crypto price historical."""
result = obb.crypto.price.historical(**params)
assert result
assert isinstance(result, OBBject)
diff --git a/openbb_platform/extensions/crypto/openbb_crypto/__init__.py b/openbb_platform/extensions/crypto/openbb_crypto/__init__.py
index e69de29bb2d..2ce9b051267 100644
--- a/openbb_platform/extensions/crypto/openbb_crypto/__init__.py
+++ b/openbb_platform/extensions/crypto/openbb_crypto/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Crypto Extension."""
diff --git a/openbb_platform/extensions/crypto/openbb_crypto/crypto_router.py b/openbb_platform/extensions/crypto/openbb_crypto/crypto_router.py
index 1f92af05c97..bc81b7db88c 100644
--- a/openbb_platform/extensions/crypto/openbb_crypto/crypto_router.py
+++ b/openbb_platform/extensions/crypto/openbb_crypto/crypto_router.py
@@ -13,7 +13,7 @@ from openbb_core.app.router import Router
from openbb_crypto.price.price_router import router as price_router
-router = Router(prefix="")
+router = Router(prefix="", description="Cryptocurrency market data.")
router.include_router(price_router)
diff --git a/openbb_platform/extensions/crypto/openbb_crypto/price/__init__.py b/openbb_platform/extensions/crypto/openbb_crypto/price/__init__.py
index e69de29bb2d..71f078e3f0a 100644
--- a/openbb_platform/extensions/crypto/openbb_crypto/price/__init__.py
+++ b/openbb_platform/extensions/crypto/openbb_crypto/price/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Crypto Price Router."""
diff --git a/openbb_platform/extensions/crypto/poetry.lock b/openbb_platform/extensions/crypto/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/crypto/poetry.lock
+++ b/openbb_platform/extensions/crypto/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/crypto/pyproject.toml b/openbb_platform/extensions/crypto/pyproject.toml
index e76484ba8e6..71e244683c7 100644
--- a/openbb_platform/extensions/crypto/pyproject.toml
+++ b/openbb_platform/extensions/crypto/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-crypto"
-version = "1.1.4"
+version = "1.1.5"
description = "Crypto extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_crypto" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/currency/integration/test_currency_api.py b/openbb_platform/extensions/currency/integration/test_currency_api.py
index bc80a5d75bc..f2f5d56e34e 100644
--- a/openbb_platform/extensions/currency/integration/test_currency_api.py
+++ b/openbb_platform/extensions/currency/integration/test_currency_api.py
@@ -13,6 +13,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -49,6 +50,7 @@ def headers():
)
@pytest.mark.integration
def test_currency_search(params, headers):
+ """Test the currency search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -141,6 +143,7 @@ def test_currency_search(params, headers):
)
@pytest.mark.integration
def test_currency_price_historical(params, headers):
+ """Test the currency historical price endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -156,6 +159,7 @@ def test_currency_price_historical(params, headers):
)
@pytest.mark.integration
def test_currency_reference_rates(params, headers):
+ """Test the currency reference rates endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -180,6 +184,7 @@ def test_currency_reference_rates(params, headers):
)
@pytest.mark.integration
def test_currency_snapshots(params, headers):
+ """Test the currency snapshots endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/currency/integration/test_currency_python.py b/openbb_platform/extensions/currency/integration/test_currency_python.py
index 445e4293838..f2352a1669c 100644
--- a/openbb_platform/extensions/currency/integration/test_currency_python.py
+++ b/openbb_platform/extensions/currency/integration/test_currency_python.py
@@ -47,6 +47,7 @@ def obb(pytestconfig):
)
@pytest.mark.integration
def test_currency_search(params, obb):
+ """Test the currency search endpoint."""
result = obb.currency.search(**params)
assert result
assert isinstance(result, OBBject)
@@ -136,6 +137,7 @@ def test_currency_search(params, obb):
)
@pytest.mark.integration
def test_currency_price_historical(params, obb):
+ """Test the currency historical price endpoint."""
result = obb.currency.price.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -148,6 +150,7 @@ def test_currency_price_historical(params, obb):
)
@pytest.mark.integration
def test_currency_reference_rates(params, obb):
+ """Test the currency reference rates endpoint."""
result = obb.currency.reference_rates(**params)
assert result
assert isinstance(result, OBBject)
@@ -169,6 +172,7 @@ def test_currency_reference_rates(params, obb):
)
@pytest.mark.integration
def test_currency_snapshots(params, obb):
+ """Test the currency snapshots endpoint."""
result = obb.currency.snapshots(**params)
assert result
assert isinstance(result, OBBject)
diff --git a/openbb_platform/extensions/currency/openbb_currency/currency_router.py b/openbb_platform/extensions/currency/openbb_currency/currency_router.py
index 669b463869a..111acd3b5e2 100644
--- a/openbb_platform/extensions/currency/openbb_currency/currency_router.py
+++ b/openbb_platform/extensions/currency/openbb_currency/currency_router.py
@@ -13,7 +13,7 @@ from openbb_core.app.router import Router
from openbb_currency.price.price_router import router as price_router
-router = Router(prefix="")
+router = Router(prefix="", description="Foreign exchange (FX) market data.")
router.include_router(price_router)
@@ -65,7 +65,7 @@ async def reference_rates(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Current, official, currency reference rates.
+ """Get current, official, currency reference rates.
Foreign exchange reference rates are the exchange rates set by a major financial institution or regulatory body,
serving as a benchmark for the value of currencies around the world.
diff --git a/openbb_platform/extensions/currency/poetry.lock b/openbb_platform/extensions/currency/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/currency/poetry.lock
+++ b/openbb_platform/extensions/currency/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/currency/pyproject.toml b/openbb_platform/extensions/currency/pyproject.toml
index d42e483b1bf..3dfe060edad 100644
--- a/openbb_platform/extensions/currency/pyproject.toml
+++ b/openbb_platform/extensions/currency/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-currency"
-version = "1.1.4"
+version = "1.1.5"
description = "Currency extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_currency" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
index aa8103f2cf0..a777ddb3bf5 100644
--- a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
+++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
@@ -13,6 +13,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -38,6 +39,7 @@ def headers():
)
@pytest.mark.integration
def test_derivatives_options_chains(params, headers):
+ """Test the options chains endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -68,6 +70,7 @@ def test_derivatives_options_chains(params, headers):
)
@pytest.mark.integration
def test_derivatives_options_unusual(params, headers):
+ """Test the unusual options endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -94,6 +97,7 @@ def test_derivatives_options_unusual(params, headers):
)
@pytest.mark.integration
def test_derivatives_futures_historical(params, headers):
+ """Test the futures historical endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -112,6 +116,7 @@ def test_derivatives_futures_historical(params, headers):
)
@pytest.mark.integration
def test_derivatives_futures_curve(params, headers):
+ """Test the futures curve endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
index 8a961c6f652..1ba7c63fd2d 100644
--- a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
+++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
@@ -35,6 +35,7 @@ def obb(pytestconfig):
)
@pytest.mark.integration
def test_derivatives_options_chains(params, obb):
+ """Test the options chains endpoint."""
result = obb.derivatives.options.chains(**params)
assert result
assert isinstance(result, OBBject)
@@ -62,6 +63,7 @@ def test_derivatives_options_chains(params, obb):
)
@pytest.mark.integration
def test_derivatives_options_unusual(params, obb):
+ """Test the unusual options endpoint."""
result = obb.derivatives.options.unusual(**params)
assert result
assert isinstance(result, OBBject)
@@ -85,6 +87,7 @@ def test_derivatives_options_unusual(params, obb):
)
@pytest.mark.integration
def test_derivatives_futures_historical(params, obb):
+ """Test the futures historical endpoint."""
result = obb.derivatives.futures.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -100,6 +103,7 @@ def test_derivatives_futures_historical(params, obb):
)
@pytest.mark.integration
def test_derivatives_futures_curve(params, obb):
+ """Test the futures curve endpoint."""
result = obb.derivatives.futures.curve(**params)
assert result
assert isinstance(result, OBBject)
diff --git a/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_router.py b/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_router.py
index 48b81aa3a5e..040720ecd60 100644
--- a/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_router.py
+++ b/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_router.py
@@ -5,6 +5,6 @@ from openbb_core.app.router import Router
from openbb_derivatives.futures.futures_router import router as futures_router
from openbb_derivatives.options.options_router import router as options_router
-router = Router(prefix="")
+router = Router(prefix="", description="Derivatives market data.")
router.include_router(options_router)
router.include_router(futures_router)
diff --git a/openbb_platform/extensions/derivatives/poetry.lock b/openbb_platform/extensions/derivatives/poetry.lock
index 12b9fd975a0..718edef43ca 100644
--- a/openbb_platform/extensions/derivatives/poetry.lock
+++ b/openbb_platform/extensions/derivatives/poetry.lock
@@ -712,13 +712,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1431,4 +1431,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/derivatives/pyproject.toml b/openbb_platform/extensions/derivatives/pyproject.toml
index 89a176987aa..bddb388ea12 100644
--- a/openbb_platform/extensions/derivatives/pyproject.toml
+++ b/openbb_platform/extensions/derivatives/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-derivatives"
-version = "1.1.4"
+version = "1.1.5"
description = "Derivatives extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_derivatives" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/devtools/pyproject.toml b/openbb_platform/extensions/devtools/pyproject.toml
index dd5f6cddd19..7840fa90c98 100644
--- a/openbb_platform/extensions/devtools/pyproject.toml
+++ b/openbb_platform/extensions/devtools/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-devtools"
-version = "1.1.5"
+version = "1.1.6"
description = "Tools for OpenBB Platform Developers"
authors = ["OpenBB Team "]
readme = "README.md"
diff --git a/openbb_platform/extensions/econometrics/integration/test_econometrics_api.py b/openbb_platform/extensions/econometrics/integration/test_econometrics_api.py
index 0cf0851b451..068c6d02e3a 100644
--- a/openbb_platform/extensions/econometrics/integration/test_econometrics_api.py
+++ b/openbb_platform/extensions/econometrics/integration/test_econometrics_api.py
@@ -14,6 +14,7 @@ data: Dict = {}
def get_headers():
+ """Get the headers for the API request."""
if "headers" in data:
return data["headers"]
@@ -33,6 +34,7 @@ def request_data(menu: str, symbol: str, provider: str):
def get_equity_data():
+ """Get equity data."""
if "equity_data" in data:
return data["equity_data"]
@@ -44,6 +46,7 @@ def get_equity_data():
def get_crypto_data():
+ """Get crypto data."""
if "crypto_data" in data:
return data["crypto_data"]
@@ -60,6 +63,7 @@ def get_crypto_data():
def get_data(menu: Literal["equity", "crypto"]):
+ """Get data based on the selected menu."""
funcs = {"equity": get_equity_data, "crypto": get_crypto_data}
return funcs[menu]()
@@ -73,6 +77,7 @@ def get_data(menu: Literal["equity", "crypto"]):
)
@pytest.mark.integration
def test_econometrics_correlation_matrix(params, data_type):
+ """Test the correlation matrix endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(get_data(data_type))
@@ -99,6 +104,7 @@ def test_econometrics_correlation_matrix(params, data_type):
)
@pytest.mark.integration
def test_econometrics_ols_regression_summary(params, data_type):
+ """Test the OLS regression summary endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(
@@ -130,6 +136,7 @@ def test_econometrics_ols_regression_summary(params, data_type):
)
@pytest.mark.integration
def test_econometrics_autocorrelation(params, data_type):
+ """Test the autocorrelation endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(
@@ -171,6 +178,7 @@ def test_econometrics_autocorrelation(params, data_type):
)
@pytest.mark.integration
def test_econometrics_residual_autocorrelation(params, data_type):
+ """Test the residual autocorrelation endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(
@@ -210,6 +218,7 @@ def test_econometrics_residual_autocorrelation(params, data_type):
)
@pytest.mark.integration
def test_econometrics_cointegration(params, data_type):
+ """Test the cointegration endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(
@@ -241,6 +250,7 @@ def test_econometrics_cointegration(params, data_type):
)
@pytest.mark.integration
def test_econometrics_causality(params, data_type):
+ """Test the causality endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(get_data(data_type))
@@ -257,6 +267,7 @@ def test_econometrics_causality(params, data_type):
)
@pytest.mark.integration
def test_econometrics_unit_root(params, data_type):
+ """Test the unit root endpoint."""
params = {p: v for p, v in params.items() if v}
body = json.dumps(get_data(data_type))
diff --git a/openbb_platform/extensions/econometrics/integration/test_econometrics_python.py b/openbb_platform/extensions/econometrics/integration/test_econometrics_python.py
index ec67c062f36..752156ed6df 100644
--- a/openbb_platform/extensions/econometrics/integration/test_econometrics_python.py
+++ b/openbb_platform/extensions/econometrics/integration/test_econometrics_python.py
@@ -25,6 +25,7 @@ data: dict = {}
def get_stocks_data():
+ """Get stocks data."""
import openbb # pylint:disable=import-outside-toplevel
if "stocks_data" in data:
@@ -40,6 +41,7 @@ def get_stocks_data():
def get_crypto_data():
+ """Get crypto data."""
import openbb # pylint:disable=import-outside-toplevel
if "crypto_data" in data:
@@ -56,6 +58,7 @@ def get_crypto_data():
def get_data(menu: Literal["equity", "crypto"]):
+ """Get data."""
funcs = {"equity": get_stocks_data, "crypto": get_crypto_data}
return funcs[menu]()
@@ -69,6 +72,7 @@ def get_data(menu: Literal["equity", "crypto"]):
)
@pytest.mark.integration
def test_econometrics_correlation_matrix(params, data_type, obb):
+ """Test the econometrics correlation matrix."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -94,6 +98,7 @@ def test_econometrics_correlation_matrix(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_ols_regression(params, data_type, obb):
+ """Test the econometrics OLS regression."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -119,6 +124,7 @@ def test_econometrics_ols_regression(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_ols_regression_summary(params, data_type, obb):
+ """Test the econometrics OLS regression summary."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -143,6 +149,7 @@ def test_econometrics_ols_regression_summary(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_autocorrelation(params, data_type, obb):
+ """Test the econometrics autocorrelation."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -177,6 +184,7 @@ def test_econometrics_autocorrelation(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_residual_autocorrelation(params, data_type, obb):
+ """Test the econometrics residual autocorrelation."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -207,6 +215,7 @@ def test_econometrics_residual_autocorrelation(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_cointegration(params, data_type, obb):
+ """Test the econometrics cointegration."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -242,6 +251,7 @@ def test_econometrics_cointegration(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_causality(params, data_type, obb):
+ """Test the econometrics causality."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -264,6 +274,7 @@ def test_econometrics_causality(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_unit_root(params, data_type, obb):
+ """Test the econometrics unit root."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -282,6 +293,7 @@ def test_econometrics_unit_root(params, data_type, obb):
)
@pytest.mark.integration
def test_econometrics_panel_random_effects(params, obb):
+ """Test the econometrics panel random effects."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
@@ -301,6 +313,7 @@ def test_econometrics_panel_random_effects(params, obb):
)
@pytest.mark.integration
def test_econometrics_panel_between(params, obb):
+ """Test the econometrics panel between."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
@@ -320,6 +333,7 @@ def test_econometrics_panel_between(params, obb):
)
@pytest.mark.integration
def test_econometrics_panel_pooled(params, obb):
+ """Test the econometrics panel pooled."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
@@ -339,6 +353,7 @@ def test_econometrics_panel_pooled(params, obb):
)
@pytest.mark.integration
def test_econometrics_panel_fixed(params, obb):
+ """Test the econometrics panel fixed."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
@@ -358,6 +373,7 @@ def test_econometrics_panel_fixed(params, obb):
)
@pytest.mark.integration
def test_econometrics_panel_first_difference(params, obb):
+ """Test the econometrics panel first difference."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
@@ -377,11 +393,13 @@ def test_econometrics_panel_first_difference(params, obb):
)
@pytest.mark.integration
def test_econometrics_panel_fmac(params, obb):
+ """Test the econometrics panel fmac."""
params = {p: v for p, v in params.items() if v}
params["data"] = mock_multi_index_data()
result = obb.econometrics.panel_fmac(**params)
+ """Test the econometrics panel fmac."""
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
diff --git a/openbb_platform/extensions/econometrics/openbb_econometrics/__init__.py b/openbb_platform/extensions/econometrics/openbb_econometrics/__init__.py
index e69de29bb2d..cd48520387f 100644
--- a/openbb_platform/extensions/econometrics/openbb_econometrics/__init__.py
+++ b/openbb_platform/extensions/econometrics/openbb_econometrics/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Econometrics Extension."""
diff --git a/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py b/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py
index f4c8f48386e..cbd7773de1d 100644
--- a/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py
+++ b/openbb_platform/extensions/econometrics/openbb_econometrics/econometrics_router.py
@@ -27,7 +27,7 @@ from statsmodels.tsa.stattools import adfuller, grangercausalitytests # type: i
from openbb_econometrics.utils import get_engle_granger_two_step_cointegration_test
-router = Router(prefix="")
+router = Router(prefix="", description="Econometrics analysis tools.")
@router.command(
diff --git a/openbb_platform/extensions/econometrics/openbb_econometrics/utils.py b/openbb_platform/extensions/econometrics/openbb_econometrics/utils.py
index 008c23fce72..6e3745fa54e 100644
--- a/openbb_platform/extensions/econometrics/openbb_econometrics/utils.py
+++ b/openbb_platform/extensions/econometrics/openbb_econometrics/utils.py
@@ -1,3 +1,5 @@
+"""Utility functions for the econometrics extension of the OpenBB platform."""
+
import warnings
from typing import Tuple
@@ -10,8 +12,9 @@ from statsmodels.tsa.stattools import adfuller
def get_engle_granger_two_step_cointegration_test(
dependent_series: pd.Series, independent_series: pd.Series
) -> Tuple[float, float, float, pd.Series, float, float]:
- """Estimates long-run and short-run cointegration relationship for series y and x and apply
- the two-step Engle & Granger test for cointegration.
+ """Estimate long-run and short-run cointegration relationship for series y and x.
+
+ Then apply the two-step Engle & Granger test for cointegration.
Uses a 2-step process to first estimate coefficients for the long-run relationship
y_t = c + gamma * x_t + z_t
@@ -87,7 +90,7 @@ def get_engle_granger_two_step_cointegration_test(
def mock_multi_index_data():
- """Creates a mock multi-index dataframe for testing purposes."""
+ """Create a mock multi-index dataframe for testing purposes."""
arrays = [
["individual_" + str(i) for i in range(1, 11) for _ in range(5)],
list(range(1, 6)) * 10,
diff --git a/openbb_platform/extensions/econometrics/poetry.lock b/openbb_platform/extensions/econometrics/poetry.lock
index a60439738d4..dfd886fe1e0 100644
--- a/openbb_platform/extensions/econometrics/poetry.lock
+++ b/openbb_platform/extensions/econometrics/poetry.lock
@@ -841,13 +841,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1794,4 +1794,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<3.12"
-content-hash = "0eb5845fa3d270ce5789362bb52c5ecbb5f8d2fc540c35cc5e99a4aa16906644"
+content-hash = "922546305cb41e3b30455219cc4c77b840601f4cf605f47d6a515050af1c23fd"
diff --git a/openbb_platform/extensions/econometrics/pyproject.toml b/openbb_platform/extensions/econometrics/pyproject.toml
index cebb10be85f..dd006eab8da 100644
--- a/openbb_platform/extensions/econometrics/pyproject.toml
+++ b/openbb_platform/extensions/econometrics/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-econometrics"
-version = "1.1.4"
+version = "1.1.5"
description = "Econometrics Toolkit for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -12,7 +12,7 @@ scipy = "^1.10.1"
statsmodels = "^0.14.0"
arch = "^5.5.0"
linearmodels = "<=4.25" # ^4.26 has setuptools-scm in setup_requires
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/economy/integration/test_economy_api.py b/openbb_platform/extensions/economy/integration/test_economy_api.py
index 8ce32c0e1db..9563b719459 100644
--- a/openbb_platform/extensions/economy/integration/test_economy_api.py
+++ b/openbb_platform/extensions/economy/integration/test_economy_api.py
@@ -1,3 +1,5 @@
+"""Test Economy API."""
+
import base64
import pytest
@@ -9,6 +11,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -49,6 +52,7 @@ def headers():
)
@pytest.mark.integration
def test_economy_calendar(params, headers):
+ """Test the economy calendar endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -87,6 +91,7 @@ def test_economy_calendar(params, headers):
)
@pytest.mark.integration
def test_economy_cpi(params, headers):
+ """Test the economy CPI endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -102,6 +107,7 @@ def test_economy_cpi(params, headers):
)
@pytest.mark.integration
def test_economy_risk_premium(params, headers):
+ """Test the economy risk premium endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -137,6 +143,7 @@ def test_economy_risk_premium(params, headers):
)
@pytest.mark.integration
def test_economy_gdp_forecast(params, headers):
+ """Test the economy GDP forecast endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -170,6 +177,7 @@ def test_economy_gdp_forecast(params, headers):
)
@pytest.mark.integration
def test_economy_gdp_nominal(params, headers):
+ """Test the economy GDP nominal endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -203,6 +211,7 @@ def test_economy_gdp_nominal(params, headers):
)
@pytest.mark.integration
def test_economy_gdp_real(params, headers):
+ """Test the economy GDP real endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -243,6 +252,7 @@ def test_economy_gdp_real(params, headers):
)
@pytest.mark.integration
def test_economy_balance_of_payments(params, headers):
+ """Test the economy balance of payments endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -319,6 +329,7 @@ def test_economy_balance_of_payments(params, headers):
)
@pytest.mark.integration
def test_economy_fred_search(params, headers):
+ """Test the economy FRED search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -358,6 +369,7 @@ def test_economy_fred_search(params, headers):
)
@pytest.mark.integration
def test_economy_fred_series(params, headers):
+ """Test the economy FRED series endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -383,6 +395,7 @@ def test_economy_fred_series(params, headers):
)
@pytest.mark.integration
def test_economy_money_measures(params, headers):
+ """Test the economy money measures endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -412,6 +425,7 @@ def test_economy_money_measures(params, headers):
)
@pytest.mark.integration
def test_economy_unemployment(params, headers):
+ """Test the economy unemployment endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -437,6 +451,7 @@ def test_economy_unemployment(params, headers):
)
@pytest.mark.integration
def test_economy_composite_leading_indicator(params, headers):
+ """Test the economy composite leading indicator endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -463,6 +478,7 @@ def test_economy_composite_leading_indicator(params, headers):
)
@pytest.mark.integration
def test_economy_short_term_interest_rate(params, headers):
+ """Test the economy short term interest rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -489,6 +505,7 @@ def test_economy_short_term_interest_rate(params, headers):
)
@pytest.mark.integration
def test_economy_long_term_interest_rate(params, headers):
+ """Test the economy long term interest rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -537,6 +554,7 @@ def test_economy_long_term_interest_rate(params, headers):
)
@pytest.mark.integration
def test_economy_fred_regional(params, headers):
+ """Test the economy FRED regional endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/economy/integration/test_economy_python.py b/openbb_platform/extensions/economy/integration/test_economy_python.py
index 8973e268884..bf9c7bfa1e7 100644
--- a/openbb_platform/extensions/economy/integration/test_economy_python.py
+++ b/openbb_platform/extensions/economy/integration/test_economy_python.py
@@ -44,6 +44,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_economy_calendar(params, obb):
+ """Test economy calendar."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.calendar(**params)
@@ -69,6 +70,7 @@ def test_economy_calendar(params, obb):
)
@pytest.mark.integration
def test_economy_cpi(params, obb):
+ """Test economy cpi."""
result = obb.economy.cpi(**params)
assert result
assert isinstance(result, OBBject)
@@ -83,6 +85,7 @@ def test_economy_cpi(params, obb):
)
@pytest.mark.integration
def test_economy_risk_premium(params, obb):
+ """Test economy risk premium."""
result = obb.economy.risk_premium(**params)
assert result
assert isinstance(result, OBBject)
@@ -114,6 +117,7 @@ def test_economy_risk_premium(params, obb):
)
@pytest.mark.integration
def test_economy_gdp_forecast(params, obb):
+ """Test economy gdp forecast."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.gdp.forecast(**params)
@@ -139,6 +143,7 @@ def test_economy_gdp_forecast(params, obb):
)
@pytest.mark.integration
def test_economy_gdp_nominal(params, obb):
+ """Test economy gdp nominal."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.gdp.nominal(**params)
@@ -164,6 +169,7 @@ def test_economy_gdp_nominal(params, obb):
)
@pytest.mark.integration
def test_economy_gdp_real(params, obb):
+ """Test economy gdp real."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.gdp.real(**params)
@@ -203,6 +209,7 @@ def test_economy_gdp_real(params, obb):
)
@pytest.mark.integration
def test_economy_balance_of_payments(params, obb):
+ """Test economy balance of payments."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.balance_of_payments(**params)
@@ -278,6 +285,7 @@ def test_economy_balance_of_payments(params, obb):
)
@pytest.mark.integration
def test_economy_fred_search(params, obb):
+ """Test economy fred search."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.fred_search(**params)
@@ -316,6 +324,7 @@ def test_economy_fred_search(params, obb):
)
@pytest.mark.integration
def test_economy_fred_series(params, obb):
+ """Test economy fred series."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.fred_series(**params)
@@ -340,6 +349,7 @@ def test_economy_fred_series(params, obb):
)
@pytest.mark.integration
def test_economy_money_measures(params, obb):
+ """Test economy money measures."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.money_measures(**params)
@@ -368,6 +378,7 @@ def test_economy_money_measures(params, obb):
)
@pytest.mark.integration
def test_economy_unemployment(params, obb):
+ """Test economy unemployment."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.unemployment(**params)
@@ -392,6 +403,7 @@ def test_economy_unemployment(params, obb):
)
@pytest.mark.integration
def test_economy_composite_leading_indicator(params, obb):
+ """Test economy composite leading indicator."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.composite_leading_indicator(**params)
@@ -417,6 +429,7 @@ def test_economy_composite_leading_indicator(params, obb):
)
@pytest.mark.integration
def test_economy_short_term_interest_rate(params, obb):
+ """Test economy short term interest rate."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.short_term_interest_rate(**params)
@@ -442,6 +455,7 @@ def test_economy_short_term_interest_rate(params, obb):
)
@pytest.mark.integration
def test_economy_long_term_interest_rate(params, obb):
+ """Test economy long term interest rate."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.long_term_interest_rate(**params)
@@ -489,6 +503,7 @@ def test_economy_long_term_interest_rate(params, obb):
)
@pytest.mark.integration
def test_economy_fred_regional(params, obb):
+ """Test economy fred regional."""
params = {p: v for p, v in params.items() if v}
result = obb.economy.fred_regional(**params)
diff --git a/openbb_platform/extensions/economy/openbb_economy/__init__.py b/openbb_platform/extensions/economy/openbb_economy/__init__.py
index e69de29bb2d..8eb8dbd57c8 100644
--- a/openbb_platform/extensions/economy/openbb_economy/__init__.py
+++ b/openbb_platform/extensions/economy/openbb_economy/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Economy Extension."""
diff --git a/openbb_platform/extensions/economy/openbb_economy/economy_router.py b/openbb_platform/extensions/economy/openbb_economy/economy_router.py
index 9cf8fdfcfd8..9f3b6e05577 100644
--- a/openbb_platform/extensions/economy/openbb_economy/economy_router.py
+++ b/openbb_platform/extensions/economy/openbb_economy/economy_router.py
@@ -13,7 +13,7 @@ from openbb_core.app.router import Router
from openbb_economy.gdp.gdp_router import router as gdp_router
-router = Router(prefix="")
+router = Router(prefix="", description="Economic data.")
router.include_router(gdp_router)
# pylint: disable=unused-argument
@@ -69,7 +69,10 @@ async def cpi(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Consumer Price Index (CPI). Returns either the rescaled index value, or a rate of change (inflation)."""
+ """Get Consumer Price Index (CPI).
+
+ Returns either the rescaled index value, or a rate of change (inflation).
+ """
return await OBBject.from_query(Query(**locals()))
@@ -83,7 +86,7 @@ async def risk_premium(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Market Risk Premium by country."""
+ """Get Market Risk Premium by country."""
return await OBBject.from_query(Query(**locals()))
@@ -115,8 +118,8 @@ async def fred_search(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """
- Search for FRED series or economic releases by ID or string.
+ """Search for FRED series or economic releases by ID or string.
+
This does not return the observation values, only the metadata.
Use this function to find series IDs for `fred_series()`.
"""
@@ -160,7 +163,10 @@ async def money_measures(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Money Measures (M1/M2 and components). The Federal Reserve publishes as part of the H.6 Release."""
+ """Get Money Measures (M1/M2 and components).
+
+ The Federal Reserve publishes as part of the H.6 Release.
+ """
return await OBBject.from_query(Query(**locals()))
@@ -188,7 +194,7 @@ async def unemployment(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Global unemployment data."""
+ """Get global unemployment data."""
return await OBBject.from_query(Query(**locals()))
@@ -205,8 +211,11 @@ async def composite_leading_indicator(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """The composite leading indicator (CLI) is designed to provide early signals of turning points
+ """Use the composite leading indicator (CLI).
+
+ It is designed to provide early signals of turning points
in business cycles showing fluctuation of the economic activity around its long term potential level.
+
CLIs show short-term economic movements in qualitative rather than quantitative terms.
"""
return await OBBject.from_query(Query(**locals()))
@@ -227,9 +236,11 @@ async def short_term_interest_rate(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """
- Short-term interest rates are the rates at which short-term borrowings are effected between
+ """Get Short-term interest rates.
+
+ They are the rates at which short-term borrowings are effected between
financial institutions or the rate at which short-term government paper is issued or traded in the market.
+
Short-term interest rates are generally averages of daily rates, measured as a percentage.
Short-term interest rates are based on three-month money market rates where available.
Typical standardised names are "money market rate" and "treasury bill rate".
@@ -252,8 +263,8 @@ async def long_term_interest_rate(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """
- Long-term interest rates refer to government bonds maturing in ten years.
+ """Get Long-term interest rates that refer to government bonds maturing in ten years.
+
Rates are mainly determined by the price charged by the lender, the risk from the borrower and the
fall in the capital value. Long-term interest rates are generally averages of daily rates,
measured as a percentage. These interest rates are implied by the prices at which the government bonds are
@@ -261,7 +272,8 @@ async def long_term_interest_rate(
In all cases, they refer to bonds whose capital repayment is guaranteed by governments.
Long-term interest rates are one of the determinants of business investment.
Low long-term interest rates encourage investment in new equipment and high interest rates discourage it.
- Investment is, in turn, a major source of economic growth."""
+ Investment is, in turn, a major source of economic growth.
+ """
return await OBBject.from_query(Query(**locals()))
@@ -289,8 +301,8 @@ async def fred_regional(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """
- Query the Geo Fred API for regional economic data by series group.
+ """Query the Geo Fred API for regional economic data by series group.
+
The series group ID is found by using `fred_search` and the `series_id` parameter.
"""
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/economy/openbb_economy/gdp/gdp_router.py b/openbb_platform/extensions/economy/openbb_economy/gdp/gdp_router.py
index 024fc8e2ff8..a257a1e2c85 100644
--- a/openbb_platform/extensions/economy/openbb_economy/gdp/gdp_router.py
+++ b/openbb_platform/extensions/economy/openbb_economy/gdp/gdp_router.py
@@ -29,7 +29,7 @@ async def forecast(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Forecasted GDP Data."""
+ """Get Forecasted GDP Data."""
return await OBBject.from_query(Query(**locals()))
@@ -46,7 +46,7 @@ async def nominal(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Nominal GDP Data."""
+ """Get Nominal GDP Data."""
return await OBBject.from_query(Query(**locals()))
@@ -63,5 +63,5 @@ async def real(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Real GDP Data."""
+ """Get Real GDP Data."""
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/economy/poetry.lock b/openbb_platform/extensions/economy/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/economy/poetry.lock
+++ b/openbb_platform/extensions/economy/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/economy/pyproject.toml b/openbb_platform/extensions/economy/pyproject.toml
index cf43f9e6d75..b4482731c85 100644
--- a/openbb_platform/extensions/economy/pyproject.toml
+++ b/openbb_platform/extensions/economy/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-economy"
-version = "1.1.4"
+version = "1.1.5"
description = "Economy extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_economy" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py
index 511b4305417..84aaba03678 100644
--- a/openbb_platform/extensions/equity/integration/test_equity_api.py
+++ b/openbb_platform/extensions/equity/integration/test_equity_api.py
@@ -13,6 +13,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -74,6 +75,7 @@ def headers():
)
@pytest.mark.integration
def test_equity_fundamental_balance(params, headers):
+ """Test the equity fundamental balance endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -89,6 +91,7 @@ def test_equity_fundamental_balance(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_balance_growth(params, headers):
+ """Test the equity fundamental balance growth endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -107,6 +110,7 @@ def test_equity_fundamental_balance_growth(params, headers):
)
@pytest.mark.integration
def test_equity_calendar_dividend(params, headers):
+ """Test the equity calendar dividend endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -124,6 +128,7 @@ def test_equity_calendar_dividend(params, headers):
)
@pytest.mark.integration
def test_equity_calendar_splits(params, headers):
+ """Test the equity calendar splits endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -143,6 +148,7 @@ def test_equity_calendar_splits(params, headers):
)
@pytest.mark.integration
def test_equity_calendar_earnings(params, headers):
+ """Test the equity calendar earnings endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -205,6 +211,7 @@ def test_equity_calendar_earnings(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_cash(params, headers):
+ """Test the equity fundamental cash endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -220,6 +227,7 @@ def test_equity_fundamental_cash(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_cash_growth(params, headers):
+ """Test the equity fundamental cash growth endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -257,6 +265,7 @@ def test_equity_fundamental_cash_growth(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_management_compensation(params, headers):
+ """Test the equity fundamental management compensation endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -272,6 +281,7 @@ def test_equity_fundamental_management_compensation(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_historical_splits(params, headers):
+ """Test the equity fundamental historical splits endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -333,6 +343,7 @@ def test_equity_fundamental_historical_splits(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_dividends(params, headers):
+ """Test the equity fundamental dividends endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -348,6 +359,7 @@ def test_equity_fundamental_dividends(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_employee_count(params, headers):
+ """Test the equity fundamental employee count endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -359,10 +371,11 @@ def test_equity_fundamental_employee_count(params, headers):
@parametrize(
"params",
- [({"symbol": "AAPL", "period": "annual", "limit": 30})],
+ [({"symbol": "AAPL,MSFT", "period": "annual", "limit": 30})],
)
@pytest.mark.integration
def test_equity_estimates_historical(params, headers):
+ """Test the equity estimates historical endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -372,6 +385,69 @@ def test_equity_estimates_historical(params, headers):
assert result.status_code == 200
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "fy",
+ "fiscal_year": None,
+ "calendar_year": None,
+ "calendar_period": None,
+ "provider": "intrinio",
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_equity_estimates_forward_sales(params, headers):
+ """Test the equity estimates forward sales endpoint."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/equity/estimates/forward_sales?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "fy",
+ "fiscal_year": None,
+ "calendar_year": None,
+ "calendar_period": None,
+ "provider": "intrinio",
+ }
+ ),
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "annual",
+ "limit": None,
+ "include_historical": False,
+ "provider": "fmp",
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_equity_estimates_forward_eps(params, headers):
+ """Test the equity estimates forward EPS endpoint."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/equity/estimates/forward_eps?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+
@parametrize(
"params",
[
@@ -426,6 +502,7 @@ def test_equity_estimates_historical(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_income(params, headers):
+ """Test the equity fundamental income endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -441,6 +518,7 @@ def test_equity_fundamental_income(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_income_growth(params, headers):
+ """Test the equity fundamental income growth endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -483,6 +561,7 @@ def test_equity_fundamental_income_growth(params, headers):
)
@pytest.mark.integration
def test_equity_ownership_insider_trading(params, headers):
+ """Test the equity ownership insider trading endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -515,6 +594,7 @@ def test_equity_ownership_insider_trading(params, headers):
)
@pytest.mark.integration
def test_equity_ownership_institutional(params, headers):
+ """Test the equity ownership institutional endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -549,6 +629,7 @@ def test_equity_ownership_institutional(params, headers):
)
@pytest.mark.integration
def test_equity_calendar_ipo(params, headers):
+ """Test the equity calendar IPO endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -577,6 +658,7 @@ def test_equity_calendar_ipo(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_metrics(params, headers):
+ """Test the equity fundamental metrics endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -595,6 +677,7 @@ def test_equity_fundamental_metrics(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_management(params, headers):
+ """Test the equity fundamental management endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -610,6 +693,7 @@ def test_equity_fundamental_management(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_overview(params, headers):
+ """Test the equity fundamental overview endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -625,6 +709,7 @@ def test_equity_fundamental_overview(params, headers):
)
@pytest.mark.integration
def test_equity_ownership_major_holders(params, headers):
+ """Test the equity ownership major holders endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -661,6 +746,7 @@ def test_equity_ownership_major_holders(params, headers):
)
@pytest.mark.integration
def test_equity_estimates_price_target(params, headers):
+ """Test the equity estimates price target endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -703,6 +789,7 @@ def test_equity_estimates_price_target(params, headers):
)
@pytest.mark.integration
def test_equity_estimates_analyst_search(params, headers):
+ """Test the equity estimates analyst search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -718,10 +805,18 @@ def test_equity_estimates_analyst_search(params, headers):
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL,AMZN,RELIANCE.NS", "provider": "yfinance"}),
({"symbol": "TD:US", "provider": "tmx"}),
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "industry_group_number": None,
+ "provider": "intrinio",
+ }
+ ),
],
)
@pytest.mark.integration
def test_equity_estimates_consensus(params, headers):
+ """Test the equity estimates consensus endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -748,6 +843,7 @@ def test_equity_estimates_consensus(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_ratios(params, headers):
+ """Test the equity fundamental ratios endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -763,6 +859,7 @@ def test_equity_fundamental_ratios(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_revenue_per_geography(params, headers):
+ """Test the equity fundamental revenue per geography endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -778,6 +875,7 @@ def test_equity_fundamental_revenue_per_geography(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_revenue_per_segment(params, headers):
+ """Test the equity fundamental revenue per segment endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -838,6 +936,7 @@ def test_equity_fundamental_revenue_per_segment(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_filings(params, headers):
+ """Test the equity fundamental filings endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -857,6 +956,7 @@ def test_equity_fundamental_filings(params, headers):
)
@pytest.mark.integration
def test_equity_ownership_share_statistics(params, headers):
+ """Test the equity ownership share statistics endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -872,6 +972,7 @@ def test_equity_ownership_share_statistics(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_transcript(params, headers):
+ """Test the equity fundamental transcript endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -887,6 +988,7 @@ def test_equity_fundamental_transcript(params, headers):
)
@pytest.mark.integration
def test_equity_compare_peers(params, headers):
+ """Test the equity compare peers endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -902,6 +1004,7 @@ def test_equity_compare_peers(params, headers):
)
@pytest.mark.integration
def test_equity_compare_groups(params, headers):
+ """Test the equity compare groups endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1095,6 +1198,7 @@ def test_equity_compare_groups(params, headers):
)
@pytest.mark.integration
def test_equity_price_historical(params, headers):
+ """Test the equity price historical endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1111,6 +1215,7 @@ def test_equity_price_historical(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_multiples(params, headers):
+ """Test the equity fundamental multiples endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1128,6 +1233,7 @@ def test_equity_fundamental_multiples(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_search_attributes(params, headers):
+ """Test the equity fundamental search attributes endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1209,6 +1315,7 @@ def test_equity_fundamental_search_attributes(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_historical_attributes(params, headers):
+ """Test the equity fundamental historical attributes endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1267,6 +1374,7 @@ def test_equity_fundamental_historical_attributes(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_latest_attributes(params, headers):
+ """Test the equity fundamental latest attributes endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1297,6 +1405,7 @@ def test_equity_fundamental_latest_attributes(params, headers):
)
@pytest.mark.integration
def test_equity_search(params, headers):
+ """Test the equity search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1335,6 +1444,7 @@ def test_equity_search(params, headers):
)
@pytest.mark.integration
def test_equity_screener(params, headers):
+ """Test the equity screener endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1357,6 +1467,7 @@ def test_equity_screener(params, headers):
)
@pytest.mark.integration
def test_equity_price_quote(params, headers):
+ """Test the equity price quote endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1379,6 +1490,7 @@ def test_equity_price_quote(params, headers):
)
@pytest.mark.integration
def test_equity_profile(params, headers):
+ """Test the equity profile endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1397,6 +1509,7 @@ def test_equity_profile(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_gainers(params, headers):
+ """Test the equity discovery gainers endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1412,6 +1525,7 @@ def test_equity_discovery_gainers(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_losers(params, headers):
+ """Test the equity discovery losers endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1427,6 +1541,7 @@ def test_equity_discovery_losers(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_active(params, headers):
+ """Test the equity discovery active endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1445,6 +1560,7 @@ def test_equity_discovery_active(params, headers):
)
@pytest.mark.integration
def test_equity_price_performance(params, headers):
+ """Test the equity price performance endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1460,6 +1576,7 @@ def test_equity_price_performance(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_undervalued_large_caps(params, headers):
+ """Test the equity discovery undervalued large caps endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1475,6 +1592,7 @@ def test_equity_discovery_undervalued_large_caps(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_undervalued_growth(params, headers):
+ """Test the equity discovery undervalued growth endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1490,6 +1608,7 @@ def test_equity_discovery_undervalued_growth(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_aggressive_small_caps(params, headers):
+ """Test the equity discovery aggressive small caps endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1507,6 +1626,7 @@ def test_equity_discovery_aggressive_small_caps(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_growth_tech(params, headers):
+ """Test the equity discovery growth tech endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1522,6 +1642,7 @@ def test_equity_discovery_growth_tech(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_top_retail(params, headers):
+ """Test the equity discovery top retail endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1537,6 +1658,7 @@ def test_equity_discovery_top_retail(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_upcoming_release_days(params, headers):
+ """Test the equity discovery upcoming release days endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1575,6 +1697,7 @@ def test_equity_discovery_upcoming_release_days(params, headers):
)
@pytest.mark.integration
def test_equity_discovery_filings(params, headers):
+ """Test the equity discovery filings endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1593,6 +1716,7 @@ def test_equity_discovery_filings(params, headers):
)
@pytest.mark.integration
def test_equity_shorts_fails_to_deliver(params, headers):
+ """Test the equity shorts fails to deliver endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1608,6 +1732,7 @@ def test_equity_shorts_fails_to_deliver(params, headers):
)
@pytest.mark.integration
def test_equity_shorts_short_volume(params, headers):
+ """Test the equity shorts short volume endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1623,6 +1748,7 @@ def test_equity_shorts_short_volume(params, headers):
)
@pytest.mark.integration
def test_equity_shorts_short_interest(params, headers):
+ """Test the equity shorts short interest endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1651,6 +1777,7 @@ def test_equity_shorts_short_interest(params, headers):
)
@pytest.mark.integration
def test_equity_price_nbbo(params, headers):
+ """Test the equity price NBBO endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1669,6 +1796,7 @@ def test_equity_price_nbbo(params, headers):
)
@pytest.mark.integration
def test_equity_darkpool_otc(params, headers):
+ """Test the equity darkpool otc endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1693,6 +1821,7 @@ def test_equity_darkpool_otc(params, headers):
)
@pytest.mark.integration
def test_equity_market_snapshots(params, headers):
+ """Test the equity market snapshots endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1718,6 +1847,7 @@ def test_equity_market_snapshots(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_historical_eps(params, headers):
+ """Test the equity fundamental historical eps endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1733,6 +1863,7 @@ def test_equity_fundamental_historical_eps(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_trailing_dividend_yield(params, headers):
+ """Test the equity fundamental trailing dividend yield endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1779,6 +1910,7 @@ def test_equity_fundamental_trailing_dividend_yield(params, headers):
)
@pytest.mark.integration
def test_equity_fundamental_reported_financials(params, headers):
+ """Test the equity fundamental reported financials endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -1805,6 +1937,7 @@ def test_equity_fundamental_reported_financials(params, headers):
)
@pytest.mark.integration
def test_equity_ownership_form_13f(params, headers):
+ """Test the equity ownership form 13f endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py
index 671df076840..9b0f5bb530d 100644
--- a/openbb_platform/extensions/equity/integration/test_equity_python.py
+++ b/openbb_platform/extensions/equity/integration/test_equity_python.py
@@ -73,6 +73,7 @@ def obb(pytestconfig):
)
@pytest.mark.integration
def test_equity_fundamental_balance(params, obb):
+ """Test the equity fundamental balance endpoint."""
result = obb.equity.fundamental.balance(**params)
assert result
assert isinstance(result, OBBject)
@@ -87,6 +88,7 @@ def test_equity_fundamental_balance(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_balance_growth(params, obb):
+ """Test the equity fundamental balance growth endpoint."""
result = obb.equity.fundamental.balance_growth(**params)
assert result
assert isinstance(result, OBBject)
@@ -102,6 +104,7 @@ def test_equity_fundamental_balance_growth(params, obb):
)
@pytest.mark.integration
def test_equity_calendar_dividend(params, obb):
+ """Test the equity calendar dividend endpoint."""
result = obb.equity.calendar.dividend(**params)
assert result
assert isinstance(result, OBBject)
@@ -116,6 +119,7 @@ def test_equity_calendar_dividend(params, obb):
)
@pytest.mark.integration
def test_equity_calendar_splits(params, obb):
+ """Test the equity calendar splits endpoint."""
result = obb.equity.calendar.splits(**params)
assert result
assert isinstance(result, OBBject)
@@ -132,6 +136,7 @@ def test_equity_calendar_splits(params, obb):
)
@pytest.mark.integration
def test_equity_calendar_earnings(params, obb):
+ """Test the equity calendar earnings endpoint."""
result = obb.equity.calendar.earnings(**params)
assert result
assert isinstance(result, OBBject)
@@ -191,6 +196,7 @@ def test_equity_calendar_earnings(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_cash(params, obb):
+ """Test the equity fundamental cash endpoint."""
result = obb.equity.fundamental.cash(**params)
assert result
assert isinstance(result, OBBject)
@@ -205,6 +211,7 @@ def test_equity_fundamental_cash(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_cash_growth(params, obb):
+ """Test the equity fundamental cash growth endpoint."""
result = obb.equity.fundamental.cash_growth(**params)
assert result
assert isinstance(result, OBBject)
@@ -239,6 +246,7 @@ def test_equity_fundamental_cash_growth(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_management_compensation(params, obb):
+ """Test the equity fundamental management compensation endpoint."""
result = obb.equity.fundamental.management_compensation(**params)
assert result
assert isinstance(result, OBBject)
@@ -253,6 +261,7 @@ def test_equity_fundamental_management_compensation(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_historical_splits(params, obb):
+ """Test the equity fundamental historical splits endpoint."""
result = obb.equity.fundamental.historical_splits(**params)
assert result
assert isinstance(result, OBBject)
@@ -311,6 +320,7 @@ def test_equity_fundamental_historical_splits(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_dividends(params, obb):
+ """Test the equity fundamental dividends endpoint."""
result = obb.equity.fundamental.dividends(**params)
assert result
assert isinstance(result, OBBject)
@@ -325,6 +335,7 @@ def test_equity_fundamental_dividends(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_employee_count(params, obb):
+ """Test the equity fundamental employee count endpoint."""
result = obb.equity.fundamental.employee_count(**params)
assert result
assert isinstance(result, OBBject)
@@ -334,11 +345,12 @@ def test_equity_fundamental_employee_count(params, obb):
@parametrize(
"params",
[
- ({"symbol": "AAPL", "period": "annual", "limit": 30}),
+ ({"symbol": "AAPL,MSFT", "period": "annual", "limit": 30}),
],
)
@pytest.mark.integration
def test_equity_estimates_historical(params, obb):
+ """Test the equity estimates historical endpoint."""
result = obb.equity.estimates.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -399,6 +411,7 @@ def test_equity_estimates_historical(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_income(params, obb):
+ """Test the equity fundamental income endpoint."""
result = obb.equity.fundamental.income(**params)
assert result
assert isinstance(result, OBBject)
@@ -411,6 +424,7 @@ def test_equity_fundamental_income(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_income_growth(params, obb):
+ """Test the equity fundamental income growth endpoint."""
result = obb.equity.fundamental.income_growth(**params)
assert result
assert isinstance(result, OBBject)
@@ -450,6 +464,7 @@ def test_equity_fundamental_income_growth(params, obb):
)
@pytest.mark.integration
def test_equity_ownership_insider_trading(params, obb):
+ """Test the equity ownership insider trading endpoint."""
result = obb.equity.ownership.insider_trading(**params)
assert result
assert isinstance(result, OBBject)
@@ -479,6 +494,7 @@ def test_equity_ownership_insider_trading(params, obb):
)
@pytest.mark.integration
def test_equity_ownership_institutional(params, obb):
+ """Test the equity ownership institutional endpoint."""
result = obb.equity.ownership.institutional(**params)
assert result
assert isinstance(result, OBBject)
@@ -510,6 +526,7 @@ def test_equity_ownership_institutional(params, obb):
)
@pytest.mark.integration
def test_equity_calendar_ipo(params, obb):
+ """Test the equity calendar IPO endpoint."""
result = obb.equity.calendar.ipo(**params)
assert result
assert isinstance(result, OBBject)
@@ -536,6 +553,7 @@ def test_equity_calendar_ipo(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_metrics(params, obb):
+ """Test the equity fundamental metrics endpoint."""
result = obb.equity.fundamental.metrics(**params)
assert result
assert isinstance(result, OBBject)
@@ -554,6 +572,7 @@ def test_equity_fundamental_metrics(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_management(params, obb):
+ """Test the equity fundamental management endpoint."""
result = obb.equity.fundamental.management(**params)
assert result
assert isinstance(result, OBBject)
@@ -568,6 +587,7 @@ def test_equity_fundamental_management(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_overview(params, obb):
+ """Test the equity fundamental overview endpoint."""
result = obb.equity.fundamental.overview(**params)
assert result
assert isinstance(result, OBBject)
@@ -582,6 +602,7 @@ def test_equity_fundamental_overview(params, obb):
)
@pytest.mark.integration
def test_equity_ownership_major_holders(params, obb):
+ """Test the equity ownership major holders endpoint."""
result = obb.equity.ownership.major_holders(**params)
assert result
assert isinstance(result, OBBject)
@@ -615,6 +636,7 @@ def test_equity_ownership_major_holders(params, obb):
)
@pytest.mark.integration
def test_equity_estimates_price_target(params, obb):
+ """Test the equity estimates price target endpoint."""
result = obb.equity.estimates.price_target(**params)
assert result
assert isinstance(result, OBBject)
@@ -640,6 +662,7 @@ def test_equity_estimates_price_target(params, obb):
)
@pytest.mark.integration
def test_equity_estimates_analyst_search(params, obb):
+ """Test the equity estimates analyst search endpoint."""
result = obb.equity.estimates.analyst_search(**params)
assert result
assert isinstance(result, OBBject)
@@ -652,16 +675,81 @@ def test_equity_estimates_analyst_search(params, obb):
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL,AMZN,RELIANCE.NS", "provider": "yfinance"}),
({"symbol": "TD:US", "provider": "tmx"}),
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "industry_group_number": None,
+ "provider": "intrinio",
+ }
+ ),
],
)
@pytest.mark.integration
def test_equity_estimates_consensus(params, obb):
+ """Test the equity estimates consensus endpoint."""
result = obb.equity.estimates.consensus(**params)
assert result
assert isinstance(result, OBBject)
assert result.results is not None
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "fy",
+ "fiscal_year": None,
+ "calendar_year": None,
+ "calendar_period": None,
+ "provider": "intrinio",
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_equity_estimates_forward_sales(params, obb):
+ """Test the equity estimates forward sales endpoint."""
+ result = obb.equity.estimates.forward_sales(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "fy",
+ "fiscal_year": None,
+ "calendar_year": None,
+ "calendar_period": None,
+ "provider": "intrinio",
+ }
+ ),
+ (
+ {
+ "symbol": "AAPL,MSFT",
+ "fiscal_period": "annual",
+ "limit": None,
+ "include_historical": False,
+ "provider": "fmp",
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_equity_estimates_forward_eps(params, obb):
+ """Test the equity estimates forward EPS endpoint."""
+ result = obb.equity.estimates.forward_eps(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+
+
@parametrize(
"params",
[
@@ -679,6 +767,7 @@ def test_equity_estimates_consensus(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_ratios(params, obb):
+ """Test the equity fundamental ratios endpoint."""
result = obb.equity.fundamental.ratios(**params)
assert result
assert isinstance(result, OBBject)
@@ -700,6 +789,7 @@ def test_equity_fundamental_ratios(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_revenue_per_geography(params, obb):
+ """Test the equity fundamental revenue per geography endpoint."""
result = obb.equity.fundamental.revenue_per_geography(**params)
assert result
assert isinstance(result, OBBject)
@@ -721,6 +811,7 @@ def test_equity_fundamental_revenue_per_geography(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_revenue_per_segment(params, obb):
+ """Test the equity fundamental revenue per segment endpoint."""
result = obb.equity.fundamental.revenue_per_segment(**params)
assert result
assert isinstance(result, OBBject)
@@ -774,6 +865,7 @@ def test_equity_fundamental_revenue_per_segment(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_filings(params, obb):
+ """Test the equity fundamental filings endpoint."""
result = obb.equity.fundamental.filings(**params)
assert result
assert isinstance(result, OBBject)
@@ -790,6 +882,7 @@ def test_equity_fundamental_filings(params, obb):
)
@pytest.mark.integration
def test_equity_ownership_share_statistics(params, obb):
+ """Test the equity ownership share statistics endpoint."""
result = obb.equity.ownership.share_statistics(**params)
assert result
assert isinstance(result, OBBject)
@@ -804,6 +897,7 @@ def test_equity_ownership_share_statistics(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_transcript(params, obb):
+ """Test the equity fundamental transcript endpoint."""
result = obb.equity.fundamental.transcript(**params)
assert result
assert isinstance(result, OBBject)
@@ -818,6 +912,7 @@ def test_equity_fundamental_transcript(params, obb):
)
@pytest.mark.integration
def test_equity_compare_peers(params, obb):
+ """Test the equity compare peers endpoint."""
result = obb.equity.compare.peers(**params)
assert result
assert isinstance(result, OBBject)
@@ -830,6 +925,7 @@ def test_equity_compare_peers(params, obb):
)
@pytest.mark.integration
def test_equity_compare_groups(params, obb):
+ """Test the equity compare groups endpoint."""
result = obb.equity.compare.groups(**params)
assert result
assert isinstance(result, OBBject)
@@ -1020,6 +1116,7 @@ def test_equity_compare_groups(params, obb):
)
@pytest.mark.integration
def test_equity_price_historical(params, obb):
+ """Test the equity price historical endpoint."""
result = obb.equity.price.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -1034,6 +1131,7 @@ def test_equity_price_historical(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_multiples(params, obb):
+ """Test the equity fundamental multiples endpoint."""
result = obb.equity.fundamental.multiples(**params)
assert result
assert isinstance(result, OBBject)
@@ -1048,6 +1146,7 @@ def test_equity_fundamental_multiples(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_search_attributes(params, obb):
+ """Test the equity fundamental search attributes endpoint."""
result = obb.equity.fundamental.search_attributes(**params)
assert result
assert isinstance(result, OBBject)
@@ -1126,6 +1225,7 @@ def test_equity_fundamental_search_attributes(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_historical_attributes(params, obb):
+ """Test the equity fundamental historical attributes endpoint."""
result = obb.equity.fundamental.historical_attributes(**params)
assert result
assert isinstance(result, OBBject)
@@ -1181,6 +1281,7 @@ def test_equity_fundamental_historical_attributes(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_latest_attributes(params, obb):
+ """Test the equity fundamental latest attributes endpoint."""
result = obb.equity.fundamental.latest_attributes(**params)
assert result
assert isinstance(result, OBBject)
@@ -1211,6 +1312,7 @@ def test_equity_fundamental_latest_attributes(params, obb):
)
@pytest.mark.integration
def test_equity_search(params, obb):
+ """Test the equity search endpoint."""
result = obb.equity.search(**params)
assert result
assert isinstance(result, OBBject)
@@ -1246,6 +1348,7 @@ def test_equity_search(params, obb):
)
@pytest.mark.integration
def test_equity_screener(params, obb):
+ """Test the equity screener endpoint."""
result = obb.equity.screener(**params)
assert result
assert isinstance(result, OBBject)
@@ -1266,6 +1369,7 @@ def test_equity_screener(params, obb):
)
@pytest.mark.integration
def test_equity_price_quote(params, obb):
+ """Test the equity price quote endpoint."""
result = obb.equity.price.quote(**params)
assert result
assert isinstance(result, OBBject)
@@ -1285,6 +1389,7 @@ def test_equity_price_quote(params, obb):
)
@pytest.mark.integration
def test_equity_profile(params, obb):
+ """Test the equity profile endpoint."""
result = obb.equity.profile(**params)
assert result
assert isinstance(result, OBBject)
@@ -1303,6 +1408,7 @@ def test_equity_profile(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_gainers(params, obb):
+ """Test the equity discovery gainers endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.gainers(**params)
@@ -1317,6 +1423,7 @@ def test_equity_discovery_gainers(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_losers(params, obb):
+ """Test the equity discovery losers endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.losers(**params)
@@ -1331,6 +1438,7 @@ def test_equity_discovery_losers(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_active(params, obb):
+ """Test the equity discovery active endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.active(**params)
@@ -1348,6 +1456,7 @@ def test_equity_discovery_active(params, obb):
)
@pytest.mark.integration
def test_equity_price_performance(params, obb):
+ """Test the equity price performance endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.price.performance(**params)
@@ -1362,6 +1471,7 @@ def test_equity_price_performance(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_undervalued_large_caps(params, obb):
+ """Test the equity discovery undervalued large caps endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.undervalued_large_caps(**params)
@@ -1376,6 +1486,7 @@ def test_equity_discovery_undervalued_large_caps(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_undervalued_growth(params, obb):
+ """Test the equity discovery undervalued growth endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.undervalued_growth(**params)
@@ -1390,6 +1501,7 @@ def test_equity_discovery_undervalued_growth(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_aggressive_small_caps(params, obb):
+ """Test the equity discovery aggressive small caps endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.aggressive_small_caps(**params)
@@ -1404,6 +1516,7 @@ def test_equity_discovery_aggressive_small_caps(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_growth_tech(params, obb):
+ """Test the equity discovery growth tech endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.growth_tech(**params)
@@ -1418,6 +1531,7 @@ def test_equity_discovery_growth_tech(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_top_retail(params, obb):
+ """Test the equity discovery top retail endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.top_retail(**params)
@@ -1432,6 +1546,7 @@ def test_equity_discovery_top_retail(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_upcoming_release_days(params, obb):
+ """Test the equity discovery upcoming release days endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.upcoming_release_days(**params)
@@ -1467,6 +1582,7 @@ def test_equity_discovery_upcoming_release_days(params, obb):
)
@pytest.mark.integration
def test_equity_discovery_filings(params, obb):
+ """Test the equity discovery filings endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.discovery.filings(**params)
@@ -1487,6 +1603,7 @@ def test_equity_discovery_filings(params, obb):
)
@pytest.mark.integration
def test_equity_shorts_fails_to_deliver(params, obb):
+ """Test the equity shorts fails to deliver endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.shorts.fails_to_deliver(**params)
@@ -1501,6 +1618,7 @@ def test_equity_shorts_fails_to_deliver(params, obb):
)
@pytest.mark.integration
def test_equity_shorts_short_volume(params, obb):
+ """Test the equity shorts short volume endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.shorts.short_volume(**params)
@@ -1515,6 +1633,7 @@ def test_equity_shorts_short_volume(params, obb):
)
@pytest.mark.integration
def test_equity_shorts_short_interest(params, obb):
+ """Test the equity shorts short interest endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.shorts.short_interest(**params)
@@ -1542,6 +1661,7 @@ def test_equity_shorts_short_interest(params, obb):
)
@pytest.mark.integration
def test_equity_price_nbbo(params, obb):
+ """Test the equity price nbbo endpoint."""
result = obb.equity.price.nbbo(**params)
assert result
assert isinstance(result, OBBject)
@@ -1557,6 +1677,7 @@ def test_equity_price_nbbo(params, obb):
)
@pytest.mark.integration
def test_equity_darkpool_otc(params, obb):
+ """Test the equity darkpool otc endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.darkpool.otc(**params)
@@ -1575,6 +1696,7 @@ def test_equity_darkpool_otc(params, obb):
)
@pytest.mark.integration
def test_equity_market_snapshots(params, obb):
+ """Test the equity market snapshots endpoint."""
result = obb.equity.market_snapshots(**params)
assert result
assert isinstance(result, OBBject)
@@ -1597,6 +1719,7 @@ def test_equity_market_snapshots(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_historical_eps(params, obb):
+ """Test the equity fundamental historical eps endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.fundamental.historical_eps(**params)
@@ -1611,6 +1734,7 @@ def test_equity_fundamental_historical_eps(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_trailing_dividend_yield(params, obb):
+ """Test the equity fundamental trailing dividend yield endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.fundamental.trailing_dividend_yield(**params)
@@ -1656,6 +1780,7 @@ def test_equity_fundamental_trailing_dividend_yield(params, obb):
)
@pytest.mark.integration
def test_equity_fundamental_reported_financials(params, obb):
+ """Test the equity fundamental reported financials endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.fundamental.reported_financials(**params)
@@ -1679,6 +1804,7 @@ def test_equity_fundamental_reported_financials(params, obb):
)
@pytest.mark.integration
def test_equity_ownership_form_13f(params, obb):
+ """Test the equity ownership form 13f endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.equity.ownership.form_13f(**params)
diff --git a/openbb_platform/extensions/equity/openbb_equity/compare/compare_router.py b/openbb_platform/extensions/equity/openbb_equity/compare/compare_router.py
index 2f585055e3f..1737cd2dd08 100644
--- a/openbb_platform/extensions/equity/openbb_equity/compare/compare_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/compare/compare_router.py
@@ -28,7 +28,8 @@ async def peers(
"""Get the closest peers for a given company.
Peers consist of companies trading on the same exchange, operating within the same sector
- and with comparable market capitalizations."""
+ and with comparable market capitalizations.
+ """
return await OBBject.from_query(Query(**locals()))
@@ -67,5 +68,6 @@ async def groups(
"""Get company data grouped by sector, industry or country and display either performance or valuation metrics.
Valuation metrics include price to earnings, price to book, price to sales ratios and price to cash flow.
- Performance metrics include the stock price change for different time periods."""
+ Performance metrics include the stock price change for different time periods.
+ """
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/equity/openbb_equity/darkpool/darkpool_router.py b/openbb_platform/extensions/equity/openbb_equity/darkpool/darkpool_router.py
index 67307cbeec0..e9db292bceb 100644
--- a/openbb_platform/extensions/equity/openbb_equity/darkpool/darkpool_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/darkpool/darkpool_router.py
@@ -35,5 +35,6 @@ async def otc(
"""Get the weekly aggregate trade data for Over The Counter deals.
ATS and non-ATS trading data for each ATS/firm
- with trade reporting obligations under FINRA rules."""
+ with trade reporting obligations under FINRA rules.
+ """
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py b/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py
index 86ccde2857d..b8fbbc1a7be 100644
--- a/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py
@@ -144,10 +144,11 @@ async def top_retail(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Tracks over $30B USD/day of individual investors trades.
+ """Track over $30B USD/day of individual investors trades.
It gives a daily view into retail activity and sentiment for over 9,500 US traded stocks,
- ADRs, and ETPs."""
+ ADRs, and ETPs.
+ """
return await OBBject.from_query(Query(**locals()))
@@ -186,8 +187,9 @@ async def filings(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Get the URLs to SEC filings reported to EDGAR database, such as 10-K, 10-Q, 8-K, and more. SEC
- filings include Form 10-K, Form 10-Q, Form 8-K, the proxy statement, Forms 3, 4, and 5, Schedule 13, Form 114,
+ """Get the URLs to SEC filings reported to EDGAR database, such as 10-K, 10-Q, 8-K, and more.
+
+ SEC filings include Form 10-K, Form 10-Q, Form 8-K, the proxy statement, Forms 3, 4, and 5, Schedule 13, Form 114,
Foreign Investment Disclosures and others. The annual 10-K report is required to be
filed annually and includes the company's financial statements, management discussion and analysis,
and audited financial statements.
diff --git a/openbb_platform/extensions/equity/openbb_equity/equity_router.py b/openbb_platform/extensions/equity/openbb_equity/equity_router.py
index 9da54b7678c..1ba9b021587 100644
--- a/openbb_platform/extensions/equity/openbb_equity/equity_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/equity_router.py
@@ -21,7 +21,7 @@ from openbb_equity.ownership.ownership_router import router as ownership_router
from openbb_equity.price.price_router import router as price_router
from openbb_equity.shorts.shorts_router import router as shorts_router
-router = Router(prefix="")
+router = Router(prefix="", description="Equity market data.")
router.include_router(calendar_router)
router.include_router(compare_router)
router.include_router(estimates_router)
@@ -68,8 +68,10 @@ async def screener(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Screen for companies meeting various criteria. These criteria include
- market cap, price, beta, volume, and dividend yield."""
+ """Screen for companies meeting various criteria.
+
+ These criteria include market cap, price, beta, volume, and dividend yield.
+ """
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py b/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py
index 07c328c334c..d321c3a9d31 100644
--- a/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py
@@ -91,3 +91,49 @@ async def analyst_search(
) -> OBBject:
"""Search for specific analysts and get their forecast track record."""
return await OBBject.from_query(Query(**locals()))
+
+
+@router.command(
+ model="ForwardSalesEstimates",
+ examples=[
+ APIEx(parameters={"symbol": "AAPL", "provider": "intrinio"}),
+ APIEx(
+ parameters={
+ "fiscal_year": 2025,
+ "fiscal_period": "fy",
+ "provider": "intrinio",
+ }
+ ),
+ ],
+)
+async def forward_sales(
+ cc: CommandContext,
+ provider_choices: ProviderChoices,
+ standard_params: StandardParams,
+ extra_params: ExtraParams,
+) -> OBBject:
+ """Get forward sales estimates."""
+ return await OBBject.from_query(Query(**locals()))
+
+
+@router.command(
+ model="ForwardEpsEstimates",
+ examples=[
+ APIEx(parameters={"symbol": "AAPL", "provider": "intrinio"}),
+ APIEx(
+ parameters={
+ "fiscal_year": 2025,
+ "fiscal_period": "fy",
+ "provider": "intrinio",
+ }
+ ),
+ ],
+)
+async def forward_eps(
+ cc: CommandContext,
+ provider_choices: ProviderChoices,
+ standard_params: StandardParams,
+ extra_params: ExtraParams,
+) -> OBBject:
+ """Get forward EPS estimates."""
+ return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/equity/openbb_equity/ownership/ownership_router.py b/openbb_platform/extensions/equity/openbb_equity/ownership/ownership_router.py
index 8095164823e..4f118d27b63 100644
--- a/openbb_platform/extensions/equity/openbb_equity/ownership/ownership_router.py
+++ b/openbb_platform/extensions/equity/openbb_equity/ownership/ownership_router.py
@@ -102,7 +102,8 @@ async def form_13f(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """
+ """Get the form 13F.
+
The Securities and Exchange Commission's (SEC) Form 13F is a quarterly report
that is required to be filed by all institutional investment managers with at least
$100 million in assets under management.
diff --git a/openbb_platform/extensions/equity/poetry.lock b/openbb_platform/extensions/equity/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/equity/poetry.lock
+++ b/openbb_platform/extensions/equity/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/equity/pyproject.toml b/openbb_platform/extensions/equity/pyproject.toml
index 0b4702b29d6..18b460a6f12 100644
--- a/openbb_platform/extensions/equity/pyproject.toml
+++ b/openbb_platform/extensions/equity/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-equity"
-version = "1.1.4"
+version = "1.1.5"
description = "Equity extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_equity" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/etf/integration/test_etf_api.py b/openbb_platform/extensions/etf/integration/test_etf_api.py
index 850998dd540..04029f6d6d9 100644
--- a/openbb_platform/extensions/etf/integration/test_etf_api.py
+++ b/openbb_platform/extensions/etf/integration/test_etf_api.py
@@ -1,3 +1,5 @@
+"""Integration tests for the ETF API."""
+
import base64
import pytest
@@ -9,6 +11,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -43,6 +46,7 @@ def headers():
)
@pytest.mark.integration
def test_etf_search(params, headers):
+ """Test the ETF search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -227,6 +231,7 @@ def test_etf_search(params, headers):
)
@pytest.mark.integration
def test_etf_historical(params, headers):
+ """Test the ETF historical endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -247,6 +252,7 @@ def test_etf_historical(params, headers):
)
@pytest.mark.integration
def test_etf_info(params, headers):
+ """Test the ETF info endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -265,6 +271,7 @@ def test_etf_info(params, headers):
)
@pytest.mark.integration
def test_etf_sectors(params, headers):
+ """Test the ETF sectors endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -282,6 +289,7 @@ def test_etf_sectors(params, headers):
)
@pytest.mark.integration
def test_etf_holdings_date(params, headers):
+ """Test the ETF holdings date endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -337,12 +345,21 @@ def test_etf_holdings_date(params, headers):
{
"symbol": "DJIA",
"provider": "intrinio",
+ "date": None,
+ }
+ ),
+ (
+ {
+ "symbol": "QQQ",
+ "provider": "intrinio",
+ "date": "2020-04-03",
}
),
],
)
@pytest.mark.integration
def test_etf_holdings(params, headers):
+ """Test the ETF holdings endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -369,6 +386,7 @@ def test_etf_holdings(params, headers):
)
@pytest.mark.integration
def test_etf_price_performance(params, headers):
+ """Test the ETF price performance endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -387,6 +405,7 @@ def test_etf_price_performance(params, headers):
)
@pytest.mark.integration
def test_etf_countries(params, headers):
+ """Test the ETF countries endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -402,6 +421,7 @@ def test_etf_countries(params, headers):
)
@pytest.mark.integration
def test_etf_discovery_gainers(params, headers):
+ """Test the ETF discovery gainers endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -417,6 +437,7 @@ def test_etf_discovery_gainers(params, headers):
)
@pytest.mark.integration
def test_etf_discovery_losers(params, headers):
+ """Test the ETF discovery losers endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -432,6 +453,7 @@ def test_etf_discovery_losers(params, headers):
)
@pytest.mark.integration
def test_etf_discovery_active(params, headers):
+ """Test the ETF discovery active endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -450,6 +472,7 @@ def test_etf_discovery_active(params, headers):
)
@pytest.mark.integration
def test_etf_holdings_performance(params, headers):
+ """Test the ETF holdings performance endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -467,6 +490,7 @@ def test_etf_holdings_performance(params, headers):
)
@pytest.mark.integration
def test_etf_equity_exposure(params, headers):
+ """Test the ETF equity exposure endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/etf/integration/test_etf_python.py b/openbb_platform/extensions/etf/integration/test_etf_python.py
index f3d966b831b..ed8ba2f99ce 100644
--- a/openbb_platform/extensions/etf/integration/test_etf_python.py
+++ b/openbb_platform/extensions/etf/integration/test_etf_python.py
@@ -42,6 +42,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_etf_search(params, obb):
+ """Test the ETF search endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.search(**params)
@@ -225,6 +226,7 @@ def test_etf_search(params, obb):
)
@pytest.mark.integration
def test_etf_historical(params, obb):
+ """Test the ETF historical endpoint."""
result = obb.equity.price.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -242,6 +244,7 @@ def test_etf_historical(params, obb):
)
@pytest.mark.integration
def test_etf_info(params, obb):
+ """Test the ETF info endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.info(**params)
@@ -259,6 +262,7 @@ def test_etf_info(params, obb):
)
@pytest.mark.integration
def test_etf_sectors(params, obb):
+ """Test the ETF sectors endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.sectors(**params)
@@ -275,6 +279,7 @@ def test_etf_sectors(params, obb):
)
@pytest.mark.integration
def test_etf_holdings_date(params, obb):
+ """Test the ETF holdings date endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.holdings_date(**params)
@@ -329,12 +334,21 @@ def test_etf_holdings_date(params, obb):
{
"symbol": "DJIA",
"provider": "intrinio",
+ "date": None,
+ }
+ ),
+ (
+ {
+ "symbol": "QQQ",
+ "provider": "intrinio",
+ "date": "2020-04-03",
}
),
],
)
@pytest.mark.integration
def test_etf_holdings(params, obb):
+ """Test the ETF holdings endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.holdings(**params)
@@ -360,6 +374,7 @@ def test_etf_holdings(params, obb):
)
@pytest.mark.integration
def test_etf_price_performance(params, obb):
+ """Test the ETF price performance endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.price_performance(**params)
@@ -377,6 +392,7 @@ def test_etf_price_performance(params, obb):
)
@pytest.mark.integration
def test_etf_countries(params, obb):
+ """Test the ETF countries endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.countries(**params)
@@ -391,6 +407,7 @@ def test_etf_countries(params, obb):
)
@pytest.mark.integration
def test_etf_discovery_gainers(params, obb):
+ """Test the ETF discovery gainers endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.discovery.gainers(**params)
@@ -405,6 +422,7 @@ def test_etf_discovery_gainers(params, obb):
)
@pytest.mark.integration
def test_etf_discovery_losers(params, obb):
+ """Test the ETF discovery losers endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.discovery.losers(**params)
@@ -419,6 +437,7 @@ def test_etf_discovery_losers(params, obb):
)
@pytest.mark.integration
def test_etf_discovery_active(params, obb):
+ """Test the ETF discovery active endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.discovery.active(**params)
@@ -436,6 +455,7 @@ def test_etf_discovery_active(params, obb):
)
@pytest.mark.integration
def test_etf_holdings_performance(params, obb):
+ """Test the ETF holdings performance endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.holdings_performance(**params)
@@ -452,6 +472,7 @@ def test_etf_holdings_performance(params, obb):
)
@pytest.mark.integration
def test_etf_equity_exposure(params, obb):
+ """Test the ETF equity exposure endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.etf.equity_exposure(**params)
diff --git a/openbb_platform/extensions/etf/openbb_etf/etf_router.py b/openbb_platform/extensions/etf/openbb_etf/etf_router.py
index 3476644510d..52c367864fe 100644
--- a/openbb_platform/extensions/etf/openbb_etf/etf_router.py
+++ b/openbb_platform/extensions/etf/openbb_etf/etf_router.py
@@ -1,5 +1,6 @@
"""ETF Router."""
+from openbb_core.app.deprecation import OpenBBDeprecationWarning
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.obbject import OBBject
@@ -13,7 +14,7 @@ from openbb_core.app.router import Router
from openbb_etf.discovery.discovery_router import router as discovery_router
-router = Router(prefix="")
+router = Router(prefix="", description="Exchange Traded Funds market data.")
router.include_router(discovery_router)
# pylint: disable=unused-argument
@@ -172,6 +173,13 @@ async def holdings_date(
@router.command(
model="EtfHoldingsPerformance",
+ deprecated=True,
+ deprecation=OpenBBDeprecationWarning(
+ message="This endpoint is deprecated; pass a list of holdings symbols directly to"
+ + " `/equity/price/performance` instead.",
+ since=(4, 1),
+ expected_removal=(4, 2),
+ ),
examples=[APIEx(parameters={"symbol": "XLK", "provider": "fmp"})],
)
async def holdings_performance(
diff --git a/openbb_platform/extensions/etf/poetry.lock b/openbb_platform/extensions/etf/poetry.lock
index 36725a9dbe9..c19757ae31a 100644
--- a/openbb_platform/extensions/etf/poetry.lock
+++ b/openbb_platform/extensions/etf/poetry.lock
@@ -678,13 +678,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1397,4 +1397,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<3.12"
-content-hash = "75f551243cc84ea3dcba416d86f4ef5669b5b8b5fcf4781a0d8e3e69360b28e4"
+content-hash = "df0539c34df30b9f361fb9099e221e5a86ba5c1ea1451a0748d7b87dba2203b0"
diff --git a/openbb_platform/extensions/etf/pyproject.toml b/openbb_platform/extensions/etf/pyproject.toml
index 05f6486a21e..0cc625ad03a 100644
--- a/openbb_platform/extensions/etf/pyproject.toml
+++ b/openbb_platform/extensions/etf/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-etf"
-version = "1.1.4"
+version = "1.1.5"
description = "ETF extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_etf" }]
[tool.poetry.dependencies]
python = ">=3.8,<3.12"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py
index 95b759beaac..da9c88c77c4 100644
--- a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py
+++ b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py
@@ -1,3 +1,5 @@
+"""Test fixedincome API endpoints."""
+
import base64
import pytest
@@ -11,6 +13,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -24,6 +27,7 @@ def headers():
)
@pytest.mark.integration
def test_fixedincome_government_treasury_rates(params, headers):
+ """Test the treasury rates endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -41,6 +45,7 @@ def test_fixedincome_government_treasury_rates(params, headers):
)
@pytest.mark.integration
def test_fixedincome_government_us_yield_curve(params, headers):
+ """Test the US yield curve endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -68,6 +73,7 @@ def test_fixedincome_government_us_yield_curve(params, headers):
)
@pytest.mark.integration
def test_fixedincome_sofr(params, headers):
+ """Test the SOFR endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -93,6 +99,7 @@ def test_fixedincome_sofr(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_estr(params, headers):
+ """Test the ESTR rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -118,6 +125,7 @@ def test_fixedincome_rate_estr(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_sonia(params, headers):
+ """Test the SONIA rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -143,6 +151,7 @@ def test_fixedincome_rate_sonia(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_ameribor(params, headers):
+ """Test the Ameribor rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -174,6 +183,7 @@ def test_fixedincome_rate_ameribor(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_effr(params, headers):
+ """Test the EFFR rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -189,6 +199,7 @@ def test_fixedincome_rate_effr(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_effr_forecast(params, headers):
+ """Test the EFFR forecast rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -204,6 +215,7 @@ def test_fixedincome_rate_effr_forecast(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_iorb(params, headers):
+ """Test the IORB rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -229,6 +241,7 @@ def test_fixedincome_rate_iorb(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_dpcredit(params, headers):
+ """Test the DPCredit rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -252,6 +265,7 @@ def test_fixedincome_rate_dpcredit(params, headers):
)
@pytest.mark.integration
def test_fixedincome_rate_ecb(params, headers):
+ """Test the ECB rate endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -281,6 +295,7 @@ def test_fixedincome_rate_ecb(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_ice_bofa(params, headers):
+ """Test the ICE BofA corporate yield index endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -296,6 +311,7 @@ def test_fixedincome_corporate_ice_bofa(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_moody(params, headers):
+ """Test the Moody's corporate yield index endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -322,6 +338,7 @@ def test_fixedincome_corporate_moody(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_commercial_paper(params, headers):
+ """Test the commercial paper endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -365,6 +382,7 @@ def test_fixedincome_corporate_commercial_paper(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_spot_rates(params, headers):
+ """Test the corporate spot rates endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -380,6 +398,7 @@ def test_fixedincome_corporate_spot_rates(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_hqm(params, headers):
+ """Test the HQM corporate yield curve endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -395,6 +414,7 @@ def test_fixedincome_corporate_hqm(params, headers):
)
@pytest.mark.integration
def test_fixedincome_spreads_tcm(params, headers):
+ """Test the TCM spreads endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -419,6 +439,7 @@ def test_fixedincome_spreads_tcm(params, headers):
)
@pytest.mark.integration
def test_fixedincome_spreads_tcm_effr(params, headers):
+ """Test the TCM EFFR spreads endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -443,6 +464,7 @@ def test_fixedincome_spreads_tcm_effr(params, headers):
)
@pytest.mark.integration
def test_fixedincome_spreads_treasury_effr(params, headers):
+ """Test the treasury EFFR spreads endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -467,6 +489,7 @@ def test_fixedincome_spreads_treasury_effr(params, headers):
)
@pytest.mark.integration
def test_fixedincome_government_eu_yield_curve(params, headers):
+ """Test the EU yield curve endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -507,6 +530,7 @@ def test_fixedincome_government_eu_yield_curve(params, headers):
)
@pytest.mark.integration
def test_fixedincome_government_treasury_auctions(params, headers):
+ """Test the treasury auctions endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -552,13 +576,14 @@ def test_fixedincome_government_treasury_auctions(params, headers):
)
@pytest.mark.integration
def test_fixedincome_government_treasury_prices(params, headers):
+ """Test the treasury prices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = (
f"http://0.0.0.0:8000/api/v1/fixedincome/government/treasury_prices?{query_str}"
)
- result = requests.get(url, headers=headers, timeout=30)
+ result = requests.get(url, headers=headers, timeout=40)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -590,10 +615,11 @@ def test_fixedincome_government_treasury_prices(params, headers):
)
@pytest.mark.integration
def test_fixedincome_corporate_bond_prices(params, headers):
+ """Test the corporate bond prices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/fixedincome/corporate/bond_prices?{query_str}"
- result = requests.get(url, headers=headers, timeout=30)
+ result = requests.get(url, headers=headers, timeout=40)
assert isinstance(result, requests.Response)
assert result.status_code == 200
diff --git a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py
index 40c73bf077d..57ba30d7fad 100644
--- a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py
+++ b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py
@@ -26,6 +26,7 @@ def obb(pytestconfig):
)
@pytest.mark.integration
def test_fixedincome_government_treasury_rates(params, obb):
+ """Test the treasury rates endpoint."""
result = obb.fixedincome.government.treasury_rates(**params)
assert result
assert isinstance(result, OBBject)
@@ -40,6 +41,7 @@ def test_fixedincome_government_treasury_rates(params, obb):
)
@pytest.mark.integration
def test_fixedincome_government_us_yield_curve(params, obb):
+ """Test the US yield curve endpoint."""
result = obb.fixedincome.government.us_yield_curve(**params)
assert result
assert isinstance(result, OBBject)
@@ -62,6 +64,7 @@ def test_fixedincome_government_us_yield_curve(params, obb):
)
@pytest.mark.integration
def test_fixedincome_sofr(params, obb):
+ """Test the SOFR endpoint."""
result = obb.fixedincome.sofr(**params)
assert result
assert isinstance(result, OBBject)
@@ -84,6 +87,7 @@ def test_fixedincome_sofr(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_estr(params, obb):
+ """Test the ESTR endpoint."""
result = obb.fixedincome.rate.estr(**params)
assert result
assert isinstance(result, OBBject)
@@ -106,6 +110,7 @@ def test_fixedincome_rate_estr(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_sonia(params, obb):
+ """Test the SONIA endpoint."""
result = obb.fixedincome.rate.sonia(**params)
assert result
assert isinstance(result, OBBject)
@@ -128,6 +133,7 @@ def test_fixedincome_rate_sonia(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_ameribor(params, obb):
+ """Test the Ameribor endpoint."""
result = obb.fixedincome.rate.ameribor(**params)
assert result
assert isinstance(result, OBBject)
@@ -156,6 +162,7 @@ def test_fixedincome_rate_ameribor(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_effr(params, obb):
+ """Test the EFFR endpoint."""
result = obb.fixedincome.rate.effr(**params)
assert result
assert isinstance(result, OBBject)
@@ -171,6 +178,7 @@ def test_fixedincome_rate_effr(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_effr_forecast(params, obb):
+ """Test the EFFR forecast endpoint."""
result = obb.fixedincome.rate.effr_forecast(**params)
assert result
assert isinstance(result, OBBject)
@@ -185,6 +193,7 @@ def test_fixedincome_rate_effr_forecast(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_iorb(params, obb):
+ """Test the IORB endpoint."""
result = obb.fixedincome.rate.iorb(**params)
assert result
assert isinstance(result, OBBject)
@@ -207,6 +216,7 @@ def test_fixedincome_rate_iorb(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_dpcredit(params, obb):
+ """Test the DPCREDIT endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.rate.dpcredit(**params)
@@ -229,6 +239,7 @@ def test_fixedincome_rate_dpcredit(params, obb):
)
@pytest.mark.integration
def test_fixedincome_rate_ecb(params, obb):
+ """Test the ECB endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.rate.ecb(**params)
@@ -257,6 +268,7 @@ def test_fixedincome_rate_ecb(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_ice_bofa(params, obb):
+ """Test the ICE BofA endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.ice_bofa(**params)
@@ -271,6 +283,7 @@ def test_fixedincome_corporate_ice_bofa(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_moody(params, obb):
+ """Test the Moody endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.moody(**params)
@@ -296,6 +309,7 @@ def test_fixedincome_corporate_moody(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_commercial_paper(params, obb):
+ """Test the commercial paper endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.commercial_paper(**params)
@@ -336,6 +350,7 @@ def test_fixedincome_corporate_commercial_paper(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_spot_rates(params, obb):
+ """Test the spot rates endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.spot_rates(**params)
@@ -350,6 +365,7 @@ def test_fixedincome_corporate_spot_rates(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_hqm(params, obb):
+ """Test the HQM endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.hqm(**params)
@@ -364,6 +380,7 @@ def test_fixedincome_corporate_hqm(params, obb):
)
@pytest.mark.integration
def test_fixedincome_spreads_tcm(params, obb):
+ """Test the TCM endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.spreads.tcm(**params)
@@ -387,6 +404,7 @@ def test_fixedincome_spreads_tcm(params, obb):
)
@pytest.mark.integration
def test_fixedincome_spreads_tcm_effr(params, obb):
+ """Test the TCM EFFR endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.spreads.tcm_effr(**params)
@@ -410,6 +428,7 @@ def test_fixedincome_spreads_tcm_effr(params, obb):
)
@pytest.mark.integration
def test_fixedincome_spreads_treasury_effr(params, obb):
+ """Test the treasury EFFR endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.spreads.treasury_effr(**params)
@@ -433,6 +452,7 @@ def test_fixedincome_spreads_treasury_effr(params, obb):
)
@pytest.mark.integration
def test_fixedincome_government_eu_yield_curve(params, obb):
+ """Test the EU yield curve endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.government.eu_yield_curve(**params)
@@ -470,6 +490,7 @@ def test_fixedincome_government_eu_yield_curve(params, obb):
)
@pytest.mark.integration
def test_fixedincome_government_treasury_auctions(params, obb):
+ """Test the treasury auctions endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.government.treasury_auctions(**params)
@@ -514,6 +535,7 @@ def test_fixedincome_government_treasury_auctions(params, obb):
)
@pytest.mark.integration
def test_fixedincome_government_treasury_prices(params, obb):
+ """Test the treasury prices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.government.treasury_prices(**params)
@@ -549,6 +571,7 @@ def test_fixedincome_government_treasury_prices(params, obb):
)
@pytest.mark.integration
def test_fixedincome_corporate_bond_prices(params, obb):
+ """Test the bond prices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.bond_prices(**params)
diff --git a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py
index feac9a5898e..0e232cb1cdb 100644
--- a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py
+++ b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py
@@ -18,7 +18,7 @@ from openbb_fixedincome.government.government_router import router as government
from openbb_fixedincome.rate.rate_router import router as rate_router
from openbb_fixedincome.spreads.spreads_router import router as spreads_router
-router = Router(prefix="")
+router = Router(prefix="", description="Fixed Income market data.")
router.include_router(rate_router)
router.include_router(spreads_router)
router.include_router(government_router)
diff --git a/openbb_platform/extensions/fixedincome/poetry.lock b/openbb_platform/extensions/fixedincome/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/fixedincome/poetry.lock
+++ b/openbb_platform/extensions/fixedincome/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/fixedincome/pyproject.toml b/openbb_platform/extensions/fixedincome/pyproject.toml
index 3b887303823..a0027750f20 100644
--- a/openbb_platform/extensions/fixedincome/pyproject.toml
+++ b/openbb_platform/extensions/fixedincome/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-fixedincome"
-version = "1.1.4"
+version = "1.1.5"
description = "Fixed income extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_fixedincome" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/index/integration/test_index_api.py b/openbb_platform/extensions/index/integration/test_index_api.py
index 0b5006c80f5..c7ec360663d 100644
--- a/openbb_platform/extensions/index/integration/test_index_api.py
+++ b/openbb_platform/extensions/index/integration/test_index_api.py
@@ -1,3 +1,5 @@
+"""Test the index API endpoints."""
+
import base64
import pytest
@@ -9,6 +11,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -29,6 +32,7 @@ def headers():
)
@pytest.mark.integration
def test_index_constituents(params, headers):
+ """Test the index constituents endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -114,6 +118,7 @@ def test_index_constituents(params, headers):
)
@pytest.mark.integration
def test_index_price_historical(params, headers):
+ """Test the index historical price endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -191,6 +196,7 @@ def test_index_price_historical(params, headers):
@pytest.mark.integration
@pytest.mark.skip(reason="Deprecating this endpoint")
def test_index_market(params, headers):
+ """Test the index market endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -211,6 +217,7 @@ def test_index_market(params, headers):
)
@pytest.mark.integration
def test_index_available(params, headers):
+ """Test the index available endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -228,6 +235,7 @@ def test_index_available(params, headers):
)
@pytest.mark.integration
def test_index_search(params, headers):
+ """Test the index search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -246,6 +254,7 @@ def test_index_search(params, headers):
)
@pytest.mark.integration
def test_index_snapshots(params, headers):
+ """Test the index snapshots endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -272,6 +281,7 @@ def test_index_snapshots(params, headers):
)
@pytest.mark.integration
def test_index_sp500_multiples(params, headers):
+ """Test the index sp500 multiples endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -289,6 +299,7 @@ def test_index_sp500_multiples(params, headers):
)
@pytest.mark.integration
def test_index_sectors(params, headers):
+ """Test the index sectors endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/index/integration/test_index_python.py b/openbb_platform/extensions/index/integration/test_index_python.py
index 1082fce00ba..af8d2b54983 100644
--- a/openbb_platform/extensions/index/integration/test_index_python.py
+++ b/openbb_platform/extensions/index/integration/test_index_python.py
@@ -28,6 +28,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_index_constituents(params, obb):
+ """Test the index constituents endpoint."""
result = obb.index.constituents(**params)
assert result
assert isinstance(result, OBBject)
@@ -110,6 +111,7 @@ def test_index_constituents(params, obb):
)
@pytest.mark.integration
def test_index_price_historical(params, obb):
+ """Test the index historical price endpoint."""
result = obb.index.price.historical(**params)
assert result
assert isinstance(result, OBBject)
@@ -184,6 +186,7 @@ def test_index_price_historical(params, obb):
@pytest.mark.integration
@pytest.mark.skip(reason="Deprecating this endpoint")
def test_index_market(params, obb):
+ """Test the index market endpoint."""
result = obb.index.market(**params)
assert result
assert isinstance(result, OBBject)
@@ -202,6 +205,7 @@ def test_index_market(params, obb):
)
@pytest.mark.integration
def test_index_available(params, obb):
+ """Test the index available endpoint."""
result = obb.index.available(**params)
assert result
assert isinstance(result, OBBject)
@@ -223,6 +227,7 @@ def test_index_available(params, obb):
)
@pytest.mark.integration
def test_index_search(params, obb):
+ """Test the index search endpoint."""
result = obb.index.search(**params)
assert result
assert isinstance(result, OBBject)
@@ -238,6 +243,7 @@ def test_index_search(params, obb):
)
@pytest.mark.integration
def test_index_snapshots(params, obb):
+ """Test the index snapshots endpoint."""
result = obb.index.snapshots(**params)
assert result
assert isinstance(result, OBBject)
@@ -261,6 +267,7 @@ def test_index_snapshots(params, obb):
)
@pytest.mark.integration
def test_index_sp500_multiples(params, obb):
+ """Test the index sp500 multiples endpoint."""
result = obb.index.sp500_multiples(**params)
assert result
assert isinstance(result, OBBject)
@@ -275,6 +282,7 @@ def test_index_sp500_multiples(params, obb):
)
@pytest.mark.integration
def test_index_sectors(params, obb):
+ """Test the index sectors endpoint."""
result = obb.index.sectors(**params)
assert result
assert isinstance(result, OBBject)
diff --git a/openbb_platform/extensions/index/openbb_index/index_router.py b/openbb_platform/extensions/index/openbb_index/index_router.py
index 80c4b7bae5e..4ad7d393726 100644
--- a/openbb_platform/extensions/index/openbb_index/index_router.py
+++ b/openbb_platform/extensions/index/openbb_index/index_router.py
@@ -14,7 +14,7 @@ from openbb_core.app.router import Router
from openbb_index.price.price_router import router as price_router
-router = Router(prefix="")
+router = Router(prefix="", description="Indices data.")
router.include_router(price_router)
# pylint: disable=unused-argument
@@ -36,7 +36,7 @@ async def market(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Historical Market Indices."""
+ """Get Historical Market Indices."""
return await OBBject.from_query(Query(**locals()))
@@ -56,7 +56,7 @@ async def constituents(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Index Constituents."""
+ """Get Index Constituents."""
return await OBBject.from_query(Query(**locals()))
@@ -107,7 +107,7 @@ async def search(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Filters indices for rows containing the query."""
+ """Filter indices for rows containing the query."""
return await OBBject.from_query(Query(**locals()))
@@ -124,7 +124,7 @@ async def sp500_multiples(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Historical S&P 500 multiples and Shiller PE ratios."""
+ """Get historical S&P 500 multiples and Shiller PE ratios."""
return await OBBject.from_query(Query(**locals()))
@@ -138,5 +138,5 @@ async def sectors(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Index Sectors. Sector weighting of an index."""
+ """Get Index Sectors. Sector weighting of an index."""
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/index/poetry.lock b/openbb_platform/extensions/index/poetry.lock
index 7f12dfce23c..f5cf8931f29 100644
--- a/openbb_platform/extensions/index/poetry.lock
+++ b/openbb_platform/extensions/index/poetry.lock
@@ -682,13 +682,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1401,4 +1401,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/index/pyproject.toml b/openbb_platform/extensions/index/pyproject.toml
index 98784a0ee69..7589baac2ff 100644
--- a/openbb_platform/extensions/index/pyproject.toml
+++ b/openbb_platform/extensions/index/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-index"
-version = "1.1.4"
+version = "1.1.5"
description = "Index extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_index" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/news/integration/test_news_api.py b/openbb_platform/extensions/news/integration/test_news_api.py
index 2b685a34857..fe8fc4dac53 100644
--- a/openbb_platform/extensions/news/integration/test_news_api.py
+++ b/openbb_platform/extensions/news/integration/test_news_api.py
@@ -1,3 +1,5 @@
+"""Test News API."""
+
import base64
import pytest
@@ -9,6 +11,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Generate headers for API requests with basic authentication."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -83,6 +86,7 @@ def headers():
)
@pytest.mark.integration
def test_news_world(params, headers):
+ """Test retrieval of world news with various parameters."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -187,6 +191,7 @@ def test_news_world(params, headers):
)
@pytest.mark.integration
def test_news_company(params, headers):
+ """Test retrieval of company-specific news with various parameters."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/news/integration/test_news_python.py b/openbb_platform/extensions/news/integration/test_news_python.py
index 38bbbbfded1..2b876d19391 100644
--- a/openbb_platform/extensions/news/integration/test_news_python.py
+++ b/openbb_platform/extensions/news/integration/test_news_python.py
@@ -82,6 +82,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
)
@pytest.mark.integration
def test_news_world(params, obb):
+ """Test the news world endpoint."""
result = obb.news.world(**params)
assert result
assert isinstance(result, OBBject)
@@ -172,6 +173,7 @@ def test_news_world(params, obb):
)
@pytest.mark.integration
def test_news_company(params, obb):
+ """Test the news company endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.news.company(**params)
diff --git a/openbb_platform/extensions/news/openbb_news/__init__.py b/openbb_platform/extensions/news/openbb_news/__init__.py
index e69de29bb2d..5d818002605 100644
--- a/openbb_platform/extensions/news/openbb_news/__init__.py
+++ b/openbb_platform/extensions/news/openbb_news/__init__.py
@@ -0,0 +1 @@
+"""OpenBB News extension."""
diff --git a/openbb_platform/extensions/news/openbb_news/news_router.py b/openbb_platform/extensions/news/openbb_news/news_router.py
index 1a964f363b9..bad1a7f21c4 100644
--- a/openbb_platform/extensions/news/openbb_news/news_router.py
+++ b/openbb_platform/extensions/news/openbb_news/news_router.py
@@ -12,7 +12,7 @@ from openbb_core.app.provider_interface import (
from openbb_core.app.query import Query
from openbb_core.app.router import Router
-router = Router(prefix="")
+router = Router(prefix="", description="Financial market news data.")
@router.command(
diff --git a/openbb_platform/extensions/news/poetry.lock b/openbb_platform/extensions/news/poetry.lock
index 1f872410758..45aff8d211b 100644
--- a/openbb_platform/extensions/news/poetry.lock
+++ b/openbb_platform/extensions/news/poetry.lock
@@ -697,13 +697,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1416,4 +1416,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "e5a1c1f22e0b8e42343cef04f6f23281c7d6441df13f2be91584098c856ab1f0"
+content-hash = "ca421fe0dbae56150a2fc3a7978ad65f3dd4e70025bc4da4a7f1338b8eca522c"
diff --git a/openbb_platform/extensions/news/pyproject.toml b/openbb_platform/extensions/news/pyproject.toml
index 86899175603..ac1e05a120f 100644
--- a/openbb_platform/extensions/news/pyproject.toml
+++ b/openbb_platform/extensions/news/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-news"
-version = "1.1.4"
+version = "1.1.5"
description = "News extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_news" }]
[tool.poetry.dependencies]
python = "^3.8"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/quantitative/README.md b/openbb_platform/extensions/quantitative/README.md
index e6958f16ef6..b1193e287d5 100644
--- a/openbb_platform/extensions/quantitative/README.md
+++ b/openbb_platform/extensions/quantitative/README.md
@@ -11,7 +11,7 @@ This extension works nicely with a companion `openbb-charting` extension
To install the extension, run the following command in this folder:
```bash
-pip install openbb-qa
+pip install openbb-quantitative
```
For development please check [Contribution Guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/blob/develop/openbb_platform/CONTRIBUTING.md).
diff --git a/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py b/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py
index 57782126fc7..ce16394488b 100644
--- a/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py
+++ b/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py
@@ -1,3 +1,5 @@
+"""Integration tests for the quantitative extension."""
+
import base64
import json
import random
@@ -15,6 +17,7 @@ data: dict = {}
def get_headers():
+ """Get the headers for the API request."""
if "headers" in data:
return data["headers"]
@@ -30,12 +33,13 @@ def request_data(
menu: str, symbol: str, provider: str, start_date: str = "", end_date: str = ""
):
"""Randomly pick a symbol and a provider and get data from the selected menu."""
- url = f"http://0.0.0.0:8000/api/v1/{menu}/price/historical?symbol={symbol}&provider={provider}&start_date={start_date}&end_date={end_date}"
+ url = f"http://0.0.0.0:8000/api/v1/{menu}/price/historical?symbol={symbol}&provider={provider}&start_date={start_date}&end_date={end_date}" # pylint: disable=line-too-long # noqa: E501
result = requests.get(url, headers=get_headers(), timeout=10)
return result.json()["results"]
def get_stocks_data():
+ """Get stocks data."""
if "stocks_data" in data:
return data["stocks_data"]
@@ -53,6 +57,7 @@ def get_stocks_data():
def get_crypto_data():
+ """Get crypto data."""
if "crypto_data" in data:
return data["crypto_data"]
@@ -71,6 +76,7 @@ def get_crypto_data():
def get_data(menu: Literal["equity", "crypto"]):
+ """Get data based on the selected menu."""
funcs = {"equity": get_stocks_data, "crypto": get_crypto_data}
return funcs[menu]()
@@ -84,6 +90,7 @@ def get_data(menu: Literal["equity", "crypto"]):
)
@pytest.mark.integration
def test_quantitative_normality(params, data_type):
+ """Test the normality endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -103,6 +110,7 @@ def test_quantitative_normality(params, data_type):
)
@pytest.mark.integration
def test_quantitative_capm(params, data_type):
+ """Test the CAPM endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -138,6 +146,7 @@ def test_quantitative_capm(params, data_type):
)
@pytest.mark.integration
def test_quantitative_performance_omega_ratio(params, data_type):
+ """Test the Omega Ratio endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -157,6 +166,7 @@ def test_quantitative_performance_omega_ratio(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_kurtosis(params, data_type):
+ """Test the rolling kurtosis endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -192,6 +202,7 @@ def test_quantitative_rolling_kurtosis(params, data_type):
)
@pytest.mark.integration
def test_quantitative_unitroot_test(params, data_type):
+ """Test the unit root test endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -229,6 +240,7 @@ def test_quantitative_unitroot_test(params, data_type):
)
@pytest.mark.integration
def test_quantitative_performance_sharpe_ratio(params, data_type):
+ """Test the Sharpe Ratio endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -270,6 +282,7 @@ def test_quantitative_performance_sharpe_ratio(params, data_type):
)
@pytest.mark.integration
def test_quantitative_performance_sortino_ratio(params, data_type):
+ """Test the Sortino Ratio endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -290,6 +303,7 @@ def test_quantitative_performance_sortino_ratio(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_skew(params, data_type):
+ """Test the rolling skew endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -308,6 +322,7 @@ def test_quantitative_rolling_skew(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_variance(params, data_type):
+ """Test the rolling variance endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -326,6 +341,7 @@ def test_quantitative_rolling_variance(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_stdev(params, data_type):
+ """Test the rolling standard deviation endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -344,6 +360,7 @@ def test_quantitative_rolling_stdev(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_mean(params, data_type):
+ """Test the rolling mean endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -381,6 +398,7 @@ def test_quantitative_rolling_mean(params, data_type):
)
@pytest.mark.integration
def test_quantitative_rolling_quantile(params, data_type):
+ """Test the rolling quantile endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -400,6 +418,7 @@ def test_quantitative_rolling_quantile(params, data_type):
)
@pytest.mark.integration
def test_quantitative_summary(params, data_type):
+ """Test the summary endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -423,6 +442,7 @@ def test_quantitative_summary(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_skew(params, data_type):
+ """Test the skew endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -441,6 +461,7 @@ def test_quantitative_stats_skew(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_kurtosis(params, data_type):
+ """Test the kurtosis endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -459,6 +480,7 @@ def test_quantitative_stats_kurtosis(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_mean(params, data_type):
+ """Test the mean endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -477,6 +499,7 @@ def test_quantitative_stats_mean(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_stdev(params, data_type):
+ """Test the standard deviation endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -495,6 +518,7 @@ def test_quantitative_stats_stdev(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_variance(params, data_type):
+ """Test the variance endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
@@ -530,6 +554,7 @@ def test_quantitative_stats_variance(params, data_type):
)
@pytest.mark.integration
def test_quantitative_stats_quantile(params, data_type):
+ """Test the quantile endpoint."""
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
diff --git a/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py b/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py
index f9fd73b832d..4a2433e8ba7 100644
--- a/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py
+++ b/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py
@@ -24,6 +24,7 @@ data: dict = {}
def get_stocks_data():
+ """Get stocks data."""
import openbb # pylint:disable=import-outside-toplevel
if "stocks_data" in data:
@@ -39,6 +40,7 @@ def get_stocks_data():
def get_crypto_data():
+ """Get crypto data."""
import openbb # pylint:disable=import-outside-toplevel
if "crypto_data" in data:
@@ -55,6 +57,7 @@ def get_crypto_data():
def get_data(menu: Literal["equity", "crypto"]):
+ """Get data."""
funcs = {"equity": get_stocks_data, "crypto": get_crypto_data}
return funcs[menu]()
@@ -68,6 +71,7 @@ def get_data(menu: Literal["equity", "crypto"]):
)
@pytest.mark.integration
def test_quantitative_normality(params, data_type, obb):
+ """Test normality."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -85,6 +89,7 @@ def test_quantitative_normality(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_capm(params, data_type, obb):
+ """Test capm."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -118,6 +123,7 @@ def test_quantitative_capm(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_performance_omega_ratio(params, data_type, obb):
+ """Test omega ratio."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -135,6 +141,7 @@ def test_quantitative_performance_omega_ratio(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_kurtosis(params, data_type, obb):
+ """Test rolling kurtosis."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -169,6 +176,7 @@ def test_quantitative_rolling_kurtosis(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_unitroot_test(params, data_type, obb):
+ """Test unitroot test."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -204,6 +212,7 @@ def test_quantitative_unitroot_test(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_performance_sharpe_ratio(params, data_type, obb):
+ """Test sharpe ratio."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -241,6 +250,7 @@ def test_quantitative_performance_sharpe_ratio(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_performance_sortino_ratio(params, data_type, obb):
+ """Test sortino ratio."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -257,6 +267,7 @@ def test_quantitative_performance_sortino_ratio(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_skew(params, data_type, obb):
+ """Test rolling skew."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -293,6 +304,7 @@ def test_quantitative_rolling_skew(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_quantile(params, data_type, obb):
+ """Test rolling quantile."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -311,6 +323,7 @@ def test_quantitative_rolling_quantile(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_summary(params, data_type, obb):
+ """Test summary."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -344,6 +357,7 @@ def test_quantitative_summary(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_stdev(params, data_type, obb):
+ """Test rolling stdev."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -378,6 +392,7 @@ def test_quantitative_rolling_stdev(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_mean(params, data_type, obb):
+ """Test rolling mean."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -412,6 +427,7 @@ def test_quantitative_rolling_mean(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_rolling_variance(params, data_type, obb):
+ """Test rolling variance."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -429,6 +445,7 @@ def test_quantitative_rolling_variance(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_skew(params, data_type, obb):
+ """Test skew."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -446,6 +463,7 @@ def test_quantitative_stats_skew(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_kurtosis(params, data_type, obb):
+ """Test kurtosis."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -463,6 +481,7 @@ def test_quantitative_stats_kurtosis(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_variance(params, data_type, obb):
+ """Test variance."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -480,6 +499,7 @@ def test_quantitative_stats_variance(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_stdev(params, data_type, obb):
+ """Test stdev."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -497,6 +517,7 @@ def test_quantitative_stats_stdev(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_mean(params, data_type, obb):
+ """Test mean."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -529,6 +550,7 @@ def test_quantitative_stats_mean(params, data_type, obb):
)
@pytest.mark.integration
def test_quantitative_stats_quantile(params, data_type, obb):
+ """Test quantile."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py
index 025df52bb20..1f5110cc409 100644
--- a/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py
+++ b/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py
@@ -1,3 +1,5 @@
+"""OpenBB Performance Extension router."""
+
from typing import List
import numpy as np
diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/quantitative_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/quantitative_router.py
index 0065ec62011..9ccd89d550a 100644
--- a/openbb_platform/extensions/quantitative/openbb_quantitative/quantitative_router.py
+++ b/openbb_platform/extensions/quantitative/openbb_quantitative/quantitative_router.py
@@ -30,7 +30,7 @@ from .models import (
UnitRootModel,
)
-router = Router(prefix="")
+router = Router(prefix="", description="Quantitative analysis tools.")
router.include_router(rolling_router)
router.include_router(stats_router)
router.include_router(performance_router)
diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py
index 34ef9be67a1..8bcfaa05143 100644
--- a/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py
+++ b/openbb_platform/extensions/quantitative/openbb_quantitative/rolling/rolling_router.py
@@ -376,8 +376,7 @@ def quantile(
def mean(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
- """
- Calculate the rolling mean (average) of a target column within a given window size.
+ """Calculate the rolling average of a target column within a given window size.
The rolling mean is a simple moving average that calculates the average of a target variable over a specified window.
This function is widely used in financial analysis to smooth short-term fluctuations and highlight longer-term trends
diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/statistics.py b/openbb_platform/extensions/quantitative/openbb_quantitative/statistics.py
index db137ac77cf..e3339107364 100644
--- a/openbb_platform/extensions/quantitative/openbb_quantitative/statistics.py
+++ b/openbb_platform/extensions/quantitative/openbb_quantitative/statistics.py
@@ -1,4 +1,4 @@
-"""Statistics Functions"""
+"""Statistics Functions."""
from typing import Union
@@ -16,26 +16,32 @@ from scipy import stats
def kurtosis_(data: Union[DataFrame, Series, ndarray]) -> float:
- """Kurtosis is a measure of the "tailedness" of the probability distribution of a real-valued random variable."""
+ """Get Kurtosis.
+
+ It is a measure of the "tailedness" of the probability distribution of a real-valued random variable.
+ """
return stats.kurtosis(data)
def skew_(data: Union[DataFrame, Series, ndarray]) -> float:
- """Skewness is a measure of the asymmetry of the probability distribution of a
- real-valued random variable about its mean."""
+ """Get Skewness.
+
+ It is a measure of the asymmetry of the probability distribution of a
+ real-valued random variable about its mean.
+ """
return stats.skew(data)
def mean_(data: Union[DataFrame, Series, ndarray]) -> float:
- """Mean is the average of the numbers."""
+ """Get Mean which is the average of the numbers."""
return mean_np(data)
def std_dev_(data: Union[DataFrame, Series, ndarray]) -> float:
- """Standard deviation is a measure of the amount of variation or dispersion of a set of values."""
+ """Get Standard deviation that is a measure of the amount of variation or dispersion of a set of values."""
return std(data)
def var_(data: Union[DataFrame, Series, ndarray]) -> float:
- """Variance is a measure of the amount of variation or dispersion of a set of values."""
+ """Get Variance that is a measure of the amount of variation or dispersion of a set of values."""
return var_np(data)
diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/stats/stats_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/stats/stats_router.py
index 68be4b675cf..2b32123aaac 100644
--- a/openbb_platform/extensions/quantitative/openbb_quantitative/stats/stats_router.py
+++ b/openbb_platform/extensions/quantitative/openbb_quantitative/stats/stats_router.py
@@ -101,8 +101,7 @@ def skew(
],
)
def variance(data: List[Data], target: str) -> OBBject[List[Data]]:
- """
- Calculate the variance of a target column.
+ """Calculate the variance of a target column.
Variance measures the dispersion of a set of data points around their mean. It is a key metric for
assessing the volatility and stability of financial returns or other time series data.
@@ -150,8 +149,7 @@ def variance(data: List[Data], target: str) -> OBBject[List[Data]]:
],
)
def stdev(data: List[Data], target: str) -> OBBject[List[Data]]:
- """
- Calculate the rolling standard deviation of a target column.
+ """Calculate the rolling standard deviation of a target column.
Standard deviation is a measure of the amount of variation or dispersion of a set of values.
It is widely used to assess the risk and volatility of financial returns or other time series data
@@ -200,8 +198,7 @@ def stdev(data: List[Data], target: str) -> OBBject[List[Data]]:
],
)
def kurtosis(data: List[Data], target: str) -> OBBject[List[Data]]:
- """
- Calculate the rolling kurtosis of a target column.
+ """Calculate the rolling kurtosis of a target column.
Kurtosis measures the "tailedness" of the probability distribution of a real-valued random variable.
High kurtosis indicates a distribution with heavy tails (outliers), suggesting a higher risk of extreme outcomes.
@@ -255,8 +252,7 @@ def quantile(
target: str,
quantile_pct: NonNegativeFloat = 0.5,
) -> OBBject[List[Data]]:
- """
- Calculate the quantile of a target column at a specified quantile percentage.
+ """Calculate the quantile of a target column at a specified quantile percentage.
Quantiles are points dividing the range of a probability distribution into intervals with equal probabilities,
or dividing the sample in the same way.
@@ -312,8 +308,7 @@ def mean(
data: List[Data],
target: str,
) -> OBBject[List[Data]]:
- """
- Calculate the mean (average) of a target column.
+ """Calculate the average of a target column.
The rolling mean is a simple moving average that calculates the average of a target variable.
This function is widely used in financial analysis to smooth short-term fluctuations and highlight longer-term trends
diff --git a/openbb_platform/extensions/quantitative/poetry.lock b/openbb_platform/extensions/quantitative/poetry.lock
index 9a36a8bfa16..550b59aa7cf 100644
--- a/openbb_platform/extensions/quantitative/poetry.lock
+++ b/openbb_platform/extensions/quantitative/poetry.lock
@@ -682,13 +682,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1538,4 +1538,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<3.12"
-content-hash = "104e6ac1cefb99e4d26f8bab3324422380c3e2bf884179d1bea5b0eede7ba294"
+content-hash = "0a8ed0269b904ec3ca1254469e4d0bbfccc50252d668bf752acb02191b0918ad"
diff --git a/openbb_platform/extensions/quantitative/pyproject.toml b/openbb_platform/extensions/quantitative/pyproject.toml
index 8b0a5b376a0..37d4f4b2d3b 100644
--- a/openbb_platform/extensions/quantitative/pyproject.toml
+++ b/openbb_platform/extensions/quantitative/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-quantitative"
-version = "1.1.4"
+version = "1.1.5"
description = "Quantitative Analysis extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -11,7 +11,7 @@ python = ">=3.8,<3.12" # scipy forces python <4.0 explicitly
scipy = "^1.10.1"
statsmodels = "^0.14.0"
pandas-ta = "^0.3.14b"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/quantitative/tests/test_quantitative_helpers.py b/openbb_platform/extensions/quantitative/tests/test_quantitative_helpers.py
index 82f6ae52b15..8a175957a0a 100644
--- a/openbb_platform/extensions/quantitative/tests/test_quantitative_helpers.py
+++ b/openbb_platform/extensions/quantitative/tests/test_quantitative_helpers.py
@@ -7,6 +7,7 @@ from extensions.quantitative.openbb_quantitative.helpers import (
def test_validate_window():
+ """Test the validate_window function."""
input_data = pd.Series(range(1, 100))
validate_window(
input_data=input_data,
diff --git a/openbb_platform/extensions/quantitative/tests/test_statistics.py b/openbb_platform/extensions/quantitative/tests/test_statistics.py
index 11c1ce2ec43..d1a34e30c8c 100644
--- a/openbb_platform/extensions/quantitative/tests/test_statistics.py
+++ b/openbb_platform/extensions/quantitative/tests/test_statistics.py
@@ -1,3 +1,5 @@
+"""Tests for the statistics module."""
+
import pandas as pd
import pytest
from openbb_quantitative.statistics import kurtosis_, mean_, skew_, std_dev_, var_
@@ -6,20 +8,25 @@ test_data = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
def test_kurtosis():
+ """Test the kurtosis function."""
assert kurtosis_(test_data) == pytest.approx(-1.224, abs=1e-3)
def test_skew():
+ """Test the skewness function."""
assert skew_(test_data) == pytest.approx(0.0, abs=1e-3)
def test_std_dev():
+ """Test the standard deviation function."""
assert std_dev_(test_data) == pytest.approx(2.872, abs=1e-3)
def test_mean():
+ """Test the mean function."""
assert mean_(test_data) == pytest.approx(5.5, abs=1e-3)
def test_var():
+ """Test the variance function."""
assert var_(test_data) == pytest.approx(8.25, abs=1e-3)
diff --git a/openbb_platform/extensions/regulators/integration/test_regulators_api.py b/openbb_platform/extensions/regulators/integration/test_regulators_api.py
index 997ce2a2a70..0ea5fa5a8f2 100644
--- a/openbb_platform/extensions/regulators/integration/test_regulators_api.py
+++ b/openbb_platform/extensions/regulators/integration/test_regulators_api.py
@@ -1,3 +1,5 @@
+"""Integration tests for the regulators API."""
+
import base64
import pytest
@@ -9,6 +11,7 @@ from openbb_core.provider.utils.helpers import get_querystring
@pytest.fixture(scope="session")
def headers():
+ """Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
@@ -28,6 +31,7 @@ def headers():
)
@pytest.mark.integration
def test_regulators_sec_cik_map(params, headers):
+ """Test the SEC CIK map endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -45,6 +49,7 @@ def test_regulators_sec_cik_map(params, headers):
)
@pytest.mark.integration
def test_regulators_sec_institutions_search(params, headers):
+ """Test the SEC institutions search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -70,6 +75,7 @@ def test_regulators_sec_institutions_search(params, headers):
)
@pytest.mark.integration
def test_regulators_sec_schema_files(params, headers):
+ """Test the SEC schema files endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -88,6 +94,7 @@ def test_regulators_sec_schema_files(params, headers):
)
@pytest.mark.integration
def test_regulators_sec_symbol_map(params, headers):
+ """Test the SEC symbol map endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -103,6 +110,7 @@ def test_regulators_sec_symbol_map(params, headers):
)
@pytest.mark.integration
def test_regulators_sec_rss_litigation(params, headers):
+ """Test the SEC RSS litigation endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -118,6 +126,7 @@ def test_regulators_sec_rss_litigation(params, headers):
)
@pytest.mark.integration
def test_regulators_sec_sic_search(params, headers):
+ """Test the SEC SIC search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -135,6 +144,7 @@ def test_regulators_sec_sic_search(params, headers):
)
@pytest.mark.integration
def test_regulators_cftc_cot_search(params, headers):
+ """Test the CFTC COT search endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -165,6 +175,7 @@ def test_regulators_cftc_cot_search(params, headers):
)
@pytest.mark.integration
def test_regulators_cftc_cot(params, headers):
+ """Test the CFTC COT endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
diff --git a/openbb_platform/extensions/regulators/integration/test_regulators_python.py b/openbb_platform/extensions/regulators/integration/test_regulators_python.py
index 6e5ce886686..77c2e811f53 100644
--- a/openbb_platform/extensions/regulators/integration/test_regulators_python.py
+++ b/openbb_platform/extensions/regulators/integration/test_regulators_python.py
@@ -28,6 +28,7 @@ def obb(pytestconfig):
)
@pytest.mark.integration
def test_regulators_sec_cik_map(params, obb):
+ """Test the SEC CIK map endpoint."""
result = obb.regulators.sec.cik_map(**params)
assert result
assert isinstance(result, OBBject)
@@ -43,6 +44,7 @@ def test_regulators_sec_cik_map(params, obb):
)
@pytest.mark.integration
def test_regulators_sec_institutions_search(params, obb):
+ """Test the SEC institutions search endpoint."""
result = obb.regulators.sec.institutions_search(**params)
assert result
assert isinstance(result, OBBject)
@@ -72,6 +74,7 @@ def test_regulators_sec_institutions_search(params, obb):
)
@pytest.mark.integration
def test_regulators_sec_schema_files(params, obb):
+ """Test the SEC schema files endpoint."""
result = obb.regulators.sec.schema_files(**params)
assert result
assert isinstance(result, OBBject)
@@ -87,6 +90,7 @@ def test_regulators_sec_schema_files(params, obb):
)
@pytest.mark.integration
def test_regulators_sec_symbol_map(params, obb):
+ """Test the SEC symbol map endpoint."""
result = obb.regulators.sec.symbol_map(**params)
assert result
assert isinstance(result, OBBject)
@@ -100,6 +104,7 @@ def test_regulators_sec_symbol_map(params, obb):
)
@pytest.mark.integration
def test_regulators_sec_rss_litigation(params, obb):
+ """Test the SEC RSS litigation endpoint."""
result = obb.regulators.sec.rss_litigation(**params)
assert result
assert isinstance(result, OBBject)
@@ -112,6 +117,7 @@ def test_regulators_sec_rss_litigation(params, obb):
)
@pytest.mark.integration
def test_regulators_sec_sic_search(params, obb):
+ """Test the SEC SIC search endpoint."""
result = obb.regulators.sec.sic_search(**params)
assert result
assert isinstance(result, OBBject)
@@ -126,6 +132,7 @@ def test_regulators_sec_sic_search(params, obb):
)
@pytest.mark.integration
def test_regulators_cftc_cot_search(params, obb):
+ """Test the CFTC COT search endpoint."""
result = obb.regulators.cftc.cot_search(**params)
assert result
assert isinstance(result, OBBject)
@@ -153,6 +160,7 @@ def test_regulators_cftc_cot_search(params, obb):
)
@pytest.mark.integration
def test_regulators_cftc_cot(params, obb):
+ """Test the CFTC COT endpoint."""
result = obb.regulators.cftc.cot(**params)
assert result
assert isinstance(result, OBBject)
diff --git a/openbb_platform/extensions/regulators/openbb_regulators/__init__.py b/openbb_platform/extensions/regulators/openbb_regulators/__init__.py
index e69de29bb2d..4167ad3a36f 100644
--- a/openbb_platform/extensions/regulators/openbb_regulators/__init__.py
+++ b/openbb_platform/extensions/regulators/openbb_regulators/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Regulators Extension."""
diff --git a/openbb_platform/extensions/regulators/openbb_regulators/cftc/cftc_router.py b/openbb_platform/extensions/regulators/openbb_regulators/cftc/cftc_router.py
index eb5e1e642aa..b2a62a7ec42 100644
--- a/openbb_platform/extensions/regulators/openbb_regulators/cftc/cftc_router.py
+++ b/openbb_platform/extensions/regulators/openbb_regulators/cftc/cftc_router.py
@@ -59,5 +59,5 @@ async def cot(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """Commitment of Traders Reports."""
+ """Get Commitment of Traders Reports."""
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/regulators/openbb_regulators/regulators_router.py b/openbb_platform/extensions/regulators/openbb_regulators/regulators_router.py
index 0a5a33f07dc..c9ad4217950 100644
--- a/openbb_platform/extensions/regulators/openbb_regulators/regulators_router.py
+++ b/openbb_platform/extensions/regulators/openbb_regulators/regulators_router.py
@@ -11,6 +11,6 @@ from .cftc.cftc_router import (
)
from .sec.sec_router import router as sec_router
-router = Router(prefix="")
+router = Router(prefix="", description="Financial market regulators data.")
router.include_router(sec_router)
router.include_router(cftc_router)
diff --git a/openbb_platform/extensions/regulators/openbb_regulators/sec/__init__.py b/openbb_platform/extensions/regulators/openbb_regulators/sec/__init__.py
index e69de29bb2d..4f3fc3a22e7 100644
--- a/openbb_platform/extensions/regulators/openbb_regulators/sec/__init__.py
+++ b/openbb_platform/extensions/regulators/openbb_regulators/sec/__init__.py
@@ -0,0 +1 @@
+"""Regulators for the SEC init."""
diff --git a/openbb_platform/extensions/regulators/openbb_regulators/sec/sec_router.py b/openbb_platform/extensions/regulators/openbb_regulators/sec/sec_router.py
index df212ad35c7..6129d346c87 100644
--- a/openbb_platform/extensions/regulators/openbb_regulators/sec/sec_router.py
+++ b/openbb_platform/extensions/regulators/openbb_regulators/sec/sec_router.py
@@ -80,7 +80,7 @@ async def schema_files(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """A tool for navigating the directory of SEC XML schema files by year."""
+ """Use tool for navigating the directory of SEC XML schema files by year."""
return await OBBject.from_query(Query(**locals()))
@@ -108,7 +108,7 @@ async def rss_litigation(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
- """The RSS feed provides links to litigation releases concerning civil lawsuits brought by the Commission in federal court.""" # noqa: E501 pylint: disable=C0301
+ """Get the RSS feed that provides links to litigation releases concerning civil lawsuits brought by the Commission in federal court.""" # noqa: E501 pylint: disable=C0301
return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/extensions/regulators/poetry.lock b/openbb_platform/extensions/regulators/poetry.lock
index d556c640c19..9119c69fb52 100644
--- a/openbb_platform/extensions/regulators/poetry.lock
+++ b/openbb_platform/extensions/regulators/poetry.lock
@@ -678,13 +678,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1397,4 +1397,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<3.12"
-content-hash = "75f551243cc84ea3dcba416d86f4ef5669b5b8b5fcf4781a0d8e3e69360b28e4"
+content-hash = "df0539c34df30b9f361fb9099e221e5a86ba5c1ea1451a0748d7b87dba2203b0"
diff --git a/openbb_platform/extensions/regulators/pyproject.toml b/openbb_platform/extensions/regulators/pyproject.toml
index f80ceb1db42..81479ba934b 100644
--- a/openbb_platform/extensions/regulators/pyproject.toml
+++ b/openbb_platform/extensions/regulators/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-regulators"
-version = "1.1.4"
+version = "1.1.5"
description = "Markets and Agency Regulators extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -8,7 +8,7 @@ packages = [{ include = "openbb_regulators" }]
[tool.poetry.dependencies]
python = ">=3.8,<3.12"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/technical/integration/test_technical_api.py b/openbb_platform/extensions/technical/integration/test_technical_api.py
index 2fec3060fa0..481fce9f356 100644
--- a/openbb_platform/extensions/technical/integration/test_technical_api.py
+++ b/openbb_platform/extensions/technical/integration/test_technical_api.py
@@ -1,3 +1,5 @@
+"""Test technical api."""
+
import base64
import json
import random
@@ -957,3 +959,42 @@ def test_technical_ema(params, data_type):
result = requests.post(url, headers=get_headers(), timeout=10, data=body)
assert isinstance(result, requests.Response)
assert result.status_code == 200
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": "",
+ "study": "price",
+ "benchmark": "SPY",
+ "long_period": 252,
+ "short_period": 21,
+ "window": 21,
+ "trading_periods": 252,
+ "chart_params": {"show_tails": False},
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_technical_relative_rotation(params):
+ params = {p: v for p, v in params.items() if v}
+ data_params = dict(
+ symbol="AAPL,MSFT,GOOGL,AMZN,SPY",
+ provider="yfinance",
+ start_date="2022-01-01",
+ end_date="2024-01-01",
+ )
+ data_query_str = get_querystring(data_params, [])
+ data_url = f"http://0.0.0.0:8000/api/v1/equity/price/historical?{data_query_str}"
+ data_result = requests.get(data_url, headers=get_headers(), timeout=10).json()[
+ "results"
+ ]
+ body = json.dumps({"data": data_result})
+ query_str = get_querystring(params, ["data"])
+ url = f"http://0.0.0.0:8000/api/v1/technical/relative_rotation?{query_str}"
+ result = requests.post(url, headers=get_headers(), timeout=10, data=body)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
diff --git a/openbb_platform/extensions/technical/integration/test_technical_python.py b/openbb_platform/extensions/technical/integration/test_technical_python.py
index ab51354904e..d866c3623c8 100644
--- a/openbb_platform/extensions/technical/integration/test_technical_python.py
+++ b/openbb_platform/extensions/technical/integration/test_technical_python.py
@@ -24,6 +24,7 @@ data: dict = {}
def get_stocks_data():
+ """Get stocks data."""
import openbb # pylint:disable=import-outside-toplevel
if "stocks_data" in data:
@@ -39,6 +40,7 @@ def get_stocks_data():
def get_crypto_data():
+ """Get crypto data."""
import openbb # pylint:disable=import-outside-toplevel
if "crypto_data" in data:
@@ -55,6 +57,7 @@ def get_crypto_data():
def get_data(menu: Literal["stocks", "crypto"]):
+ """Get data."""
funcs = {"stocks": get_stocks_data, "crypto": get_crypto_data}
return funcs[menu]()
@@ -88,6 +91,7 @@ def get_data(menu: Literal["stocks", "crypto"]):
)
@pytest.mark.integration
def test_technical_atr(params, data_type, obb):
+ """Test atr."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -126,6 +130,7 @@ def test_technical_atr(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_fib(params, data_type, obb):
+ """Test fib."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -144,6 +149,7 @@ def test_technical_fib(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_obv(params, data_type, obb):
+ """Test obv."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -162,6 +168,7 @@ def test_technical_obv(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_fisher(params, data_type, obb):
+ """Test fisher."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -198,6 +205,7 @@ def test_technical_fisher(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_adosc(params, data_type, obb):
+ """Test adosc."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -238,6 +246,7 @@ def test_technical_adosc(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_bbands(params, data_type, obb):
+ """Test bbands."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -274,6 +283,7 @@ def test_technical_bbands(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_zlma(params, data_type, obb):
+ """Test zlma."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -300,6 +310,7 @@ def test_technical_zlma(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_aroon(params, data_type, obb):
+ """Test aroon."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -336,6 +347,7 @@ def test_technical_aroon(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_sma(params, data_type, obb):
+ """Test sma."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -374,6 +386,7 @@ def test_technical_sma(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_demark(params, data_type, obb):
+ """Test demark."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -392,6 +405,7 @@ def test_technical_demark(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_vwap(params, data_type, obb):
+ """Test vwap."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -430,6 +444,7 @@ def test_technical_vwap(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_macd(params, data_type, obb):
+ """Test macd."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -466,6 +481,7 @@ def test_technical_macd(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_hma(params, data_type, obb):
+ """Test hma."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -502,6 +518,7 @@ def test_technical_hma(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_donchian(params, data_type, obb):
+ """Test donchian."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -542,6 +559,7 @@ def test_technical_donchian(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_ichimoku(params, data_type, obb):
+ """Test ichimoku."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -568,6 +586,7 @@ def test_technical_ichimoku(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_clenow(params, data_type, obb):
+ """Test clenow."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -604,6 +623,7 @@ def test_technical_clenow(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_adx(params, data_type, obb):
+ """Test adx."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -622,6 +642,7 @@ def test_technical_adx(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_ad(params, data_type, obb):
+ """Test ad."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -658,6 +679,7 @@ def test_technical_ad(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_wma(params, data_type, obb):
+ """Test wma."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -684,6 +706,7 @@ def test_technical_wma(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_cci(params, data_type, obb):
+ """Test cci."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -722,6 +745,7 @@ def test_technical_cci(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_rsi(params, data_type, obb):
+ """Test rsi."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -758,6 +782,7 @@ def test_technical_rsi(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_stoch(params, data_type, obb):
+ """Test stoch."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -796,6 +821,7 @@ def test_technical_stoch(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_kc(params, data_type, obb):
+ """Test kc."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -814,6 +840,7 @@ def test_technical_kc(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_cg(params, data_type, obb):
+ """Test cg."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -854,6 +881,7 @@ def test_technical_cg(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_cones(params, data_type, obb):
+ """Test cones."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -890,6 +918,7 @@ def test_technical_cones(params, data_type, obb):
)
@pytest.mark.integration
def test_technical_ema(params, data_type, obb):
+ """Test ema."""
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
@@ -897,3 +926,45 @@ def test_technical_ema(params, data_type, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": "",
+ "study": "price",
+ "benchmark": "SPY",
+ "long_period": 252,
+ "short_period": 21,
+ "window": 21,
+ "trading_periods": 252,
+ "chart_params": {"show_tails": False},
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_technical_relative_rotation(params, obb):
+ params["data"] = obb.equity.price.historical(
+ "AAPL,MSFT,GOOGL,AMZN,SPY",
+ provider="yfinance",
+ start_date="2022-01-01",
+ end_date="2024-01-01",
+ ).results
+ result = obb.technical.relative_rotation(
+ data=params["data"],
+ benchmark=params["benchmark"],
+ study=params["study"],
+ long_period=params["long_period"],
+ short_period=params["short_period"],
+ window=params["window"],
+ trading_periods=params["trading_periods"],
+ )
+ assert result
+ assert isinstance(result, OBBject)
+ assert hasattr(result.results, "rs_ratios")
+ assert len(result.results.rs_ratios) > 0 # type: ignore
+ assert hasattr(result.results, "rs_momentum")
+ assert len(result.results.rs_momentum) > 0 # type: ignore
diff --git a/openbb_platform/extensions/technical/openbb_technical/helpers.py b/openbb_platform/extensions/technical/openbb_technical/helpers.py
index 948a32fede3..7c7fec91dc7 100644
--- a/openbb_platform/extensions/technical/openbb_technical/helpers.py
+++ b/openbb_platform/extensions/technical/openbb_technical/helpers.py
@@ -1,13 +1,11 @@
"""Technical Analysis Helpers."""
-import warnings
from typing import Any, List, Literal, Optional, Tuple, Union
+from warnings import warn
import numpy as np
import pandas as pd
-_warn = warnings.warn
-
def validate_data(data: list, length: Union[int, List[int]]) -> None:
"""Validate data."""
@@ -51,11 +49,11 @@ def parkinson(
Dataframe with results.
"""
if window < 1:
- _warn("Error: Window must be at least 1, defaulting to 30.")
+ warn("Error: Window must be at least 1, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -82,7 +80,7 @@ def standard_deviation(
is_crypto: bool = False,
clean: bool = True,
) -> pd.DataFrame:
- """Standard deviation.
+ """Calculate the Standard deviation.
Measures how widely returns are dispersed from the average return.
It is the most common (and biased) estimator of volatility.
@@ -106,11 +104,11 @@ def standard_deviation(
Dataframe with results.
"""
if window < 2:
- _warn("Error: Window must be at least 2, defaulting to 30.")
+ warn("Error: Window must be at least 2, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -159,11 +157,11 @@ def garman_klass(
Dataframe with results.
"""
if window < 1:
- _warn("Error: Window must be at least 1, defaulting to 30.")
+ warn("Error: Window must be at least 1, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -220,11 +218,11 @@ def hodges_tompkins(
>>> df = obb.technical.hodges_tompkins(data, is_crypto = True)
"""
if window < 2:
- _warn("Error: Window must be at least 2, defaulting to 30.")
+ warn("Error: Window must be at least 2, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -280,11 +278,11 @@ def rogers_satchell(
Pandas Series with results.
"""
if window < 1:
- _warn("Error: Window must be at least 1, defaulting to 30.")
+ warn("Error: Window must be at least 1, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -337,11 +335,11 @@ def yang_zhang(
Dataframe with results.
"""
if window < 2:
- _warn("Error: Window must be at least 2, defaulting to 30.")
+ warn("Error: Window must be at least 2, defaulting to 30.")
window = 30
if trading_periods and is_crypto:
- _warn("is_crypto is overridden by trading_periods.")
+ warn("is_crypto is overridden by trading_periods.")
if not trading_periods:
trading_periods = 365 if is_crypto else 252
@@ -547,12 +545,12 @@ def calculate_fib_levels(
if start_date and end_date:
if start_date not in data.index:
date0 = data.index[data.index.get_indexer([end_date], method="nearest")[0]]
- _warn(f"Start date not in data. Using nearest: {date0}")
+ warn(f"Start date not in data. Using nearest: {date0}")
else:
date0 = start_date
if end_date not in data.index:
date1 = data.index[data.index.get_indexer([end_date], method="nearest")[0]]
- _warn(f"End date not in data. Using nearest: {date1}")
+ warn(f"End date not in data. Using nearest: {date1}")
else:
date1 = end_date
diff --git a/openbb_platform/extensions/technical/openbb_technical/relative_rotation.py b/openbb_platform/extensions/technical/openbb_technical/relative_rotation.py
new file mode 100644
index 00000000000..5bd3f7a295f
--- /dev/null
+++ b/openbb_platform/extensions/technical/openbb_technical/relative_rotation.py
@@ -0,0 +1,554 @@
+"""Relative Rotation Model."""
+
+# pylint: disable=too-many-arguments, too-many-instance-attributes, protected-access
+# pylint: disable=too-many-locals, too-few-public-methods, unused-argument
+
+import contextlib
+from typing import Any, Dict, List, Literal, Optional, Tuple, Union
+
+import numpy as np
+from openbb_core.app.model.obbject import OBBject
+from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel, df_to_basemodel
+from openbb_core.provider.abstract.data import Data
+from openbb_core.provider.abstract.fetcher import Fetcher
+from openbb_core.provider.abstract.query_params import QueryParams
+from pandas import DataFrame, Series, to_datetime
+from pydantic import Field, field_validator
+
+
+def absolute_maximum_scale(data: Series) -> Series:
+ """Absolute Maximum Scale Normaliztion Method."""
+ return data / data.abs().max()
+
+
+def min_max_scaling(data: Series) -> Series:
+ """Min/Max ScalingNormalization Method."""
+ return (data - data.min()) / (data.max() - data.min())
+
+
+def z_score_standardization(data: Series) -> Series:
+ """Z-Score Standardization Method."""
+ return (data - data.mean()) / data.std()
+
+
+def normalize(data: DataFrame, method: Literal["z", "m", "a"] = "z") -> DataFrame:
+ """
+ Normalize a Pandas DataFrame based on method.
+
+ Parameters
+ ----------
+ data: DataFrame
+ Pandas DataFrame with any number of columns to be normalized.
+ method: Literal["z", "m", "a"]
+ Normalization method.
+ z: Z-Score Standardization
+ m: Min/Max Scaling
+ a: Absolute Maximum Scale
+
+ Returns
+ -------
+ DataFrame
+ Normalized DataFrame.
+ """
+ methods = {
+ "z": z_score_standardization,
+ "m": min_max_scaling,
+ "a": absolute_maximum_scale,
+ }
+
+ df = data.copy()
+
+ for col in df.columns:
+ df.loc[:, col] = methods[f"{method}"](df.loc[:, col])
+
+ return df
+
+
+def standard_deviation(
+ data: DataFrame,
+ window: int = 21,
+ trading_periods: int = 252,
+) -> DataFrame:
+ """
+ Measures how widely returns are dispersed from the average return.
+
+ It is the most common (and biased) estimator of volatility.
+
+ Parameters
+ ----------
+ data : pd.DataFrame
+ Dataframe of OHLC prices.
+ window : int [default: 21]
+ Length of window to calculate over.
+ trading_periods : Optional[int] [default: 252]
+ Number of trading periods in a year.
+
+ Returns
+ -------
+ pd.DataFrame : results
+ Dataframe with results.
+ """
+ data = data.copy()
+ results = DataFrame()
+ if window < 2:
+ window = 21
+
+ for col in data.columns.tolist():
+ log_return = (data[col] / data[col].shift(1)).apply(np.log)
+
+ result = log_return.rolling(window=window, center=False).std() * np.sqrt(
+ trading_periods
+ )
+ results[col] = result
+
+ return results.dropna()
+
+
+def calculate_momentum(
+ data: Series, long_period: int = 252, short_period: int = 21
+) -> Series:
+ """
+ Momentum is calculated as the log trailing 12-month return minus trailing one-month return.
+
+ Higher values indicate larger, positive momentum exposure.
+
+ Momentum = ln(1 + r12) - ln(1 + r1)
+
+ Parameters
+ ----------
+ data: Series
+ Time series data to calculate the momentum for.
+ long_period: Optional[int]
+ Long period to base the calculation on. Default is one standard trading year.
+ short_period: Optional[int]
+ Short period to subtract from the long period. Default is one trading month.
+
+ Returns
+ -------
+ Series
+ Pandas Series with the calculated momentum.
+ """
+ df = data.copy()
+ epsilon = 1e-10
+ momentum_long = np.log(1 + df.pct_change(long_period) + epsilon)
+ momentum_short = np.log(1 + df.pct_change(short_period) + epsilon)
+ data = momentum_long - momentum_short # type: ignore
+
+ return data
+
+
+def get_momentum(
+ data: DataFrame, long_period: int = 252, short_period: int = 21
+) -> DataFrame:
+ """
+ Calculate the Relative-Strength Momentum Indicator.
+
+ Takes the Relative Strength Ratio as the input.
+
+ Parameters
+ ----------
+ data: DataFrame
+ Indexed time series data formatted with each column representing a ticker.
+ long_period: Optional[int]
+ Long period to base the calculation on. Default is one standard trading year.
+ short_period: Optional[int]
+ Short period to subtract from the long period. Default is one trading month.
+
+ Returns
+ -------
+ DataFrame
+ Pandas DataFrame with the calculated historical momentum factor exposure score.
+ """
+ df = data.copy()
+ rs_momentum = DataFrame()
+ for ticker in df.columns.to_list():
+ rs_momentum.loc[:, ticker] = calculate_momentum(
+ df.loc[:, ticker], long_period, short_period
+ ) # type: ignore
+
+ return rs_momentum
+
+
+def calculate_relative_strength_ratio(
+ symbols_data: DataFrame,
+ benchmark_data: DataFrame,
+) -> DataFrame:
+ """Calculate the Relative Strength Ratio for each ticker (column) in a DataFrame against the benchmark.
+
+ Symbols data and benchmark data should have the same index,
+ and each column should represent a ticker.
+
+ Parameters
+ ----------
+ symbols_data: DataFrame
+ Pandas DataFrame with the symbols data to compare against the benchmark.
+ benchmark_data: DataFrame
+ Pandas DataFrame with the benchmark data.
+
+ Returns
+ -------
+ DataFrame
+ Pandas DataFrame with the calculated relative strength
+ ratio for each ticker joined with the benchmark values.
+ """
+ return (
+ symbols_data.div(benchmark_data.iloc[:, 0], axis=0)
+ .multiply(100)
+ .join(benchmark_data.iloc[:, 0])
+ .dropna()
+ )
+
+
+def process_data(
+ symbols_data: DataFrame,
+ benchmark_data: DataFrame,
+ long_period: int = 252,
+ short_period: int = 21,
+ normalize_method: Literal["z", "m", "a"] = "z",
+) -> Tuple[DataFrame, DataFrame]:
+ """Process the raw data into normalized indicator values.
+
+ Parameters
+ ----------
+ symbols_data: DataFrame
+ Indexed time series data formatted with each column representing a ticker.
+ benchmark_data: DataFrame
+ Indexed time series data of the benchmark symbol.
+ long_period: Optional[int]
+ Long period to base the calculation on. Default is one standard trading year.
+ short_period: Optional[int]
+ Short period to subtract from the long period. Default is one trading month.
+ normalize_method: Literal["z", "m", "a"]
+
+ Returns
+ -------
+ Tuple[DataFrame, DataFrame]
+ Tuple of Pandas DataFrames with the normalized ratio and momentum indicator values.
+ """
+ ratio_data = calculate_relative_strength_ratio(symbols_data, benchmark_data)
+ momentum_data = get_momentum(ratio_data, long_period, short_period)
+ normalized_ratio = normalize(ratio_data, normalize_method)
+ normalized_momentum = normalize(momentum_data, normalize_method)
+
+ return normalized_ratio, normalized_momentum
+
+
+class RelativeRotation:
+ """Relative Rotation Class."""
+
+ def __init__(
+ self,
+ data: Union[List[Data], DataFrame],
+ benchmark: str,
+ study: Optional[Literal["price", "volume", "volatility"]] = "price",
+ long_period: Optional[int] = 252,
+ short_period: Optional[int] = 21,
+ window: Optional[int] = 21,
+ trading_periods: Optional[int] = 252,
+ ):
+ """Initialize the class."""
+ benchmark = benchmark.upper()
+ df = DataFrame()
+
+ target_col = "volume" if study == "volume" else "close"
+
+ if isinstance(data, OBBject):
+ data = data.results # type: ignore
+
+ if isinstance(data, List) and (
+ all(isinstance(d, Data) for d in data)
+ or all(isinstance(d, dict) for d in data)
+ ):
+ with contextlib.suppress(Exception):
+ df = basemodel_to_df(convert_to_basemodel(data), index="date")
+
+ if isinstance(data, DataFrame) and not df.empty:
+ df = data.copy()
+ if "date" in df.columns:
+ df.set_index("date", inplace=True)
+
+ if df.empty:
+ raise ValueError(
+ "Data must be a list of Data objects or a DataFrame with a 'date' column."
+ )
+
+ if "symbol" in df.columns:
+ df = df.pivot(columns="symbol", values=target_col)
+
+ if benchmark not in df.columns:
+ raise RuntimeError("The benchmark symbol was not found in the data.")
+
+ benchmark_data = df.pop(benchmark).to_frame()
+ symbols_data = df
+
+ if len(symbols_data) <= 252 and study in ["price", "volume"]: # type: ignore
+ raise ValueError(
+ "Supplied data must be daily intervals and have more than one year of back data to calculate"
+ " the most recent day in the time series."
+ )
+
+ if study == "volatility" and len(symbols_data) <= 504: # type: ignore
+ raise ValueError(
+ "Supplied data must be daily intervals and have more than two years of back data to calculate"
+ " the most recent day in the time series as a volatility study."
+ )
+ self.symbols = df.columns.to_list()
+ self.benchmark = benchmark
+ self.study = study
+ self.long_period = long_period
+ self.short_period = short_period
+ self.window = window
+ self.trading_periods = trading_periods
+ self.symbols_data = symbols_data # type: ignore
+ self.benchmark_data = benchmark_data # type: ignore
+ self._process_data() # type: ignore
+ self.symbols_data = df_to_basemodel(self.symbols_data.reset_index()) # type: ignore
+ self.benchmark_data = df_to_basemodel(self.benchmark_data.reset_index()) # type: ignore
+
+ def _process_data(self):
+ """Process the data."""
+ if self.study == "volatility":
+ self.symbols_data = standard_deviation(
+ self.symbols_data, # type: ignore
+ window=self.window, # type: ignore
+ trading_periods=self.trading_periods, # type: ignore
+ )
+ self.benchmark_data = standard_deviation(
+ self.benchmark_data, # type: ignore
+ window=self.window, # type: ignore
+ trading_periods=self.trading_periods, # type: ignore
+ )
+ ratios, momentum = process_data(
+ self.symbols_data, # type: ignore
+ self.benchmark_data, # type: ignore
+ long_period=self.long_period, # type: ignore
+ short_period=self.short_period, # type: ignore
+ )
+ # Re-index rs_ratios using the new index
+ index_after_dropping_nans = momentum.dropna().index
+ ratios = ratios.reindex(index_after_dropping_nans)
+ self.rs_ratios = df_to_basemodel(ratios.reset_index())
+ self.rs_momentum = df_to_basemodel(momentum.dropna().reset_index())
+ self.end_date = to_datetime(ratios.index[-1]).strftime("%Y-%m-%d")
+ self.start_date = to_datetime(ratios.index[0]).strftime("%Y-%m-%d")
+ return self
+
+
+def _get_type_name(t):
+ """Get the type name of a type hint."""
+ if hasattr(t, "__origin__"):
+ if hasattr(t.__origin__, "__name__"):
+ return f"{t.__origin__.__name__}[{', '.join([_get_type_name(arg) for arg in t.__args__])}]"
+ if hasattr(t.__origin__, "_name"):
+ return f"{t.__origin__._name}[{', '.join([_get_type_name(arg) for arg in t.__args__])}]"
+ if isinstance(t, str):
+ return t
+ if hasattr(t, "__name__"):
+ return t.__name__
+ if hasattr(t, "_name"):
+ return t._name
+ return str(t)
+
+
+class RelativeRotationQueryParams(QueryParams):
+ """Relative Rotation Query Parameters."""
+
+ data: List[Data] = Field(
+ description="The data to be used for the relative rotation calculations."
+ + " This should be the multi-symbol output from the"
+ + " 'equity.price.historical' endpoint, or similar, at a daily interval."
+ + " Or a pivot table with the 'date' column as the index, the symbols as the columns,"
+ + " and the 'study' as the values."
+ + " It is recommended to use the 'equity.price.historical' endpoint to get the data,"
+ + " and feed the results as-is."
+ )
+ benchmark: str = Field(description="The symbol to be used as the benchmark.")
+ study: Literal["price", "volume", "volatility"] = Field(
+ default="price",
+ description="The data point for the calculations."
+ + " If 'price', the closing price will be used."
+ + " If 'volatility', the standard deviation of the closing price will be used."
+ + " If 'data' is supplied as a pivot table,"
+ + " the 'study' will assume the values are the closing price and 'volume' will be ignored.",
+ )
+ long_period: Optional[int] = Field(
+ default=252,
+ description="The length of the long period for momentum calculation, by default is 252."
+ + " Adjust this value, to 365, when supplying assets such as crypto.",
+ )
+ short_period: Optional[int] = Field(
+ default=21,
+ description="The length of the short period for momentum calculation, by default is 21."
+ + " Adjust this value, to 30, when supplying assets such as crypto.",
+ )
+ window: Optional[int] = Field(
+ default=21,
+ description="The length of window for the standard deviation calculation, by default is 21."
+ + " Adjust this value, to 30, when supplying assets such as crypto.",
+ )
+ trading_periods: Optional[int] = Field(
+ default=252,
+ description="The number of trading periods per year,"
+ + " for the standard deviation calculation, by default is 252."
+ + " Adjust this value, to 365, when supplying assets such as crypto.",
+ )
+ chart_params: Optional[Dict[str, Any]] = Field(
+ default=None,
+ description="Additional parameters to pass when `chart=True` and the `openbb-charting` extension is installed."
+ + " Parameters can be passed again to redraw the chart using the charting.to_chart() method of the response."
+ + "\n"
+ + "\n ChartParams"
+ + "\n -----------"
+ + "\n date: Optional[str]"
+ + "\n A target end date within the data, by default is the last date in the data."
+ + "\n show_tails: bool"
+ + "\n Show the tails on the chart, by default is True."
+ + "\n tail_periods: Optional[int]"
+ + "\n Number of periods to show in the tails, by default is 16."
+ + "\n tail_interval: Literal['day', 'week', 'month']"
+ + "\n Interval to show the tails, by default is 'week'."
+ + "\n title: Optional[str]"
+ + "\n Title of the chart.",
+ )
+
+ @field_validator("benchmark", mode="before", check_fields=False)
+ @classmethod
+ def to_upper(cls, v):
+ """Convert the benchmark symbol to uppercase."""
+ return v.upper()
+
+ @field_validator("data", mode="before", check_fields=False)
+ @classmethod
+ def convert_data(cls, v):
+ """Validate the data format."""
+ if isinstance(v, OBBject):
+ return v.results
+ if isinstance(v, Data):
+ return v
+ if isinstance(v, (list, dict)):
+ return convert_to_basemodel(v)
+ if isinstance(v, DataFrame):
+ return df_to_basemodel(v.reset_index())
+ return v
+
+ def __init__(self, **data):
+ """Initialize the class."""
+ super().__init__(**data)
+ fields = self.__class__.model_fields
+ doc_str = (
+ "\n"
+ + self.__class__.__name__
+ + "\n\n"
+ + " Parameters\n"
+ + " ----------\n"
+ + "\n".join(
+ [
+ f" {k} : {_get_type_name(v.annotation)}\n {v.description}"
+ for k, v in fields.items()
+ ]
+ )
+ + "\n"
+ )
+ self.__doc__ = doc_str
+
+
+class RelativeRotationData(Data):
+ """Relative Rotation Data Model."""
+
+ symbols: List[str] = Field(
+ description="The symbols that are being compared against the benchmark."
+ )
+ benchmark: str = Field(description="The benchmark symbol, as entered by the user.")
+ study: Literal["price", "volume", "volatility"] = Field(
+ description="The data point for the study, as entered by the user."
+ )
+ long_period: int = Field(
+ description="The length of the long period for momentum calculation,"
+ + " as entered by the user."
+ )
+ short_period: int = Field(
+ description="The length of the short period for momentum calculation,"
+ + " as entered by the user."
+ )
+ window: int = Field(
+ description="The length of window for the standard deviation calculation,"
+ + " as entered by the user.",
+ )
+ trading_periods: int = Field(
+ description="The number of trading periods per year,"
+ + " for the standard deviation calculation, as entered by the user."
+ )
+ start_date: str = Field(
+ description="The start date of the data after adjusting"
+ + " the length of the data for the calculations."
+ )
+ end_date: str = Field(description="The end date of the data.")
+ symbols_data: List[Data] = Field(
+ description="The data representing the selected 'study' for each symbol."
+ )
+ benchmark_data: List[Data] = Field(
+ description="The data representing the selected 'study' for the benchmark."
+ )
+ rs_ratios: List[Data] = Field(
+ description="The normalized relative strength ratios data."
+ )
+ rs_momentum: List[Data] = Field(
+ description="The normalized relative strength momentum data."
+ )
+
+ def __init__(self, **data):
+ """Initialize the class."""
+ super().__init__(**data)
+ fields = self.__class__.model_fields
+ doc_str = (
+ "\n"
+ + self.__class__.__name__
+ + "\n\n"
+ + " Attributes\n"
+ + " ----------\n"
+ + "\n".join(
+ [
+ f" {k} : {_get_type_name(v.annotation)}\n {v.description}"
+ for k, v in fields.items()
+ ]
+ )
+ + "\n"
+ )
+ self.__doc__ = doc_str
+
+
+class RelativeRotationFetcher(
+ Fetcher[RelativeRotationQueryParams, RelativeRotationData]
+):
+ """Relative Rotation Fetcher."""
+
+ @staticmethod
+ def transform_query(params: Dict[str, Any]) -> RelativeRotationQueryParams:
+ """Transform the query parameters."""
+ return RelativeRotationQueryParams.model_validate(**params)
+
+ @staticmethod
+ def extract_data(
+ query: RelativeRotationQueryParams,
+ credentials: Optional[Dict[str, str]],
+ **kwargs: Any,
+ ) -> Dict:
+ """Extract the data."""
+ return RelativeRotation(
+ query.data,
+ query.benchmark,
+ study=query.study,
+ long_period=query.long_period,
+ short_period=query.short_period,
+ window=query.window,
+ trading_periods=query.trading_periods,
+ ).__dict__
+
+ @staticmethod
+ def transform_data(
+ query: RelativeRotationQueryParams,
+ data: Dict,
+ **kwargs: Any,
+ ) -> RelativeRotationData:
+ """Transform the data."""
+ return RelativeRotationData.model_validate(data)
diff --git a/openbb_platform/extensions/technical/openbb_technical/technical_router.py b/openbb_platform/extensions/technical/openbb_technical/technical_router.py
index e4faa20ef90..47aa24d2700 100644
--- a/openbb_platform/extensions/technical/openbb_technical/technical_router.py
+++ b/openbb_platform/extensions/technical/openbb_technical/technical_router.py
@@ -1,7 +1,7 @@
"""Technical Analysis Router."""
# pylint: disable=too-many-lines
-from typing import List, Literal, Optional
+from typing import Any, Dict, List, Literal, Optional
import pandas as pd
import pandas_ta as ta
@@ -17,15 +17,157 @@ from openbb_core.app.utils import (
from openbb_core.provider.abstract.data import Data
from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt
-from .helpers import (
+from openbb_technical.helpers import (
calculate_cones,
calculate_fib_levels,
clenow_momentum,
validate_data,
)
+from openbb_technical.relative_rotation import (
+ RelativeRotationData,
+ RelativeRotationFetcher,
+ RelativeRotationQueryParams,
+)
# TODO: Split this into multiple files
-router = Router(prefix="")
+router = Router(prefix="", description="Technical Analysis tools.")
+
+
+@router.command(
+ methods=["POST"],
+ examples=[
+ PythonEx(
+ description="Calculate the Relative Strength Ratio and Relative Strength Momentum"
+ + " for a group of symbols against a benchmark.",
+ code=[
+ "stock_data = obb.equity.price.historical("
+ + "symbol='AAPL,MSFT,GOOGL,META,AMZN,TSLA,SPY', start_date='2022-01-01', provider='yfinance')",
+ "rr_data = obb.technical.relative_rotation(data=stock_data.results, benchmark='SPY')",
+ "rs_ratios = rr_data.results.rs_ratios",
+ "rs_momentum = rr_data.results.rs_momentum",
+ ],
+ ),
+ PythonEx(
+ description="When the assets are not traded 252 days per year,"
+ + "adjust the momentum and volatility periods accordingly.",
+ code=[
+ "crypto_data = obb.crypto.price.historical("
+ + " symbol='BTCUSD,ETHUSD,SOLUSD', start_date='2021-01-01', provider='yfinance')",
+ "rr_data = obb.technical.relative_rotation(data=crypto_data.results, benchmark='BTCUSD',"
+ + " long_period=365, short_period=30, window=30, trading_periods=365)",
+ ],
+ ),
+ APIEx(
+ description="Note that the mock data displayed here is insufficient."
+ + " It must contain multiple symbols, with the benchmark, and be daily data at least 1 year in length.",
+ parameters={"benchmark": "SPY", "data": APIEx.mock_data("timeseries")},
+ ),
+ ],
+)
+async def relative_rotation(
+ data: List[Data],
+ benchmark: str,
+ study: Literal["price", "volume", "volatility"] = "price",
+ long_period: Optional[int] = 252,
+ short_period: Optional[int] = 21,
+ window: Optional[int] = 21,
+ trading_periods: Optional[int] = 252,
+ chart_params: Optional[Dict[str, Any]] = None,
+) -> OBBject[RelativeRotationData]:
+ """Calculate the Relative Strength Ratio and Relative Strength Momentum for a group of symbols against a benchmark.
+
+ Parameters
+ ----------
+ data : list[Data]
+ The data to be used for the relative rotation calculations.
+ This should be the multi-symbol output from the 'equity.price.historical' endpoint, or similar.
+ Or a pivot table with the 'date' column as the index, the symbols as the columns, and the 'study' as the values.
+ It is recommended to use the 'equity.price.historical' endpoint to get the data, and feed the results as-is.
+ benchmark : str
+ The symbol to be used as the benchmark.
+ study : Literal[price, volume, volatility]
+ The data point for the calculations. If 'price', the closing price will be used.
+ If 'volatility', the standard deviation of the closing price will be used.
+ If 'data' is supplied as a pivot table,
+ the 'study' will assume the values are the closing price and 'volume' will be ignored.
+ long_period : int, optional
+ The length of the long period for momentum calculation, by default 252.
+ Adjust this value when supplying a time series with an interval that is not daily.
+ For example, if the data is monthly, the long period should be 12.
+ short_period : int, optional
+ The length of the short period for momentum calculation, by default 21.
+ Adjust this value when supplying a time series with an interval that is not daily.
+ window : int, optional
+ The length of window for the standard deviation calculation, by default 21.
+ Adjust this value when supplying a time series with an interval that is not daily.
+ trading_periods : int, optional
+ The number of trading periods per year, for the standard deviation calculation, by default 252.
+ Adjust this value when supplying a time series with an interval that is not daily.
+ chart_params : dict[str, Any], optional
+ Additional parameters to pass when `chart=True` and the `openbb-charting` extension is installed.
+ Parameters can be passed again to redraw the chart using the charting.to_chart() method of the response.
+
+ ChartParams
+ -----------
+ date : str, optional
+ A target end date within the data to use for the chart, by default is the last date in the data.
+ show_tails : bool
+ Show the tails on the chart, by default True.
+ tail_periods : int
+ Number of periods to show in the tails, by default 16.
+ tail_interval : Literal[day, week, month]
+ Interval to show the tails, by default 'week'.
+ title : str, optional
+ Title of the chart.
+
+ Returns
+ -------
+ OBBject[RelativeRotationData]
+ results : RelativeRotationData
+ symbols : list[str]:
+ The symbols that are being compared against the benchmark.
+ benchmark : str
+ The benchmark symbol.
+ study : Literal[price, volume, volatility]
+ The data point for the selected.
+ long_period : int
+ The length of the long period for momentum calculation, as entered by the user.
+ short_period : int
+ The length of the short period for momentum calculation, as entered by the user.
+ window : int
+ The length of window for the standard deviation calculation.
+ trading_periods : int
+ The number of trading periods per year, for the standard deviation calculation.
+ start_date : str
+ The start date of the data after adjusting the length of the data for the calculations.
+ end_date : str
+ The end date of the data.
+ symbols_data : list[Data]
+ The data representing the selected 'study' for each symbol.
+ benchmark_data : list[Data]
+ The data representing the selected 'study' for the benchmark.
+ rs_ratios : list[Data]
+ The normalized relative strength ratios data.
+ rs_momentum : list[Data]
+ The normalized relative strength momentum data.
+ """
+
+ params = RelativeRotationQueryParams(
+ data=data,
+ benchmark=benchmark,
+ study=study,
+ long_period=long_period,
+ short_period=short_period,
+ window=window,
+ trading_periods=trading_periods,
+ chart_params=chart_params,
+ )
+
+ return OBBject(
+ results=RelativeRotationFetcher.transform_data(
+ params, RelativeRotationFetcher.extract_data(params, {})
+ )
+ )
@router.command(
@@ -489,7 +631,7 @@ def aroon(
data: List[Data],
index: str = "date",
length: int = 25,
- scalar: int = 100,
+ scalar: float = 100,
) -> OBBject[List[Data]]:
"""Calculate the Aroon Indicator.
@@ -513,7 +655,7 @@ def aroon(
Index column name to use with `data`, by default "date".
length : int, optional
Number of periods to be used for the calculation, by default 25.
- scalar : int, optional
+ scalar : float, optional
Scalar to be used for the calculation, by default 100.
Returns
diff --git a/openbb_platform/extensions/technical/poetry.lock b/openbb_platform/extensions/technical/poetry.lock
index ceb9ffc842f..d081a56833d 100644
--- a/openbb_platform/extensions/technical/poetry.lock
+++ b/openbb_platform/extensions/technical/poetry.lock
@@ -648,13 +648,13 @@ files = [
[[package]]
name = "openbb-core"
-version = "1.1.5"
+version = "1.1.6"
description = "OpenBB package with core functionality"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "openbb_core-1.1.5-py3-none-any.whl", hash = "sha256:abac970b9503a40ed12c3a153b734c419e46bdc7ecd9e9d51c384c0b6bf140bd"},
- {file = "openbb_core-1.1.5.tar.gz", hash = "sha256:c280bfbf579cf1accf32bbd6a4de581c39b5e88793a3d2c362c85c87a3fe31ad"},
+ {file = "openbb_core-1.1.6-py3-none-any.whl", hash = "sha256:4161d6e7654cab2185fcc39e5172385c8226e3cf6bfd5d4b1ff1076f70a6374c"},
+ {file = "openbb_core-1.1.6.tar.gz", hash = "sha256:bf56a8a0dc26d72e679a2c68b1f57bf6703ed130b52348a77a2f6cfa32c9fcae"},
]
[package.dependencies]
@@ -1562,4 +1562,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<3.12"
-content-hash = "0e3d117096b03b113888e7be6639504b3552aabcb58a4c9f648d7a6fd1c4c21e"
+content-hash = "3d13f50e40aed069054b8016c306389ee3ff6649cdb88583fd2bc6d9501e7408"
diff --git a/openbb_platform/extensions/technical/pyproject.toml b/openbb_platform/extensions/technical/pyproject.toml
index 18b1eceb67c..8d38641d074 100644
--- a/openbb_platform/extensions/technical/pyproject.toml
+++ b/openbb_platform/extensions/technical/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openbb-technical"
-version = "1.1.5"
+version = "1.1.6"
description = "Technical Analysis extension for OpenBB"
authors = ["OpenBB Team "]
readme = "README.md"
@@ -12,7 +12,7 @@ scipy = "^1.10.1"
statsmodels = "^0.14.0"
scikit-learn = "^1.3.1"
pandas-ta = "^0.3.14b"
-openbb-core = "^1.1.5"
+openbb-core = "^1.1.6"
[build-system]
requires = ["poetry-core"]
diff --git a/openbb_platform/extensions/technical/tests/test_technical_helpers.py b/openbb_platform/extensions/technical/tests/test_technical_helpers.py
index 82ef6919fed..9b4b7a9d102 100644
--- a/openbb_platform/extensions/technical/tests/test_technical_helpers.py
+++ b/openbb_platform/extensions/technical/tests/test_technical_helpers.py
@@ -20,6 +20,7 @@ from extensions.technical.openbb_technical.helpers import (
@pytest.fixture(scope="module")
def mock_data(top_range: int = 100):
+ """Mock data for testing."""
_open = pd.Series(np.arange(1, top_range))
_high = pd.Series(np.arange(1, top_range))
_low = pd.Series(np.arange(1, top_range))
diff --git a/openbb_platform/extensions/tests/__init__.py b/openbb_platform/extensions/tests/__init__.py
index e69de29bb2d..d420712d8bb 100644
--- a/openbb_platform/extensions/tests/__init__.py
+++ b/openbb_platform/extensions/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests."""
diff --git a/openbb_platform/extensions/tests/conftest.py b/openbb_platform/extensions/tests/conftest.py
index b8849169816..c71c47faab5 100644
--- a/openbb_platform/extensions/tests/conftest.py
+++ b/openbb_platform/extensions/tests/conftest.py
@@ -14,7 +14,7 @@ commands = list(cm.map.keys())
def parametrize(argnames: str, argvalues: List, **kwargs):
- """Custom parametrize decorator that filters test cases based on the environment."""
+ """Apply a custom parametrize decorator that filters test cases based on the environment."""
routers, providers, obbject_ext = list_openbb_extensions()
diff --git a/openbb_platform/extensions/tests/utils/__init__.py b/openbb_platform/extensions/tests/utils/__init__.py
index e69de29bb2d..479fdbbe1fb 100644
--- a/openbb_platform/extensions/tests/utils/__init__.py
+++ b/openbb_platform/extensions/tests/utils/__init__.py
@@ -0,0 +1 @@
+"""Testing utilities."""
diff --git a/openbb_platform/extensions/tests/utils/helpers.py b/openbb_platform/extensions/tests/utils/helpers.py
index 8d35fced671..2411a85484b 100644
--- a/openbb_platform/extensions/tests/utils/helpers.py
+++ b/openbb_platform/extensions/tests/utils/helpers.py
@@ -242,7 +242,7 @@ def find_missing_router_function_models(
def parse_example_string(example_string: str) -> Dict[str, Any]:
- """Parses a string of examples into nested dictionaries.
+ """Parse a string of examples into nested dictionaries.
This is capturing all instances of PythonEx and APIEx, including their "parameters", "code", and "description".
"""
diff --git a/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py b/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py
index f89c1b318bf..5066d5a40b7 100644
--- a/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py
+++ b/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py
@@ -1,3 +1,5 @@
+"""Integration tests for charting API."""
+
import base64
import json
@@ -49,7 +51,7 @@ def get_equity_data():
[
(
{
- "provider": "fmp",
+ "provider": "yfinance",
"symbol": "AAPL",
"chart": True,
}
@@ -58,7 +60,7 @@ def get_equity_data():
)
@pytest.mark.integration
def test_charting_equity_price_historical(params, headers):
- """Test chart equity load."""
+ """Test chart equity price historical.."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
@@ -75,6 +77,130 @@ def test_charting_equity_price_historical(params, headers):
assert list(chart.keys()) == ["content", "format"]
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "USDGBP",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_currency_price_historical(params, headers):
+ """Test chart currency price historical."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/currency/price/historical?{query_str}"
+ result = requests.get(url, headers=headers, timeout=40)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "QQQ",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_historical(params, headers):
+ """Test chart etf historical."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/etf/historical?{query_str}"
+ result = requests.get(url, headers=headers, timeout=40)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "NDX",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_index_price_historical(params, headers):
+ """Test chart index price historical."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/index/price/historical?{query_str}"
+ result = requests.get(url, headers=headers, timeout=40)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "BTCUSD",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_crypto_price_historical(params, headers):
+ """Test chart crypto price historical."""
+ params = {p: v for p, v in params.items() if v}
+
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/crypto/price/historical?{query_str}"
+ result = requests.get(url, headers=headers, timeout=40)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
@parametrize(
"params",
[
@@ -444,3 +570,152 @@ def test_charting_economy_fred_series(params, headers):
assert chart
assert not fig
assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": "",
+ "study": "price",
+ "benchmark": "SPY",
+ "long_period": 252,
+ "short_period": 21,
+ "window": 21,
+ "trading_periods": 252,
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_technical_relative_rotation(params):
+ params = {p: v for p, v in params.items() if v}
+ data_params = dict(
+ symbol="AAPL,MSFT,GOOGL,AMZN,SPY",
+ provider="yfinance",
+ start_date="2022-01-01",
+ end_date="2024-01-01",
+ )
+ data_query_str = get_querystring(data_params, [])
+ data_url = f"http://0.0.0.0:8000/api/v1/equity/price/historical?{data_query_str}"
+ data_result = requests.get(data_url, headers=get_headers(), timeout=10).json()[
+ "results"
+ ]
+ body = json.dumps({"data": data_result})
+ query_str = get_querystring(params, ["data"])
+ url = f"http://0.0.0.0:8000/api/v1/technical/relative_rotation?{query_str}"
+ result = requests.post(url, headers=get_headers(), timeout=10, data=body)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
+ "chart": True,
+ "provider": "finviz",
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_equity_price_performance(params, headers):
+ """Test chart equity price performance."""
+ params = {p: v for p, v in params.items() if v}
+ body = (
+ json.dumps(
+ {"extra_params": {"chart_params": {"limit": 4, "orientation": "h"}}}
+ ),
+ )
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/equity/price/performance?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10, json=body)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
+ "chart": True,
+ "provider": "intrinio",
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_price_performance(params, headers):
+ """Test chart equity price performance."""
+ params = {p: v for p, v in params.items() if v}
+ body = (json.dumps({"extra_params": {"chart_params": {"orientation": "v"}}}),)
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/etf/price_performance?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10, json=body)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT",
+ "chart": True,
+ "provider": "fmp",
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_holdings(params, headers):
+ """Test chart etf holdings."""
+ params = {p: v for p, v in params.items() if v}
+ body = (
+ json.dumps(
+ {"extra_params": {"chart_params": {"orientation": "v", "limit": 10}}}
+ ),
+ )
+ query_str = get_querystring(params, [])
+ url = f"http://0.0.0.0:8000/api/v1/etf/holdings?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10, json=body)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+
+ chart = result.json()["chart"]
+ fig = chart.pop("fig", {})
+
+ assert chart
+ assert not fig
+ assert list(chart.keys()) == ["content", "format"]
diff --git a/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py b/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py
index 1995eabf011..f8bc9275e59 100644
--- a/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py
+++ b/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py
@@ -42,7 +42,7 @@ def get_equity_data():
[
(
{
- "provider": "fmp",
+ "provider": "yfinance",
"symbol": "AAPL",
"chart": True,
}
@@ -60,6 +60,98 @@ def test_charting_equity_price_historical(params, obb):
assert isinstance(result.chart.fig, OpenBBFigure)
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "JPYUSD",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_currency_price_historical(params, obb):
+ """Test chart currency price historical."""
+ result = obb.currency.price.historical(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "BTCUSD",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_crypto_price_historical(params, obb):
+ """Test chart crypto price historical."""
+ result = obb.crypto.price.historical(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "NDX",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_index_price_historical(params, obb):
+ """Test chart index price historical."""
+ result = obb.index.price.historical(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "yfinance",
+ "symbol": "QQQ",
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_historical(params, obb):
+ """Test chart etf historical."""
+ result = obb.etf.historical(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
@parametrize(
"params",
[
@@ -381,3 +473,120 @@ def test_charting_economy_fred_series(params, obb):
assert len(result.results) > 0
assert result.chart.content
assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": "",
+ "study": "price",
+ "benchmark": "SPY",
+ "long_period": 252,
+ "short_period": 21,
+ "window": 21,
+ "trading_periods": 252,
+ "chart": True,
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_charting_technical_relative_rotation(params, obb):
+ params["data"] = obb.equity.price.historical(
+ "AAPL,MSFT,GOOGL,AMZN,SPY",
+ provider="yfinance",
+ start_date="2022-01-01",
+ end_date="2024-01-01",
+ ).results
+ result = obb.technical.relative_rotation(
+ data=params["data"],
+ benchmark=params["benchmark"],
+ study=params["study"],
+ long_period=params["long_period"],
+ short_period=params["short_period"],
+ window=params["window"],
+ trading_periods=params["trading_periods"],
+ chart=params["chart"],
+ )
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results.rs_ratios) > 0 # type: ignore
+ assert result.chart.content # type: ignore
+ assert isinstance(result.chart.fig, OpenBBFigure) # type: ignore
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
+ "chart": True,
+ "provider": "finviz",
+ "chart_params": {"limit": 4, "orientation": "h"},
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_equity_price_performance(params, obb):
+ """Test chart equity price performance."""
+ result = obb.equity.price.performance(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
+ "chart": True,
+ "provider": "intrinio",
+ "chart_params": {"orientation": "v"},
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_price_performance(params, obb):
+ """Test chart etf price performance."""
+ result = obb.etf.price_performance(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "data": None,
+ "symbol": "XRT",
+ "chart": True,
+ "provider": "fmp",
+ "chart_params": {"orientation": "v", "limit": 10},
+ }
+ )
+ ],
+)
+@pytest.mark.integration
+def test_charting_etf_holdings(params, obb):
+ """Test chart etf holdings."""
+ result = obb.etf.holdings(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
+ assert result.chart.content
+ assert isinstance(result.chart.fig, OpenBBFigure)
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py b/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py
index ac585fefada..c79dc270784 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py
@@ -1,25 +1,40 @@
"""OpenBB OBBject extension for charting."""
+# pylint: disable=too-many-arguments,unused-argument
+
+import warnings
from typing import (
Any,
Callable,
Dict,
List,
+ Literal,
Optional,
+ Tuple,
Union,
)
+from warnings import warn
import numpy as np
import pandas as pd
from openbb_core.app.model.charts.chart import Chart
from openbb_core.app.model.extension import Extension
+from openbb_core.app.model.obbject import OBBject
from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel
from openbb_core.provider.abstract.data import Data
+from plotly.graph_objs import Figure
from openbb_charting import charting_router
-from openbb_charting.core.to_chart import ChartIndicators, to_chart
+from openbb_charting.core.backend import Backend, create_backend, get_backend
+from openbb_charting.core.openbb_figure import OpenBBFigure
+from openbb_charting.query_params import ChartParams, IndicatorsParams
+from openbb_charting.utils.generic_charts import bar_chart, line_chart
from openbb_charting.utils.helpers import get_charting_functions
+warnings.filterwarnings(
+ "ignore", category=UserWarning, module="openbb_core.app.model.extension", lineno=47
+)
+
ext = Extension(name="charting")
@@ -33,74 +48,323 @@ class Charting:
show
Display chart and save it to the OBBject.
to_chart
- Returns the plotly json representation of the chart.
+ Redraw the chart and save it to the OBBject, with an optional entry point for Data.
functions
- Returns a list of Platform commands with charting functions.
+ Return a list of Platform commands with charting functions.
+ get_params
+ Return the charting parameters for the function the OBBject was created from.
indicators
- Returns a list of the available indicators to use with the `to_chart` method.
+ Return the list of the available technical indicators to use with the `to_chart` method and OHLC+V data.
+ table
+ Display an interactive table.
+ create_line_chart
+ Create a line chart from external data.
+ create_bar_chart
+ Create a bar chart, on a single x-axis with one or more values for the y-axis, from external data.
"""
def __init__(self, obbject):
"""Initialize Charting extension."""
# pylint: disable=import-outside-toplevel
from openbb_core.app.model.charts.charting_settings import ChartingSettings
- from openbb_core.app.model.obbject import OBBject
self._obbject: OBBject = obbject
self._charting_settings = ChartingSettings(
user_settings=self._obbject._user_settings, # type: ignore
system_settings=self._obbject._system_settings, # type: ignore
)
- self._handle_backend()
+ self._backend: Backend = self._handle_backend()
@classmethod
def indicators(cls):
- """Returns a list of the available indicators."""
- return ChartIndicators.get_available_indicators()
+ """
+ Return an instance of the IndicatorsParams class, containing all available indicators and their parameters.
+ Without assigning to a variable, it will print the the information to the console.
+ """
+ return IndicatorsParams()
@classmethod
def functions(cls):
- """Returns a list of the available functions."""
+ """Return a list of the available functions."""
return get_charting_functions()
- def _handle_backend(self):
- # pylint: disable=import-outside-toplevel
- from openbb_charting.core.backend import create_backend, get_backend
+ def _handle_backend(self) -> Backend:
+ """Create and start the backend."""
create_backend(self._charting_settings)
- get_backend().start(debug=self._charting_settings.debug_mode)
+ backend = get_backend()
+ backend.start(debug=self._charting_settings.debug_mode)
+ return backend
@staticmethod
def _get_chart_function(route: str) -> Callable:
"""Given a route, it returns the chart function. The module must contain the given route."""
+
+ if route is None:
+ raise ValueError("OBBject was initialized with no function route.")
adjusted_route = route.replace("/", "_")[1:]
return getattr(charting_router, adjusted_route)
- def show(self, render: bool = True, **kwargs):
- """Display chart and save it to the OBBject."""
- charting_function = self._get_chart_function(
+ def get_params(self) -> ChartParams:
+ """
+ Return the ChartQueryParams class for the function the OBBject was created from.
+ Without assigning to a variable, it will print the docstring to the console.
+ """
+ if self._obbject._route is None: # pylint: disable=protected-access
+ raise ValueError("OBBject was initialized with no function route.")
+ charting_function = (
self._obbject._route # pylint: disable=protected-access
+ ).replace("/", "_")[1:]
+ if hasattr(ChartParams, charting_function):
+ return getattr(ChartParams, charting_function)()
+ raise ValueError(
+ f"Error: No chart parameters are defined for the route: {charting_function}"
)
- kwargs["obbject_item"] = self._obbject.results
- kwargs["charting_settings"] = self._charting_settings
- if (
- hasattr(self._obbject, "_standard_params")
- and self._obbject._standard_params # pylint: disable=protected-access
- ):
- kwargs["standard_params"] = (
- self._obbject._standard_params.__dict__ # pylint: disable=protected-access
- )
- kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access
- kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access
- kwargs["warnings"] = self._obbject.warnings # pylint: disable=protected-access
- fig, content = charting_function(**kwargs)
- self._obbject.chart = Chart(
- fig=fig, content=content, format=charting_router.CHART_FORMAT
+
+ def _prepare_data_as_df(
+ self, data: Optional[Union[pd.DataFrame, pd.Series]]
+ ) -> Tuple[pd.DataFrame, bool]:
+ """Convert supplied data to a DataFrame."""
+ has_data = (
+ isinstance(data, (Data, pd.DataFrame, pd.Series)) and not data.empty
+ ) or (bool(data))
+ index = (
+ data.index.name
+ if has_data and isinstance(data, (pd.DataFrame, pd.Series))
+ else None
+ )
+ data_as_df: pd.DataFrame = (
+ basemodel_to_df(convert_to_basemodel(data), index=index)
+ if has_data
+ else self._obbject.to_dataframe(index=index)
+ )
+ if "date" in data_as_df.columns:
+ data_as_df = data_as_df.set_index("date")
+ if "provider" in data_as_df.columns:
+ data_as_df.drop(columns="provider", inplace=True)
+ return data_as_df, has_data
+
+ # pylint: disable=too-many-locals
+ def create_line_chart(
+ self,
+ data: Union[
+ list,
+ dict,
+ pd.DataFrame,
+ List[pd.DataFrame],
+ pd.Series,
+ List[pd.Series],
+ np.ndarray,
+ Data,
+ ],
+ index: Optional[str] = None,
+ target: Optional[str] = None,
+ title: Optional[str] = None,
+ x: Optional[str] = None,
+ xtitle: Optional[str] = None,
+ y: Optional[Union[str, List[str]]] = None,
+ ytitle: Optional[str] = None,
+ y2: Optional[Union[str, List[str]]] = None,
+ y2title: Optional[str] = None,
+ layout_kwargs: Optional[dict] = None,
+ scatter_kwargs: Optional[dict] = None,
+ normalize: bool = False,
+ returns: bool = False,
+ same_axis: bool = False,
+ render: bool = True,
+ **kwargs,
+ ) -> Union[OpenBBFigure, Figure, None]:
+ """Create a line chart from external data and render a chart or return the OpenBBFigure.
+
+ Parameters
+ ----------
+ data : Union[Data, pd.DataFrame, pd.Series]
+ Data to be plotted (OHLCV data).
+ index : Optional[str], optional
+ Index column, by default None
+ target : Optional[str], optional
+ Target column to be plotted, by default None
+ title : Optional[str], optional
+ Chart title, by default None
+ x : Optional[str], optional
+ X-axis column, by default None
+ xtitle : Optional[str], optional
+ X-axis title, by default None
+ y : Optional[Union[str, List[str]]], optional
+ Y-axis column(s), by default None
+ If None are supplied, the layout is optimized for the contents of data.
+ Where many units/scales are present,
+ it will attempt to divide based on the range of values.
+ ytitle : Optional[str], optional
+ Y-axis title, by default None
+ y2 : Optional[Union[str, List[str]]], optional
+ Y2-axis column(s), by default None
+ y2title : Optional[str], optional
+ Y2-axis title, by default None
+ layout_kwargs : Optional[dict], optional
+ Additional Plotly Layout parameters for `fig.update_layout`, by default None
+ scatter_kwargs : Optional[dict], optional
+ Additional Plotly parameters applied on creation of each scatter plot, by default None
+ normalize : bool, optional
+ Normalize the data with Z-Score Standardization, by default False
+ returns : bool, optional
+ Convert the data to cumulative returns, by default False
+ same_axis: bool, optional
+ If True, forces all data onto the same Y-axis, by default False
+ render: bool, optional
+ If True, the chart will be rendered, by default True
+ **kwargs: Dict[str, Any]
+ Extra parameters to be passed to `figure.show()`
+ """
+ fig = line_chart(
+ data=data,
+ index=index,
+ target=target,
+ title=title,
+ x=x,
+ xtitle=xtitle,
+ y=y,
+ ytitle=ytitle,
+ y2=y2,
+ y2title=y2title,
+ layout_kwargs=layout_kwargs,
+ scatter_kwargs=scatter_kwargs,
+ normalize=normalize,
+ returns=returns,
+ same_axis=same_axis,
+ **kwargs,
)
if render:
- fig.show(**kwargs)
+ return fig.show(**kwargs)
- # pylint: disable=too-many-arguments
+ return fig
+
+ def create_bar_chart(
+ self,
+ data: Union[
+ list,
+ dict,
+ pd.DataFrame,
+ List[pd.DataFrame],
+ pd.Series,
+ List[pd.Series],
+ np.ndarray,
+ Data,
+ ],
+ x: str,
+ y: Union[str, List[str]],
+ barmode: Literal["group", "stack", "relative", "overlay"] = "group",
+ xtype: Literal[
+ "category", "multicategory", "date", "log", "linear"
+ ] = "category",
+ title: Optional[str] = None,
+ xtitle: Optional[str] = None,
+ ytitle: Optional[str] = None,
+ orientation: Literal["h", "v"] = "v",
+ colors: Optional[List[str]] = None,
+ layout_kwargs: Optional[Dict[str, Any]] = None,
+ bar_kwargs: Optional[Dict[str, Any]] = None,
+ render: bool = True,
+ **kwargs,
+ ) -> Union[OpenBBFigure, Figure, None]:
+ """Create a bar chart on a single x-axis with one or more values for the y-axis.
+
+ Parameters
+ ----------
+ data : Union[list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data]
+ Data to plot.
+ x : str
+ The x-axis column name.
+ y : Union[str, List[str]]
+ The y-axis column name(s).
+ barmode : Literal["group", "stack", "relative", "overlay"], optional
+ The bar mode, by default "group".
+ xtype : Literal["category", "multicategory", "date", "log", "linear"], optional
+ The x-axis type, by default "category".
+ title : str, optional
+ The title of the chart, by default None.
+ xtitle : str, optional
+ The x-axis title, by default None.
+ ytitle : str, optional
+ The y-axis title, by default None.
+ colors: List[str], optional
+ Manually set the colors to cycle through for each column in 'y', by default None.
+ bar_kwargs : Dict[str, Any], optional
+ Additional keyword arguments to apply with figure.add_bar(), by default None.
+ layout_kwargs : Dict[str, Any], optional
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
+ Returns
+ -------
+ OpenBBFigure
+ The OpenBBFigure object.
+ """
+
+ fig = bar_chart(
+ data=data,
+ x=x,
+ y=y,
+ barmode=barmode,
+ xtype=xtype,
+ title=title,
+ xtitle=xtitle,
+ ytitle=ytitle,
+ orientation=orientation,
+ colors=colors,
+ bar_kwargs=bar_kwargs,
+ layout_kwargs=layout_kwargs,
+ )
+
+ if render:
+ return fig.show(**kwargs)
+
+ return fig
+
+ # pylint: disable=inconsistent-return-statements
+ def show(self, render: bool = True, **kwargs):
+ """Display chart and save it to the OBBject."""
+
+ try:
+ charting_function = self._get_chart_function(
+ self._obbject._route # pylint: disable=protected-access
+ )
+ kwargs["obbject_item"] = self._obbject.results
+ kwargs["charting_settings"] = self._charting_settings
+ if (
+ hasattr(self._obbject, "_standard_params")
+ and self._obbject._standard_params # pylint: disable=protected-access
+ ):
+ kwargs["standard_params"] = (
+ self._obbject._standard_params.__dict__ # pylint: disable=protected-access
+ )
+ kwargs["provider"] = (
+ self._obbject.provider
+ ) # pylint: disable=protected-access
+ kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access
+
+ if "kwargs" in kwargs:
+ _kwargs = kwargs.pop("kwargs")
+ kwargs.update(_kwargs.get("chart_params", {}))
+ fig, content = charting_function(**kwargs)
+ self._obbject.chart = Chart(
+ fig=fig, content=content, format=charting_router.CHART_FORMAT
+ )
+ if render:
+ fig.show(**kwargs)
+ except Exception:
+ try:
+ fig = self.create_line_chart(data=self._obbject.results, render=False, **kwargs) # type: ignore
+ content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
+ self._obbject.chart = Chart(
+ fig=fig, content=content, format=charting_router.CHART_FORMAT
+ )
+ if render:
+ return fig.show(**kwargs) # type: ignore
+ except Exception as e:
+ raise RuntimeError(
+ "Failed to automatically create a generic chart with the data provided."
+ ) from e
+
+ # pylint: disable=too-many-locals,inconsistent-return-statements
def to_chart(
self,
data: Optional[
@@ -115,100 +379,147 @@ class Charting:
Data,
]
] = None,
- indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = None,
+ target: Optional[str] = None,
+ index: Optional[str] = None,
+ indicators: Optional[Dict[str, Dict[str, Any]]] = None,
symbol: str = "",
candles: bool = True,
volume: bool = True,
- prepost: bool = False,
volume_ticks_x: int = 7,
render: bool = True,
**kwargs,
):
"""
- Creates a OpenBBFigure with user customizations (if any) and saves it to the OBBject.
+ Create an OpenBBFigure with user customizations (if any) and save it to the OBBject.
+ This function is used to populate, or re-populate, the OBBject with a chart using the data within
+ the OBBject or external data supplied via the `data` parameter.
- This function is used so it can be called at the module level and used out of the box,
- which allows some more flexibility, ease of use and doesn't require the user to know
- about the PlotlyTA class.
+ This function modifies the original OBBject by overwriting the existing chart.
Parameters
----------
- data : Union[pd.DataFrame, pd.Series]
- Data to be plotted Data to be plotted (OHLCV data).
- indicators : Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]], optional
+ data : Union[Data, pd.DataFrame, pd.Series]
+ Data to be plotted.
+ indicators : Dict[str, Dict[str, Any]], optional
Indicators to be plotted, by default None
symbol : str, optional
- Symbol to be plotted, by default ""
+ Symbol to be plotted. This is used for labels and titles, by default ""
candles : bool, optional
If True, candles will be plotted, by default True
volume : bool, optional
If True, volume will be plotted, by default True
- prepost : bool, optional
- If True, prepost will be plotted, by default False
volume_ticks_x : int, optional
Volume ticks, by default 7
render : bool, optional
If True, the chart will be rendered, by default True
+ kwargs: Dict[str, Any]
+ Extra parameters to be passed to the chart constructor.
Examples
--------
- 1) Plotting a time series with TA indicators
- > from openbb import obb
- > res = obb.equity.price.historical("AAPL")
- > indicators = dict(
- > sma=dict(length=[20,30,50]),
- > adx=dict(length=14),
- > rsi=dict(length=14),
- > macd=dict(fast=12, slow=26, signal=9),
- > bbands=dict(length=20, std=2),
- > stoch=dict(length=14),
- > ema=dict(length=[20,30,50]),
- > )
- > res.charting.to_chart(**{"indicators": indicators})
+ Plotting a time series with TA indicators
- 2) Get all the available indicators
- > from openbb_charting.core.plotly_ta.data_classes import ChartIndicators
- > ChartIndicators.get_available_indicators()
- or
- > from openbb_charting import Charting
- > Charting.indicators()
+ >>> from openbb import obb
+ >>> res = obb.equity.price.historical("AAPL")
+ >>> indicators = dict(
+ >>> sma=dict(length=[20,30,50]),
+ >>> adx=dict(length=14),
+ >>> rsi=dict(length=14),
+ >>> macd=dict(fast=12, slow=26, signal=9),
+ >>> bbands=dict(length=20, std=2),
+ >>> stoch=dict(length=14),
+ >>> ema=dict(length=[20,30,50]),
+ >>> )
+ >>> res.charting.to_chart(**{"indicators": indicators})
+
+ Get all the available indicators
+
+ >>> res = obb.equity.price.historical("AAPL")
+ >>> indicators = res.charting.indicators()
+ >>> indicators?
"""
- has_data = (isinstance(data, (pd.DataFrame, pd.Series)) and not data.empty) or (
- data
- )
- index = (
- data.index.name
- if has_data and isinstance(data, (pd.DataFrame, pd.Series))
- else None
- )
- data_as_df: pd.DataFrame = (
- basemodel_to_df(convert_to_basemodel(data), index=index)
- if has_data
- else self._obbject.to_dataframe()
- )
- if "date" in data_as_df.columns:
- data_as_df = data_as_df.set_index("date")
+ data_as_df, has_data = self._prepare_data_as_df(data) # type: ignore
+ if target is not None:
+ data_as_df = data_as_df[[target]]
+ kwargs["candles"] = candles
+ kwargs["volume"] = volume
+ kwargs["volume_ticks_x"] = volume_ticks_x
+ kwargs["indicators"] = indicators if indicators else {}
+ kwargs["symbol"] = symbol
+ kwargs["target"] = target
+ kwargs["index"] = index
+ kwargs["obbject_item"] = self._obbject.results
+ kwargs["charting_settings"] = self._charting_settings
+ if (
+ hasattr(self._obbject, "_standard_params")
+ and self._obbject._standard_params # pylint: disable=protected-access
+ ):
+ kwargs["standard_params"] = (
+ self._obbject._standard_params.__dict__ # pylint: disable=protected-access
+ )
+ kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access
+ kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access
+ if "kwargs" in kwargs:
+ _kwargs = kwargs.pop("kwargs")
+ kwargs.update(_kwargs.get("chart_params", {}))
try:
- fig, content = to_chart(
- data_as_df,
- indicators=indicators,
- symbol=symbol,
- candles=candles,
- volume=volume,
- prepost=prepost,
- volume_ticks_x=volume_ticks_x,
- )
- self._obbject.chart = Chart(
- fig=fig, content=content, format=charting_router.CHART_FORMAT
- )
- if render:
- fig.show(**kwargs)
-
+ if has_data:
+ self.show(data=data_as_df, render=render, **kwargs)
+ else:
+ self.show(**kwargs, render=render)
except Exception:
try:
- if has_data:
- self.show(data=data_as_df, symbol=symbol, render=render, **kwargs)
- else:
- self.show(**kwargs)
+ fig = self.create_line_chart(data=data_as_df, render=False, **kwargs)
+ content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
+ self._obbject.chart = Chart(
+ fig=fig, content=content, format=charting_router.CHART_FORMAT
+ )
+ if render:
+ return fig.show(**kwargs) # type: ignore
except Exception as e:
- raise RuntimeError("Could not create chart from the OBBject.") from e
+ raise RuntimeError(
+ "Failed to automatically create a generic chart with the data provided."
+ ) from e
+
+ def table(
+ self,
+ data: Optional[Union[pd.DataFrame, pd.Series]] = None,
+ title: str = "",
+ ):
+ """
+ Display an interactive table.
+
+ Parameters
+ ----------
+ data : Optional[Union[pd.DataFrame, pd.Series]], optional
+ Data to be plotted, by default None.
+ If no data is provided the OBBject results will be used.
+ title : str, optional
+ Title of the table, by default "".
+ """
+ data_as_df, _ = self._prepare_data_as_df(data)
+ if isinstance(data_as_df.index, pd.RangeIndex):
+ data_as_df.reset_index(inplace=True, drop=True)
+ else:
+ data_as_df.reset_index(inplace=True)
+ if self._backend.isatty:
+ try:
+ self._backend.send_table(
+ df_table=data_as_df,
+ title=title
+ or self._obbject._route, # pylint: disable=protected-access
+ theme=self._charting_settings.table_style,
+ )
+ except Exception as e:
+ warn(f"Failed to show figure with backend. {e}")
+
+ else:
+ from plotly import ( # pylint:disable=import-outside-toplevel
+ optional_imports,
+ )
+
+ ipython_display = optional_imports.get_module("IPython.display")
+ if ipython_display:
+ ipython_display.display(ipython_display.HTML(data_as_df.to_html()))
+ else:
+ warn("IPython.display is not available.")
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py b/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py
index 08c27d8c12c..fc271fbeaa1 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py
@@ -1,145 +1,793 @@
"""Charting router."""
-import json
+# pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements, unused-argument, too-many-lines
+
from typing import Any, Dict, Optional, Tuple, Union
+from warnings import warn
import pandas as pd
from openbb_core.app.model.charts.chart import ChartFormat
from openbb_core.app.utils import basemodel_to_df
+from plotly.graph_objs import Figure
from openbb_charting.core.chart_style import ChartStyle
from openbb_charting.core.openbb_figure import OpenBBFigure
from openbb_charting.core.plotly_ta.ta_class import PlotlyTA
-from openbb_charting.query_params import (
- FredSeriesChartQueryParams,
- TechnicalConesChartQueryParams,
+from openbb_charting.core.to_chart import to_chart
+from openbb_charting.styles.colors import LARGE_CYCLER
+from openbb_charting.utils import relative_rotation
+from openbb_charting.utils.generic_charts import bar_chart
+from openbb_charting.utils.helpers import (
+ calculate_returns,
+ heikin_ashi,
+ should_share_axis,
+ z_score_standardization,
)
CHART_FORMAT = ChartFormat.plotly
-# if TYPE_CHECKING:
-# from .core.openbb_figure_table import OpenBBFigureTable
+def equity_price_performance(
+ **kwargs,
+) -> Tuple[Union[OpenBBFigure, Figure], Dict[str, Any]]: # noqa: PLR0912
+ """Equity Price Performance Chart."""
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
+ data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "symbol")) # type: ignore
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "symbol") # type: ignore
+ )
-def equity_price_historical(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Equity price chart."""
+ cols = [
+ "one_day",
+ "one_week",
+ "one_month",
+ "three_month",
+ "six_month",
+ "ytd",
+ "one_year",
+ "two_year",
+ "three_year",
+ "four_year",
+ "five_year",
+ ]
- def handle_indicators(ma):
- """Handle indicators."""
- k = {}
- if ma:
- k["rma"] = dict(length=ma)
- return k
+ df = pd.DataFrame()
+ chart_df = pd.DataFrame()
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
- standard_params = kwargs["standard_params"]
- ma = standard_params.get("ma", None)
- prepost = standard_params.get("prepost", False)
- symbol = standard_params.get("symbol", "")
+ if "symbol" in data.columns:
+ data = data.set_index("symbol")
+ chart_cols = []
- ta = PlotlyTA()
- fig = ta.plot(
- data,
- indicators=dict(**handle_indicators(ma)),
- symbol=f"{symbol} historical data",
- prepost=prepost,
+ if len(data) == 0:
+ raise ValueError("No data was found in the DataFrame.")
+
+ data = data.drop_duplicates(keep="first")
+
+ for col in cols:
+ if col in data.columns and data[col].notnull().any():
+ df[col.replace("_", " ").title() if col != "ytd" else col.upper()] = data[
+ col
+ ].apply(lambda x: round(x * 100, 4) if x is not None else None)
+
+ if df.empty:
+ raise ValueError(f"No columns matching, {cols}, were found in the data.")
+
+ chart_df = df.T
+ chart_cols = chart_df.columns.to_list()
+
+ if "limit" in kwargs and isinstance(kwargs.get("limit"), int):
+ limit = kwargs.pop("limit", 10)
+ chart_df = chart_df.head(limit) # type: ignore
+
+ layout_kwargs: Dict[str, Any] = kwargs["layout_kwargs"] if "layout_kwargs" in kwargs else {} # type: ignore
+
+ title = (
+ f"{kwargs.pop('title')}" if "title" in kwargs else "Equity Price Performance"
)
+ orientation = (
+ kwargs.pop("orientation")
+ if "orientation" in kwargs and kwargs.get("orientation") is not None
+ else "v"
+ )
+
+ ytitle = "Performance (%)"
+ xtitle = None
+
+ if orientation == "h":
+ xtitle = ytitle # type: ignore
+ ytitle = None # type: ignore
+
+ fig = bar_chart(
+ chart_df.reset_index(),
+ x="index",
+ y=chart_cols,
+ title=title,
+ xtitle=xtitle,
+ ytitle=ytitle,
+ orientation=orientation, # type: ignore
+ )
+ fig.update_traces(
+ hovertemplate=(
+ "%{fullData.name}:%{y:.2f}%"
+ if orientation == "v"
+ else "%{fullData.name}:%{x:.2f}%"
+ )
+ )
+
+ fig.update_layout(**layout_kwargs)
+ content = fig.show(external=True).to_plotly_json() # type: ignore
+
+ return fig, content
+
+
+def etf_price_performance(
+ **kwargs,
+) -> Tuple[Union[OpenBBFigure, Figure], Dict[str, Any]]:
+ """ETF Historical Chart."""
+ fig, content = equity_price_performance(**kwargs)
+ if "title" in kwargs and kwargs.get("title") is not None:
+ fig.set_title(kwargs.get("title")) # type: ignore
+ else:
+ fig.set_title("ETF Price Performance") # type: ignore
+
+ content = fig.show(external=True).to_plotly_json() # type: ignore
+ return fig, content
+
+
+def etf_holdings(**kwargs) -> Tuple[Union[OpenBBFigure, Figure], Dict[str, Any]]:
+ """Equity Compare Groups Chart."""
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
+ data = basemodel_to_df(kwargs["data"], index=None) # type: ignore
+ else:
+ data = basemodel_to_df(kwargs["obbject_item"], index=None) # type: ignore
+
+ if "weight" not in data.columns:
+ raise ValueError("No 'weight' column found in the data.")
+
+ orientation = kwargs.get("orientation", "h")
+ limit = kwargs.get("limit", 20)
+ symbol = kwargs["standard_params"].get("symbol") # type: ignore
+ title = kwargs.get("title", f"Top {limit} {symbol} Holdings")
+ layout_kwargs = kwargs.get("layout_kwargs", {})
+
+ data = data.sort_values("weight", ascending=False)
+ limit = min(limit, len(data)) # type: ignore
+ target = data.head(limit)[["symbol", "weight"]].set_index("symbol")
+ target = target.multiply(100)
+ axis_title = "Weight (%)"
+
+ fig = bar_chart(
+ target.reset_index(),
+ "symbol",
+ ["weight"],
+ title=title, # type: ignore
+ xtitle=axis_title if orientation == "h" else None,
+ ytitle=axis_title if orientation == "v" else None,
+ orientation=orientation, # type: ignore
+ )
+
+ fig.update_layout(
+ hovermode="x" if orientation == "v" else "y",
+ margin=dict(r=0, l=50) if orientation == "h" else None,
+ )
+
+ fig.update_traces(
+ hovertemplate=(
+ "%{y:.3f}%"
+ if orientation == "v"
+ else "%{x:.3f}%"
+ )
+ )
+
+ if layout_kwargs:
+ fig.update_layout(**layout_kwargs) # type: ignore
+
+ content = fig.show(external=True).to_plotly_json() # type: ignore
+
+ return fig, content
+
+
+def equity_price_historical( # noqa: PLR0912
+ **kwargs,
+) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Equity Price Historical Chart."""
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
+ data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date")) # type: ignore
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "date") # type: ignore
+ )
+
+ if "date" in data.columns:
+ data = data.set_index("date")
+
+ target = str(kwargs.get("target"))
+ normalize = kwargs.get("normalize") is True
+ returns = kwargs.get("returns") is True
+ same_axis = kwargs.get("same_axis") is True
+ text_color = "black" if ChartStyle().plt_style == "light" else "white"
+ title = f"{kwargs.get('title')}" if "title" in kwargs else "Historical Prices"
+ y1title = ""
+ y2title = ""
+ candles = True
+ multi_symbol = (
+ bool(kwargs.get("multi_symbol") is True)
+ or (
+ "symbol" in data.columns
+ and target in data.columns
+ and len(data.symbol.unique()) > 1
+ )
+ or ("target" in kwargs and kwargs.get("target") is not None)
+ or "symbol" in data.columns
+ or (
+ "symbol" not in data.columns
+ and bool(data.columns.isin(["open", "high", "low", "close"]).all())
+ )
+ )
+ target = "close" if target is None or target == "None" or target == "" else target
+
+ if multi_symbol is True:
+ if "symbol" not in data.columns and target in data.columns:
+ data = data[[target]]
+ y1title = target.title()
+ if "symbol" in data.columns and target in data.columns:
+ data = data.pivot(columns="symbol", values=target)
+ y1title = target
+ title = f"Historical {target.title()}"
+
+ indicators = kwargs.get("indicators", {})
+ candles = bool(~data.columns.isin(["open", "high", "low", "close"]).all())
+ candles = candles if kwargs.get("candles", True) else False
+ volume = kwargs.get("volume", True) if "volume" in data.columns else False
+
+ if normalize is True:
+ if "symbol" not in data.columns and target in data.columns:
+ data = data[[target]]
+ multi_symbol = True
+ candles = False
+ volume = False
+
+ if returns is True:
+ if "symbol" not in data.columns and target in data.columns:
+ data = data[[target]]
+ multi_symbol = True
+ candles = False
+ volume = False
+ if ( # pylint: disable = R0916
+ multi_symbol is False
+ and normalize is False
+ and returns is False
+ and candles is True
+ ) or (indicators and multi_symbol is False):
+ if (
+ "heikin_ashi" in kwargs
+ and kwargs["heikin_ashi"] is True
+ and candles is True
+ ):
+ data = heikin_ashi(data)
+ title = f"{title} - Heikin Ashi"
+ _volume = False
+ if "atr" in indicators: # type: ignore
+ _volume = volume
+ volume = False
+ ta = PlotlyTA()
+ fig = ta.plot( # type: ignore
+ data,
+ indicators=indicators if indicators else {}, # type: ignore
+ symbol=target if candles is False else "",
+ candles=candles,
+ volume=volume, # type: ignore
+ )
+ if _volume is True and "atr" in indicators: # type: ignore
+ fig.add_inchart_volume(data)
+ fig.update_layout(
+ paper_bgcolor="rgba(0,0,0,0)",
+ plot_bgcolor="rgba(0,0,0,0)",
+ font=dict(color=text_color),
+ showlegend=True,
+ legend=dict(
+ orientation="v",
+ yanchor="top",
+ xanchor="right",
+ y=0.95,
+ x=-0.01,
+ xref="paper",
+ font=dict(size=12),
+ bgcolor="rgba(0,0,0,0)",
+ ),
+ xaxis=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ zeroline=True,
+ mirror=True,
+ showline=True,
+ ),
+ xaxis2=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ zeroline=True,
+ mirror=True,
+ showline=True,
+ ),
+ yaxis=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ zeroline=True,
+ mirror=True,
+ showline=True,
+ tickfont=dict(size=14),
+ ),
+ yaxis2=dict(
+ ticklen=0,
+ gridcolor="rgba(128,128,128,0.3)",
+ ),
+ yaxis3=dict(
+ ticklen=0,
+ gridcolor="rgba(128,128,128,0.3)",
+ ),
+ dragmode="pan",
+ hovermode="x",
+ )
+
+ if kwargs.get("title"):
+ title = kwargs["title"]
+ fig.update_layout(title=dict(text=title, x=0.5))
+
+ content = fig.to_plotly_json()
+
+ return fig, content
+
+ if multi_symbol is True or candles is False:
+
+ if "symbol" not in data.columns and target in data.columns:
+ data = data[[target]]
+
+ if "symbol" in data.columns:
+ data = data.pivot(columns="symbol", values=target)
+
+ title: str = kwargs.get("title", "Historical Prices") # type: ignore
+
+ y1title = data.iloc[:, 0].name
+ y2title = ""
+
+ if len(data.columns) > 2 or normalize is True or returns is True:
+ if returns is True or (len(data.columns) > 2 and normalize is False):
+ data = data.apply(calculate_returns)
+ title = f"{title} - Cumulative Returns"
+ y1title = "Percent"
+ if normalize is True:
+ if returns is True:
+ title = f"{title.replace(' - Cumulative Returns', '')} - Normalized Cumulative Returns"
+ else:
+ title = title + " - Normalized"
+ data = data.apply(z_score_standardization)
+ y1title = None # type: ignore
+ y2title = None # type: ignore
+
+ fig = OpenBBFigure()
+
+ for i, col in enumerate(data.columns):
+
+ hovertemplate = f"{data[col].name}: %{{y}}"
+ yaxis = "y1"
+ if y1title and y1title != "Percent":
+ yaxis = (
+ (
+ "y1"
+ if should_share_axis(data, col, y1title) # type: ignore
+ or col == y1title
+ or normalize is True
+ or returns is True
+ else "y2"
+ )
+ if same_axis is False
+ else "y1"
+ )
+
+ if yaxis == "y2":
+ y2title = data[col].name
+
+ fig.add_scatter(
+ x=data.index,
+ y=data[col],
+ name=data[col].name,
+ mode="lines",
+ hovertemplate=hovertemplate,
+ line=dict(width=1, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]),
+ yaxis=yaxis,
+ )
+
+ if normalize is True or returns is True:
+ y1title = "Percent" if returns is True else None # type: ignore
+ y2title = None # type: ignore
+
+ if same_axis is True:
+ y1title = None # type: ignore
+ y2title = None # type: ignore
+
+ fig.update_layout(
+ paper_bgcolor="rgba(0,0,0,0)",
+ plot_bgcolor="rgba(0,0,0,0)",
+ legend=(
+ dict(
+ orientation="v",
+ yanchor="top",
+ xanchor="right",
+ y=0.95,
+ x=-0.01,
+ bgcolor="rgba(0,0,0,0)",
+ )
+ if len(data.columns) > 2
+ else dict(
+ orientation="h",
+ yanchor="bottom",
+ xanchor="right",
+ y=1.02,
+ x=0.98,
+ bgcolor="rgba(0,0,0,0)",
+ )
+ ),
+ yaxis1=(
+ dict(
+ side="right",
+ ticklen=0,
+ showgrid=True,
+ showline=True,
+ mirror=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ title=dict(
+ text=y1title if y1title else None, standoff=20, font=dict(size=20)
+ ),
+ tickfont=dict(size=14),
+ anchor="x",
+ )
+ ),
+ yaxis2=(
+ dict(
+ overlaying="y",
+ side="left",
+ ticklen=0,
+ showgrid=False,
+ title=dict(
+ text=y2title if y2title else None, standoff=10, font=dict(size=20)
+ ),
+ tickfont=dict(size=14),
+ anchor="x",
+ )
+ if y2title
+ else None
+ ),
+ xaxis=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ showline=True,
+ mirror=True,
+ ),
+ margin=dict(l=20, r=20, b=20, t=20),
+ dragmode="pan",
+ hovermode="x",
+ )
+ if kwargs.get("title"):
+ title = kwargs["title"]
+ fig.update_layout(title=dict(text=title, x=0.5))
+
content = fig.show(external=True).to_plotly_json()
return fig, content
-def _ta_ma(ma_type: str, **kwargs):
- """Plot moving average helper."""
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
- window = kwargs.get("window", 50)
- offset = kwargs.get("offset", 0)
- symbol = kwargs.get("symbol", "")
+def etf_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """ETF Historical Chart."""
+ return equity_price_historical(**kwargs)
- ta = PlotlyTA()
- fig = ta.plot(
- data,
- {f"{ma_type.lower()}": dict(length=window, offset=offset)},
- f"{symbol.upper()} {ma_type.upper()}",
- False,
- volume=False,
+
+def index_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Index Price Historical Chart."""
+ return equity_price_historical(**kwargs)
+
+
+def currency_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Currency Price Historical Chart."""
+ return equity_price_historical(**kwargs)
+
+
+def crypto_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Crypto Price Historical Chart."""
+ return equity_price_historical(**kwargs)
+
+
+def _ta_ma(**kwargs):
+ """Plot moving average helper."""
+
+ index = (
+ kwargs.get("index")
+ if "index" in kwargs and kwargs.get("index") is not None
+ else "date"
+ )
+ data = kwargs.get("data")
+ ma_type = (
+ kwargs["ma_type"]
+ if "ma_type" in kwargs and kwargs.get("ma_type") is not None
+ else "sma"
+ )
+ ma_types = ma_type.split(",") if isinstance(ma_type, str) else ma_type
+
+ if isinstance(data, pd.DataFrame) and not data.empty:
+ data = data.set_index(index) if index in data.columns else data
+
+ if data is None:
+ data = basemodel_to_df(kwargs["obbject_item"], index=index)
+
+ if isinstance(data, list):
+ data = basemodel_to_df(data, index=index)
+
+ window = (
+ kwargs.get("length", [])
+ if "length" in kwargs and kwargs.get("length") is not None
+ else [50]
+ )
+ offset = kwargs.get("offset", 0)
+ target = (
+ kwargs.get("target")
+ if "target" in kwargs and kwargs.get("target") is not None
+ else "close"
+ )
+
+ if target not in data.columns and "close" in data.columns:
+ target = "close"
+
+ if target not in data.columns and "close" not in data.columns:
+ raise ValueError(f"Column '{target}', or 'close', not found in the data.")
+
+ df = data.copy()
+ if target in data.columns:
+ df = df[[target]]
+ df.columns = ["close"]
+ title = (
+ kwargs.get("title")
+ if "title" in kwargs and kwargs.get("title") is not None
+ else f"{ma_type.upper()}"
+ )
+
+ fig = OpenBBFigure()
+ fig = fig.create_subplots(
+ 1,
+ 1,
+ shared_xaxes=True,
+ vertical_spacing=0.06,
+ horizontal_spacing=0.01,
+ row_width=[1],
+ specs=[[{"secondary_y": True}]],
)
fig.update_layout(ChartStyle().plotly_template.get("layout", {}))
+
+ ma_df = pd.DataFrame()
+ window = [window] if isinstance(window, int) else window
+ for w in window:
+ for ma_type in ma_types:
+ ma_df[f"{ma_type.upper()} {w}"] = getattr(df.ta, ma_type)(
+ length=w, offset=offset
+ )
+
+ if kwargs.get("dropnan") is True:
+ ma_df = ma_df.dropna()
+ data = data.iloc[-len(ma_df) :]
+
+ color = 0
+
+ if (
+ "candles" in kwargs
+ and kwargs.get("candles") is True
+ and kwargs.get("target") is None
+ ):
+ volume = kwargs.get("volume") is True
+ fig, _ = to_chart(data, candles=True, volume=volume)
+
+ else:
+ ma_df[f"{target}".title()] = data[target]
+
+ for col in ma_df.columns:
+ name = col.replace("_", " ")
+ fig.add_scatter(
+ x=ma_df.index,
+ y=ma_df[col],
+ name=name,
+ mode="lines",
+ hovertemplate=f"{name}: %{{y}}",
+ line=dict(width=1, color=LARGE_CYCLER[color]),
+ showlegend=True,
+ )
+ color += 1
+
+ fig.update_layout(
+ title=dict(text=title, x=0.5, font=dict(size=16)),
+ paper_bgcolor="rgba(0,0,0,0)",
+ plot_bgcolor="rgba(0,0,0,0)",
+ showlegend=True,
+ legend=dict(
+ orientation="h",
+ yanchor="bottom",
+ xanchor="right",
+ y=1.02,
+ x=0.95,
+ bgcolor="rgba(0,0,0,0)",
+ ),
+ xaxis=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ zeroline=True,
+ mirror=True,
+ ),
+ yaxis=dict(
+ ticklen=0,
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ zeroline=True,
+ mirror=True,
+ autorange=True,
+ ),
+ )
+
content = fig.show(external=True).to_plotly_json()
return fig, content
-def technical_zlma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def technical_sma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Plot simple moving average chart."""
+ if "ma_type" not in kwargs:
+ kwargs["ma_type"] = "sma"
+ return _ta_ma(**kwargs)
+
+
+def technical_ema(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Exponential moving average chart."""
+ if "ma_type" not in kwargs:
+ kwargs["ma_type"] = "ema"
+ return _ta_ma(**kwargs)
+
+
+def technical_hma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Hull moving average chart."""
+ if "ma_type" not in kwargs:
+ kwargs["ma_type"] = "hma"
+ return _ta_ma(**kwargs)
+
+
+def technical_wma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Weighted moving average chart."""
+ if "ma_type" not in kwargs:
+ kwargs["ma_type"] = "wma"
+ return _ta_ma(**kwargs)
+
+
+def technical_zlma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""Zero lag moving average chart."""
- ma_type = "zlma"
- return _ta_ma(ma_type, **kwargs)
+ if "ma_type" not in kwargs:
+ kwargs["ma_type"] = "zlma"
+ return _ta_ma(**kwargs)
-def technical_aroon(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Aroon chart."""
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
+def technical_aroon(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
+ """Technical Aroon Chart."""
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "date")
+ )
+
+ if "date" in data.columns:
+ data = data.set_index("date")
+
+ if "symbol" in data.columns and len(data.symbol.unique()) > 1:
+ raise ValueError(
+ "Please provide data with only one symbol and columns for OHLC."
+ )
+
+ symbol = kwargs.get("symbol", "")
+
+ volume = kwargs.get("volume") is True
+ title = f"Aroon Indicator & Oscillator {symbol}"
+
length = kwargs.get("length", 25)
scalar = kwargs.get("scalar", 100)
symbol = kwargs.get("symbol", "")
ta = PlotlyTA()
- fig = ta.plot(
+ fig = ta.plot( # type: ignore
data,
dict(aroon=dict(length=length, scalar=scalar)),
- f"Aroon on {symbol}",
+ title,
False,
- volume=False,
+ volume=volume,
)
+
content = fig.show(external=True).to_plotly_json()
return fig, content
-def technical_sma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Plot simple moving average chart."""
- ma_type = "sma"
- return _ta_ma(ma_type, **kwargs)
-
-
-def technical_macd(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def technical_macd(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""Plot moving average convergence divergence chart."""
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "date")
+ )
+
+ if "date" in data.columns:
+ data = data.set_index("date")
+
+ if "symbol" in data.columns and len(data.symbol.unique()) > 1:
+ raise ValueError(
+ "Please provide data with only one symbol and columns for OHLC."
+ )
+
fast = kwargs.get("fast", 12)
slow = kwargs.get("slow", 26)
signal = kwargs.get("signal", 9)
symbol = kwargs.get("symbol", "")
+ title = f"{symbol.upper()} MACD"
+ volume = kwargs.get("volume") is True
+
ta = PlotlyTA()
- fig = ta.plot(
+ fig = ta.plot( # type: ignore
data,
dict(macd=dict(fast=fast, slow=slow, signal=signal)),
- f"{symbol.upper()} MACD",
+ title,
False,
- volume=False,
+ volume=volume,
)
content = fig.show(external=True).to_plotly_json()
return fig, content
-def technical_hma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Hull moving average chart."""
- ma_type = "hma"
- return _ta_ma(ma_type, **kwargs)
-
-
-def technical_adx(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def technical_adx(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""Average directional movement index chart."""
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "date")
+ )
+
+ if "date" in data.columns:
+ data = data.set_index("date")
+
+ if "symbol" in data.columns and len(data.symbol.unique()) > 1:
+ raise ValueError(
+ "Please provide data with only one symbol and columns for OHLC."
+ )
+
length = kwargs.get("length", 14)
scalar = kwargs.get("scalar", 100.0)
drift = kwargs.get("drift", 1)
symbol = kwargs.get("symbol", "")
ta = PlotlyTA()
- fig = ta.plot(
+ fig = ta.plot( # type: ignore
data,
dict(adx=dict(length=length, scalar=scalar, drift=drift)),
f"Average Directional Movement Index (ADX) {symbol}",
@@ -151,22 +799,31 @@ def technical_adx(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
return fig, content
-def technical_wma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Weighted moving average chart."""
- ma_type = "wma"
- return _ta_ma(ma_type, **kwargs)
-
-
-def technical_rsi(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def technical_rsi(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""Relative strength index chart."""
- data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
+
+ if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame):
+ data = kwargs["data"]
+ else:
+ data = basemodel_to_df(
+ kwargs["obbject_item"], index=kwargs.get("index", "date")
+ )
+
+ if "date" in data.columns:
+ data = data.set_index("date")
+
+ if "symbol" in data.columns and len(data.symbol.unique()) > 1:
+ raise ValueError(
+ "Please provide data with only one symbol and columns for OHLC."
+ )
+
window = kwargs.get("window", 14)
scalar = kwargs.get("scalar", 100.0)
drift = kwargs.get("drift", 1)
symbol = kwargs.get("symbol", "")
ta = PlotlyTA()
- fig = ta.plot(
+ fig = ta.plot( # type: ignore
data,
dict(rsi=dict(length=window, scalar=scalar, drift=drift)),
f"{symbol.upper()} RSI {window}",
@@ -178,15 +835,7 @@ def technical_rsi(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
return fig, content
-def technical_ema(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
- """Exponential moving average chart."""
- ma_type = "ema"
- return _ta_ma(ma_type, **kwargs)
-
-
-def technical_cones(
- **kwargs: TechnicalConesChartQueryParams,
-) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def technical_cones(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""Volatility Cones Chart."""
data = kwargs.get("data")
@@ -264,6 +913,11 @@ def technical_cones(
),
yaxis=dict(
ticklen=0,
+ showgrid=True,
+ showline=True,
+ mirror=True,
+ zeroline=False,
+ gridcolor="rgba(128,128,128,0.3)",
),
xaxis=dict(
type="category",
@@ -273,6 +927,8 @@ def technical_cones(
ticktext=df_ta.index,
title_text="Period",
showgrid=False,
+ showline=True,
+ mirror=True,
zeroline=False,
),
margin=dict(l=20, r=20, b=20),
@@ -284,9 +940,9 @@ def technical_cones(
return fig, content
-def economy_fred_series(
- **kwargs: Union[Any, FredSeriesChartQueryParams],
-) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+def economy_fred_series( # noqa: PLR0912
+ **kwargs,
+) -> Tuple[OpenBBFigure, Dict[str, Any]]:
"""FRED Series Chart."""
ytitle_dict = {
"chg": "Change",
@@ -299,20 +955,6 @@ def economy_fred_series(
"log": "Natural Log",
}
- colors = [
- "#1f77b4",
- "#7f7f7f",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]
-
provider = kwargs.get("provider")
if provider != "fred":
@@ -352,21 +994,12 @@ def economy_fred_series(
if df_ta.empty or len(df_ta) < 2:
raise ValueError(
"No data is left after dropping NaN values. Try setting `dropnan = False`,"
- + " or use the `frequency` parameter on request ."
+ + " or use the `frequency` parameter on request."
)
columns = df_ta.columns.to_list()
- def z_score_standardization(data: pd.Series) -> pd.Series:
- """Z-Score Standardization Method."""
- return (data - data.mean()) / data.std()
-
- if normalize:
- df_ta = df_ta.apply(z_score_standardization)
-
- # Extract the metadata from the warnings.
- warnings = kwargs.get("warnings")
- metadata = json.loads(warnings[0].message) if warnings else {} # type: ignore
+ metadata = kwargs["extra"].get("results_metadata", {}) # type: ignore
# Check if the request was transformed by the FRED API.
params = kwargs["extra_params"] if kwargs.get("extra_params") else {}
@@ -374,8 +1007,23 @@ def economy_fred_series(
# Get a unique list of all units of measurement in the DataFrame.
y_units = list({metadata.get(col).get("units") for col in columns if col in metadata}) # type: ignore
+ if has_params is True and not y_units:
+ y_units = [ytitle_dict.get(params.transform)] # type: ignore
- if len(y_units) > 2 and has_params is False and allow_unsafe is True:
+ if normalize or (
+ kwargs.get("bar") is True
+ and len(y_units) > 1
+ and (
+ has_params is False
+ or not any(
+ i in params.transform for i in ["pc1", "pch", "pca", "cch", "cca", "log"] # type: ignore
+ )
+ )
+ ):
+ normalize = True
+ df_ta = df_ta.apply(z_score_standardization)
+
+ if len(y_units) > 2 and has_params is False and allow_unsafe is False:
raise RuntimeError(
"This method supports up to 2 y-axis units."
+ " Please use the 'transform' parameter, in the data request,"
@@ -386,7 +1034,7 @@ def economy_fred_series(
y1_units = y_units[0] if y_units else None
y1title = y1_units
y2title = y_units[1] if len(y_units) > 1 else None
- xtitle = ""
+ xtitle = str(kwargs.get("xtitle", ""))
# If the request was transformed, the y-axis will be shared under these conditions.
if has_params and any(
@@ -398,9 +1046,9 @@ def economy_fred_series(
# Set the title for the chart.
title: str = ""
if isinstance(kwargs, dict) and title in kwargs:
- title = kwargs["title"]
+ title = kwargs["title"] # type: ignore
else:
- if metadata.get(columns[0]):
+ if metadata.get(columns[0]): # type: ignore
title = metadata.get(columns[0]).get("title") if len(columns) == 1 else "FRED Series" # type: ignore
else:
title = "FRED Series"
@@ -410,12 +1058,39 @@ def economy_fred_series(
# Define this to use as a check.
y3title: Optional[str] = ""
+ if kwargs.get("plot_bar") is True or len(df_ta.index) < 100:
+ margin = dict(l=10, r=5, b=75 if xtitle else 30)
+ try:
+ if normalize:
+ y1title = None
+ title = f"{title} - Normalized" if title else "Normalized"
+ bar_mode = kwargs["barmode"] if "barmode" in kwargs else "group"
+ fig = bar_chart(
+ df_ta.reset_index(),
+ "date",
+ df_ta.columns.to_list(),
+ title=title,
+ xtitle=xtitle,
+ ytitle=y1title,
+ barmode=bar_mode, # type: ignore
+ layout_kwargs=dict(margin=margin), # type: ignore
+ )
+ if kwargs.get("layout_kwargs"):
+ fig.update_layout(kwargs.get("layout_kwargs"))
+
+ if kwargs.get("title"):
+ fig.set_title(str(kwargs.get("title"))) # type: ignore
+
+ content = fig.to_plotly_json()
+
+ return fig, content # type: ignore
+ except Exception as _:
+ warn("Bar chart failed. Attempting line chart.")
+
# Create the figure object with subplots.
fig = OpenBBFigure().create_subplots(
rows=1, cols=1, shared_xaxes=True, shared_yaxes=False
)
- fig.update_layout(ChartStyle().plotly_template.get("layout", {}))
- text_color = "black" if ChartStyle().plt_style == "light" else "white"
# For each series in the DataFrame, add a scatter plot.
for i, col in enumerate(df_ta.columns):
@@ -423,27 +1098,29 @@ def economy_fred_series(
# Check if the y-axis should be shared for this series.
on_y1 = (
(
- metadata.get(col).get("units") == y1_units
+ metadata.get(col).get("units") == y1_units # type: ignore
or y2title is None # type: ignore
+ or kwargs.get("same_axis") is True
)
- if metadata.get(col)
+ if metadata.get(col) # type: ignore
else False
)
if normalize:
on_y1 = True
+
yaxes = "y2" if not on_y1 else "y1"
- on_y3 = not metadata.get(col) and normalize is False
+ on_y3 = not metadata.get(col) and normalize is False # type: ignore
if on_y3:
yaxes = "y3"
- y3title = df_ta[col].name
+ y3title = df_ta[col].name # type: ignore
fig.add_scatter(
x=df_ta.index,
y=df_ta[col],
name=df_ta[col].name,
mode="lines",
hovertemplate=f"{df_ta[col].name}: %{{y}}",
- line=dict(width=1, color=colors[i % len(colors)]),
- yaxis=yaxes,
+ line=dict(width=1, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]),
+ yaxis="y1" if kwargs.get("same_axis") is True else yaxes,
)
# Set the y-axis titles, if supplied.
@@ -466,22 +1143,25 @@ def economy_fred_series(
title=dict(text=title, x=0.5, font=dict(size=16)),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
- font=dict(color=text_color),
legend=dict(
orientation="h",
yanchor="bottom",
xanchor="right",
y=1.02,
- x=1,
+ x=0.95,
bgcolor="rgba(0,0,0,0)",
+ font=dict(size=12),
),
yaxis=(
dict(
ticklen=0,
side="right",
- title=dict(text=y1title, standoff=30, font=dict(size=18)),
+ showline=True,
+ mirror=True,
+ title=dict(text=y1title, standoff=30, font=dict(size=16)),
tickfont=dict(size=14),
anchor="x",
+ gridcolor="rgba(128,128,128,0.3)",
)
if y1title
else None
@@ -493,7 +1173,7 @@ def economy_fred_series(
ticklen=0,
showgrid=False,
title=dict(
- text=y2title if y2title else None, standoff=10, font=dict(size=18)
+ text=y2title if y2title else None, standoff=10, font=dict(size=16)
),
tickfont=dict(size=14),
anchor="x",
@@ -514,7 +1194,7 @@ def economy_fred_series(
if y3title
else None
),
- tickfont=dict(size=12, color="rgba(128,128,128,0.75)"),
+ tickfont=dict(size=12, color="rgba(128,128,128,0.9)"),
anchor="free",
)
if y3title
@@ -522,17 +1202,93 @@ def economy_fred_series(
),
xaxis=dict(
ticklen=0,
- showgrid=False,
+ showgrid=True,
+ showline=True,
+ mirror=True,
title=(
- dict(text=xtitle, standoff=30, font=dict(size=18)) if xtitle else None
+ dict(text=xtitle, standoff=30, font=dict(size=16)) if xtitle else None
),
+ gridcolor="rgba(128,128,128,0.3)",
domain=[0.095, 0.95] if y3title else None,
),
- margin=dict(r=25, l=25) if normalize is False else None,
+ margin=dict(r=25, l=25, b=75 if xtitle else 30) if normalize is False else None,
autosize=True,
dragmode="pan",
)
-
+ if kwargs.get("layout_kwargs"):
+ fig.update_layout(kwargs.get("layout_kwargs"))
+ if kwargs.get("title"):
+ fig.set_title(str(kwargs.get("title")))
content = fig.to_plotly_json()
return fig, content
+
+
+def technical_relative_rotation(
+ **kwargs: Any,
+) -> Tuple["OpenBBFigure", Dict[str, Any]]:
+ """Relative Rotation Chart."""
+
+ ratios_df = basemodel_to_df(kwargs["obbject_item"].rs_ratios, index="date") # type: ignore
+ momentum_df = basemodel_to_df(kwargs["obbject_item"].rs_momentum, index="date") # type: ignore
+ benchmark_symbol = kwargs["obbject_item"].benchmark # type: ignore
+ study = kwargs.get("study", None)
+ study = str(kwargs["obbject_item"].study) if study is None else str(study)
+ show_tails = kwargs.get("show_tails")
+ show_tails = True if show_tails is None else show_tails
+ tail_periods = int(kwargs.get("tail_periods")) if "tail_periods" in kwargs else 16 # type: ignore
+ tail_interval = str(kwargs.get("tail_interval")) if "tail_interval" in kwargs else "week" # type: ignore
+ date = kwargs.get("date") if "date" in kwargs else None # type: ignore
+ show_tails = False if date is not None else show_tails
+ if ratios_df.empty or momentum_df.empty:
+ raise RuntimeError("Error: No data to plot.")
+
+ if show_tails is True:
+ fig = relative_rotation.create_rrg_with_tails(
+ ratios_df, momentum_df, study, benchmark_symbol, tail_periods, tail_interval # type: ignore
+ )
+
+ if show_tails is False:
+ fig = relative_rotation.create_rrg_without_tails(
+ ratios_df, momentum_df, benchmark_symbol, study, date # type: ignore
+ )
+
+ figure = OpenBBFigure(fig)
+ font_color = "black" if ChartStyle().plt_style == "light" else "white"
+ figure.update_layout(
+ paper_bgcolor="rgba(0,0,0,0)",
+ plot_bgcolor="rgba(255,255,255,1)",
+ font=dict(color=font_color),
+ yaxis=dict(
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ side="left",
+ showline=True,
+ zeroline=True,
+ mirror=True,
+ ticklen=0,
+ tickfont=dict(size=14),
+ titlefont=dict(size=16),
+ ),
+ xaxis=dict(
+ showgrid=True,
+ gridcolor="rgba(128,128,128,0.3)",
+ showline=True,
+ zeroline=True,
+ mirror=True,
+ ticklen=0,
+ tickfont=dict(size=14),
+ titlefont=dict(size=16),
+ hoverformat="",
+ ),
+ hoverlabel=dict(
+ font_size=12,
+ ),
+ hovermode="x",
+ hoverdistance=50,
+ )
+ if kwargs.get("title") is not None:
+ figure.set_title(str(kwargs.get("title")))
+ content = figure.to_plotly_json()
+
+ return figure, content
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py
index e69de29bb2d..91c890e79c2 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Charting core."""
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py
index 2307f3993fc..79d4c56e18b 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py
@@ -74,6 +74,7 @@ class Backend(PyWry):
max_retries: int = 30,
proc_name: str = "OpenBB Terminal",
):
+ """Create a new instance of the backend."""
self.charting_settings = charting_settings
has_version = hasattr(PyWry, "__version__")
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py
index e69de29bb2d..9997eeed1a7 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py
@@ -0,0 +1 @@
+"""OpenBB Charting core configuration."""
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py
index c3e60e66528..1872e689822 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py
@@ -1,3 +1,5 @@
+"""OpenBB Charting Styles."""
+
from typing import Any, List, Optional
import pandas as pd
@@ -35,10 +37,10 @@ PLT_CANDLESTICKS = dict(
increasing=dict(line_color=PLT_STYLE_INCREASING, fillcolor=PLT_STYLE_INCREASING),
decreasing=dict(line_color=PLT_STYLE_DECREASING, fillcolor=PLT_STYLE_DECREASING),
)
-PLT_STYLE_INCREASING_GREEN = "#009600"
-PLT_STYLE_DECREASING_RED = "#c80000"
-PLT_FONT = dict(family="Fira Code", size=16)
-PLOTLY_FONT = dict(family="Fira Code", size=18)
+PLT_STYLE_INCREASING_GREEN = "#00ACFF"
+PLT_STYLE_DECREASING_RED = "#e4003a"
+PLT_FONT = dict(family="Arial", size=16)
+PLOTLY_FONT = dict(family="Arial", size=16)
PLT_COLORWAY = [
"#ffed00",
@@ -153,8 +155,9 @@ def de_increasing_color_list(
increasing_color: str = PLT_STYLE_INCREASING,
decreasing_color: str = PLT_STYLE_DECREASING,
) -> List[str]:
- """Makes a colorlist for decrease/increase if value in df_column
- contains "{contains_str}" default is "-"
+ """Make a colorlist for decrease/increase if value in df_column.
+
+ Contains "{contains_str}" default is "-"
Parameters
----------
@@ -197,8 +200,10 @@ PLOTLY_THEME = dict(
nticks=15,
showline=True,
showgrid=True,
+ ticklen=0,
),
yaxis2=dict(
+ side="left",
zeroline=False,
fixedrange=False,
anchor="x",
@@ -207,8 +212,9 @@ PLOTLY_THEME = dict(
nticks=6,
tick0=0.5,
title_standoff=10,
- tickfont=dict(size=15),
- showline=True,
+ tickfont=dict(size=12),
+ showline=False,
+ ticklen=0,
),
yaxis3=dict(
zeroline=False,
@@ -219,8 +225,9 @@ PLOTLY_THEME = dict(
nticks=6,
tick0=0.5,
title_standoff=10,
- tickfont=dict(size=15),
+ tickfont=dict(size=12),
showline=True,
+ ticklen=0,
),
yaxis4=dict(
zeroline=False,
@@ -231,24 +238,53 @@ PLOTLY_THEME = dict(
nticks=6,
tick0=0.5,
title_standoff=10,
- tickfont=dict(size=15),
+ tickfont=dict(size=12),
showline=True,
+ ticklen=0,
),
xaxis=dict(
showgrid=True,
- zeroline=True,
+ zeroline=False,
showline=True,
rangeslider=dict(visible=False),
tickfont=dict(size=16),
title_standoff=20,
+ ticklen=0,
+ ),
+ xaxis2=dict(
+ showgrid=True,
+ zeroline=False,
+ showline=True,
+ rangeslider=dict(visible=False),
+ tickfont=dict(size=12),
+ title_standoff=20,
+ ticklen=0,
+ ),
+ xaxis3=dict(
+ showgrid=True,
+ zeroline=False,
+ showline=True,
+ rangeslider=dict(visible=False),
+ tickfont=dict(size=12),
+ title_standoff=20,
+ ticklen=0,
+ ),
+ xaxis4=dict(
+ showgrid=True,
+ zeroline=False,
+ showline=True,
+ rangeslider=dict(visible=False),
+ tickfont=dict(size=12),
+ title_standoff=20,
+ ticklen=0,
),
legend=dict(
- yanchor="top",
- y=0.99,
- xanchor="left",
- font_size=15,
- bgcolor="rgba(0, 0, 0, 0)",
- x=0.01,
+ orientation="h",
+ yanchor="bottom",
+ y=1.02,
+ xanchor="right",
+ x=0.95,
+ font=dict(size=12),
),
dragmode="pan",
hovermode="x",
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py
index 2742185f768..8b584bff6aa 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py
@@ -1,3 +1,5 @@
+"""Dummy backend for charting to avoid import errors."""
+
import asyncio
import warnings
from queue import Queue
@@ -52,7 +54,7 @@ class DummyBackend:
return cls.instance
def __init__(self, daemon: bool = True, max_retries: int = 30):
- """Dummy init to avoid import errors."""
+ """Use cummy init to avoid import errors."""
self.daemon = daemon
self.max_retries = max_retries
try:
@@ -66,14 +68,14 @@ class DummyBackend:
dotenv.set_key(SETTINGS_ENV_FILE, "PLOT_ENABLE_PYWRY", "0")
def close(self, reset: bool = False): # pylint: disable=W0613
- pass
+ """Close the backend."""
def start(self, debug: bool = False): # pylint: disable=W0613
- pass
+ """Start the backend."""
def send_outgoing(self, outgoing: dict):
- pass
+ """Send outgoing data to the backend."""
async def check_backend(self):
- """Dummy check backend method to avoid errors and revert to browser."""
+ """Check backend method to avoid errors and revert to browser."""
raise Exception
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py b/openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py
index 32e5876817f..6d3315feee3 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py
@@ -42,6 +42,8 @@ if TYPE_CHECKING:
TimeSeriesT = TypeVar("TimeSeriesT", bound="TimeSeries")
+pio.default_renderers = "notebook"
+
class OpenBBFigure(go.Figure):
"""Custom Figure class for OpenBB Terminal.
@@ -84,6 +86,7 @@ class OpenBBFigure(go.Figure):
plotlyjs_path: Path = PLOTLYJS_PATH
def __init__(self, fig: Optional[go.Figure] = None, **kwargs) -> None:
+ """Initialize the OpenBBFigure."""
super().__init__()
if fig:
self.__dict__ = fig.__dict__
@@ -238,8 +241,7 @@ class OpenBBFigure(go.Figure):
secondary_y: bool = False,
**kwargs,
):
- """
- Add a trend line to the figure.
+ """Add a trend line to the figure.
Parameters
----------
@@ -291,8 +293,7 @@ class OpenBBFigure(go.Figure):
row: int = 1,
col: int = 1,
) -> None:
- """
- Add a histogram with a curve and rug plot if desired.
+ """Add a histogram with a curve and rug plot if desired.
Parameters
----------
@@ -560,8 +561,7 @@ class OpenBBFigure(go.Figure):
legendrank: Optional[int] = None,
**kwargs,
) -> None:
- """
- Add a horizontal line with a legend label.
+ """Add a horizontal line with a legend label.
Parameters
----------
@@ -598,8 +598,7 @@ class OpenBBFigure(go.Figure):
legendrank: Optional[int] = None,
**kwargs,
) -> None:
- """
- Add a vertical line with a legend label.
+ """Add a vertical line with a legend label.
Parameters
----------
@@ -667,8 +666,7 @@ class OpenBBFigure(go.Figure):
def chart_volume_scaling(
df_volume: pd.DataFrame, volume_ticks_x: int = 7
) -> Dict[str, list]:
- """
- Takes df_volume and returns volume_ticks, tickvals for chart volume scaling.
+ """Take df_volume and returns volume_ticks, tickvals for chart volume scaling.
Parameters
----------
@@ -716,8 +714,7 @@ class OpenBBFigure(go.Figure):
col: Optional[int] = 1,
volume_ticks_x: int = 7,
) -> None:
- """
- Add in-chart volume to a subplot.
+ """Add in-chart volume to a subplot.
Parameters
----------
@@ -748,15 +745,18 @@ class OpenBBFigure(go.Figure):
yaxis="y2",
row=row,
col=col,
- opacity=0.7,
+ opacity=0.5,
secondary_y=True,
+ showlegend=False,
+ hovertemplate="%{y}",
)
ticksize = 13 - (self.subplots_kwargs["rows"] // 2)
self.update_layout(
yaxis2=dict(
- fixedrange=True,
+ fixedrange=False,
side="left",
- nticks=10,
+ nticks=8,
+ autorange=False,
range=vol_scale["range"],
tickvals=vol_scale["ticks"],
showgrid=False,
@@ -846,8 +846,7 @@ class OpenBBFigure(go.Figure):
export_image: Optional[Union[Path, str]] = "",
**kwargs,
) -> "OpenBBFigure":
- """
- Show the figure.
+ """Show the figure.
Parameters
----------
@@ -1234,8 +1233,7 @@ class OpenBBFigure(go.Figure):
def to_plotly_json(
self, ndarray: bool = False, np_nan: bool = False
) -> Dict[str, Any]:
- """
- Convert figure to a JSON representation as a Python dict.
+ """Convert figure to a JSON representation as a Python dict.
Parameters
----------
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html b/openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html
index 0968c0a6998..6912deac3b6 100644
--- a/openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html
+++ b/openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html
@@ -4008,7 +4008,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
l-51.45-160.536h39.787l19.526,67.894c5.479,19.046,10.479,37.386,14.299,57.397h0.709c4.048-19.298,9.045-38.352,14.526-56.693
l20.487-68.598h38.599L427.518,380.583z`, width: 550, height: 550, transform: "translate(4, 0)"
}, downloadImage: { path: "M22.71,6.29a1,1,0,0,0-1.42,0L20,7.59V2a1,1,0,0,0-2,0V7.59l-1.29-1.3a1,1,0,0,0-1.42,1.42l3,3a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l3-3A1,1,0,0,0,22.71,6.29ZM19,13a1,1,0,0,0-1,1v.38L16.52,12.9a2.79,2.79,0,0,0-3.93,0l-.7.7L9.41,11.12a2.85,2.85,0,0,0-3.93,0L4,12.6V7A1,1,0,0,1,5,6h8a1,1,0,0,0,0-2H5A3,3,0,0,0,2,7V19a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V14A1,1,0,0,0,19,13ZM5,20a1,1,0,0,1-1-1V15.43l2.9-2.9a.79.79,0,0,1,1.09,0l3.17,3.17,0,0L15.46,20Zm13-1a.89.89,0,0,1-.18.53L13.31,15l.7-.7a.77.77,0,0,1,1.1,0L18,17.21Z", width: 21, height: 21, transform: "translate(-2, -2)" }
- }, $7 = { data: { candlestick: [{ decreasing: { fillcolor: "#e4003a", line: { color: "#e4003a" } }, increasing: { fillcolor: "#00ACFF", line: { color: "#00ACFF" } }, type: "candlestick" }] }, layout: { annotationdefaults: { showarrow: !1 }, autotypenumbers: "strict", colorway: ["#ffed00", "#ef7d00", "#e4003a", "#c13246", "#822661", "#48277c", "#005ca9", "#00aaff", "#9b30d9", "#af005f", "#5f00af", "#af87ff"], dragmode: "pan", font: { family: "Fira Code", size: 18 }, hoverlabel: { align: "left" }, mapbox: { style: "dark" }, hovermode: "x", legend: { bgcolor: "rgba(0, 0, 0, 0)", x: .01, xanchor: "left", y: .99, yanchor: "top", font: { size: 15 } }, paper_bgcolor: "#000000", plot_bgcolor: "#000000", xaxis: { automargin: !0, autorange: !0, rangeslider: { visible: !1 }, showgrid: !0, showline: !0, tickfont: { size: 14 }, zeroline: !1, tick0: 1, title: { standoff: 20 }, linecolor: "#F5EFF3", mirror: !0, ticks: "outside" }, yaxis: { anchor: "x", automargin: !0, fixedrange: !1, zeroline: !1, showgrid: !0, showline: !0, side: "right", tick0: .5, title: { standoff: 20 }, gridcolor: "#283442", linecolor: "#F5EFF3", mirror: !0, ticks: "outside" } } }, K7 = { data: { barpolar: [{ marker: { line: { color: "white", width: .5 }, pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "barpolar" }], bar: [{ error_x: { color: "#2a3f5f" }, error_y: { color: "#2a3f5f" }, marker: { line: { color: "white", width: .5 }, pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "bar" }], carpet: [{ aaxis: { endlinecolor: "#2a3f5f", gridcolor: "#C8D4E3", linecolor: "#C8D4E3", minorgridcolor: "#C8D4E3", startlinecolor: "#2a3f5f" }, baxis: { endlinecolor: "#2a3f5f", gridcolor: "#C8D4E3", linecolor: "#C8D4E3", minorgridcolor: "#C8D4E3", startlinecolor: "#2a3f5f" }, type: "carpet" }], choropleth: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "choropleth" }], contourcarpet: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "contourcarpet" }], contour: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "contour" }], heatmapgl: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "heatmapgl" }], heatmap: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "heatmap" }], histogram2dcontour: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "histogram2dcontour" }], histogram2d: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "histogram2d" }], histogram: [{ marker: { pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "histogram" }], mesh3d: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "mesh3d" }], parcoords: [{ line: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "parcoords" }], pie: [{ automargin: !0, type: "pie" }], scatter3d: [{ line: { colorbar: { outlinewidth: 0, ticks: "" } }, marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatter3d" }], scattercarpet: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattercarpet" }], scattergeo: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattergeo" }], scattergl: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattergl" }], scattermapbox: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattermapbox" }], scatterpolargl: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterpolargl" }], scatterpolar: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterpolar" }], scatter: [{ fillpattern: { fillmode: "overlay", size: 10, solidity: .2 }, type: "scatter" }], scatterternary: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterternary" }], surface: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "surface" }], table: [{ cells: { fill: { color: "#EBF0F8" }, line: { color: "white" } }, header: { fill: { color: "#C8D4E3" }, line: { color: "white" } }, type: "table" }], candlestick: [{ decreasing: { fillcolor: "#c80000", line: { color: "#990000" } }, increasing: { fillcolor: "#009600", line: { color: "#007500" } }, type: "candlestick" }] }, layout: { annotationdefaults: { arrowcolor: "#2a3f5f", arrowhead: 0, arrowwidth: 1, showarrow: !1 }, autotypenumbers: "strict", coloraxis: { colorbar: { outlinewidth: 0, ticks: "" } }, colorscale: { diverging: [[0, "#8e0152"], [.1, "#c51b7d"], [.2, "#de77ae"], [.3, "#f1b6da"], [.4, "#fde0ef"], [.5, "#f7f7f7"], [.6, "#e6f5d0"], [.7, "#b8e186"], [.8, "#7fbc41"], [.9, "#4d9221"], [1, "#276419"]], sequential: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], sequentialminus: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]] }, colorway: ["#254495", "#c13246", "#48277c", "#e4003a", "#ef7d00", "#822661", "#ffed00", "#00aaff", "#9b30d9", "#af005f", "#5f00af", "#af87ff"], font: { color: "#2a3f5f" }, geo: { bgcolor: "white", lakecolor: "white", landcolor: "white", showlakes: !0, showland: !0, subunitcolor: "#C8D4E3" }, hoverlabel: { align: "left" }, hovermode: "x", mapbox: { style: "light" }, paper_bgcolor: "white", plot_bgcolor: "white", polar: { angularaxis: { gridcolor: "#EBF0F8", linecolor: "#EBF0F8", ticks: "" }, bgcolor: "white", radialaxis: { gridcolor: "#EBF0F8", linecolor: "#EBF0F8", ticks: "" } }, scene: { xaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" }, yaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" }, zaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" } }, shapedefaults: { line: { color: "#2a3f5f" } }, ternary: { aaxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" }, baxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" }, bgcolor: "white", caxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" } }, title: { x: .05 }, xaxis: { automargin: !0, ticks: "outside", zerolinewidth: 2, rangeslider: { visible: !1 }, showgrid: !0, showline: !0, tickfont: { size: 15 }, title: { standoff: 20 }, mirror: !0, zeroline: !1 }, yaxis: { automargin: !0, ticks: "outside", tickfont: { size: 15 }, zerolinewidth: 2, fixedrange: !1, title: { standoff: 20 }, nticks: 8, showgrid: !0, showline: !0, side: "right", mirror: !0, zeroline: !1 }, dragmode: "pan", legend: { bgcolor: "rgba(0, 0, 0, 0)", x: 1.03, xanchor: "left", y: .99, yanchor: "top" } } }; function zo() { return zo = Object.assign ? Object.assign.bind() : function (L) { for (var I = 1; I < arguments.length; I++) { var $ = arguments[I]; for (var se in $) Object.prototype.hasOwnProperty.call($, se) && (L[se] = $[se]) } return L }, zo.apply(this, arguments) } function wf(L, I, { checkForDefaultPrevented: $ = !0 } = {}) { return function (Pe) { if (L == null || L(Pe), $ === !1 || !Pe.defaultPrevented) return I == null ? void 0 : I(Pe) } } function J7(L, I) { typeof L == "function" ? L(I) : L != null && (L.current = I) } function S5(...L) { return I => L.forEach($ => J7($, I)) } function pd(...L) { return In.useCallback(S5(...L), L) } function Q7(L, I = []) { let $ = []; function se(R, E) { const e = In.createContext(E), w = $.length; $ = [...$, E]; function k(_) { const { scope: y, children: h, ...l } = _, t = (y == null ? void 0 : y[L][w]) || e, o = In.useMemo(() => l, Object.values(l)); return In.createElement(t.Provider, { value: o }, h) } function i(_, y) { const h = (y == null ? void 0 : y[L][w]) || e, l = In.useContext(h); if (l) return l; if (E !== void 0) return E; throw new Error(`\`${_}\` must be used within \`${R}\``) } return k.displayName = R + "Provider", [k, i] } const Pe = () => { const R = $.map(E => In.createContext(E)); return function (e) { const w = (e == null ? void 0 : e[L]) || R; return In.useMemo(() => ({ [`__scope${L}`]: { ...e, [L]: w } }), [e, w]) } }; return Pe.scopeName = L, [se, eA(Pe, ...I)] } function eA(...L) { const I = L[0]; if (L.length === 1) return I; const $ = () => { const se = L.map(Pe => ({ useScope: Pe(), scopeName: Pe.scopeName })); return function (R) { const E = se.reduce((e, { useScope: w, scopeName: k }) => { const _ = w(R)[`__scope${k}`]; return { ...e, ..._ } }, {}); return In.useMemo(() => ({ [`__scope${I.scopeName}`]: E }), [E]) } }; return $.scopeName = I.scopeName, $ } const Tm = globalThis != null && globalThis.document ? In.useLayoutEffect : () => { }, tA = Z6["useId".toString()] || (() => { }); let nA = 0; function sv(L) { const [I, $] = In.useState(tA()); return Tm(() => { L || $(se => se ?? String(nA++)) }, [L]), L || (I ? `radix-${I}` : "") } function Cf(L) { const I = In.useRef(L); return In.useEffect(() => { I.current = L }), In.useMemo(() => (...$) => { var se; return (se = I.current) === null || se === void 0 ? void 0 : se.call(I, ...$) }, []) } function rA({ prop: L, defaultProp: I, onChange: $ = () => { } }) { const [se, Pe] = iA({ defaultProp: I, onChange: $ }), R = L !== void 0, E = R ? L : se, e = Cf($), w = In.useCallback(k => { if (R) { const _ = typeof k == "function" ? k(L) : k; _ !== L && e(_) } else Pe(k) }, [R, L, Pe, e]); return [E, w] } function iA({ defaultProp: L, onChange: I }) { const $ = In.useState(L), [se] = $, Pe = In.useRef(se), R = Cf(I); return In.useEffect(() => { Pe.current !== se && (R(se), Pe.current = se) }, [se, Pe, R]), $ } const Fy = In.forwardRef((L, I) => { const { children: $, ...se } = L, Pe = In.Children.toArray($), R = Pe.find(oA); if (R) { const E = R.props.children, e = Pe.map(w => w === R ? In.Children.count(E) > 1 ? In.Children.only(null) : In.isValidElement(E) ? E.props.children : null : w); return In.createElement(Am, zo({}, se, { ref: I }), In.isValidElement(E) ? In.cloneElement(E, void 0, e) : null) } return In.createElement(Am, zo({}, se, { ref: I }), $) }); Fy.displayName = "Slot"; const Am = In.forwardRef((L, I) => { const { children: $, ...se } = L; return In.isValidElement($) ? In.cloneElement($, { ...sA(se, $.props), ref: I ? S5(I, $.ref) : $.ref }) : In.Children.count($) > 1 ? In.Children.only(null) : null }); Am.displayName = "SlotClone"; const aA = ({ children: L }) => In.createElement(In.Fragment, null, L); function oA(L) { return In.isValidElement(L) && L.type === aA } function sA(L, I) { const $ = { ...I }; for (const se in I) { const Pe = L[se], R = I[se]; /^on[A-Z]/.test(se) ? Pe && R ? $[se] = (...e) => { R(...e), Pe(...e) } : Pe && ($[se] = Pe) : se === "style" ? $[se] = { ...Pe, ...R } : se === "className" && ($[se] = [Pe, R].filter(Boolean).join(" ")) } return { ...L, ...$ } } const lA = ["a", "button", "div", "form", "h2", "h3", "img", "input", "label", "li", "nav", "ol", "p", "span", "svg", "ul"], Nh = lA.reduce((L, I) => { const $ = In.forwardRef((se, Pe) => { const { asChild: R, ...E } = se, e = R ? Fy : I; return In.useEffect(() => { window[Symbol.for("radix-ui")] = !0 }, []), In.createElement(e, zo({}, E, { ref: Pe })) }); return $.displayName = `Primitive.${I}`, { ...L, [I]: $ } }, {}); function uA(L, I) { L && Ay.flushSync(() => L.dispatchEvent(I)) } function cA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L); In.useEffect(() => { const se = Pe => { Pe.key === "Escape" && $(Pe) }; return I.addEventListener("keydown", se), () => I.removeEventListener("keydown", se) }, [$, I]) } const Mm = "dismissableLayer.update", fA = "dismissableLayer.pointerDownOutside", hA = "dismissableLayer.focusOutside"; let f_; const pA = In.createContext({ layers: new Set, layersWithOutsidePointerEventsDisabled: new Set, branches: new Set }), dA = In.forwardRef((L, I) => { var $; const { disableOutsidePointerEvents: se = !1, onEscapeKeyDown: Pe, onPointerDownOutside: R, onFocusOutside: E, onInteractOutside: e, onDismiss: w, ...k } = L, i = In.useContext(pA), [_, y] = In.useState(null), h = ($ = _ == null ? void 0 : _.ownerDocument) !== null && $ !== void 0 ? $ : globalThis == null ? void 0 : globalThis.document, [, l] = In.useState({}), t = pd(I, b => y(b)), o = Array.from(i.layers), [r] = [...i.layersWithOutsidePointerEventsDisabled].slice(-1), n = o.indexOf(r), a = _ ? o.indexOf(_) : -1, s = i.layersWithOutsidePointerEventsDisabled.size > 0, u = a >= n, p = gA(b => { const g = b.target, f = [...i.branches].some(v => v.contains(g)); !u || f || (R == null || R(b), e == null || e(b), b.defaultPrevented || w == null || w()) }, h), c = vA(b => { const g = b.target;[...i.branches].some(v => v.contains(g)) || (E == null || E(b), e == null || e(b), b.defaultPrevented || w == null || w()) }, h); return cA(b => { a === i.layers.size - 1 && (Pe == null || Pe(b), !b.defaultPrevented && w && (b.preventDefault(), w())) }, h), In.useEffect(() => { if (_) return se && (i.layersWithOutsidePointerEventsDisabled.size === 0 && (f_ = h.body.style.pointerEvents, h.body.style.pointerEvents = "none"), i.layersWithOutsidePointerEventsDisabled.add(_)), i.layers.add(_), h_(), () => { se && i.layersWithOutsidePointerEventsDisabled.size === 1 && (h.body.style.pointerEvents = f_) } }, [_, h, se, i]), In.useEffect(() => () => { _ && (i.layers.delete(_), i.layersWithOutsidePointerEventsDisabled.delete(_), h_()) }, [_, i]), In.useEffect(() => { const b = () => l({}); return document.addEventListener(Mm, b), () => document.removeEventListener(Mm, b) }, []), In.createElement(Nh.div, zo({}, k, { ref: t, style: { pointerEvents: s ? u ? "auto" : "none" : void 0, ...L.style }, onFocusCapture: wf(L.onFocusCapture, c.onFocusCapture), onBlurCapture: wf(L.onBlurCapture, c.onBlurCapture), onPointerDownCapture: wf(L.onPointerDownCapture, p.onPointerDownCapture) })) }); function gA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L), se = In.useRef(!1), Pe = In.useRef(() => { }); return In.useEffect(() => { const R = e => { if (e.target && !se.current) { let i = function () { E5(fA, $, k, { discrete: !0 }) }; var w = i; const k = { originalEvent: e }; e.pointerType === "touch" ? (I.removeEventListener("click", Pe.current), Pe.current = i, I.addEventListener("click", Pe.current, { once: !0 })) : i() } se.current = !1 }, E = window.setTimeout(() => { I.addEventListener("pointerdown", R) }, 0); return () => { window.clearTimeout(E), I.removeEventListener("pointerdown", R), I.removeEventListener("click", Pe.current) } }, [I, $]), { onPointerDownCapture: () => se.current = !0 } } function vA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L), se = In.useRef(!1); return In.useEffect(() => { const Pe = R => { R.target && !se.current && E5(hA, $, { originalEvent: R }, { discrete: !1 }) }; return I.addEventListener("focusin", Pe), () => I.removeEventListener("focusin", Pe) }, [I, $]), { onFocusCapture: () => se.current = !0, onBlurCapture: () => se.current = !1 } } function h_() { const L = new CustomEvent(Mm); document.dispatchEvent(L) } function E5(L, I, $, { discrete: se }) { const Pe = $.originalEvent.target, R = new CustomEvent(L, { bubbles: !1, cancelable: !0, detail: $ }); I && Pe.addEventListener(L, I, { once: !0 }), se ? uA(Pe, R) : Pe.dispatchEvent(R) } const lv = "focusScope.autoFocusOnMount", uv = "focusScope.autoFocusOnUnmount", p_ = { bubbles: !1, cancelable: !0 }, mA = In.forwardRef((L, I) => { const { loop: $ = !1, trapped: se = !1, onMountAutoFocus: Pe, onUnmountAutoFocus: R, ...E } = L, [e, w] = In.useState(null), k = Cf(Pe), i = Cf(R), _ = In.useRef(null), y = pd(I, t => w(t)), h = In.useRef({ paused: !1, pause() { this.paused = !0 }, resume() { this.paused = !1 } }).current; In.useEffect(() => { if (se) { let n = function (p) { if (h.paused || !e) return; const c = p.target; e.contains(c) ? _.current = c : kc(_.current, { select: !0 }) }, a = function (p) { if (h.paused || !e) return; const c = p.relatedTarget; c !== null && (e.contains(c) || kc(_.current, { select: !0 })) }, s = function (p) { const c = document.activeElement; for (const b of p) b.removedNodes.length > 0 && (e != null && e.contains(c) || kc(e)) }; var t = n, o = a, r = s; document.addEventListener("focusin", n), document.addEventListener("focusout", a); const u = new MutationObserver(s); return e && u.observe(e, { childList: !0, subtree: !0 }), () => { document.removeEventListener("focusin", n), document.removeEventListener("focusout", a), u.disconnect() } } }, [se, e, h.paused]), In.useEffect(() => { if (e) { g_.add(h); const t = document.activeElement; if (!e.contains(t)) { const r = new CustomEvent(lv, p_); e.addEventListener(lv, k), e.dispatchEvent(r), r.defaultPrevented || (yA(kA(C5(e)), { select: !0 }), document.activeElement === t && kc(e)) } return () => { e.removeEventListener(lv, k), setTimeout(() => { const r = new CustomEvent(uv, p_); e.addEventListener(uv, i), e.dispatchEvent(r), r.defaultPrevented || kc(t ?? document.body, { select: !0 }), e.removeEventListener(uv, i), g_.remove(h) }, 0) } } }, [e, k, i, h]); const l = In.useCallback(t => { if (!$ && !se || h.paused) return; const o = t.key === "Tab" && !t.altKey && !t.ctrlKey && !t.metaKey, r = document.activeElement; if (o && r) { const n = t.currentTarget, [a, s] = xA(n); a && s ? !t.shiftKey && r === s ? (t.preventDefault(), $ && kc(a, { select: !0 })) : t.shiftKey && r === a && (t.preventDefault(), $ && kc(s, { select: !0 })) : r === n && t.preventDefault() } }, [$, se, h.paused]); return In.createElement(Nh.div, zo({ tabIndex: -1 }, E, { ref: y, onKeyDown: l })) }); function yA(L, { select: I = !1 } = {}) { const $ = document.activeElement; for (const se of L) if (kc(se, { select: I }), document.activeElement !== $) return } function xA(L) { const I = C5(L), $ = d_(I, L), se = d_(I.reverse(), L); return [$, se] } function C5(L) { const I = [], $ = document.createTreeWalker(L, NodeFilter.SHOW_ELEMENT, { acceptNode: se => { const Pe = se.tagName === "INPUT" && se.type === "hidden"; return se.disabled || se.hidden || Pe ? NodeFilter.FILTER_SKIP : se.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP } }); for (; $.nextNode();)I.push($.currentNode); return I } function d_(L, I) { for (const $ of L) if (!bA($, { upTo: I })) return $ } function bA(L, { upTo: I }) { if (getComputedStyle(L).visibility === "hidden") return !0; for (; L;) { if (I !== void 0 && L === I) return !1; if (getComputedStyle(L).display === "none") return !0; L = L.parentElement } return !1 } function _A(L) { return L instanceof HTMLInputElement && "select" in L } function kc(L, { select: I = !1 } = {}) { if (L && L.focus) { const $ = document.activeElement; L.focus({ preventScroll: !0 }), L !== $ && _A(L) && I && L.select() } } const g_ = wA(); function wA() { let L = []; return { add(I) { const $ = L[0]; I !== $ && ($ == null || $.pause()), L = v_(L, I), L.unshift(I) }, remove(I) { var $; L = v_(L, I), ($ = L[0]) === null || $ === void 0 || $.resume() } } } function v_(L, I) { const $ = [...L], se = $.indexOf(I); return se !== -1 && $.splice(se, 1), $ } function kA(L) { return L.filter(I => I.tagName !== "A") } function TA(L, I) { return In.useReducer(($, se) => { const Pe = I[$][se]; return Pe ?? $ }, L) } const By = L => { const { present: I, children: $ } = L, se = AA(I), Pe = typeof $ == "function" ? $({ present: se.isPresent }) : In.Children.only($), R = pd(se.ref, Pe.ref); return typeof $ == "function" || se.isPresent ? In.cloneElement(Pe, { ref: R }) : null }; By.displayName = "Presence"; function AA(L) { const [I, $] = In.useState(), se = In.useRef({}), Pe = In.useRef(L), R = In.useRef("none"), E = L ? "mounted" : "unmounted", [e, w] = TA(E, { mounted: { UNMOUNT: "unmounted", ANIMATION_OUT: "unmountSuspended" }, unmountSuspended: { MOUNT: "mounted", ANIMATION_END: "unmounted" }, unmounted: { MOUNT: "mounted" } }); return In.useEffect(() => { const k = b0(se.current); R.current = e === "mounted" ? k : "none" }, [e]), Tm(() => { const k = se.current, i = Pe.current; if (i !== L) { const y = R.current, h = b0(k); L ? w("MOUNT") : h === "none" || (k == null ? void 0 : k.display) === "none" ? w("UNMOUNT") : w(i && y !== h ? "ANIMATION_OUT" : "UNMOUNT"), Pe.current = L } }, [L, w]), Tm(() => { if (I) { const k = _ => { const h = b0(se.current).includes(_.animationName); _.target === I && h && Ay.flushSync(() => w("ANIMATION_END")) }, i = _ => { _.target === I && (R.current = b0(se.current)) }; return I.addEventListener("animationstart", i), I.addEventListener("animationcancel", k), I.addEventListener("animationend", k), () => { I.removeEventListener("animationstart", i), I.removeEventListener("animationcancel", k), I.removeEventListener("animationend", k) } } else w("ANIMATION_END") }, [I, w]), { isPresent: ["mounted", "unmountSuspended"].includes(e), ref: In.useCallback(k => { k && (se.current = getComputedStyle(k)), $(k) }, []) } } function b0(L) { return (L == null ? void 0 : L.animationName) || "none" } let cv = 0; function MA() { In.useEffect(() => { var L, I; const $ = document.querySelectorAll("[data-radix-focus-guard]"); return document.body.insertAdjacentElement("afterbegin", (L = $[0]) !== null && L !== void 0 ? L : m_()), document.body.insertAdjacentElement("beforeend", (I = $[1]) !== null && I !== void 0 ? I : m_()), cv++, () => { cv === 1 && document.querySelectorAll("[data-radix-focus-guard]").forEach(se => se.remove()), cv-- } }, []) } function m_() { const L = document.createElement("span"); return L.setAttribute("data-radix-focus-guard", ""), L.tabIndex = 0, L.style.cssText = "outline: none; opacity: 0; position: fixed; pointer-events: none", L } var ku = function () { return ku = Object.assign || function (I) { for (var $, se = 1, Pe = arguments.length; se < Pe; se++) { $ = arguments[se]; for (var R in $) Object.prototype.hasOwnProperty.call($, R) && (I[R] = $[R]) } return I }, ku.apply(this, arguments) }; function L5(L, I) { var $ = {}; for (var se in L) Object.prototype.hasOwnProperty.call(L, se) && I.indexOf(se) < 0 && ($[se] = L[se]); if (L != null && typeof Object.getOwnPropertySymbols == "function") for (var Pe = 0, se = Object.getOwnPropertySymbols(L); Pe < se.length; Pe++)I.indexOf(se[Pe]) < 0 && Object.prototype.propertyIsEnumerable.call(L, se[Pe]) && ($[se[Pe]] = L[se[Pe]]); return $ } function SA(L, I, $) { if ($ || arguments.length === 2) for (var se = 0, Pe = I.length, R; se < Pe; se++)(R || !(se in I)) && (R || (R = Array.prototype.slice.call(I, 0, se)), R[se] = I[se]); return L.concat(R || Array.prototype.slice.call(I)) } var H0 = "right-scroll-bar-position", q0 = "width-before-scroll-bar", EA = "with-scroll-bars-hidden", CA = "--removed-body-scroll-bar-size"; function LA(L, I) { return typeof L == "function" ? L(I) : L && (L.current = I), L } function PA(L, I) { var $ = In.useState(function () { return { value: L, callback: I, facade: { get current() { return $.value }, set current(se) { var Pe = $.value; Pe !== se && ($.value = se, $.callback(se, Pe)) } } } })[0]; return $.callback = I, $.facade } function OA(L, I) { return PA(I || null, function ($) { return L.forEach(function (se) { return LA(se, $) }) }) } function IA(L) { return L } function RA(L, I) { I === void 0 && (I = IA); var $ = [], se = !1, Pe = { read: function () { if (se) throw new Error("Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`."); return $.length ? $[$.length - 1] : L }, useMedium: function (R) { var E = I(R, se); return $.push(E), function () { $ = $.filter(function (e) { return e !== E }) } }, assignSyncMedium: function (R) { for (se = !0; $.length;) { var E = $; $ = [], E.forEach(R) } $ = { push: function (e) { return R(e) }, filter: function () { return $ } } }, assignMedium: function (R) { se = !0; var E = []; if ($.length) { var e = $; $ = [], e.forEach(R), E = $ } var w = function () { var i = E; E = [], i.forEach(R) }, k = function () { return Promise.resolve().then(w) }; k(), $ = { push: function (i) { E.push(i), k() }, filter: function (i) { return E = E.filter(i), $ } } } }; return Pe } function zA(L) { L === void 0 && (L = {}); var I = RA(null); return I.options = ku({ async: !0, ssr: !1 }, L), I } var P5 = function (L) { var I = L.sideCar, $ = L5(L, ["sideCar"]); if (!I) throw new Error("Sidecar: please provide `sideCar` property to import the right car"); var se = I.read(); if (!se) throw new Error("Sidecar medium not found"); return In.createElement(se, ku({}, $)) }; P5.isSideCarExport = !0; function DA(L, I) { return L.useMedium(I), P5 } var O5 = zA(), fv = function () { }, B1 = In.forwardRef(function (L, I) { var $ = In.useRef(null), se = In.useState({ onScrollCapture: fv, onWheelCapture: fv, onTouchMoveCapture: fv }), Pe = se[0], R = se[1], E = L.forwardProps, e = L.children, w = L.className, k = L.removeScrollBar, i = L.enabled, _ = L.shards, y = L.sideCar, h = L.noIsolation, l = L.inert, t = L.allowPinchZoom, o = L.as, r = o === void 0 ? "div" : o, n = L5(L, ["forwardProps", "children", "className", "removeScrollBar", "enabled", "shards", "sideCar", "noIsolation", "inert", "allowPinchZoom", "as"]), a = y, s = OA([$, I]), u = ku(ku({}, n), Pe); return In.createElement(In.Fragment, null, i && In.createElement(a, { sideCar: O5, removeScrollBar: k, shards: _, noIsolation: h, inert: l, setCallbacks: R, allowPinchZoom: !!t, lockRef: $ }), E ? In.cloneElement(In.Children.only(e), ku(ku({}, u), { ref: s })) : In.createElement(r, ku({}, u, { className: w, ref: s }), e)) }); B1.defaultProps = { enabled: !0, removeScrollBar: !0, inert: !1 }; B1.classNames = { fullWidth: q0, zeroRight: H0 }; var y_, FA = function () { if (y_) return y_; if (typeof __webpack_nonce__ < "u") return __webpack_nonce__ }; function BA() { if (!document) return null; var L = document.createElement("style"); L.type = "text/css"; var I = FA(); return I && L.setAttribute("nonce", I), L } function NA(L, I) { L.styleSheet ? L.styleSheet.cssText = I : L.appendChild(document.createTextNode(I)) } function jA(L) { var I = document.head || document.getElementsByTagName("head")[0]; I.appendChild(L) } var UA = function () { var L = 0, I = null; return { add: function ($) { L == 0 && (I = BA()) && (NA(I, $), jA(I)), L++ }, remove: function () { L--, !L && I && (I.parentNode && I.parentNode.removeChild(I), I = null) } } }, VA = function () { var L = UA(); return function (I, $) { In.useEffect(function () { return L.add(I), function () { L.remove() } }, [I && $]) } }, I5 = function () { var L = VA(), I = function ($) { var se = $.styles, Pe = $.dynamic; return L(se, Pe), null }; return I }, HA = { left: 0, top: 0, right: 0, gap: 0 }, hv = function (L) { return parseInt(L || "", 10) || 0 }, qA = function (L) { var I = window.getComputedStyle(document.body), $ = I[L === "padding" ? "paddingLeft" : "marginLeft"], se = I[L === "padding" ? "paddingTop" : "marginTop"], Pe = I[L === "padding" ? "paddingRight" : "marginRight"]; return [hv($), hv(se), hv(Pe)] }, GA = function (L) { if (L === void 0 && (L = "margin"), typeof window > "u") return HA; var I = qA(L), $ = document.documentElement.clientWidth, se = window.innerWidth; return { left: I[0], top: I[1], right: I[2], gap: Math.max(0, se - $ + I[2] - I[0]) } }, WA = I5(), ZA = function (L, I, $, se) {
+ }, $7 = { data: { candlestick: [{ decreasing: { fillcolor: "#e4003a", line: { color: "#e4003a" } }, increasing: { fillcolor: "#00ACFF", line: { color: "#00ACFF" } }, type: "candlestick" }] }, layout: { annotationdefaults: { showarrow: !1 }, autotypenumbers: "strict", colorway: ["#ffed00", "#ef7d00", "#e4003a", "#c13246", "#822661", "#48277c", "#005ca9", "#00aaff", "#9b30d9", "#af005f", "#5f00af", "#af87ff"], dragmode: "pan", font: { family: "Arial", size: 18 }, hoverlabel: { align: "left" }, mapbox: { style: "dark" }, hovermode: "x", legend: { bgcolor: "rgba(0, 0, 0, 0)", x: .01, xanchor: "left", y: .99, yanchor: "top", font: { size: 15 } }, paper_bgcolor: "#000000", plot_bgcolor: "#000000", xaxis: { automargin: !0, autorange: !0, rangeslider: { visible: !1 }, showgrid: !0, showline: !0, tickfont: { size: 14 }, zeroline: !1, tick0: 1, title: { standoff: 20 }, linecolor: "#F5EFF3", mirror: !0, ticks: "outside" }, yaxis: { anchor: "x", automargin: !0, fixedrange: !1, zeroline: !1, showgrid: !0, showline: !0, side: "right", tick0: .5, title: { standoff: 20 }, gridcolor: "#283442", linecolor: "#F5EFF3", mirror: !0, ticks: "outside" } } }, K7 = { data: { barpolar: [{ marker: { line: { color: "white", width: .5 }, pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "barpolar" }], bar: [{ error_x: { color: "#2a3f5f" }, error_y: { color: "#2a3f5f" }, marker: { line: { color: "white", width: .5 }, pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "bar" }], carpet: [{ aaxis: { endlinecolor: "#2a3f5f", gridcolor: "#C8D4E3", linecolor: "#C8D4E3", minorgridcolor: "#C8D4E3", startlinecolor: "#2a3f5f" }, baxis: { endlinecolor: "#2a3f5f", gridcolor: "#C8D4E3", linecolor: "#C8D4E3", minorgridcolor: "#C8D4E3", startlinecolor: "#2a3f5f" }, type: "carpet" }], choropleth: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "choropleth" }], contourcarpet: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "contourcarpet" }], contour: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "contour" }], heatmapgl: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "heatmapgl" }], heatmap: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "heatmap" }], histogram2dcontour: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "histogram2dcontour" }], histogram2d: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "histogram2d" }], histogram: [{ marker: { pattern: { fillmode: "overlay", size: 10, solidity: .2 } }, type: "histogram" }], mesh3d: [{ colorbar: { outlinewidth: 0, ticks: "" }, type: "mesh3d" }], parcoords: [{ line: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "parcoords" }], pie: [{ automargin: !0, type: "pie" }], scatter3d: [{ line: { colorbar: { outlinewidth: 0, ticks: "" } }, marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatter3d" }], scattercarpet: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattercarpet" }], scattergeo: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattergeo" }], scattergl: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattergl" }], scattermapbox: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scattermapbox" }], scatterpolargl: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterpolargl" }], scatterpolar: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterpolar" }], scatter: [{ fillpattern: { fillmode: "overlay", size: 10, solidity: .2 }, type: "scatter" }], scatterternary: [{ marker: { colorbar: { outlinewidth: 0, ticks: "" } }, type: "scatterternary" }], surface: [{ colorbar: { outlinewidth: 0, ticks: "" }, colorscale: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], type: "surface" }], table: [{ cells: { fill: { color: "#EBF0F8" }, line: { color: "white" } }, header: { fill: { color: "#C8D4E3" }, line: { color: "white" } }, type: "table" }], candlestick: [{ decreasing: { fillcolor: "#e4003a", line: { color: "#990000" } }, increasing: { fillcolor: "#00ACFF", line: { color: "#00ACFF" } }, type: "candlestick" }] }, layout: { annotationdefaults: { arrowcolor: "#2a3f5f", arrowhead: 0, arrowwidth: 1, showarrow: !1 }, autotypenumbers: "strict", coloraxis: { colorbar: { outlinewidth: 0, ticks: "" } }, colorscale: { diverging: [[0, "#8e0152"], [.1, "#c51b7d"], [.2, "#de77ae"], [.3, "#f1b6da"], [.4, "#fde0ef"], [.5, "#f7f7f7"], [.6, "#e6f5d0"], [.7, "#b8e186"], [.8, "#7fbc41"], [.9, "#4d9221"], [1, "#276419"]], sequential: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]], sequentialminus: [[0, "#0d0887"], [.1111111111111111, "#46039f"], [.2222222222222222, "#7201a8"], [.3333333333333333, "#9c179e"], [.4444444444444444, "#bd3786"], [.5555555555555556, "#d8576b"], [.6666666666666666, "#ed7953"], [.7777777777777778, "#fb9f3a"], [.8888888888888888, "#fdca26"], [1, "#f0f921"]] }, colorway: ["#254495", "#c13246", "#48277c", "#e4003a", "#ef7d00", "#822661", "#ffed00", "#00aaff", "#9b30d9", "#af005f", "#5f00af", "#af87ff"], font: { color: "#2a3f5f" }, geo: { bgcolor: "white", lakecolor: "white", landcolor: "white", showlakes: !0, showland: !0, subunitcolor: "#C8D4E3" }, hoverlabel: { align: "left" }, hovermode: "x", mapbox: { style: "light" }, paper_bgcolor: "white", plot_bgcolor: "white", polar: { angularaxis: { gridcolor: "#EBF0F8", linecolor: "#EBF0F8", ticks: "" }, bgcolor: "white", radialaxis: { gridcolor: "#EBF0F8", linecolor: "#EBF0F8", ticks: "" } }, scene: { xaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" }, yaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" }, zaxis: { backgroundcolor: "white", gridcolor: "#DFE8F3", gridwidth: 2, linecolor: "#EBF0F8", showbackground: !0, ticks: "", zerolinecolor: "#EBF0F8" } }, shapedefaults: { line: { color: "#2a3f5f" } }, ternary: { aaxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" }, baxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" }, bgcolor: "white", caxis: { gridcolor: "#DFE8F3", linecolor: "#A2B1C6", ticks: "" } }, title: { x: .05 }, xaxis: { automargin: !0, ticks: "outside", zerolinewidth: 2, rangeslider: { visible: !1 }, showgrid: !0, showline: !0, tickfont: { size: 15 }, title: { standoff: 20 }, mirror: !0, zeroline: !1 }, yaxis: { automargin: !0, ticks: "outside", tickfont: { size: 15 }, zerolinewidth: 2, fixedrange: !1, title: { standoff: 20 }, nticks: 8, showgrid: !0, showline: !0, side: "right", mirror: !0, zeroline: !1 }, dragmode: "pan", legend: { bgcolor: "rgba(0, 0, 0, 0)", x: 1.03, xanchor: "left", y: .99, yanchor: "top" } } }; function zo() { return zo = Object.assign ? Object.assign.bind() : function (L) { for (var I = 1; I < arguments.length; I++) { var $ = arguments[I]; for (var se in $) Object.prototype.hasOwnProperty.call($, se) && (L[se] = $[se]) } return L }, zo.apply(this, arguments) } function wf(L, I, { checkForDefaultPrevented: $ = !0 } = {}) { return function (Pe) { if (L == null || L(Pe), $ === !1 || !Pe.defaultPrevented) return I == null ? void 0 : I(Pe) } } function J7(L, I) { typeof L == "function" ? L(I) : L != null && (L.current = I) } function S5(...L) { return I => L.forEach($ => J7($, I)) } function pd(...L) { return In.useCallback(S5(...L), L) } function Q7(L, I = []) { let $ = []; function se(R, E) { const e = In.createContext(E), w = $.length; $ = [...$, E]; function k(_) { const { scope: y, children: h, ...l } = _, t = (y == null ? void 0 : y[L][w]) || e, o = In.useMemo(() => l, Object.values(l)); return In.createElement(t.Provider, { value: o }, h) } function i(_, y) { const h = (y == null ? void 0 : y[L][w]) || e, l = In.useContext(h); if (l) return l; if (E !== void 0) return E; throw new Error(`\`${_}\` must be used within \`${R}\``) } return k.displayName = R + "Provider", [k, i] } const Pe = () => { const R = $.map(E => In.createContext(E)); return function (e) { const w = (e == null ? void 0 : e[L]) || R; return In.useMemo(() => ({ [`__scope${L}`]: { ...e, [L]: w } }), [e, w]) } }; return Pe.scopeName = L, [se, eA(Pe, ...I)] } function eA(...L) { const I = L[0]; if (L.length === 1) return I; const $ = () => { const se = L.map(Pe => ({ useScope: Pe(), scopeName: Pe.scopeName })); return function (R) { const E = se.reduce((e, { useScope: w, scopeName: k }) => { const _ = w(R)[`__scope${k}`]; return { ...e, ..._ } }, {}); return In.useMemo(() => ({ [`__scope${I.scopeName}`]: E }), [E]) } }; return $.scopeName = I.scopeName, $ } const Tm = globalThis != null && globalThis.document ? In.useLayoutEffect : () => { }, tA = Z6["useId".toString()] || (() => { }); let nA = 0; function sv(L) { const [I, $] = In.useState(tA()); return Tm(() => { L || $(se => se ?? String(nA++)) }, [L]), L || (I ? `radix-${I}` : "") } function Cf(L) { const I = In.useRef(L); return In.useEffect(() => { I.current = L }), In.useMemo(() => (...$) => { var se; return (se = I.current) === null || se === void 0 ? void 0 : se.call(I, ...$) }, []) } function rA({ prop: L, defaultProp: I, onChange: $ = () => { } }) { const [se, Pe] = iA({ defaultProp: I, onChange: $ }), R = L !== void 0, E = R ? L : se, e = Cf($), w = In.useCallback(k => { if (R) { const _ = typeof k == "function" ? k(L) : k; _ !== L && e(_) } else Pe(k) }, [R, L, Pe, e]); return [E, w] } function iA({ defaultProp: L, onChange: I }) { const $ = In.useState(L), [se] = $, Pe = In.useRef(se), R = Cf(I); return In.useEffect(() => { Pe.current !== se && (R(se), Pe.current = se) }, [se, Pe, R]), $ } const Fy = In.forwardRef((L, I) => { const { children: $, ...se } = L, Pe = In.Children.toArray($), R = Pe.find(oA); if (R) { const E = R.props.children, e = Pe.map(w => w === R ? In.Children.count(E) > 1 ? In.Children.only(null) : In.isValidElement(E) ? E.props.children : null : w); return In.createElement(Am, zo({}, se, { ref: I }), In.isValidElement(E) ? In.cloneElement(E, void 0, e) : null) } return In.createElement(Am, zo({}, se, { ref: I }), $) }); Fy.displayName = "Slot"; const Am = In.forwardRef((L, I) => { const { children: $, ...se } = L; return In.isValidElement($) ? In.cloneElement($, { ...sA(se, $.props), ref: I ? S5(I, $.ref) : $.ref }) : In.Children.count($) > 1 ? In.Children.only(null) : null }); Am.displayName = "SlotClone"; const aA = ({ children: L }) => In.createElement(In.Fragment, null, L); function oA(L) { return In.isValidElement(L) && L.type === aA } function sA(L, I) { const $ = { ...I }; for (const se in I) { const Pe = L[se], R = I[se]; /^on[A-Z]/.test(se) ? Pe && R ? $[se] = (...e) => { R(...e), Pe(...e) } : Pe && ($[se] = Pe) : se === "style" ? $[se] = { ...Pe, ...R } : se === "className" && ($[se] = [Pe, R].filter(Boolean).join(" ")) } return { ...L, ...$ } } const lA = ["a", "button", "div", "form", "h2", "h3", "img", "input", "label", "li", "nav", "ol", "p", "span", "svg", "ul"], Nh = lA.reduce((L, I) => { const $ = In.forwardRef((se, Pe) => { const { asChild: R, ...E } = se, e = R ? Fy : I; return In.useEffect(() => { window[Symbol.for("radix-ui")] = !0 }, []), In.createElement(e, zo({}, E, { ref: Pe })) }); return $.displayName = `Primitive.${I}`, { ...L, [I]: $ } }, {}); function uA(L, I) { L && Ay.flushSync(() => L.dispatchEvent(I)) } function cA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L); In.useEffect(() => { const se = Pe => { Pe.key === "Escape" && $(Pe) }; return I.addEventListener("keydown", se), () => I.removeEventListener("keydown", se) }, [$, I]) } const Mm = "dismissableLayer.update", fA = "dismissableLayer.pointerDownOutside", hA = "dismissableLayer.focusOutside"; let f_; const pA = In.createContext({ layers: new Set, layersWithOutsidePointerEventsDisabled: new Set, branches: new Set }), dA = In.forwardRef((L, I) => { var $; const { disableOutsidePointerEvents: se = !1, onEscapeKeyDown: Pe, onPointerDownOutside: R, onFocusOutside: E, onInteractOutside: e, onDismiss: w, ...k } = L, i = In.useContext(pA), [_, y] = In.useState(null), h = ($ = _ == null ? void 0 : _.ownerDocument) !== null && $ !== void 0 ? $ : globalThis == null ? void 0 : globalThis.document, [, l] = In.useState({}), t = pd(I, b => y(b)), o = Array.from(i.layers), [r] = [...i.layersWithOutsidePointerEventsDisabled].slice(-1), n = o.indexOf(r), a = _ ? o.indexOf(_) : -1, s = i.layersWithOutsidePointerEventsDisabled.size > 0, u = a >= n, p = gA(b => { const g = b.target, f = [...i.branches].some(v => v.contains(g)); !u || f || (R == null || R(b), e == null || e(b), b.defaultPrevented || w == null || w()) }, h), c = vA(b => { const g = b.target;[...i.branches].some(v => v.contains(g)) || (E == null || E(b), e == null || e(b), b.defaultPrevented || w == null || w()) }, h); return cA(b => { a === i.layers.size - 1 && (Pe == null || Pe(b), !b.defaultPrevented && w && (b.preventDefault(), w())) }, h), In.useEffect(() => { if (_) return se && (i.layersWithOutsidePointerEventsDisabled.size === 0 && (f_ = h.body.style.pointerEvents, h.body.style.pointerEvents = "none"), i.layersWithOutsidePointerEventsDisabled.add(_)), i.layers.add(_), h_(), () => { se && i.layersWithOutsidePointerEventsDisabled.size === 1 && (h.body.style.pointerEvents = f_) } }, [_, h, se, i]), In.useEffect(() => () => { _ && (i.layers.delete(_), i.layersWithOutsidePointerEventsDisabled.delete(_), h_()) }, [_, i]), In.useEffect(() => { const b = () => l({}); return document.addEventListener(Mm, b), () => document.removeEventListener(Mm, b) }, []), In.createElement(Nh.div, zo({}, k, { ref: t, style: { pointerEvents: s ? u ? "auto" : "none" : void 0, ...L.style }, onFocusCapture: wf(L.onFocusCapture, c.onFocusCapture), onBlurCapture: wf(L.onBlurCapture, c.onBlurCapture), onPointerDownCapture: wf(L.onPointerDownCapture, p.onPointerDownCapture) })) }); function gA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L), se = In.useRef(!1), Pe = In.useRef(() => { }); return In.useEffect(() => { const R = e => { if (e.target && !se.current) { let i = function () { E5(fA, $, k, { discrete: !0 }) }; var w = i; const k = { originalEvent: e }; e.pointerType === "touch" ? (I.removeEventListener("click", Pe.current), Pe.current = i, I.addEventListener("click", Pe.current, { once: !0 })) : i() } se.current = !1 }, E = window.setTimeout(() => { I.addEventListener("pointerdown", R) }, 0); return () => { window.clearTimeout(E), I.removeEventListener("pointerdown", R), I.removeEventListener("click", Pe.current) } }, [I, $]), { onPointerDownCapture: () => se.current = !0 } } function vA(L, I = globalThis == null ? void 0 : globalThis.document) { const $ = Cf(L), se = In.useRef(!1); return In.useEffect(() => { const Pe = R => { R.target && !se.current && E5(hA, $, { originalEvent: R }, { discrete: !1 }) }; return I.addEventListener("focusin", Pe), () => I.removeEventListener("focusin", Pe) }, [I, $]), { onFocusCapture: () => se.current = !0, onBlurCapture: () => se.current = !1 } } function h_() { const L = new CustomEvent(Mm); document.dispatchEvent(L) } function E5(L, I, $, { discrete: se }) { const Pe = $.originalEvent.target, R = new CustomEvent(L, { bubbles: !1, cancelable: !0, detail: $ }); I && Pe.addEventListener(L, I, { once: !0 }), se ? uA(Pe, R) : Pe.dispatchEvent(R) } const lv = "focusScope.autoFocusOnMount", uv = "focusScope.autoFocusOnUnmount", p_ = { bubbles: !1, cancelable: !0 }, mA = In.forwardRef((L, I) => { const { loop: $ = !1, trapped: se = !1, onMountAutoFocus: Pe, onUnmountAutoFocus: R, ...E } = L, [e, w] = In.useState(null), k = Cf(Pe), i = Cf(R), _ = In.useRef(null), y = pd(I, t => w(t)), h = In.useRef({ paused: !1, pause() { this.paused = !0 }, resume() { this.paused = !1 } }).current; In.useEffect(() => { if (se) { let n = function (p) { if (h.paused || !e) return; const c = p.target; e.contains(c) ? _.current = c : kc(_.current, { select: !0 }) }, a = function (p) { if (h.paused || !e) return; const c = p.relatedTarget; c !== null && (e.contains(c) || kc(_.current, { select: !0 })) }, s = function (p) { const c = document.activeElement; for (const b of p) b.removedNodes.length > 0 && (e != null && e.contains(c) || kc(e)) }; var t = n, o = a, r = s; document.addEventListener("focusin", n), document.addEventListener("focusout", a); const u = new MutationObserver(s); return e && u.observe(e, { childList: !0, subtree: !0 }), () => { document.removeEventListener("focusin", n), document.removeEventListener("focusout", a), u.disconnect() } } }, [se, e, h.paused]), In.useEffect(() => { if (e) { g_.add(h); const t = document.activeElement; if (!e.contains(t)) { const r = new CustomEvent(lv, p_); e.addEventListener(lv, k), e.dispatchEvent(r), r.defaultPrevented || (yA(kA(C5(e)), { select: !0 }), document.activeElement === t && kc(e)) } return () => { e.removeEventListener(lv, k), setTimeout(() => { const r = new CustomEvent(uv, p_); e.addEventListener(uv, i), e.dispatchEvent(r), r.defaultPrevented || kc(t ?? document.body, { select: !0 }), e.removeEventListener(uv, i), g_.remove(h) }, 0) } } }, [e, k, i, h]); const l = In.useCallback(t => { if (!$ && !se || h.paused) return; const o = t.key === "Tab" && !t.altKey && !t.ctrlKey && !t.metaKey, r = document.activeElement; if (o && r) { const n = t.currentTarget, [a, s] = xA(n); a && s ? !t.shiftKey && r === s ? (t.preventDefault(), $ && kc(a, { select: !0 })) : t.shiftKey && r === a && (t.preventDefault(), $ && kc(s, { select: !0 })) : r === n && t.preventDefault() } }, [$, se, h.paused]); return In.createElement(Nh.div, zo({ tabIndex: -1 }, E, { ref: y, onKeyDown: l })) }); function yA(L, { select: I = !1 } = {}) { const $ = document.activeElement; for (const se of L) if (kc(se, { select: I }), document.activeElement !== $) return } function xA(L) { const I = C5(L), $ = d_(I, L), se = d_(I.reverse(), L); return [$, se] } function C5(L) { const I = [], $ = document.createTreeWalker(L, NodeFilter.SHOW_ELEMENT, { acceptNode: se => { const Pe = se.tagName === "INPUT" && se.type === "hidden"; return se.disabled || se.hidden || Pe ? NodeFilter.FILTER_SKIP : se.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP } }); for (; $.nextNode();)I.push($.currentNode); return I } function d_(L, I) { for (const $ of L) if (!bA($, { upTo: I })) return $ } function bA(L, { upTo: I }) { if (getComputedStyle(L).visibility === "hidden") return !0; for (; L;) { if (I !== void 0 && L === I) return !1; if (getComputedStyle(L).display === "none") return !0; L = L.parentElement } return !1 } function _A(L) { return L instanceof HTMLInputElement && "select" in L } function kc(L, { select: I = !1 } = {}) { if (L && L.focus) { const $ = document.activeElement; L.focus({ preventScroll: !0 }), L !== $ && _A(L) && I && L.select() } } const g_ = wA(); function wA() { let L = []; return { add(I) { const $ = L[0]; I !== $ && ($ == null || $.pause()), L = v_(L, I), L.unshift(I) }, remove(I) { var $; L = v_(L, I), ($ = L[0]) === null || $ === void 0 || $.resume() } } } function v_(L, I) { const $ = [...L], se = $.indexOf(I); return se !== -1 && $.splice(se, 1), $ } function kA(L) { return L.filter(I => I.tagName !== "A") } function TA(L, I) { return In.useReducer(($, se) => { const Pe = I[$][se]; return Pe ?? $ }, L) } const By = L => { const { present: I, children: $ } = L, se = AA(I), Pe = typeof $ == "function" ? $({ present: se.isPresent }) : In.Children.only($), R = pd(se.ref, Pe.ref); return typeof $ == "function" || se.isPresent ? In.cloneElement(Pe, { ref: R }) : null }; By.displayName = "Presence"; function AA(L) { const [I, $] = In.useState(), se = In.useRef({}), Pe = In.useRef(L), R = In.useRef("none"), E = L ? "mounted" : "unmounted", [e, w] = TA(E, { mounted: { UNMOUNT: "unmounted", ANIMATION_OUT: "unmountSuspended" }, unmountSuspended: { MOUNT: "mounted", ANIMATION_END: "unmounted" }, unmounted: { MOUNT: "mounted" } }); return In.useEffect(() => { const k = b0(se.current); R.current = e === "mounted" ? k : "none" }, [e]), Tm(() => { const k = se.current, i = Pe.current; if (i !== L) { const y = R.current, h = b0(k); L ? w("MOUNT") : h === "none" || (k == null ? void 0 : k.display) === "none" ? w("UNMOUNT") : w(i && y !== h ? "ANIMATION_OUT" : "UNMOUNT"), Pe.current = L } }, [L, w]), Tm(() => { if (I) { const k = _ => { const h = b0(se.current).includes(_.animationName); _.target === I && h && Ay.flushSync(() => w("ANIMATION_END")) }, i = _ => { _.target === I && (R.current = b0(se.current)) }; return I.addEventListener("animationstart", i), I.addEventListener("animationcancel", k), I.addEventListener("animationend", k), () => { I.removeEventListener("animationstart", i), I.removeEventListener("animationcancel", k), I.removeEventListener("animationend", k) } } else w("ANIMATION_END") }, [I, w]), { isPresent: ["mounted", "unmountSuspended"].includes(e), ref: In.useCallback(k => { k && (se.current = getComputedStyle(k)), $(k) }, []) } } function b0(L) { return (L == null ? void 0 : L.animationName) || "none" } let cv = 0; function MA() { In.useEffect(() => { var L, I; const $ = document.querySelectorAll("[data-radix-focus-guard]"); return document.body.insertAdjacentElement("afterbegin", (L = $[0]) !== null && L !== void 0 ? L : m_()), document.body.insertAdjacentElement("beforeend", (I = $[1]) !== null && I !== void 0 ? I : m_()), cv++, () => { cv === 1 && document.querySelectorAll("[data-radix-focus-guard]").forEach(se => se.remove()), cv-- } }, []) } function m_() { const L = document.createElement("span"); return L.setAttribute("data-radix-focus-guard", ""), L.tabIndex = 0, L.style.cssText = "outline: none; opacity: 0; position: fixed; pointer-events: none", L } var ku = function () { return ku = Object.assign || function (I) { for (var $, se = 1, Pe = arguments.length; se < Pe; se++) { $ = arguments[se]; for (var R in $) Object.prototype.hasOwnProperty.call($, R) && (I[R] = $[R]) } return I }, ku.apply(this, arguments) }; function L5(L, I) { var $ = {}; for (var se in L) Object.prototype.hasOwnProperty.call(L, se) && I.indexOf(se) < 0 && ($[se] = L[se]); if (L != null && typeof Object.getOwnPropertySymbols == "function") for (var Pe = 0, se = Object.getOwnPropertySymbols(L); Pe < se.length; Pe++)I.indexOf(se[Pe]) < 0 && Object.prototype.propertyIsEnumerable.call(L, se[Pe]) && ($[se[Pe]] = L[se[Pe]]); return $ } function SA(L, I, $) { if ($ || arguments.length === 2) for (var se = 0, Pe = I.length, R; se < Pe; se++)(R || !(se in I)) && (R || (R = Array.prototype.slice.call(I, 0, se)), R[se] = I[se]); return L.concat(R || Array.prototype.slice.call(I)) } var H0 = "right-scroll-bar-position", q0 = "width-before-scroll-bar", EA = "with-scroll-bars-hidden", CA = "--removed-body-scroll-bar-size"; function LA(L, I) { return typeof L == "function" ? L(I) : L && (L.current = I), L } function PA(L, I) { var $ = In.useState(function () { return { value: L, callback: I, facade: { get current() { return $.value }, set current(se) { var Pe = $.value; Pe !== se && ($.value = se, $.callback(se, Pe)) } } } })[0]; return $.callback = I, $.facade } function OA(L, I) { return PA(I || null, function ($) { return L.forEach(function (se) { return LA(se, $) }) }) } function IA(L) { return L } function RA(L, I) { I === void 0 && (I = IA); var $ = [], se = !1, Pe = { read: function () { if (se) throw new Error("Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`."); return $.length ? $[$.length - 1] : L }, useMedium: function (R) { var E = I(R, se); return $.push(E), function () { $ = $.filter(function (e) { return e !== E }) } }, assignSyncMedium: function (R) { for (se = !0; $.length;) { var E = $; $ = [], E.forEach(R) } $ = { push: function (e) { return R(e) }, filter: function () { return $ } } }, assignMedium: function (R) { se = !0; var E = []; if ($.length) { var e = $; $ = [], e.forEach(R), E = $ } var w = function () { var i = E; E = [], i.forEach(R) }, k = function () { return Promise.resolve().then(w) }; k(), $ = { push: function (i) { E.push(i), k() }, filter: function (i) { return E = E.filter(i), $ } } } }; return Pe } function zA(L) { L === void 0 && (L = {}); var I = RA(null); return I.options = ku({ async: !0, ssr: !1 }, L), I } var P5 = function (L) { var I = L.sideCar, $ = L5(L, ["sideCar"]); if (!I) throw new Error("Sidecar: please provide `sideCar` property to import the right car"); var se = I.read(); if (!se) throw new Error("Sidecar medium not found"); return In.createElement(se, ku({}, $)) }; P5.isSideCarExport = !0; function DA(L, I) { return L.useMedium(I), P5 } var O5 = zA(), fv = function () { }, B1 = In.forwardRef(function (L, I) { var $ = In.useRef(null), se = In.useState({ onScrollCapture: fv, onWheelCapture: fv, onTouchMoveCapture: fv }), Pe = se[0], R = se[1], E = L.forwardProps, e = L.children, w = L.className, k = L.removeScrollBar, i = L.enabled, _ = L.shards, y = L.sideCar, h = L.noIsolation, l = L.inert, t = L.allowPinchZoom, o = L.as, r = o === void 0 ? "div" : o, n = L5(L, ["forwardProps", "children", "className", "removeScrollBar", "enabled", "shards", "sideCar", "noIsolation", "inert", "allowPinchZoom", "as"]), a = y, s = OA([$, I]), u = ku(ku({}, n), Pe); return In.createElement(In.Fragment, null, i && In.createElement(a, { sideCar: O5, removeScrollBar: k, shards: _, noIsolation: h, inert: l, setCallbacks: R, allowPinchZoom: !!t, lockRef: $ }), E ? In.cloneElement(In.Children.only(e), ku(ku({}, u), { ref: s })) : In.createElement(r, ku({}, u, { className: w, ref: s }), e)) }); B1.defaultProps = { enabled: !0, removeScrollBar: !0, inert: !1 }; B1.classNames = { fullWidth: q0, zeroRight: H0 }; var y_, FA = function () { if (y_) return y_; if (typeof __webpack_nonce__ < "u") return __webpack_nonce__ }; function BA() { if (!document) return null; var L = document.createElement("style"); L.type = "text/css"; var I = FA(); return I && L.setAttribute("nonce", I), L } function NA(L, I) { L.styleSheet ? L.styleSheet.cssText = I : L.appendChild(document.createTextNode(I)) } function jA(L) { var I = document.head || document.getElementsByTagName("head")[0]; I.appendChild(L) } var UA = function () { var L = 0, I = null; return { add: function ($) { L == 0 && (I = BA()) && (NA(I, $), jA(I)), L++ }, remove: function () { L--, !L && I && (I.parentNode && I.parentNode.removeChild(I), I = null) } } }, VA = function () { var L = UA(); return function (I, $) { In.useEffect(function () { return L.add(I), function () { L.remove() } }, [I && $]) } }, I5 = function () { var L = VA(), I = function ($) { var se = $.styles, Pe = $.dynamic; return L(se, Pe), null }; return I }, HA = { left: 0, top: 0, right: 0, gap: 0 }, hv = function (L) { return parseInt(L || "", 10) || 0 }, qA = function (L) { var I = window.getComputedStyle(document.body), $ = I[L === "padding" ? "paddingLeft" : "marginLeft"], se = I[L === "padding" ? "paddingTop" : "marginTop"], Pe = I[L === "padding" ? "paddingRight" : "marginRight"]; return [hv($), hv(se), hv(Pe)] }, GA = function (L) { if (L === void 0 && (L = "margin"), typeof window > "u") return HA; var I = qA(L), $ = document.documentElement.clientWidth, se = window.innerWidth; return { left: I[0], top: I[1], right: I[2], gap: Math.max(0, se - $ + I[2] - I[0]) } }, WA = I5(), ZA = function (L, I, $, se) {
var Pe = L.left, R = L.top, E = L.right, e = L.gap; return $ === void 0 && ($ = "margin"), `
.`.concat(EA, ` {
overflow: hidden `).concat(se, `;
@@ -4068,7 +4068,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
})
})
} function z9({ csvData: L, plotlyData: I, yaxisOptions: $, traceType: se, traceColor: Pe, traceName: R, options: E, increasingColor: e, decreasingColor: w }) { console.log("options", E); const k = I.data[0] || {}; k.xaxis === void 0 && (k.xaxis = "x"), k.yaxis === void 0 && (k.yaxis = "y"); let i = k.yaxis, _; const h = Object.keys(I.layout).filter(o => o.startsWith("yaxis")).map(o => I.layout[o]).filter(o => o.side === "left" && (o.overlaying === "y" || o.fixedrange !== void 0 && o.fixedrange === !0)).length > 0 ? " " : ""; if ($.sameYaxis !== !0) { const o = Object.keys(I.layout).filter(r => r.startsWith("yaxis")).map(r => I.layout[r]); _ = `y${o.length + 1}`, i = `yaxis${o.length + 1}`, I.layout[i] = { ...I9, title: { text: R, font: { size: 14 }, standoff: 0 }, ticksuffix: h, layer: "below traces" } } else _ = k.yaxis.replace("yaxis", "y"); const l = { type: se, name: R, showlegend: !0, yaxis: _ }; let t = {}; if (["scatter", "bar"].includes(se)) { if (!L || L.length === 0) return I; const o = L.findIndex(n => n[E.y] !== null && n[E.y] !== 0); if (o === -1) return I; const r = { x: [], y: [], customdata: [] }; L.forEach(n => { let a = n[E.y]; r.customdata.push(a), $.percentChange && (se === "scatter" || se === "line") && (a = (n[E.y] - L[o][E.y]) / L[o][E.y]), r.x.push(n[E.x]), r.y.push(a) }), t = { ...l, x: r.x, y: r.y, customdata: r.customdata, hovertemplate: "%{customdata:.2f}", connectgaps: !0, marker: { color: Pe } }, se === "bar" && (t.orientation = E.orientation, t.marker.opacity = .7, t.connectgaps = void 0, t.hovertemplate = void 0, t.customdata = void 0) } else if (se === "candlestick") { const o = { x: [], open: [], high: [], low: [], close: [] }; L.forEach(r => { o.x.push(r[E.x]), o.open.push(r[E.open]), o.high.push(r[E.high]), o.low.push(r[E.low]), o.close.push(r[E.close]) }), t = { ...l, x: o.x, open: o.open, high: o.high, low: o.low, close: o.close, increasing: { line: { color: e, width: .8 }, fillcolor: e }, decreasing: { line: { color: w, width: .8 }, fillcolor: w } } } return { ...I, data: [...I.data, t] } } const A_ = { padding: "5px 2px 2px 5px", margin: "2px 0" }; function D9({ open: L, close: I, addAnnotation: $, deleteAnnotation: se, popupData: Pe }) { var l, t, o, r, n; const R = { text: "", color: "#0088CC", size: 18, bordercolor: "#822661", yanchor: "above" }, [E, e] = In.useState(R), [w, k] = In.useState(R); Pe && Pe !== E && Pe.annotation && (Pe.annotation = (Pe == null ? void 0 : Pe.annotation) || {}, e(Pe), k(Pe)); function i() { console.log("closing"), e(R), k(R), I() } function _(a) { console.log(a.target.id.replace("addtext_", ""), a.target.value); const s = a.target.id.replace("addtext_", ""), u = a.target.value; k({ ...w, [s]: u }) } function y() { console.log("submitting", w), w.text !== "" ? (E != null && E.annotation && k({ ...w, annotation: E.annotation }), $(w), I()) : (document.getElementById("popup_textarea_warning").style.display = "block", document.getElementById("addtext_text").style.border = "1px solid red") } function h() { se(E), i() } return hr(dd, { title: "Add Text to Chart", description: "Change the titles on the chart.", open: L, close: i, children: Xr("div", { id: "popup_title", className: "popup_content", children: [Xr("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [Xr("div", { style: { marginBottom: 20 }, children: [Xr("label", { htmlFor: "popup_text", children: [hr("b", { children: "Text:" }), hr("div", { id: "popup_textarea_warning", className: "popup_warning", children: "Text is required" })] }), hr("textarea", { id: "addtext_text", style: { ...A_, width: "100%", maxWidth: "100%", maxHeight: "200px", marginTop: "8px" }, rows: 4, cols: 50, placeholder: "Enter text here", onChange: _, defaultValue: ((l = E == null ? void 0 : E.annotation) == null ? void 0 : l.text) || (w == null ? void 0 : w.text) })] }), Xr("div", { style: { display: "flex", gap: 15, alignItems: "center", flexWrap: "wrap", columnCount: 2, justifyContent: "space-between", marginBottom: 20 }, children: [hr("label", { htmlFor: "addtext_color", children: hr("b", { children: "Font color" }) }), hr("input", { type: "color", id: "addtext_color", style: { margin: "2px 2px 2px 15px" }, defaultValue: ((t = E == null ? void 0 : E.annotation) == null ? void 0 : t.color) || (w == null ? void 0 : w.color), onChange: _ }), hr("label", { htmlFor: "addtext_bordercolor", style: { marginLeft: 20 }, children: hr("b", { children: "Border color" }) }), hr("input", { type: "color", id: "addtext_bordercolor", style: { margin: "2px 2px 10px 15px" }, defaultValue: ((o = E == null ? void 0 : E.annotation) == null ? void 0 : o.bordercolor) || (w == null ? void 0 : w.bordercolor), onChange: _ }), hr("label", { htmlFor: "addtext_size", children: hr("b", { children: "Font size" }) }), hr("input", { style: { ...A_, width: "52px", margin: "0px 0px 0px 2px" }, type: "number", id: "addtext_size", onChange: _, defaultValue: ((r = E == null ? void 0 : E.annotation) == null ? void 0 : r.size) || (w == null ? void 0 : w.size) }), Xr("div", { children: [hr("label", { htmlFor: "addtext_yanchor", style: { marginRight: 31 }, children: hr("b", { children: "Position" }) }), Xr("select", { id: "addtext_yanchor", name: "yanchor", style: { width: "100px" }, defaultValue: ((n = E == null ? void 0 : E.annotation) == null ? void 0 : n.yanchor) || (w == null ? void 0 : w.yanchor), onChange: _, children: [hr("option", { value: "above", children: "Above" }), hr("option", { value: "below", children: "Below" })] })] })] })] }), Xr("div", { style: { float: "right", marginTop: 20 }, children: [hr("button", { className: "_btn-tertiary ph-capture", id: "title_cancel", onClick: i, children: "Cancel" }), hr("button", { className: "_btn ph-capture", id: "title_delete", onClick: h, children: "Delete" }), hr("button", { className: "_btn ph-capture", id: "title_submit", onClick: y, children: "Submit" })] })] }) }) } function F9({ plotlyData: L, open: I, close: $, defaultTitle: se, updateTitle: Pe, updateAxesTitles: R }) { const [E, e] = In.useState(se), w = Object.keys(L.layout || {}).filter(y => y.startsWith("yaxis") && L.layout[y].range != null), k = Object.keys(L.layout || {}).filter(y => { var h; return y.startsWith("xaxis") && L.layout[y].showticklabels != null && ((h = L.layout[y]) == null ? void 0 : h.anchor) }), [i, _] = In.useState({}); return hr(dd, { title: "Chart Titles", description: "Change the titles on the chart.", open: I, close: $, children: Xr("div", { id: "popup_title", className: "popup_content", children: [Xr("div", { style: { display: "flex", flexDirection: "column", gap: 0 }, children: [Xr("div", { children: [hr("label", { htmlFor: "title_text", children: hr("b", { children: "Title:" }) }), hr("textarea", { id: "title_text", style: { ...q5, width: "100%", maxWidth: "100%", maxHeight: "200px", marginTop: "8px", marginLeft: "0px" }, rows: 2, cols: 20, value: E, onChange: y => e(y.target.value) })] }), hr("div", { id: "xaxis_div", className: "csv_column_container", style: { marginTop: 5, marginBottom: -5 }, children: k.map((y, h) => { var l, t; return Xr("div", { style: { marginTop: 5, marginBottom: 5 }, children: [hr("label", { htmlFor: `title_${y}`, children: h === 0 ? hr("b", { children: "X axis:" }) : Xr("b", { children: ["X axis ", h + 1, ":"] }) }), hr("input", { id: `title_${y}`, style: { marginLeft: "0px", padding: "5px 2px 2px 5px" }, type: "text", defaultValue: ((t = (l = L == null ? void 0 : L.layout[y]) == null ? void 0 : l.title) == null ? void 0 : t.text) || "", onChange: o => { _({ ...i, [y]: o.target.value }) } })] }, y) }) }), hr("div", { id: "yaxis_div", className: "csv_column_container", style: { marginTop: 5, marginBottom: 5 }, children: w.map((y, h) => { var l, t; return Xr("div", { style: { marginTop: 10 }, children: [hr("label", { htmlFor: `title_${y}`, children: h === 0 ? hr("b", { children: "Y axis:" }) : Xr("b", { children: ["Y axis ", h + 1, ":"] }) }), hr("input", { id: `title_${y}`, style: { marginLeft: "0px", padding: "5px 2px 2px 5px" }, type: "text", defaultValue: ((t = (l = L == null ? void 0 : L.layout[y]) == null ? void 0 : l.title) == null ? void 0 : t.text) || "", onChange: o => { _({ ...i, [y]: o.target.value }) } })] }, y) }) })] }), Xr("div", { style: { float: "right", marginTop: 20 }, children: [hr("button", { className: "_btn-tertiary ph-capture", id: "title_cancel", onClick: $, children: "Cancel" }), hr("button", { className: "_btn ph-capture", id: "title_submit", onClick: () => { Pe(E), R(i), $() }, children: "Submit" })] })] }) }) } function Cm() { return Cm = Object.assign ? Object.assign.bind() : function (L) { for (var I = 1; I < arguments.length; I++) { var $ = arguments[I]; for (var se in $) Object.prototype.hasOwnProperty.call($, se) && (L[se] = $[se]) } return L }, Cm.apply(this, arguments) } var G5 = ["shift", "alt", "meta", "mod", "ctrl"], B9 = { esc: "escape", return: "enter", ".": "period", ",": "comma", "-": "slash", " ": "space", "`": "backquote", "#": "backslash", "+": "bracketright", ShiftLeft: "shift", ShiftRight: "shift", AltLeft: "alt", AltRight: "alt", MetaLeft: "meta", MetaRight: "meta", OSLeft: "meta", OSRight: "meta", ControlLeft: "ctrl", ControlRight: "ctrl" }; function Ic(L) { return (B9[L] || L).trim().toLowerCase().replace(/key|digit|numpad|arrow/, "") } function N9(L) { return G5.includes(L) } function dv(L, I) { return I === void 0 && (I = ","), L.split(I) } function gv(L, I, $) { I === void 0 && (I = "+"); var se = L.toLocaleLowerCase().split(I).map(function (E) { return Ic(E) }), Pe = { alt: se.includes("alt"), ctrl: se.includes("ctrl") || se.includes("control"), shift: se.includes("shift"), meta: se.includes("meta"), mod: se.includes("mod") }, R = se.filter(function (E) { return !G5.includes(E) }); return Cm({}, Pe, { keys: R, description: $ }) } (function () { typeof document < "u" && (document.addEventListener("keydown", function (L) { L.key !== void 0 && W5([Ic(L.key), Ic(L.code)]) }), document.addEventListener("keyup", function (L) { L.key !== void 0 && Z5([Ic(L.key), Ic(L.code)]) })), typeof window < "u" && window.addEventListener("blur", function () { Rc.clear() }) })(); var Rc = new Set; function j9(L, I) { I === void 0 && (I = ","); var $ = Array.isArray(L) ? L : L.split(I); return $.every(function (se) { return Rc.has(se.trim().toLowerCase()) }) } function W5(L) { var I = Array.isArray(L) ? L : [L]; Rc.has("meta") && Rc.forEach(function ($) { return !N9($) && Rc.delete($.toLowerCase()) }), I.forEach(function ($) { return Rc.add($.toLowerCase()) }) } function Z5(L) { var I = Array.isArray(L) ? L : [L]; L === "meta" ? Rc.clear() : I.forEach(function ($) { return Rc.delete($.toLowerCase()) }) } function U9(L, I, $) { (typeof $ == "function" && $(L, I) || $ === !0) && L.preventDefault() } function V9(L, I, $) { return typeof $ == "function" ? $(L, I) : $ === !0 || $ === void 0 } function H9(L) { return Y5(L, ["input", "textarea", "select"]) } function Y5(L, I) { var $ = L.target; I === void 0 && (I = !1); var se = $ && $.tagName; return I instanceof Array ? !!(se && I && I.some(function (Pe) { return Pe.toLowerCase() === se.toLowerCase() })) : !!(se && I && I === !0) } function q9(L, I) { return L.length === 0 && I ? (console.warn('A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a '), !0) : I ? L.some(function ($) { return I.includes($) }) || L.includes("*") : !0 } var G9 = function (I, $, se) { se === void 0 && (se = !1); var Pe = $.alt, R = $.meta, E = $.mod, e = $.shift, w = $.ctrl, k = $.keys, i = I.key, _ = I.code, y = I.ctrlKey, h = I.metaKey, l = I.shiftKey, t = I.altKey, o = Ic(_), r = i.toLowerCase(); if (!se) { if (Pe === !t && r !== "alt" || e === !l && r !== "shift") return !1; if (E) { if (!h && !y) return !1 } else if (R === !h && r !== "meta" && r !== "os" || w === !y && r !== "ctrl" && r !== "control") return !1 } return k && k.length === 1 && (k.includes(r) || k.includes(o)) ? !0 : k ? j9(k) : !k }, W9 = In.createContext(void 0), Z9 = function () { return In.useContext(W9) }; function X5(L, I) { return L && I && typeof L == "object" && typeof I == "object" ? Object.keys(L).length === Object.keys(I).length && Object.keys(L).reduce(function ($, se) { return $ && X5(L[se], I[se]) }, !0) : L === I } var Y9 = In.createContext({ hotkeys: [], enabledScopes: [], toggleScope: function () { }, enableScope: function () { }, disableScope: function () { } }), X9 = function () { return In.useContext(Y9) }; function $9(L) { var I = In.useRef(void 0); return X5(I.current, L) || (I.current = L), I.current } var M_ = function (I) { I.stopPropagation(), I.preventDefault(), I.stopImmediatePropagation() }, K9 = typeof window < "u" ? In.useLayoutEffect : In.useEffect; function _c(L, I, $, se) { var Pe = In.useRef(null), R = In.useRef(!1), E = $ instanceof Array ? se instanceof Array ? void 0 : se : $, e = L instanceof Array ? L.join(E == null ? void 0 : E.splitKey) : L, w = $ instanceof Array ? $ : se instanceof Array ? se : void 0, k = In.useCallback(I, w ?? []), i = In.useRef(k); w ? i.current = k : i.current = I; var _ = $9(E), y = X9(), h = y.enabledScopes, l = Z9(); return K9(function () { if (!((_ == null ? void 0 : _.enabled) === !1 || !q9(h, _ == null ? void 0 : _.scopes))) { var t = function (s, u) { var p; if (u === void 0 && (u = !1), !(H9(s) && !Y5(s, _ == null ? void 0 : _.enableOnFormTags)) && !(_ != null && _.ignoreEventWhen != null && _.ignoreEventWhen(s))) { if (Pe.current !== null && document.activeElement !== Pe.current && !Pe.current.contains(document.activeElement)) { M_(s); return } (p = s.target) != null && p.isContentEditable && !(_ != null && _.enableOnContentEditable) || dv(e, _ == null ? void 0 : _.splitKey).forEach(function (c) { var b, g = gv(c, _ == null ? void 0 : _.combinationKey); if (G9(s, g, _ == null ? void 0 : _.ignoreModifiers) || (b = g.keys) != null && b.includes("*")) { if (u && R.current) return; if (U9(s, g, _ == null ? void 0 : _.preventDefault), !V9(s, g, _ == null ? void 0 : _.enabled)) { M_(s); return } i.current(s, g), u || (R.current = !0) } }) } }, o = function (s) { s.key !== void 0 && (W5(Ic(s.code)), ((_ == null ? void 0 : _.keydown) === void 0 && (_ == null ? void 0 : _.keyup) !== !0 || _ != null && _.keydown) && t(s)) }, r = function (s) { s.key !== void 0 && (Z5(Ic(s.code)), R.current = !1, _ != null && _.keyup && t(s, !0)) }, n = Pe.current || (E == null ? void 0 : E.document) || document; return n.addEventListener("keyup", r), n.addEventListener("keydown", o), l && dv(e, _ == null ? void 0 : _.splitKey).forEach(function (a) { return l.addHotkey(gv(a, _ == null ? void 0 : _.combinationKey, _ == null ? void 0 : _.description)) }), function () { n.removeEventListener("keyup", r), n.removeEventListener("keydown", o), l && dv(e, _ == null ? void 0 : _.splitKey).forEach(function (a) { return l.removeHotkey(gv(a, _ == null ? void 0 : _.combinationKey, _ == null ? void 0 : _.description)) }) } } }, [e, _, h]), Pe } function Lc(L = !0) { return new Promise(I => { window.MODEBAR || (window.MODEBAR = window.document.getElementsByClassName("modebar-container")[0], window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`), window.MODEBAR && (window.MODEBAR.style.cssText.includes("display: none") || !L ? window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;` : window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:none;`, I(!0)) }) } function J9({ setModal: L, changeTheme: I, autoScaling: $, Loading: se, changeColor: Pe, downloadFinished: R }) { return _c("ctrl+shift+t", () => { L({ name: "titleDialog" }) }, { preventDefault: !0 }), _c("ctrl+t", () => { L({ name: "textDialog" }) }, { preventDefault: !0 }), _c("ctrl+o", () => { L({ name: "overlayChart" }) }, { preventDefault: !0 }), _c(["ctrl+shift+h", "ctrl+h"], () => { Lc() }, { preventDefault: !0 }), _c("ctrl+e", () => { Pe(!0) }, { preventDefault: !0 }), _c("ctrl+shift+s", async () => { L({ name: "downloadCsv" }), await s_(document.getElementById("plotlyChart"), R) }, { preventDefault: !0 }), _c("ctrl+s", async () => { Lc(), u_("MainChart", Lc, se, R) }, { preventDefault: !0 }), _c("ctrl+w", () => { window.close() }, { preventDefault: !0 }), { plotGlPixelRatio: 1, scrollZoom: !0, responsive: !0, displaylogo: !1, displayModeBar: !0, modeBarButtonsToRemove: ["lasso2d", "select2d", "downloadImage"], modeBarButtons: [[{ name: "Download CSV (Ctrl+Shift+S)", icon: qu.downloadCsv, click: async function (e) { await s_(e, R) } }, { name: "Download Chart as Image (Ctrl+S)", icon: qu.downloadImage, click: async function () { Lc(), await u_("MainChart", Lc, se, R) } }], [{ name: "Edit Color (Ctrl+E)", icon: qu.changeColor, click: function () { Pe(!0) } }, "drawline", "drawopenpath", "drawcircle", "drawrect", "eraseshape"], [{ name: "Overlay chart from CSV (Ctrl+O)", icon: qu.plotCsv, click: function () { L({ name: "overlayChart" }) } }, { name: "Add Text (Ctrl+T)", icon: qu.addText, click: function () { L({ name: "textDialog", data: { text: "" } }) } }, { name: "Change Titles (Ctrl+Shift+T)", icon: qu.changeTitle, click: function () { L({ name: "titleDialog" }) } }, { name: "Change Theme", icon: qu.sunIcon, click: function () { I(!0) } }], ["hoverClosestCartesian", "hoverCompareCartesian", "toggleSpikelines"], [{ name: "Auto Scale (Ctrl+Shift+A)", icon: ds.Icons.autoscale, click: function () { $(!0) } }, "zoomIn2d", "zoomOut2d", "autoScale2d", "zoom2d", "pan2d"]] } } async function Q9({ plotData: L, volumeBars: I, setMaximizePlot: $ }) { const se = Object.keys(L.layout).filter(i => i.startsWith("xaxis")).filter(i => L.layout[i].showticklabels || L.layout[i].matches === void 0), Pe = L.data.filter(i => { var _; return ((_ = i == null ? void 0 : i.name) == null ? void 0 : _.trim()) === "Volume" }), R = {}, E = I || { old_nticks: {} }, e = window.innerWidth, w = window.innerHeight, k = w > 420 && e < 920 ? 8 : w > 420 && e < 500 ? 9 : 7; return e < 750 ? (Pe.forEach(i => { i.type === "bar" && (i.opacity = 1, i.marker.line.width = .09, I.yaxis === void 0 && (E.yaxis = `yaxis${i.yaxis.replace("y", "")}`, R[`${E.yaxis}.tickfont.size`] = k, E.tickfont = L.layout[E.yaxis].tickfont || {}, L.layout.margin.l -= 40)) }), se.forEach(i => { var _; ((_ = I.old_nticks) == null ? void 0 : _[i]) === void 0 && (R[`${i}.nticks`] = 6, E.old_nticks[i] = L.layout[i].nticks || 10) }), $(!0), await Lc()) : window.MODEBAR.style.cssText.includes("display: none") && (await Lc(!1), $(!1), I.old_nticks !== void 0 && se.forEach(i => { I.old_nticks[i] !== void 0 && (R[`${i}.nticks`] = E.old_nticks[i], E.old_nticks[i] = void 0) }), I.yaxis !== void 0 && Pe.forEach(i => { i.type === "bar" && (i.opacity = .5, i.marker.line.width = .2, R[`${E.yaxis}.tickfont.size`] = E.tickfont.size + 3, L.layout.margin.l += 40, E.yaxis = void 0) })), { volume_update: E, layout_update: R, plotData: L } } const eM = j7(R7); function $5(L, I) { var se, Pe; I || (I = [(se = L[0]) == null ? void 0 : se.x[L[0].x.length - 1e3], (Pe = L[0]) == null ? void 0 : Pe.x[L[0].x.length - 1]]); const $ = []; return L.forEach(R => { const E = { ...R }, e = ["x", "y", "low", "high", "open", "close", "text", "customdata"], w = R.x ? R.x : [], k = []; for (let _ = 0; _ < w.length; _++)w[_] >= I[0] && w[_] <= I[1] && k.push(_); e.forEach(_ => { R[_] && Array.isArray(R[_]) && (E[_] = R[_].filter((y, h) => k.includes(h))) }), ["marker", "line"].forEach(_ => { var y; (y = R[_]) != null && y.color && Array.isArray(R[_].color) && (E[_] = { ...R[_] }, E[_].color = R[_].color.filter((h, l) => k.includes(l))) }), $.push(E) }), $.length === 0 ? L : $ } async function S_({ event: L, figure: I }) { var $, se; try { const Pe = I.data.filter(e => e.x !== void 0 && e.x.length > 0 && e.x[0] !== void 0); if (Pe.length === 0) return I; const R = L ? [L["xaxis.range[0]"], L["xaxis.range[1]"]] : [($ = Pe[0]) == null ? void 0 : $.x[Pe[0].x.length - 1e3], (se = Pe[0]) == null ? void 0 : se.x[Pe[0].x.length - 1]], E = $5(I.data, R); return I.data = E, I.layout.xaxis.range = R, I } catch (Pe) { console.log("error", Pe) } } function tM({ json: L, date: I, cmd: $, title: se, globals: Pe, theme: R, info: E }) {
- var De, Ee, Me, he; const e = P7(); In.useEffect(() => { e && e.capture("chart", E) }, []), L.layout.width = void 0, L.layout.height = void 0, (Ee = (De = L.layout) == null ? void 0 : De.title) != null && Ee.text && (L.layout.title.text = ""); const [w, k] = In.useState(L), [i, _] = In.useState({}), [y, h] = In.useState(!1), [l, t] = In.useState(se), [o, r] = In.useState({}), [n, a] = In.useState(!1), [s, u] = In.useState({ name: "" }), [p, c] = In.useState(!1), [b, g] = In.useState(null), [f, v] = In.useState({ old_nticks: {} }), [m, x] = In.useState(!1), [T, d] = In.useState(!1), [M, A] = In.useState(!1), [C, P] = In.useState(w), [O, z] = In.useState([]), [B, Y] = In.useState(!1), [D, G] = In.useState(!0), [V, j] = In.useState(!1), [W, Z] = In.useState(!1), [ie, te] = In.useState(!1), [J, X] = In.useState({}), [ee, re] = In.useState([]), [oe, ue] = In.useState([]), ce = () => u({ name: "" }); function de(fe) { var be, Ie, Te, Ne, qe; console.log("onDeleteAnnotation", fe); const ae = (Ie = (be = C == null ? void 0 : C.layout) == null ? void 0 : be.annotations) == null ? void 0 : Ie.findIndex(ve => ve.text === fe.text); console.log("index", ae), ae > -1 && ((Ne = (Te = C == null ? void 0 : C.layout) == null ? void 0 : Te.annotations) == null || Ne.splice(ae, 1), P({ ...C }), z((qe = C == null ? void 0 : C.layout) == null ? void 0 : qe.annotations)) } function me(fe) { V7({ plotData: C, popupData: fe, setPlotData: P, setModal: u, setOnAnnotationClick: X, setAnnotations: z, onAnnotationClick: J, ohlcAnnotation: ee, setOhlcAnnotation: re, annotations: O, plotDiv: b }) } In.useEffect(() => { T && (u({ name: "downloadFinished" }), d(!1)) }, [T]), In.useEffect(() => { o && Object.keys(o).length > 0 && (Object.keys(o).forEach(fe => { C.layout[fe].title = { ...C.layout[fe].title || {}, text: o[fe] }, C.layout[fe].showticklabels = !0 }), r({})) }, [o]); function _e(fe) { const ae = b.layout.shapes; if (!ae || ae.length === 0) return; const be = ae[ae.length - 1]; be.line.color = fe, ds.update(b, {}, { shapes: ae }) } function Se(fe, ae = !1) { const be = i[fe] || document.querySelector(`[data-title="${fe}"]`); ae ? (be.style.border = "transparent", be.style.boxShadow = "none") : (be.style.border = "1px solid rgba(0, 151, 222, 1.0)", be.style.borderRadius = "5px", be.style.borderpadding = "5px", be.style.boxShadow = "0 0 5px rgba(0, 151, 222, 1.0)"), _({ ...i, [fe]: be }) } function we() { const fe = "Auto Scale (Ctrl+Shift+A)", ae = i[fe] || document.querySelector(`[data-title="${fe}"]`); let be = !0; ae.style.border === "transparent" ? (b.removeAllListeners("plotly_relayout"), be = !1, b.on("plotly_relayout", Sh(async function (Ie) { if (Ie["xaxis.range[0]"] !== void 0) if (M) { const Te = { ...w }; await S_({ event: Ie, figure: Te }).then(async Ne => { P(Ne), ds.react(b, Ne.data, Ne.layout); const qe = await c_(Ie, b); ue(qe.yaxis_fixedrange), ds.update(b, {}, qe.to_update) }) } else { const Te = await c_(Ie, b); ue(Te.yaxis_fixedrange), ds.update(b, {}, Te.to_update) } }, 100))) : (b.removeAllListeners("plotly_relayout"), oe.forEach(Ie => { b.layout[Ie].fixedrange = !1 }), ue([]), M && b.on("plotly_relayout", Sh(async function (Ie) { if (Ie["xaxis.range[0]"] === void 0) return; const Te = { ...w }; await S_({ event: Ie, figure: Te }).then(async Ne => { P(Ne), ds.react(b, Ne.data, Ne.layout) }) }, 100))), Se(fe, be) } function xe() { const fe = "Edit Color (Ctrl+E)", ae = i[fe] || document.querySelector(`[data-title="${fe}"]`); let be = !0; ae.style.border === "transparent" && (be = !1), te(!be), Se(fe, be) } return In.useEffect(() => { V && (console.log("activateAutoScale", !V), we(), j(!1)) }, [V]), In.useEffect(() => { W && (xe(), Z(!1)) }, [W]), In.useEffect(() => { if (B) try { console.log("changeTheme", B); const fe = C == null ? void 0 : C.data.filter(qe => { var ve; return ((ve = qe == null ? void 0 : qe.name) == null ? void 0 : ve.trim()) === "Volume" }), ae = !D; window.document.body.style.backgroundColor = ae ? "#000" : "#fff", C.layout.font = { ...C.layout.font || {}, color: ae ? "#fff" : "#000" }; const be = ae ? qu.sunIcon : qu.moonIcon; document.querySelector('[data-title="Change Theme"]').getElementsByTagName("path")[0].setAttribute("d", be.path), document.querySelector('[data-title="Change Theme"]').getElementsByTagName("svg")[0].setAttribute("viewBox", be.viewBox); const Ne = ae ? { "#009600": "#00ACFF", "#c80000": "#e4003a" } : { "#e4003a": "#c80000", "#00ACFF": "#009600" }; fe.forEach(qe => { qe.type === "bar" && Array.isArray(qe.marker.color) && (qe.marker.color = qe.marker.color.map(ve => Ne[ve] || ve)) }), C.layout.template = ae ? $7 : K7, P({ ...C }), ds.react(b, C.data, C.layout), G(ae), Y(!1) } catch (fe) { console.log("error", fe) } }, [B]), In.useEffect(() => {
+ var De, Ee, Me, he; const e = P7(); In.useEffect(() => { e && e.capture("chart", E) }, []), L.layout.width = void 0, L.layout.height = void 0, (Ee = (De = L.layout) == null ? void 0 : De.title) != null && Ee.text && (L.layout.title.text = ""); const [w, k] = In.useState(L), [i, _] = In.useState({}), [y, h] = In.useState(!1), [l, t] = In.useState(se), [o, r] = In.useState({}), [n, a] = In.useState(!1), [s, u] = In.useState({ name: "" }), [p, c] = In.useState(!1), [b, g] = In.useState(null), [f, v] = In.useState({ old_nticks: {} }), [m, x] = In.useState(!1), [T, d] = In.useState(!1), [M, A] = In.useState(!1), [C, P] = In.useState(w), [O, z] = In.useState([]), [B, Y] = In.useState(!1), [D, G] = In.useState(!0), [V, j] = In.useState(!1), [W, Z] = In.useState(!1), [ie, te] = In.useState(!1), [J, X] = In.useState({}), [ee, re] = In.useState([]), [oe, ue] = In.useState([]), ce = () => u({ name: "" }); function de(fe) { var be, Ie, Te, Ne, qe; console.log("onDeleteAnnotation", fe); const ae = (Ie = (be = C == null ? void 0 : C.layout) == null ? void 0 : be.annotations) == null ? void 0 : Ie.findIndex(ve => ve.text === fe.text); console.log("index", ae), ae > -1 && ((Ne = (Te = C == null ? void 0 : C.layout) == null ? void 0 : Te.annotations) == null || Ne.splice(ae, 1), P({ ...C }), z((qe = C == null ? void 0 : C.layout) == null ? void 0 : qe.annotations)) } function me(fe) { V7({ plotData: C, popupData: fe, setPlotData: P, setModal: u, setOnAnnotationClick: X, setAnnotations: z, onAnnotationClick: J, ohlcAnnotation: ee, setOhlcAnnotation: re, annotations: O, plotDiv: b }) } In.useEffect(() => { T && (u({ name: "downloadFinished" }), d(!1)) }, [T]), In.useEffect(() => { o && Object.keys(o).length > 0 && (Object.keys(o).forEach(fe => { C.layout[fe].title = { ...C.layout[fe].title || {}, text: o[fe] }, C.layout[fe].showticklabels = !0 }), r({})) }, [o]); function _e(fe) { const ae = b.layout.shapes; if (!ae || ae.length === 0) return; const be = ae[ae.length - 1]; be.line.color = fe, ds.update(b, {}, { shapes: ae }) } function Se(fe, ae = !1) { const be = i[fe] || document.querySelector(`[data-title="${fe}"]`); ae ? (be.style.border = "transparent", be.style.boxShadow = "none") : (be.style.border = "1px solid rgba(0, 151, 222, 1.0)", be.style.borderRadius = "5px", be.style.borderpadding = "5px", be.style.boxShadow = "0 0 5px rgba(0, 151, 222, 1.0)"), _({ ...i, [fe]: be }) } function we() { const fe = "Auto Scale (Ctrl+Shift+A)", ae = i[fe] || document.querySelector(`[data-title="${fe}"]`); let be = !0; ae.style.border === "transparent" ? (b.removeAllListeners("plotly_relayout"), be = !1, b.on("plotly_relayout", Sh(async function (Ie) { if (Ie["xaxis.range[0]"] !== void 0) if (M) { const Te = { ...w }; await S_({ event: Ie, figure: Te }).then(async Ne => { P(Ne), ds.react(b, Ne.data, Ne.layout); const qe = await c_(Ie, b); ue(qe.yaxis_fixedrange), ds.update(b, {}, qe.to_update) }) } else { const Te = await c_(Ie, b); ue(Te.yaxis_fixedrange), ds.update(b, {}, Te.to_update) } }, 100))) : (b.removeAllListeners("plotly_relayout"), oe.forEach(Ie => { b.layout[Ie].fixedrange = !1 }), ue([]), M && b.on("plotly_relayout", Sh(async function (Ie) { if (Ie["xaxis.range[0]"] === void 0) return; const Te = { ...w }; await S_({ event: Ie, figure: Te }).then(async Ne => { P(Ne), ds.react(b, Ne.data, Ne.layout) }) }, 100))), Se(fe, be) } function xe() { const fe = "Edit Color (Ctrl+E)", ae = i[fe] || document.querySelector(`[data-title="${fe}"]`); let be = !0; ae.style.border === "transparent" && (be = !1), te(!be), Se(fe, be) } return In.useEffect(() => { V && (console.log("activateAutoScale", !V), we(), j(!1)) }, [V]), In.useEffect(() => { W && (xe(), Z(!1)) }, [W]), In.useEffect(() => { if (B) try { console.log("changeTheme", B); const fe = C == null ? void 0 : C.data.filter(qe => { var ve; return ((ve = qe == null ? void 0 : qe.name) == null ? void 0 : ve.trim()) === "Volume" }), ae = !D; window.document.body.style.backgroundColor = ae ? "#000" : "#fff", C.layout.font = { ...C.layout.font || {}, color: ae ? "#fff" : "#000" }; const be = ae ? qu.sunIcon : qu.moonIcon; document.querySelector('[data-title="Change Theme"]').getElementsByTagName("path")[0].setAttribute("d", be.path), document.querySelector('[data-title="Change Theme"]').getElementsByTagName("svg")[0].setAttribute("viewBox", be.viewBox); const Ne = ae ? { "#00ACFF": "#00ACFF", "#c80000": "#e4003a" } : { "#e4003a": "#c80000", "#00ACFF": "#00ACFF" }; fe.forEach(qe => { qe.type === "bar" && Array.isArray(qe.marker.color) && (qe.marker.color = qe.marker.color.map(ve => Ne[ve] || ve)) }), C.layout.template = ae ? $7 : K7, P({ ...C }), ds.react(b, C.data, C.layout), G(ae), Y(!1) } catch (fe) { console.log("error", fe) } }, [B]), In.useEffect(() => {
var fe, ae, be, Ie; if (n) {
G(!0), j(!1); const Te = ["Download CSV", "Download Chart as Image", "Overlay chart from CSV", "Add Text", "Change Titles", "Auto Scale (Ctrl+Shift+A)", "Reset Axes"], Ne = document.querySelector('[data-title="Autoscale"]'); Ne && (Ne.getElementsByTagName("path")[0].setAttribute("d", ds.Icons.home.path), Ne.setAttribute("data-title", "Reset Axes")), window.MODEBAR = document.getElementsByClassName("modebar-container")[0]; const qe = window.MODEBAR.getElementsByClassName("modebar-btn"); if (window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`, qe) { const Ae = {}; for (let ze = 0; ze < qe.length; ze++) { const Ue = qe[ze]; Te.includes(Ue.getAttribute("data-title")) && Ue.classList.add("ph-capture"), Ue.style.border = "transparent", Ae[Ue.getAttribute("data-title")] = Ue } _(Ae) } if (((ae = (fe = C == null ? void 0 : C.layout) == null ? void 0 : fe.yaxis) == null ? void 0 : ae.type) !== void 0 && (C.layout.yaxis.type === "log" && !y && (console.log("yaxis.type changed to log"), h(!0)), C.layout.yaxis.type === "linear" && y)) { console.log("yaxis.type changed to linear"), h(!1); const Ae = { "yaxis.exponentformat": "none", "yaxis.tickformat": null, "yaxis.exponentbase": 10 }; ds.update(b, {}, Ae) } if (window.export_image !== void 0) { const Ae = window.export_image.split("/").pop(), ze = Ae.split(".").pop().replace("jpg", "jpeg"); if (["jpeg", "png", "svg", "pdf"].includes(ze)) return Sh(async function () { await Lc(), await M5("MainChart", Ae.split(".")[0], ze) }, 2)() } window.addEventListener("resize", async function () { const Ae = await Q9({ plotData: C, volumeBars: f, setMaximizePlot: x }), ze = Ae.layout_update, Ue = Ae.plotData, Ve = Ae.volume_update; Object.keys(ze).length > 0 && (P(Ue), v(Ve), ds.relayout(b, ze)) }), R !== "dark" && Y(!0); const ve = w.data.map(Ae => Ae.type === "candlestick"); if (((be = w.data[0]) == null ? void 0 : be.x) !== void 0 && ((Ie = w.data[0]) == null ? void 0 : Ie.x.length) <= 1e3 || !ve.includes(!0)) return; u({
name: "alertDialog", data: {
@@ -4082,7 +4082,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
+
+
-
+
-
-
-
-
-
-
-
-
-