Merge remote-tracking branch 'OpenBB-finance/develop' into feature/argparse_translator

This commit is contained in:
hjoaquim
2023-12-19 07:41:10 +00:00
4688 changed files with 1404810 additions and 559512 deletions

45
.codespell.ignore vendored Normal file
View File

@@ -0,0 +1,45 @@
ake
alle
ba
brin
buda
buidl
buil
certi
commun
coo
creat
datas
ether
fo
gard
hel
hist
hsi
ist
jewl
ket
mape
mis
navagation
nd
nwe
operatio
ot
parana
pres
ro
ser
shold
shs
statics
te
toke
vai
varian
vie
welp
wew
yeld
zar
zlot

16
.codespell.skip vendored Normal file
View File

@@ -0,0 +1,16 @@
./tests
.git
*.css
*.csv
*.html
*.ini
*.ipynb
*.js
*.json
*.lock
*.scss
*.txt
*.yaml
./build/pyinstaller
./website/config.toml
./**/node_modules

2
.coveragerc vendored
View File

@@ -2,4 +2,6 @@
omit =
env/*
tests/*
**/tests/**
**/package/**
source = .

View File

@@ -1,31 +1,80 @@
# Description
# Pull Request OpenBB
- [ ] Summary of the change / bug fix.
## The OpenBBTerminal
<details>
<summary> Pull Request for the OpenBBTerminal</summary>
### Description
- [ ] Summary of the change/ bug fix.
- [ ] Link # issue, if applicable.
- [ ] Screenshot of the feature or the bug before/after fix, if applicable.
- [ ] Relevant motivation and context.
- [ ] List any dependencies that are required for this change.
### How has this been tested?
# How has this been tested?
- Please describe the tests that you ran to verify your changes.
- Please provide instructions so we can reproduce.
- Please also list any relevant details for your test configuration.
* Please describe the tests that you ran to verify your changes.
* Provide instructions so we can reproduce.
* Please also list any relevant details for your test configuration.
- [ ] Make sure affected commands still run in terminal
- [ ] Ensure the SDK still works
- [ ] Check any related reports
- [ ] Ensure the affected commands still execute in the OpenBB Terminal.
- [ ] Ensure the Platform (previously named SDK) is working as intended.
- [ ] Check any related reports.
### Checklist
# Checklist:
- [ ] I ensure I have self-reviewed my code.
- [ ] I have commented/documented my code, particularly in hard-to-understand sections.
- [ ] I have adhered to the GitFlow naming convention and my branch name is in the format of `feature/feature-name` or `hotfix/hotfix-name`.
- [ ] Update [our documentation](https://openbb-finance.github.io/OpenBBTerminal/) following [these guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/tree/main/website). Update any user guides that are affected by the changes.
- [ ] Update our tests following [these guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/tree/main/tests).
- [ ] Make sure you are following our [CONTRIBUTING guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/blob/main/CONTRIBUTING.md).
- [ ] If a feature was added make sure to add it to the corresponding [integration test script](https://github.com/OpenBB-finance/OpenBBTerminal/tree/develop/openbb_terminal/miscellaneous/integration_tests_scripts).
</details>
## The OpenBB Platform
<details>
<summary> Pull Request for the OpenBB Platform</summary>
### Description
- [ ] Summary of the change/ bug fix.
- [ ] Link # issue, if applicable.
- [ ] Screenshot of the feature or the bug before/after fix, if applicable.
- [ ] Relevant motivation and context.
- [ ] List any dependencies that are required for this change.
### How has this been tested?
- Please describe the tests that you ran to verify your changes.
- Please provide instructions so we can reproduce.
- Please also list any relevant details for your test configuration.
- [ ] Ensure all unit and integration tests pass.
- If you modified/added command(s):
- [ ] Ensure the command(s) execute with the expected output.
- [ ] API.
- [ ] Python Interface.
- [ ] If applicable, please add new tests for the command (see [CONTRIBUTING.md](/openbb_platform/CONTRIBUTING.md) to leverage semi-automated testing).
- If a new provider was introduced or a new fetcher was added to an existing provider:
- [ ] Ensure the existing tests pass.
- [ ] Ensure the new provider and/or fetcher is stable and usable.
- [ ] If applicable, please add new tests for the provider and/or fetcher (see [CONTRIBUTING.md](/openbb_platform/CONTRIBUTING.md) to leverage semi-automated testing).
- If a new provider or extension was added:
- [ ] Update the list of [Extensions](/openbb_platform/EXTENSIONS.md).
- [ ] Update the list of [Providers](/openbb_platform/PROVIDERS.md).
- [ ] If it's a community extension or provider, update the [integration tests GitHub Action workflow](/.github/workflows/platform-api-integration-test.yml).
### Checklist
# Others
- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, particularly in hard-to-understand areas.
- [ ] I have adhered to the GitFlow naming convention and my branch name is in the format of `feature/feature-name` or `hotfix/hotfix-name`.
- [ ] I ensure that I am following the [CONTRIBUTING guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/blob/main/CONTRIBUTING.md).
- [ ] (If applicable) I have updated tests following [these guidelines](/openbb_platform/CONTRIBUTING.md#qa-your-extension).
</details>

View File

@@ -26,12 +26,12 @@ jobs:
echo "Source-branch=$source_branch" >> $GITHUB_OUTPUT
echo "target-branch=$target_branch" >> $GITHUB_OUTPUT
- name: Show Output result for source-branch and target-branch
run: |
echo "source-branch=${{ steps.branch-name-check.outputs.source-branch }}"
echo "target-branch=${{ steps.branch-name-check.outputs.target-branch }}"
- name: Check branch name for develop PRs
if: ${{ steps.branch-name-check.outputs.target-branch == 'develop' }}
run: |
@@ -50,4 +50,4 @@ jobs:
else
echo "PR is not from a hotfix or release branch. Pull requests must be from a hotfix or release branch and target the main branch"
exit 1
fi
fi

View File

@@ -10,8 +10,7 @@ env:
on:
push:
branches:
- release/*
- main
- release/v3/*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -5,7 +5,7 @@ on: workflow_dispatch
jobs:
publish-image:
runs-on: ubuntu-latest
env:
env:
OPENBBTERMINAL_DOCKER_GITHUB_REPOSITORY: ${{ github.repository }}
OPENBB_LOGGING_APP_NAME: "gst_docker"
@@ -45,4 +45,4 @@ jobs:
- name: Publishing the latest Docker image
run: |
docker push "${OPENBBTERMINAL_DOCKER_POETRY_IMAGE_LATEST}"
if: startsWith(github.ref, 'refs/heads/main')
if: startsWith(github.ref, 'refs/heads/main')

View File

@@ -6,9 +6,10 @@ on:
- main
- release/*
pull_request:
types: [closed]
branches:
- develop
types: [opened, synchronize, reopened, closed]
branches:
- develop
- 'docs/*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -64,7 +65,13 @@ jobs:
source $VENV
pip uninstall Brotli -y
pytest tests/website --autodoc
python website/generate_sdk_markdown.py && python website/generate_terminal_markdown.py
python website/generate_sdk_v3_markdown.py
python website/generate_terminal_v3_markdown.py
pip install -U poetry
python openbb_platform/dev_install.py -e all
pip uninstall nbmake -y
python website/generate_platform_v4_markdown.py
python website/generate_widgets_library.py
- name: Setup Node
uses: actions/setup-node@v3
@@ -90,7 +97,7 @@ jobs:
(github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'develop' &&
startsWith(github.head_ref, 'docs/'))
startsWith(github.head_ref, 'docs/'))
with:
github_token: ${{ secrets.DEPLOY_TOKEN }}
publish_dir: ./website/build

View File

@@ -10,7 +10,7 @@ jobs:
with:
mode: minimum
count: 1
labels: "guides, bug, build, do not merge, docker, docs, duplicate, enhancement, feat XS, feat S, feat M, feat L, feat XL, help wanted, refactor, tests, discord, dependencies, notebookreports, dashboards, analytics, portfolio, installer, SDK, release "
labels: "guides, bug, build, docker, docs, feat XS, feat S, feat M, feat L, feat XL, help wanted, refactor, tests, dependencies, release"
- uses: mheap/github-action-required-labels@v1
with:
mode: exactly

View File

@@ -30,6 +30,12 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # actions/checkout v3.0.2
with:
ref: ${{ github.head_ref }}
fetch-depth: 20
- name: Fetch base branch
run: git fetch --no-tags --depth=20 origin ${{ github.base_ref }}
- name: Setup Python 3.9
uses: actions/setup-python@v4
@@ -37,6 +43,15 @@ jobs:
python-version: "3.9"
architecture: x64
- name: Get changed files in openbb_terminal and openbb_platform for PR
if: github.event_name == 'pull_request'
run: |
# "Checking PR diff"
echo "terminal_files=$(git diff --name-only origin/${{ github.base_ref }}...${{ github.head_ref }} | grep 'openbb_terminal/.*\.py$' | xargs)" >> $GITHUB_ENV
echo $terminal_files
echo "platform_files=$(git diff --name-only origin/${{ github.base_ref }}...${{ github.head_ref }} | grep 'openbb_platform/.*\.py$' | grep -v 'openbb_platform/openbb/package' | grep -v 'integration' | grep -v 'tests' | xargs)" >> $GITHUB_ENV
echo $platform_files
- uses: actions/cache@v3
with:
path: ~/.cache/pip
@@ -44,14 +59,28 @@ jobs:
restore-keys: ${{ runner.os }}-linting-${{ hashFiles('**/poetry.lock') }}
- run: |
pip install bandit black codespell mypy==1.1.1 pylint==2.17.0 ruff==0.0.256
pip install bandit black codespell==2.2.5 mypy==1.5.1 pylint==2.17.0 ruff==0.0.285
pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil types-setuptools types-six
- run: bandit -x ./tests -r . || true
- run: black --diff --check .
- run: codespell --ignore-words-list=vai,mis,gard,commun,parana,statics,ro,zar,zlot,jewl,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist,varian,datas,ake,creat,vie,hel,ket,toke,certi,buidl,ot,te,buda,shs,welp --quiet-level=2 --skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml,./build/pyinstaller,./website/config.toml -x openbb_terminal/economy/fedreserve_model.py
- run: codespell --ignore-words=.codespell.ignore --skip="$(tr '\n' ',' < .codespell.skip | sed 's/,$//')" --quiet-level=2
- run: ruff .
- run: mypy --ignore-missing-imports openbb_terminal
- run: pylint terminal.py openbb_terminal tests
- run: |
# Run linters for openbb_terminal
if [ -n "${{ env.terminal_files }}" ]; then
mypy --ignore-missing-imports ${{ env.terminal_files }}
pylint terminal.py ${{ env.terminal_files }}
else
echo "No Python files changed in openbb_terminal"
fi
- run: |
# Run linters for openbb_platform
if [ -n "${{ env.platform_files }}" ]; then
# Add mypy to this part of the linting workflow once we're ready
pylint ${{ env.platform_files }}
else
echo "No Python files changed in openbb_platform"
fi
markdown-link-check:
name: Markdown Linting

View File

@@ -53,3 +53,12 @@ jobs:
with:
workflow: pypi-nightly.yml
token: ${{ secrets.GITHUB_TOKEN }}
trigger-api-integration-test:
runs-on: ubuntu-latest
steps:
- name: Trigger Platform API Integration Test
uses: aurelien-baudet/workflow-dispatch@v2
with:
workflow: platform-api-integration-test.yml
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,120 @@
name: API Integration Tests
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
api-integration-tests:
runs-on: ubuntu-latest
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
OPENBB_API_AUTH: true
OPENBB_API_USERNAME: "USER"
OPENBB_API_PASSWORD: "PASS"
steps:
- name: Slack Notification - Starting
uses: act10ns/slack@v1
with:
status: starting
channel: "#workflows"
message: Starting API Integration Test...
if: always()
- name: Checkout code
uses: actions/checkout@v3
- name: Checkout specific ref
if: github.event_name == 'pull_request'
run: git fetch origin ${{ github.event.pull_request.head.ref }} && git checkout FETCH_HEAD
- name: Setup Python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
architecture: x64
- name: Load cached venv
id: cached-python-environment
uses: actions/cache@v3
with:
path: runner_env
key: runner_env-${{ runner.os }}-v1-${{ hashFiles('**/poetry.lock', '**/dev_install.py') }}
- name: Install Dependencies
if: steps.cached-python-environment.outputs.cache-hit != 'true'
run: |
python -m venv runner_env
source runner_env/bin/activate
pip install poetry toml
python openbb_platform/dev_install.py -e all
- name: Populate System Setting Files
run: |
mkdir -p ~/.openbb_platform
touch ~/.openbb_platform/system_settings.json
echo '{"log_collect":false}' > ~/.openbb_platform/system_settings.json
cat ~/.openbb_platform/system_settings.json
- name: Create and Populate user_settings.json from GitHub Secrets
run: |
touch ~/.openbb_platform/user_settings.json
echo '{
"credentials": {
"benzinga_api_key": "${{ secrets.BENZINGA_API_KEY }}",
"fmp_api_key": "${{ secrets.FMP_API_KEY }}",
"polygon_api_key": "${{ secrets.POLYGON_API_KEY }}",
"alpha_vantage_api_key": "${{ secrets.ALPHA_VANTAGE_API_KEY }}",
"fred_api_key": "${{ secrets.FRED_API_KEY }}",
"intrinio_api_key": "${{ secrets.INTRINIO_API_KEY }}",
"tradingeconomics_api_key": "${{ secrets.TRADINGECONOMICS_API_KEY }}",
"quandl_api_key": "${{ secrets.QUANDL_API_KEY }}",
"biztoc_api_key": "${{ secrets.BIZTOC_API_KEY }}",
"nasdaq_api_key": "${{ secrets.NASDAQ_API_KEY }}",
"tiingo_token": "${{ secrets.TIINGO_TOKEN }}"
}
}' > ~/.openbb_platform/user_settings.json
- name: Launch the Uvicorn Process
run: |
source runner_env/bin/activate
uvicorn openbb_core.api.rest_api:app --host 0.0.0.0 --port 8000 --reload &
- name: Wait for 42 seconds
run: sleep 42
- name: Run API Integration Tests
run: |
source runner_env/bin/activate
pip list
cd openbb_platform
pytest -m integration
- name: Cache pip packages
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: runner_env-${{ runner.os }}-v1-${{ hashFiles('**/poetry.lock', '**/dev_install.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Upload summary to Slack
uses: adrey/slack-file-upload-action@master
with:
token: ${{ secrets.SLACK_API_TOKEN }}
initial_comment: "API Integration test summary"
title: "Integration test summary"
path: summary.txt
channel: ${{ secrets.SLACK_CHANNEL_ID }}
- name: Slack Notification - Success/Failure
uses: act10ns/slack@v1
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: "#workflows"
if: always()

46
.github/workflows/platform-core.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Test Platform V4
on:
pull_request:
branches:
- develop
paths:
- 'openbb_platform/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
unit_tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python_version:
["3.9", "3.10", "3.11"]
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Python ${{ matrix.python_version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python_version }}
allow-prereleases: true
cache: "pip"
- name: Cache pip packages
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('openbb_platform/poetry.lock') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Run tests
run: |
pip install nox
nox -s tests --python ${{ matrix.python_version }}

View File

@@ -12,37 +12,52 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
- uses: actions/checkout@v3
- name: Setup Python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Update version in pyproject.toml and Edit docs
run: |
sed -i 's/name = ".*"/name = "openbb-nightly"/' pyproject.toml
sed -i "3s/version = \"\(.*\)\"/version = \"\1.dev$(date +%Y%m%d)\"/" pyproject.toml
head pyproject.toml
sed -i 's/pip install openbb/pip install openbb-nightly/g; s/from openbb_terminal.sdk import openbb/from openbb_terminal.sdk import openbb/g' ./website/pypi.md
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: OpenBBTerminal - Update version in pyproject.toml and Edit docs
run: |
sed -i 's/name = ".*"/name = "openbb-terminal-nightly"/' pyproject.toml
sed -i "3s/version = \"\(.*\)\"/version = \"\1.dev$(date +%Y%m%d%H%M)\"/" pyproject.toml
sed -i 's|pip install openbb-terminal|pip install openbb-terminal-nightly|g' ./website/pypi.md
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: OpenBBTerminal - Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: OpenBBTerminal - Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
password: ${{ secrets.NIGHTLY_PYPI_API_TOKEN_OBB_TERMINAL_NIGHTLY }}
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.NIGHTLY_PYPI_API_TOKEN }}
- name: OpenBB Platform - Update version in pyproject.toml and Edit docs
run: |
sed -i 's/name = ".*"/name = "openbb-nightly"/' openbb_platform/pyproject.toml
sed -i "3s/version = \"\(.*\)\"/version = \"\1.dev$(date +%Y%m%d%H%M)\"/" openbb_platform/pyproject.toml
- name: OpenBB Platform - Create the dinamically generated wheel
run: |
python -m pip install poetry toml
python build/pypi/openbb_platform/nightly.py
- name: OpenBB Platform - Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: openbb_platform/dist/
password: ${{ secrets.NIGHTLY_PYPI_API_TOKEN }}

View File

@@ -3,8 +3,7 @@ name: Deploy to PyPI
on:
push:
branches:
- release/*
- main
- release/v3/*
workflow_dispatch:

View File

@@ -0,0 +1,68 @@
name: Deploy the OpenBB Platform and the OpenBBTerminal to Test PyPI
on:
push:
branches:
- feature/v4-pypi
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-test-pypi:
name: Build and publish 📦 to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: OpenBBTerminal - Update version in pyproject.toml and Edit docs
run: |
sed -i 's/name = ".*"/name = "openbb-terminal-nightly"/' pyproject.toml
sed -i "3s/version = \"\(.*\)\"/version = \"\1.dev$(date +%Y%m%d%H%M)\"/" pyproject.toml
sed -i 's|pip install openbb-terminal|pip install openbb-terminal-nightly|g' ./website/pypi.md
- name: OpenBBTerminal - Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: OpenBBTerminal - Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
password: ${{ secrets.TEST_PYPI_API_TOKEN_OBB_TERMINAL_NIGHTLY }}
repository-url: https://test.pypi.org/legacy/
- name: OpenBB Platform - Update version in pyproject.toml and Edit docs
run: |
sed -i 's/name = ".*"/name = "openbb-nightly"/' openbb_platform/pyproject.toml
sed -i "3s/version = \"\(.*\)\"/version = \"\1.dev$(date +%Y%m%d%H%M)\"/" openbb_platform/pyproject.toml
- name: OpenBB Platform - Create the dinamically generated wheel
run: |
python -m pip install poetry toml
python build/pypi/openbb_platform/nightly.py
- name: OpenBB Platform - Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: openbb_platform/dist/
password: ${{ secrets.TEST_PYPI_API_TOKEN_OBB_PLATFORM_NIGHTLY }}
repository-url: https://test.pypi.org/legacy/

10
.gitignore vendored
View File

@@ -76,7 +76,11 @@ cache/
# lightning logs
lightning_logs/
# user script to install all extensions and providers
!openbb_sdk/install_all.sh
openbb_sdk/sdk/core/tests/app/model/charts/test_chart_settings.py
# testing artifacts
*/mocked_path
# CA certificates
*.pem
# Platform auto generated files
openbb_platform/openbb/package/*

View File

@@ -14,19 +14,20 @@ repos:
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.0.256"
rev: "v0.0.285"
hooks:
- id: ruff
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
additional_dependencies: [tomli]
name: pydocstyle
entry: pydocstyle
language: python
types: [python]
files: '^openbb_sdk/.*\.py$'
exclude: 'tests/.*\.py|openbb_sdk/test_.*\.py'
files: '^openbb_platform/.*\.py$'
exclude: 'tests/.*\.py|openbb_platform/test_.*\.py'
args: ["--config=pyproject.toml"]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.5
@@ -35,9 +36,9 @@ repos:
entry: codespell
args:
[
"--ignore-words-list=VAI,MIS,shs,gard,te,commun,parana,ro,zar,vie,hel,jewl,zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist,varian,datas,ake,creat,statics,ket,toke,certi,buidl,ot",
"--ignore-words-list=VAI,MIS,shs,gard,te,commun,parana,ro,zar,vie,hel,jewl,zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist,varian,datas,ake,creat,statics,ket,toke,certi,buidl,ot,fo",
"--quiet-level=2",
"--skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml,build/pyinstaller/*,./website/config.toml",
"--skip=./**/tests/**,./**/test_*.py,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml,build/pyinstaller/*,./website/config.toml",
"-x=openbb_terminal/economy/fedreserve_model.py",
"-x=.github/workflows/linting.yml"
]
@@ -72,7 +73,7 @@ repos:
"--baseline",
".secrets.baseline",
"--exclude-files",
"cassettes/.*|record/.*|website/content/api/.*|openbb_sdk/extensions/charting/openbb_charting/infrastructure/assets/.*\\.js|openbb_sdk/extensions/charting/openbb_charting/infrastructure/.*\\.html",
"cassettes/.*|record/.*|website/content/api/.*|openbb_platform/extensions/charting/openbb_charting/infrastructure/assets/.*\\.js|openbb_platform/extensions/charting/openbb_charting/infrastructure/.*\\.html",
]
exclude: package.lock.json
- repo: local

11
.secrets.baseline vendored
View File

@@ -115,16 +115,21 @@
{
"path": "detect_secrets.filters.regex.should_exclude_line",
"pattern": [
".*example.*"
".*example.*",
".*_api_key.*"
]
},
{
"path": "detect_secrets.filters.regex.should_exclude_secret",
"pattern": [
"example"
"example",
"REPLACE_ME",
"my_email",
"my_password",
"my_pat"
]
}
],
"results": {},
"generated_at": "2022-11-16T18:07:51Z"
"generated_at": "2023-12-14T18:07:51Z"
}

39
CONTRIBUTING.md vendored
View File

@@ -13,7 +13,8 @@ Use your best judgment, and feel free to propose changes to this document in a p
- [Data sources](#data-sources)
- [View](#view)
- [Controller](#controller)
- [Add SDK endpoint](#add-sdk-endpoint)
- [Add SDK endpoint (V3)](#add-sdk-endpoint-v3)
- [Add OpenBB Platform endpoint (V4)](#add-openbb-platform-endpoint-v4)
- [Add Unit Tests](#add-unit-tests)
- [Open a Pull Request](#open-a-pull-request)
- [Review Process](#review-process)
@@ -134,7 +135,7 @@ def get_price_targets(cls, symbol: str) -> pd.DataFrame:
url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}"
response = request(url)
# Check if response is valid
# Check if the response is valid
if response.status_code != 200 or "Error Message" in response.json():
message = f"Error, Status Code: {response.status_code}."
message = (
@@ -222,7 +223,7 @@ Now that we have added the model function getting, we need to specify that this
If you are adding a new function with a new data source, make a new value in the file. If the data source requires an
API key, please refer to the guide below for adding them. Instructions for obtaining the new api key
should be included in the file `OpenBBTerminal/website/content/terminal/usage/guides/api-keys.md`.
should be included in the file `OpenBBTerminal/website/content/terminal/usage/data/api-keys.md`.
### View
@@ -344,7 +345,7 @@ Now that we have the model and views, it is time to add to the controller.
- Call the function contained in a `_view.py` file with the arguments parsed by argparse.
Note that the function self.parse_known_args_and_warn() has some additional options we can add. If the function is showing a chart, but we want the option to show raw data, we can add the `raw=True` keyword and the resulting namespace will have the `raw` attribute.
Same with limit, we can pass limit=10 to add the `-l` flag with default=10. Here we also specify the export, and whether it is data only, plots only or anything. This function also adds the `source` attribute to the namespace. In our example, this is important because we added an additional source.
Same with limit, we can pass limit=10 to add the `-l` flag with default=10. Here we also specify the export, and whether it is data only, plots only or anything else. This function also adds the `source` attribute to the namespace. In our example, this is important because we added an additional source.
Our new function will be:
@@ -452,7 +453,7 @@ The **import only occurs inside this menu call**, this is so that the loading ti
In addition, note the `self.load_class` which allows to not create a new `FundamentalAnalysisController` instance but re-load the previous created one. Unless the arguments `self.ticker`, `self.start` or `self.stock` have changed since. The `self.queue` list of commands is passed around as it contains the commands that the terminal must perform.
### Add SDK endpoint
### Add SDK endpoint (V3)
In order to add a command to the SDK, follow these steps:
@@ -508,13 +509,17 @@ In order to add a command to the SDK, follow these steps:
python generate_sdk.py sort
```
### Add OpenBB Platform endpoint (V4)
Refer to the documentation [here](https://docs.openbb.co/platform/development).
### Add Unit Tests
This is a vital part of the contribution process. We have a set of unit tests that are run on every Pull Request. These tests are located in the `tests` folder.
Unit tests minimize errors in code and quickly find errors when they do arise. Integration tests are standard usage examples, which are also used to identify errors.
A thorough introduction on the usage of unit tests and integration tests in OpenBBTerminal can be found on the following page respectively:
A thorough introduction on the usage of unit tests and integration tests in OpenBBTerminal can be found on the following page:
[Unit Test README](tests/README.md)
@@ -524,7 +529,7 @@ Any new features that do not contain unit tests will not be accepted.
### Open a Pull Request
For starters, you should ensure that you branch is up to date with the `develop` branch. To do that, one can run the following commands:
For starters, you should ensure that your branch is up to date with the `develop` branch. To do that, one can run the following commands:
```bash
git fetch upstream
@@ -573,7 +578,7 @@ CLI :computer: → `_controller.py` :robot: →&nbsp;`_view.py` :art: &nbsp;&nbs
| **File&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;** | **Role** | **Description** |
| :------------------------- | :------------- | :----------------------------------------------------- |
| **_controller.py** :robot: | The router/input validator | The controller file should hold the least amount of logic possible. Its role is to be a stupid (no logic) router and redirect the command correctly while checking the input with argparser. |
| **_view.py** :art: | The artist | The view file should only output or visualise the data it gets from the `_model` file! The `_view` can limit the data coming from the `_model`, otherwise the data object should be identical in the `_view` and the `_model` files. |
| **_view.py** :art: | The artist | The view file should only output or visualize the data it gets from the `_model` file! The `_view` can limit the data coming from the `_model`, otherwise the data object should be identical in the `_view` and the `_model` files. |
| **_model.py** 🧠 |The brain | The model file is where everything fun happens. The data is gathered (external APIs), processed and returned here. |
| **sdk.py** 🏭 |The SDK Factory | The SDK file is where the callable functions are created for the SDK. There is only one SDK file in the openbb_terminal folder. |
@@ -582,7 +587,7 @@ CLI :computer: → `_controller.py` :robot: →&nbsp;`_view.py` :art: &nbsp;&nbs
| **Item** | **Description** | **Example** |
| :----------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
| **CONTEXT** | Specific instrument _world_ to analyse. | `stocks`, `crypto`, `economy` |
| **CATEGORY** | Group of similar COMMANDS to do on the instrument <br /> There are the specialized categories, specific to each CONTEXT and there are common categories which are not specific to one CONTEXT. | `due_diligence`, `technical_analysis`, `insider` |
| **CATEGORY** | Group of similar COMMANDS to do on the instrument <br /> There are specialized categories, specific to each CONTEXT and there are common categories which are not specific to one CONTEXT. | `due_diligence`, `technical_analysis`, `insider` |
| **COMMAND** | Operation on one or no instrument that retrieves data in form of string, table or plot. | `rating`, `supplier`, `sentiment` |
The following layout is expected: `/<context>/<category>/<command_files>`
@@ -625,7 +630,7 @@ With:
1. Each function should have default values for non critical kwargs
- Why? It increases code readability and acts as an input example for the functions arguments. This increases the ease of use of the functions through the SDK, but also just generally.
- Why? It increases code readability and acts as an input example for the function's arguments. This increases the ease of use of the functions through the SDK, but also just generally.
> Watch out, add default values whenever possible, but take care for not adding mutable default arguments! [More info](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments)
@@ -701,7 +706,7 @@ With:
3. Each function needs to have a docstring explaining what it does, its parameters and what it returns.
- Why? You can use the function without reading its source code. This improves the developing experience and SDK usage. The SDK factory also can’t handle functions with out docstrings.
- Why? You can use the function without reading its source code. This improves the developing experience and SDK usage. The SDK factory also can’t handle functions without docstrings.
<br>
@@ -1399,12 +1404,12 @@ In the `_view.py` files it is common having at the end of each function `export_
Let's go into each of these arguments:
- `export` corresponds to the type of file we are exporting.
- If the user doesn't has anything selected, then this function doesn't do anything.
- If the user doesn't have anything selected, then this function doesn't do anything.
- The user can export multiple files and even name the files.
- The allowed type of files `json,csv,xlsx` for raw data and `jpg,pdf,png,svg` for figures depends on the `export_allowed` variable defined in `parse_known_args_and_warn`.
- `os.path.dirname(os.path.abspath(__file__))` corresponds to the directory path
- This is important when `export folder` selected is the default because the data gets stored based on where it is called.
- If this is called from a `common` folder, we can use `os.path.dirname(os.path.abspath(__file__)).replace("common", "stocks")` insteaad
- If this is called from a `common` folder, we can use `os.path.dirname(os.path.abspath(__file__)).replace("common", "stocks")` instead
- `"pt"` corresponds to the name of the exported file (+ unique datetime) if the user doesn't provide one
- `df_analyst_data` corresponds to the dataframe with data.
- `sheet_name` corresponds to the name of the sheet in the excel file.
@@ -1523,7 +1528,7 @@ class ForexController(BaseController):
In case the user is interested in a **DYNAMIC** list of options which changes based on user's state, then a class method must be defined.
The example below shows the an excerpt from `update_runtime_choices` method in the [`options` controller](/openbb_terminal/stocks/options/options_controller.py).
The example below shows an excerpt from `update_runtime_choices` method in the [`options` controller](/openbb_terminal/stocks/options/options_controller.py).
```python
def update_runtime_choices(self):
@@ -1554,8 +1559,8 @@ This is storing every logged message inside the following location :
Where $HOME is the user home directory, for instance:
- `C:\Users\foo` if your are in Windows and your name is foo
- `/home/bar/` if you are is macOS or Linux and your name is bar
- `C:\Users\foo` if you are in Windows and your name is foo
- `/home/bar/` if you are in macOS or Linux and your name is bar
The user can override this location using the settings key `OPENBB_USER_DATA_DIRECTORY`.
@@ -1610,7 +1615,7 @@ becomes
stocks/NEWS: mais recentes notícias da empresa
```
Note: To speed up translation, the team developed a [script](/i18n/help_translation.ipynb) that uses Google translator API to help translating the entire `en.yml` document to the language of choice. Then the output still needs to be reviewed, but this can be an useful bootstrap.
Note: To speed up translation, the team developed a [script](/i18n/help_translation.ipynb) that uses Google translator API to help translating the entire `en.yml` document to the language of choice. Then the output still needs to be reviewed, but this can be a useful bootstrap.
This is the convention in use for creating a new key/value pair:

85
README.md vendored
View File

@@ -1,57 +1,50 @@
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
[![Stargazers][stars-shield]][stars-url]
[![Forks][forks-shield]][forks-url]
[![Contributors][contributors-shield]][contributors-url]
[![MIT License][license-shield]][license-url]
[![Issues][issues-shield]][issues-url]
[![Bugs Open][bugs-open-shield]][bugs-open-url]
[![Bugs Closed][bugs-closed-shield]][bugs-closed-url]
[![GitHub release](https://img.shields.io/github/release/OpenBB-finance/OpenBBTerminal.svg?maxAge=3600)](https://github.com/OpenBB-finance/OpenBBTerminal/releases)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/OpenBB-finance/OpenBBTerminal/main)](https://www.tickgit.com/browse?repo=github.com/OpenBB-finance/OpenBBTerminal&branch=main)
![Discord Shield](https://discordapp.com/api/guilds/831165782750789672/widget.png?style=shield)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/openbb_finance.svg?style=social&label=Follow%20%40openbb_finance)](https://twitter.com/openbb_finance)
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/OpenBB-finance/OpenBBTerminal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/OpenBB-finance/OpenBBTerminal)
<!-- PROJECT LOGO -->
| OpenBB is committed to build the future of investment research by focusing on an open source infrastructure accessible to everyone, everywhere. |
|:--:|
| [![openbb.jpg](/images/openbb_gradient.png)](https://openbb.co) |
| Check our website at [openbb.co](https://openbb.co) |
<br />
<img src="https://github.com/OpenBB-finance/OpenBBTerminal/blob/main/images/openbb-logo-dark.svg?raw=true#gh-light-mode-only" alt="OpenBB Terminal logo" width="600">
<img src="https://github.com/OpenBB-finance/OpenBBTerminal/blob/main/images/openbb-logo-light.svg?raw=true#gh-dark-mode-only" alt="OpenBB Terminal logo" width="600">
<br />
<br />
<p align="center">
<h3 align="center">OpenBB Terminal 🚀</h3>
<h4 align="center">Documentation can be found at: https://openbb.co/docs </h4>
<p align="center">Click on the GIF below for a DEMO of the terminal.</p>
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/openbb_finance.svg?style=social&label=Follow%20%40openbb_finance)](https://twitter.com/openbb_finance)
[![GitHub release](https://img.shields.io/github/release/OpenBB-finance/OpenBBTerminal.svg?maxAge=3600)](https://github.com/OpenBB-finance/OpenBBTerminal/releases)
![Discord Shield](https://discordapp.com/api/guilds/831165782750789672/widget.png?style=shield)
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/OpenBB-finance/OpenBBTerminal)
<a href="https://codespaces.new/OpenBB-finance/OpenBBTerminal">
<img src="https://github.com/codespaces/badge.svg" height="20" />
</a>
<a target="_blank" href="https://colab.research.google.com/github/OpenBB-finance/OpenBBTerminal/blob/develop/examples/googleColab.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
The first financial terminal that is free and fully open source. With over 600 commands, the terminal has access to equity, options, crypto, forex, macro economy, fixed income, alternative datasets, and more.
Sign up to the [OpenBB Hub](https://my.openbb.co/login) to use our Windows or MacOS installers to get started.
<a href="https://www.youtube.com/playlist?list=PLXcRIKcHAUP45_ZZCxxvPyXFKHdq9kM8T">
<p align="center">Getting started with the OpenBB Terminal</p>
</a>
<p align="center">
<p align="center">
<a href="https://www.youtube.com/watch?v=fqGPK8OVHLk" rel="OpenBB Terminal Demo">
<a href="https://www.youtube.com/playlist?list=PLXcRIKcHAUP45_ZZCxxvPyXFKHdq9kM8T" rel="OpenBB Terminal Demo">
<img src="images/openbb_terminal_illustration.gif" alt="OpenBB Terminal Illustration" width="100%"/>
</a>
</p>
<p align="center">
<a href="https://docs.openbb.co/terminal/installation"><strong>≪ GETTING STARTED</strong></a>
&nbsp · &nbsp <a href="https://github.com/OpenBB-finance/OpenBBTerminal/tree/master/CONTRIBUTING.md"><strong>CONTRIBUTING</strong></a> &nbsp · &nbsp
<a href="https://docs.openbb.co/terminal">
<strong>SEE FEATURES »</strong></a>
<a href="https://docs.openbb.co/terminal/installation">
<strong>≪ INSTALL</strong>
</a>
&nbsp · &nbsp
<a href="https://my.openbb.co/app/terminal/data">
<strong>DATA</strong>
</a>
&nbsp · &nbsp
<a href="https://my.openbb.co/app/terminal/features">
<strong>SEE FEATURES</strong>
</a>
&nbsp · &nbsp
<a href="https://github.com/OpenBB-finance/OpenBBTerminal/tree/master/CONTRIBUTING.md">
<strong>CONTRIBUTING »</strong>
</a>
</p>
</p>

14
build/README.md vendored
View File

@@ -1,20 +1,20 @@
# Building Installers
This guide serves to assist developers construct OpenBB Terminal installers on MacOs and Windows.
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.
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.
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 it’s dependencies onto your system through brew.
This installs create-dmg and its dependencies onto your system through brew.
1. `poetry install -E installer`
This install pyinstaller and it’s dependencies onto your environment.
This install pyinstaller and its dependencies onto your environment.
3. `build/pyinstaller/build4mac.sh`
@@ -27,7 +27,7 @@ In this section we detail the steps necessary to construct an installer locally
### Windows Steps
1. `poetry install -E installer`
This install pyinstaller and it’s dependencies onto your environment.
This install pyinstaller and its dependencies onto your environment.
2. `pyinstaller build/pyinstaller/terminal.spec`
@@ -49,7 +49,7 @@ In this section we detail the steps necessary to construct an installer locally
## 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 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 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.

View File

@@ -19,10 +19,10 @@ FROM base AS builder
WORKDIR /openbb
COPY ./openbb_sdk ./openbb_sdk
COPY ./openbb_platform ./openbb_platform
# Install the SDK
RUN pip install /openbb/openbb_sdk
RUN pip install /openbb/openbb_platform
# ---- Copy Files ----
FROM base

View File

@@ -1,2 +1,2 @@
OPENBBTERMINAL_DOCKER_REGISTRY="ghcr.io"
OPENBBTERMINAL_DOCKER_RELEASE_VERSION="3.2.1"
OPENBBTERMINAL_DOCKER_RELEASE_VERSION="3.2.4"

View File

@@ -10,18 +10,18 @@
!define NAME "OpenBB Terminal"
!define COMPANY "OpenBB"
!define APPFILE "OpenBBTerminal.exe"
!define VERSION "3.2.1"
!define VERSION "3.2.4"
!define SLUG "${NAME} v${VERSION}"
;--------------------------------
; Info for Installer.exe
VIProductVersion 3.2.0.0
VIProductVersion 3.2.4.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.1.0
VIAddVersionKey ProductVersion 3.2.1.0
VIAddVersionKey FileVersion 3.2.4.0
VIAddVersionKey ProductVersion 3.2.4.0
VIAddVersionKey InternalName "OpenBB Terminal"
;--------------------------------

View File

@@ -24,4 +24,3 @@ OPENBB_USE_DATETIME=true
OPENBB_OPEN_REPORT_AS_HTML=true
OPENBB_ENABLE_QUICK_EXIT=false
OPENBB_ENABLE_EXIT_AUTO_HELP=false
PYWRY_EXECUTABLE=OpenBBPlotsBackend

View File

@@ -1,3 +1,3 @@
from PyInstaller.utils.hooks import copy_metadata, collect_data_files
from PyInstaller.utils.hooks import collect_data_files, copy_metadata
datas = copy_metadata("rich") + collect_data_files("rich")

View File

@@ -119,6 +119,7 @@ signFiles() {
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() {

View File

@@ -9,7 +9,7 @@
<h3>WSO2 API Manager</h3>
<p style="color: #020202; font-size: 11px;">Thank you for installing WSO2 API MAnager.</p>
<h5>Run API Manager</h5>
<p style="color: #020202; font-size: 11px;">Open a new terminal and run the following command to get start with APIM server:</p>
<p style="color: #020202; font-size: 11px;">Open a new terminal and run the following command to get started with APIM server:</p>
<code style="color: #c6a72b; font-size: 11px;">&nbsp; $ <span style="color: #abb0b0">wso2am-2.6.0 [command]</span></code>
</div>
<div style="font-family: Helvetica; padding-left: 10px;" align="left">

View File

@@ -42,7 +42,7 @@ VERSION=__VERSION__
PRODUCT=__PRODUCT__
echo "Application uninstalling process started"
# remove link to shorcut file
# remove link to shortcut file
find "/usr/local/bin/" -name "__PRODUCT__-__VERSION__" | xargs rm
if [ $? -eq 0 ]
then

View File

@@ -15,7 +15,24 @@ echo " |___/ "
BOOTUP_MESSAGE="
The boot up process might take a while. Please be patient...
Have you checked out Community Routines yet? You can access them through the OpenBB Hub!
Get started in less than 3 minutes: https://www.youtube.com/watch?v=UFh_Ku0fdtY
"
echo "$BOOTUP_MESSAGE"
# Add some messages we can print out
messages=("" "" "" "" \
"Did you know that you can search for stocks from a given country by running stocks/search --exchangecountry COUNTRY" \
"" "" "" \
"You can jump between menus by starting your command with a backslash. To go from funds to stocks/fa, you can run /stocks/fa" \
"" "" "" \
"Every command has a help flag. To access this use the -h flag after your command.")
# Pick a random one
index=$((RANDOM % ${#messages[@]}))
echo ${messages[$index]}
"$SCRIPTDIR"/.OpenBB/OpenBBTerminal

View File

@@ -13,8 +13,8 @@ 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 OpenBBPlotsBackend icon relative to script location
-- 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/OpenBBPlotsBackend"
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)

View File

@@ -1,5 +1,6 @@
# -*- mode: python ; coding: utf-8 -*- # noqa
import os
from shutil import which
import subprocess
import sys
from pathlib import Path
@@ -27,11 +28,10 @@ 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:
if "site-packages" in list(venv_path.iterdir()):
pathex = str(venv_path / "site-packages")
else:
pathex = str(venv_path / "lib" / "site-packages")
pathex = str(venv_path / "lib" / "site-packages")
pathex = Path(pathex).resolve()
@@ -76,6 +76,7 @@ added_files = [
(str(pathex / "llama_index/VERSION"), "llama_index"),
(str(pathex / "tiktoken"), "tiktoken"),
(str(pathex / "tiktoken_ext"), "tiktoken_ext"),
(which("pywry"), "."),
]
if is_win:
@@ -112,7 +113,7 @@ hidden_imports = [
"_sysconfigdata__darwin_darwin",
"prophet",
"debugpy",
"pywry.pywry",
"pywry",
"scipy.sparse.linalg._isolve._iterative",
"whisper",
"transformers",
@@ -154,45 +155,6 @@ if is_win:
if is_darwin:
exe_icon = (str(repo_path / "images/openbb.icns"),)
block_cipher = None
# PyWry
pywry_a = Analysis(
[str(pathex / "pywry/backend.py")],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pywry_pyz = PYZ(pywry_a.pure, pywry_a.zipped_data, cipher=block_cipher)
# PyWry EXE
pywry_exe = EXE(
pywry_pyz,
pywry_a.scripts,
[],
exclude_binaries=True,
name="OpenBBPlotsBackend",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
console=True,
disable_windowed_traceback=False,
target_arch="x86_64",
codesign_identity=None,
entitlements_file=None,
icon=exe_icon,
)
exe_args = [
pyz,
@@ -252,14 +214,9 @@ if is_darwin:
exe_kwargs["argv_emulation"] = True
exe = EXE(*exe_args, **exe_kwargs)
pywry_collect_args = [
pywry_a.binaries,
pywry_a.zipfiles,
pywry_a.datas,
]
if build_type == "folder":
coll = COLLECT(
*([exe] + collect_args + [pywry_exe] + pywry_collect_args),
*([exe] + collect_args),
**collect_kwargs,
)

View File

@@ -6,8 +6,8 @@ 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, 0, 0, 0),
prodvers=(3, 0, 0, 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.
@@ -32,10 +32,10 @@ VSVersionInfo(
[StringStruct('Comments', 'The OpenBB Terminal. For additional details, visit OpenBB.co'),
StringStruct('CompanyName', 'OpenBB'),
StringStruct('FileDescription', 'OpenBB Terminal Program'),
StringStruct('FileVersion', '3.2.1.0'),
StringStruct('FileVersion', '3.2.4.0'),
StringStruct('InternalName', 'OpenBB Terminal'),
StringStruct('ProductName', 'OpenBB Terminal'),
StringStruct('ProductVersion', '3.2.1.0')])
StringStruct('ProductVersion', '3.2.4.0')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]

51
build/pypi/openbb_platform/PUBLISH.md vendored Normal file
View File

@@ -0,0 +1,51 @@
# Publishing to PyPI
Publishing checklist:
> Note: you need to have the appropriate credentials and permissions to publish to PyPI
1. Ensure all unit tests pass: `pytest openbb_platform -m "not integration"`
2. Ensure all integration tests pass: `pytest openbb_platform -m integration`
3. Run the publishing script: `python build/pypi/openbb_platform/publish.py`
Consider using the `--dry-run` flag to check if everything is correct before publishing.
Also, it might be a good idea to run the script in batches to ensure that the packages are published correctly and the dependencies pick the correct versions.
Example, the extension packages need to pick the latest `openbb-core` version.
Suggested batch order:
1. Batch 1. Core
1. `openbb-core`
2. Batch 2. Extensions
1. `openbb-charting`
2. `openbb-crypto`
3. `openbb-currency`
4. ...
3. Batch 3. Toolkits
1. `openbb-quantitative`
2. `openbb-technical`
3. `openbb-econometrics`
4. ...
4. Batch 4. Providers
1. `openbb-alpha-vantage`
2. `openbb-benzinga`
3. `openbb-biztoc`
4. ...
5. Batch 5. The meta-packages
1. `openbb`
When publishing this package:
- Bump the dependency package versions
- Re-build the static assets that are bundled with the package
> Note that, in order for packages to pick up the latest versions of dependencies, it might be necessary to clear the local cache of the dependencies:
>
> We can do that with `pip cache purge` and `poetry cache clear pypi --all`
>
> Also, sometimes there might be some delay in the PyPI API, so it might be necessary to wait a few minutes before publishing the next package.
4. Update poetry files: `python build/pypi/openbb_platform/poetry_update.py`
5. Open a PR so that changes are reflected on the main branch
Finally, check if everything works:
1. Install and test the package from Pypi on a clean environment.
2. Check if all the `pyproject.toml` files are correct, including the `openbb_platform` one.
3. Double check if there is any new extension or provider that needs to be added to [integration tests GitHub Action workflow](/.github/workflows/platform-api-integration-test.yml).

View File

@@ -0,0 +1,85 @@
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict
import toml
PLATFORM_PATH = Path(__file__).parent.parent.parent.parent.resolve() / "openbb_platform"
PYPROJECT = PLATFORM_PATH / "pyproject.toml"
SUB_PACKAGES = {}
DEPENDENCIES: Dict[str, Any] = {}
PLUGINS: Dict[str, Dict] = {
"openbb_core_extension": {},
"openbb_provider_extension": {},
}
CMD = [sys.executable, "-m", "poetry", "build"]
PYPROJECT_TOML = toml.load(PYPROJECT)
POETRY_DICT: Dict[str, dict] = PYPROJECT_TOML["tool"]["poetry"]
POETRY_DICT.pop("extras", None)
original_pyproject = PYPROJECT.read_text()
def gather_metadata(sub_path: str):
"""Gather metadata from the pyproject.toml files.
Parameters
----------
sub_path : str
The path to the sub packages.
"""
for path in PLATFORM_PATH.rglob(f"{sub_path}/**/pyproject.toml"):
pyproject_toml = toml.load(path)
poetry_dict: Dict[str, dict] = pyproject_toml["tool"]["poetry"]
package_name = poetry_dict["packages"][0]["include"]
for extension in list(PLUGINS.keys()):
PLUGINS[extension].update(poetry_dict.get("plugins", {}).get(extension, {}))
DEPENDENCIES.update(pyproject_toml["tool"]["poetry"]["dependencies"])
SUB_PACKAGES[package_name] = path.relative_to(PLATFORM_PATH).parent.as_posix()
def build():
"""Build the Platform package."""
for sub_path in ["core", "providers", "extensions"]:
gather_metadata(sub_path)
# need to pop these from the dependencies
DEPENDENCIES.pop("openbb-core", None)
# add the sub packages
for package_name, path in SUB_PACKAGES.items():
POETRY_DICT["packages"].append({"include": package_name, "from": f"./{path}"})
# add the plugins extensions
for extension, plugins in PLUGINS.items():
POETRY_DICT.setdefault("plugins", {}).setdefault(extension, {}).update(plugins)
# update the dependencies and platform poetry dict
POETRY_DICT["dependencies"] = DEPENDENCIES
PYPROJECT_TOML["tool"]["poetry"] = POETRY_DICT
temp_pyproject = toml.dumps(PYPROJECT_TOML)
try:
with open(PYPROJECT, "w", encoding="utf-8", newline="\n") as f:
f.write(temp_pyproject)
subprocess.run(CMD, cwd=PLATFORM_PATH, check=True) # noqa: S603,PLW1510
except (Exception, KeyboardInterrupt) as e:
print(e) # noqa: T201
print("Restoring pyproject.toml") # noqa: T201
# we restore the original pyproject.toml
with open(PYPROJECT, "w", encoding="utf-8", newline="\n") as f:
f.write(original_pyproject)
if __name__ == "__main__":
build()

View File

@@ -0,0 +1,29 @@
import subprocess
import sys
from pathlib import Path
from typing import List, Optional
PLATFORM_PATH = Path(__file__).parent.parent.parent.parent.resolve() / "openbb_platform"
def update_lock_file(packages: Optional[List[str]] = None) -> None:
"""Update the poetry lock files in all the packages or in the given packages."""
for path in PLATFORM_PATH.rglob("pyproject.toml"):
if packages and path.parent.name not in packages:
print(f"Skipping {path.parent.name}") # noqa: T201
continue
print(f"\nUpdating lock file in {path.parent.name}") # noqa: T201
CMD = [sys.executable, "-m", "poetry", "lock", "--no-update"]
subprocess.run(CMD, cwd=path.parent, check=True) # noqa: S603,PLW1510
lock_file = path.parent / "poetry.lock"
lock_text = lock_file.read_text()
# Fix line endings and encoding
with open(lock_file, "w", encoding="utf-8", newline="\n") as f:
f.write(lock_text)
if __name__ == "__main__":
args = sys.argv[1:]
update_lock_file(packages=args if args else None)

View File

@@ -0,0 +1,106 @@
"""Publish the OpenBB Platform to PyPi."""
import argparse
import subprocess
import sys
from pathlib import Path
PLATFORM_PATH = Path(__file__).parent.parent.parent.parent.resolve() / "openbb_platform"
CORE_PACKAGES = ["core"]
EXTENSION_PACKAGES = ["extensions", "providers"]
CMD = [sys.executable, "-m", "poetry"]
EXTENSION_DEPENDENCIES_UPDATE_CMD = ["add", "openbb-core=latest"]
VERSION_BUMP_CMD = ["version", "prerelease"]
PUBLISH_CMD = ["publish", "--build"]
def parse_args():
parser = argparse.ArgumentParser(
description="Publish OpenBB Platform to PyPi with optional core or extensions flag."
)
parser.add_argument(
"-c", "--core", action="store_true", help="Publish core packages.", dest="core"
)
parser.add_argument(
"-e",
"--extensions",
action="store_true",
help="Publish extension packages.",
dest="extensions",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Run the commands without actually publishing.",
default=False,
dest="dry_run",
)
return parser.parse_args()
def update_extension_dependencies(path: Path):
"""Update the extension dependencies"""
subprocess.run(
CMD + EXTENSION_DEPENDENCIES_UPDATE_CMD, # noqa: S603
cwd=path.parent,
check=True,
)
def bump_version(path: Path):
"""Bump the version of the package"""
subprocess.run(CMD + VERSION_BUMP_CMD, cwd=path.parent, check=True) # noqa: S603
def publish(dry_run: bool = False, core: bool = False, extensions: bool = False):
"""Publish the Platform to PyPi with optional core or extensions."""
package_paths = []
if core:
print("Working with core packages...") # noqa: T201
package_paths.extend(CORE_PACKAGES)
if extensions:
print("Working with extensions...") # noqa: T201
package_paths.extend(EXTENSION_PACKAGES)
for sub_path in package_paths:
is_extension = sub_path in EXTENSION_PACKAGES
for path in PLATFORM_PATH.rglob(f"{sub_path}/**/pyproject.toml"):
try:
# Update dependencies
if is_extension and "devtools" not in str(path):
update_extension_dependencies(path)
# Bump version
bump_version(path)
# Publish (if not dry run)
if not dry_run:
subprocess.run(
CMD + PUBLISH_CMD, cwd=path.parent, check=True # noqa: S603
)
except Exception as e:
print(f"Error publishing {path.parent}:\n{e}") # noqa: T201
if __name__ == "__main__":
msg = """
You are about to publish a new version of OpenBB Platform to PyPI.
Please ensure you've read the "PUBLISH.md" file.
Also, please double check with `poetry config --list` if you're publishing to PyPI or TestPyPI.
"""
args = parse_args()
res = input(f"{msg}\n\nDo you want to continue? [y/N] ")
if res.lower() == "y":
publish(dry_run=args.dry_run, core=args.core, extensions=args.extensions)
openbb_package_msg = """
In order to publish the `openbb` package you need to manually update the
versions in the `pyproject.toml` file. Follow the steps below:
1. Bump version: `poetry version prerelease --next-phase`
2. Re-build the static assets that are bundled with the package
3. Publish: `poetry publish --build`
"""
print(openbb_package_msg) # noqa: T201

View File

@@ -67,12 +67,12 @@ def main(ignore_files: Optional[str], ignore_commands: Optional[str]):
if not undocumented:
sys.exit(0)
else:
print("The following commands do not have documentation:")
print("The following commands do not have documentation:") # noqa: T201
undocumented = list(undocumented)
undocumented.sort()
for item in undocumented:
print(item)
print(item) # noqa: T201
sys.exit(1)

View File

@@ -35,7 +35,7 @@ def process_file(file_path: str, exp: str):
if long_arg is not None:
long_arg = long_arg.group()
if long_arg not in ("HistoryManager.hist_file", RESERVED_ARGS[short_arg]):
print(
print( # noqa: T201
f"{file_path}: "
f"'-{short_arg}' argument expected '{RESERVED_ARGS[short_arg]}'"
f" but assigned to '{long_arg}'"

84
examples/README.md vendored
View File

@@ -1,18 +1,43 @@
# Jupyter Notebook Examples Using the OpenBB SDK
# Jupyter Notebook Examples Using the OpenBB Platform
This folder is a collection of example notebooks that demonstrate some of the ways to get started using the OpenBB SDK. To run them, ensure that the active kernel selected is the same Python virtual environment where OpenBB was installed.
This folder is a collection of example notebooks that demonstrate some of the ways to get started with using the OpenBB Platform. To run them, ensure that the active kernel selected is the same Python virtual environment where OpenBB was installed.
## Table of Contents
### googleColab
This notebook installs the OpenBB Platform in a Google Colab environment with examples for:
- Logging into OpenBB Hub
- Setting the output preference
- Fetching options and company fundamentals data
- Creating bar chart visualizations
### findSymbols
This notebook provides an introduction to discovering, finding, and searching ticker symbols.
- Search
- Find company and institutional filings
- Screen stocks by region and metrics
### loadHistoricalPriceData
This notebook walks through collecting historical price data using a variety of methods and sources.
This notebook walks through collecting historical price data, at different intervals, using a variety of sources.
- Loading data with different intervals, and changing sources.
- A brief explanation of ticker symbology.
- Using other functions and modules to load data with besides `openbb.stocks.load()`.
- Gang-loading a list of tickers' price data in a single call.
- Drawing candle and line charts.
- Loading data with different intervals, and changing sources
- A brief explanation of ticker symbology
- Resampling a time series index
- Some differences between providers, and comparing outputs
### financialStatements
This set of examples introduces financial statements in the OpenBB Platform and compares the free cash flow yields of large-cap retail industry companies.
- Financial statements
- What to expect with data from different sources
- Financial attributes
- Ratios and other metrics
### copperToGoldRatio
@@ -21,45 +46,4 @@ This notebook explains how to calculate and plot the Copper-to-Gold ratio.
- Loading historical front-month futures prices.
- Getting the historical series from FRED for the 10-year constant maturity US treasury bill.
- Performing basic DataFrame operations.
- Creating OpenBB Figure objects, and plotting on two y-axis.
### realizedVolatilityModels
This notebook demonstrates the six, realized volatility models with in the Technical Analysis module.
- Explore differences between:
- Standard Deviation
- Parkinson
- Hodges-Tompkins
- Garman-Klass
- Rogers-Satchell
- Yang-Zhang
- Creating and plotting realized volatility cones.
- Overlaying multiple time series.
- Use the calculated outputs as inputs to a forecast model.
### usdLiquidityIndex
This notebook demonstrates how to query the Federal Reserve Economic Database and recreate the USD Liquidity Index.
- Search FRED for series IDs.
- Load multiple series as a single call.
- Unpacking the data response from the FRED query.
- Perform arithmetic operations on a DataFrame.
- Normalization methods for a series or DataFrame.
- Simple processes for creating charts.
### optionsChains
This notebook demonstrates how to fetch and work with options chains data.
- Use the new method for loading options chains data.
- Examine the differences of the data, between sources.
- Calculate and visualize volume and open interest by strike and expiration.
- Calculate and visualize the Net Delta Exposure, in dollars, by strike and expiration.
- Visualize the term structure of the implied volatility skew.
- Get tables with multiple single-leg options strategies.
### googleColabInstallation
This notebook demonstrates how to install the OpenBB SDK within Google Colab.
- Creating charts with Plotly Graph Objects.

File diff suppressed because it is too large Load Diff

1059
examples/financialStatements.ipynb vendored Normal file

File diff suppressed because it is too large Load Diff

1409
examples/findSymbols.ipynb vendored Normal file

File diff suppressed because it is too large Load Diff

2125
examples/googleColab.ipynb vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

79
examples/v3/README.md vendored Normal file
View File

@@ -0,0 +1,79 @@
# Jupyter Notebook Examples Using the OpenBB SDK
This folder is a collection of example notebooks that demonstrate some of the ways to get started using the OpenBB SDK. To run them, ensure that the active kernel selected is the same Python virtual environment where OpenBB was installed.
## Table of Contents
:::note
These examples are for a deprecated version, the code and examples may have bugs and outdated syntax.
:::
### loadHistoricalPriceData
This notebook walks through collecting historical price data using a variety of methods and sources.
- Loading data with different intervals, and changing sources.
- A brief explanation of ticker symbology.
- Using other functions and modules to load data with besides `openbb.stocks.load()`.
- Gang-loading a list of tickers' price data in a single call.
- Drawing candle and line charts.
### copperToGoldRatio
This notebook explains how to calculate and plot the Copper-to-Gold ratio.
- Loading historical front-month futures prices.
- Getting the historical series from FRED for the 10-year constant maturity US treasury bill.
- Performing basic DataFrame operations.
- Creating OpenBB Figure objects, and plotting on two y-axis.
### realizedVolatilityModels
This notebook demonstrates the six, realized volatility models within the Technical Analysis module.
- Explore differences between:
- Standard Deviation
- Parkinson
- Hodges-Tompkins
- Garman-Klass
- Rogers-Satchell
- Yang-Zhang
- Creating and plotting realized volatility cones.
- Overlaying multiple time series.
- Use the calculated outputs as inputs to a forecast model.
### usdLiquidityIndex
This notebook demonstrates how to query the Federal Reserve Economic Database and recreate the USD Liquidity Index.
- Search FRED for series IDs.
- Load multiple series as a single call.
- Unpacking the data response from the FRED query.
- Perform arithmetic operations on a DataFrame.
- Normalization methods for a series or DataFrame.
- Simple processes for creating charts.
### optionsChains
This notebook demonstrates how to fetch and work with options chains data.
- Use the new method for loading options chains data.
- Examine the differences of the data, between sources.
- Calculate and visualize volume and open interest by strike and expiration.
- Calculate and visualize the Net Delta Exposure, in dollars, by strike and expiration.
- Visualize the term structure of the implied volatility skew.
- Get tables with multiple single-leg options strategies.
### googleColabInstallation
This notebook demonstrates how to install the OpenBB SDK within Google Colab.
### volumeAtPrice
Volume-at-price is a chart used by technical analysts as a visual gauge of where the trading levels are concentrated, relative to price and volume. In short, it is the sum of volume at different price levels. This notebook utilizes individual components of the OpenBB SDK and combines them to build a new view.
- Price
- Volume
- VWAP
- SMA
- OpenBBFigure

95896
examples/v3/copperToGoldRatio.ipynb vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1852,7 +1852,7 @@
"cell_type": "code",
"source": [
"# The source for the data can be defined by using the `source` argument.\n",
"# Instructions for obtaining and entering API keys are found here: https://docs.openbb.co/sdk/usage/guides/api-keys\n",
"# Instructions for obtaining and entering API keys are found here: https://docs.openbb.co/sdk/usage/api-keys\n",
"\n",
"balance_sheet_growth = openbb.stocks.fa.balance(symbol, ratios = True).transpose()\n",
"\n",
@@ -2488,4 +2488,4 @@
]
}
]
}
}

28256
examples/v3/loadHistoricalPriceData.ipynb vendored Normal file

File diff suppressed because it is too large Load Diff

6441
examples/v3/volumeAtPrice.ipynb vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -536,7 +536,9 @@ class BuildCategoryModelClasses:
f.write(content.replace(b"\r", b"\n"))
# We run black to make sure the code is formatted correctly
subprocess.check_call(["black", "openbb_terminal"]) # nosec: B603, B607
subprocess.check_call( # noqa: S603 # nosec: B603, B607
["black", "openbb_terminal"] # noqa: S603,S607
)
def generate_sdk(sort: bool = False) -> bool:

17
images/openbb-logo-dark.svg vendored Normal file
View File

@@ -0,0 +1,17 @@
<svg width="444" height="19" viewBox="0 0 444 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M125.279 0.891602V2.01694V3.13959V4.26225V5.38758V6.51292V7.63557V8.76091V9.88357V11.0089V12.1316V13.2569V14.3796L124.152 13.2569L123.028 12.1316L121.901 11.0089L120.777 9.88357L119.65 8.76091L118.523 7.63557L117.399 6.51292L116.273 5.38758L115.148 4.26225L114.022 3.13959L112.898 2.01694L111.771 0.891602H110.647H109.52V2.01694V3.13959V4.26225V5.38758V6.51292V7.63557V8.76091V9.88357V11.0089V12.1316V13.2569V14.3796V15.5049V16.6302V17.7529V18.8755H110.647H111.771V17.7529V16.6302V15.5049V14.3796V13.2569V12.1316V11.0089V9.88357V8.76091V7.63557V6.51292V5.38758V4.26225L112.898 5.38758L114.022 6.51292L115.148 7.63557L116.273 8.76091L117.399 9.88357L118.523 11.0089L119.65 12.1316L120.777 13.2569L121.901 14.3796L123.028 15.5049L124.152 16.6302L125.279 17.7529L126.403 18.8755H127.529V17.7529V16.6302V15.5049V14.3796V13.2569V12.1316V11.0089V9.88357V8.76091V7.63557V6.51292V5.38758V4.26225V3.13959V2.01694V0.891602H126.403H125.279Z" fill="black"/>
<path d="M161.517 7.63557H159.266V0.891602H145.761V18.8782H163.773V7.62486H161.522L161.517 7.63557ZM148.012 6.50488V3.1396H157.015V7.63557H148.012V6.50488ZM161.517 11.0009V16.6275H148.012V9.88625H161.517V11.0009Z" fill="black"/>
<path d="M198.866 7.63557H195.491V0.891602H181.983V18.8782H199.99V7.62486L198.866 7.63557ZM184.234 6.50488V3.1396H193.24V7.63557H184.234V6.50488ZM197.742 11.0009V16.6275H184.234V9.88625H197.742V11.0009Z" fill="black"/>
<path d="M89.1901 0.891602H89.1847H88.0633H86.9392H85.8125H84.6884H83.5616H82.4376H81.3108H80.1867H79.06H77.9359H76.8091H75.685H74.5583H73.4315V2.01694V3.13959V4.26225V5.38758V6.51292V7.63557V8.76091V9.88357V11.0089V12.1316V13.2569V14.3796V15.5049V16.6302V17.7529V18.8755H74.5583H75.685H76.8091H77.9359H79.06H80.1867H81.3108H82.4376H83.5616H84.6884H85.8125H86.9392H88.0633H89.1847H89.1901H90.3115H91.4355V17.7529V16.6302H90.3115H89.1901H89.1847H88.0633H86.9392H85.8125H84.6884H83.5616H82.4376H81.3108H80.1867H79.06H77.9359H76.8091H75.685V15.5049V14.3796V13.2569V12.1316H76.8091H77.9359H79.06H80.1867H81.3108H82.4376H83.5616H84.6884H85.8071H85.8125H86.9339H88.0579H89.1847V11.0089V9.88357H88.0579H86.9339H85.8125H85.8071H84.6884H83.5616H82.4376H81.3108H80.1867H79.06H77.9359H76.8091H75.685V8.76091V7.63557V6.51292V5.38758V4.26225V3.13959H76.8091H77.9359H79.06H80.1867H81.3108H82.4376H83.5616H84.6884H85.8125H86.9392H88.0633H89.1847H89.1901H90.3115H91.4355V2.01694V0.891602H90.3115H89.1901Z" fill="black"/>
<path d="M16.6851 0.891602H0.929199V18.8782H18.9386V0.891602H16.6878H16.6851ZM16.6851 4.26493V16.6302H3.17736V3.1396H16.6851V4.26493Z" fill="black"/>
<path d="M52.9584 0.891323H37.2025V18.8779H39.4534V12.1313H55.2092V0.87793H52.9584V0.891323ZM52.9584 4.26466V9.89133H39.4534V3.13932H52.9584V4.26466Z" fill="black"/>
<path d="M382.696 0.87793V2.00091V3.12122V4.24152V5.3645V6.48749V7.60779V8.73077V9.85108V10.9741V12.0944V13.2173V14.3377L381.572 13.2173L380.45 12.0944L379.326 10.9741L378.204 9.85108L377.08 8.73077L375.955 7.60779L374.834 6.48749L373.709 5.3645L372.588 4.24152L371.463 3.12122L370.342 2.00091L369.217 0.87793H368.095H366.971V2.00091V3.12122V4.24152V5.3645V6.48749V7.60779V8.73077V9.85108V10.9741V12.0944V13.2173V14.3377V15.4606V16.5836V17.7039V18.8242H368.095H369.217V17.7039V16.5836V15.4606V14.3377V13.2173V12.0944V10.9741V9.85108V8.73077V7.60779V6.48749V5.3645V4.24152L370.342 5.3645L371.463 6.48749L372.588 7.60779L373.709 8.73077L374.834 9.85108L375.955 10.9741L377.08 12.0944L378.204 13.2173L379.326 14.3377L380.45 15.4606L381.572 16.5836L382.696 17.7039L383.818 18.8242H384.942V17.7039V16.5836V15.4606V14.3377V13.2173V12.0944V10.9741V9.85108V8.73077V7.60779V6.48749V5.3645V4.24152V3.12122V2.00091V0.87793H383.818H382.696Z" fill="black"/>
<path d="M322.41 18.8301V0.87793H324.691L331.916 8.17812L338.075 0.87793H340.356V18.8301H338.075V4.37594L333.056 10.4594H330.775L324.691 4.37594V18.8301H322.41Z" fill="black"/>
<path d="M277.828 0.87793H277.823H276.704H275.582H274.458H273.336H272.212H271.09H269.966H268.844H267.719H266.598H265.473H264.352H263.227H262.103V2.00091V3.12122V4.24152V5.3645V6.48749V7.60779V8.73077V9.85108V10.9741V12.0944V13.2173V14.3377V15.4606V16.5836V17.7039V18.8242H263.227H264.352H265.473H266.598H267.719H268.844H269.966H271.09H272.212H273.336H274.458H275.582H276.704H277.823H277.828H278.947H280.069V17.7039V16.5836H278.947H277.828H277.823H276.704H275.582H274.458H273.336H272.212H271.09H269.966H268.844H267.719H266.598H265.473H264.352V15.4606V14.3377V13.2173V12.0944H265.473H266.598H267.719H268.844H269.966H271.09H272.212H273.336H274.452H274.458H275.577H276.698H277.823V10.9741V9.85108H276.698H275.577H274.458H274.452H273.336H272.212H271.09H269.966H268.844H267.719H266.598H265.473H264.352V8.73077V7.60779V6.48749V5.3645V4.24152V3.12122H265.473H266.598H267.719H268.844H269.966H271.09H272.212H273.336H274.458H275.582H276.704H277.823H277.828H278.947H280.069V2.00091V0.87793H278.947H277.828Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M231.99 0.87793L231.99 3.15924L239.822 3.15924V18.8242H242.104V3.15924L249.936 3.15924V0.87793L242.104 0.87793H239.822L231.99 0.87793Z" fill="black"/>
<rect x="352.523" y="0.87793" width="2.28129" height="17.9463" fill="black"/>
<path d="M427.281 0.87793H429.562V16.8471H443.25V18.8242H427.281V0.87793Z" fill="black"/>
<path d="M307.994 18.8779L303.881 11.7958H306.13L310.243 18.8779H307.994Z" fill="black"/>
<path d="M307.989 0.891322H292.236V18.8757H294.486V12.1299H310.24V0.87793H307.989V0.891322ZM307.989 9.89022H294.486V3.13904H307.989V9.89022Z" fill="black"/>
<path d="M412.863 0.891322H397.109V18.8757H399.36V12.1299H412.863V18.8757H415.113V0.87793H412.863V0.891322ZM412.863 9.89022H399.36V3.13904H412.863V9.89022Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

17
images/openbb-logo-light.svg vendored Normal file
View File

@@ -0,0 +1,17 @@
<svg width="443" height="18" viewBox="0 0 443 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124.349 0.0136719V1.13901V2.26166V3.38432V4.50965V5.63499V6.75765V7.88298V9.00564V10.131V11.2536V12.379V13.5016L123.223 12.379L122.099 11.2536L120.972 10.131L119.848 9.00564L118.721 7.88298L117.594 6.75765L116.47 5.63499L115.343 4.50965L114.219 3.38432L113.093 2.26166L111.968 1.13901L110.842 0.0136719H109.718H108.591V1.13901V2.26166V3.38432V4.50965V5.63499V6.75765V7.88298V9.00564V10.131V11.2536V12.379V13.5016V14.627V15.7523V16.8749V17.9976H109.718H110.842V16.8749V15.7523V14.627V13.5016V12.379V11.2536V10.131V9.00564V7.88298V6.75765V5.63499V4.50965V3.38432L111.968 4.50965L113.093 5.63499L114.219 6.75765L115.343 7.88298L116.47 9.00564L117.594 10.131L118.721 11.2536L119.848 12.379L120.972 13.5016L122.099 14.627L123.223 15.7523L124.349 16.8749L125.473 17.9976H126.6V16.8749V15.7523V14.627V13.5016V12.379V11.2536V10.131V9.00564V7.88298V6.75765V5.63499V4.50965V3.38432V2.26166V1.13901V0.0136719H125.473H124.349Z" fill="white"/>
<path d="M160.588 6.75764H158.337V0.0136719H144.832V18.0003H162.844V6.74693H160.593L160.588 6.75764ZM147.083 5.62695V2.26167H156.086V6.75764H147.083V5.62695ZM160.588 10.1229V15.7496H147.083V9.00832H160.588V10.1229Z" fill="white"/>
<path d="M197.937 6.75764H194.562V0.0136719H181.054V18.0003H199.061V6.74693L197.937 6.75764ZM183.305 5.62695V2.26167H192.311V6.75764H183.305V5.62695ZM196.813 10.1229V15.7496H183.305V9.00832H196.813V10.1229Z" fill="white"/>
<path d="M88.2609 0.0136719H88.2555H87.1341H86.01H84.8833H83.7592H82.6324H81.5084H80.3816H79.2575H78.1308H77.0067H75.8799H74.7558H73.6291H72.5023V1.13901V2.26166V3.38432V4.50965V5.63499V6.75765V7.88298V9.00564V10.131V11.2536V12.379V13.5016V14.627V15.7523V16.8749V17.9976H73.6291H74.7558H75.8799H77.0067H78.1308H79.2575H80.3816H81.5084H82.6324H83.7592H84.8833H86.01H87.1341H88.2555H88.2609H89.3823H90.5063V16.8749V15.7523H89.3823H88.2609H88.2555H87.1341H86.01H84.8833H83.7592H82.6324H81.5084H80.3816H79.2575H78.1308H77.0067H75.8799H74.7558V14.627V13.5016V12.379V11.2536H75.8799H77.0067H78.1308H79.2575H80.3816H81.5084H82.6324H83.7592H84.8779H84.8833H86.0047H87.1287H88.2555V10.131V9.00564H87.1287H86.0047H84.8833H84.8779H83.7592H82.6324H81.5084H80.3816H79.2575H78.1308H77.0067H75.8799H74.7558V7.88298V6.75765V5.63499V4.50965V3.38432V2.26166H75.8799H77.0067H78.1308H79.2575H80.3816H81.5084H82.6324H83.7592H84.8833H86.01H87.1341H88.2555H88.2609H89.3823H90.5063V1.13901V0.0136719H89.3823H88.2609Z" fill="white"/>
<path d="M15.7559 0.0136719H0V18.0003H18.0094V0.0136719H15.7586H15.7559ZM15.7559 3.387V15.7523H2.24816V2.26167H15.7559V3.387Z" fill="white"/>
<path d="M52.0292 0.0133936H36.2733V18H38.5242V11.2534H54.28V0H52.0292V0.0133936ZM52.0292 3.38673V9.0134H38.5242V2.26139H52.0292V3.38673Z" fill="white"/>
<path d="M381.767 0V1.12298V2.24329V3.36359V4.48657V5.60956V6.72986V7.85284V8.97315V10.0961V11.2164V12.3394V13.4597L380.643 12.3394L379.521 11.2164L378.397 10.0961L377.275 8.97315L376.151 7.85284L375.026 6.72986L373.905 5.60956L372.78 4.48657L371.658 3.36359L370.534 2.24329L369.412 1.12298L368.288 0H367.166H366.042V1.12298V2.24329V3.36359V4.48657V5.60956V6.72986V7.85284V8.97315V10.0961V11.2164V12.3394V13.4597V14.5827V15.7057V16.826V17.9463H367.166H368.288V16.826V15.7057V14.5827V13.4597V12.3394V11.2164V10.0961V8.97315V7.85284V6.72986V5.60956V4.48657V3.36359L369.412 4.48657L370.534 5.60956L371.658 6.72986L372.78 7.85284L373.905 8.97315L375.026 10.0961L376.151 11.2164L377.275 12.3394L378.397 13.4597L379.521 14.5827L380.643 15.7057L381.767 16.826L382.889 17.9463H384.013V16.826V15.7057V14.5827V13.4597V12.3394V11.2164V10.0961V8.97315V7.85284V6.72986V5.60956V4.48657V3.36359V2.24329V1.12298V0H382.889H381.767Z" fill="white"/>
<path d="M321.481 17.9522V0H323.762L330.986 7.30019L337.146 0H339.427V17.9522H337.146V3.49801L332.127 9.5815H329.846L323.762 3.49801V17.9522H321.481Z" fill="white"/>
<path d="M276.899 0H276.894H275.775H274.653H273.529H272.407H271.282H270.161H269.036H267.915H266.79H265.669H264.544H263.422H262.298H261.174V1.12298V2.24329V3.36359V4.48657V5.60956V6.72986V7.85284V8.97315V10.0961V11.2164V12.3394V13.4597V14.5827V15.7057V16.826V17.9463H262.298H263.422H264.544H265.669H266.79H267.915H269.036H270.161H271.282H272.407H273.529H274.653H275.775H276.894H276.899H278.018H279.14V16.826V15.7057H278.018H276.899H276.894H275.775H274.653H273.529H272.407H271.282H270.161H269.036H267.915H266.79H265.669H264.544H263.422V14.5827V13.4597V12.3394V11.2164H264.544H265.669H266.79H267.915H269.036H270.161H271.282H272.407H273.523H273.529H274.648H275.769H276.894V10.0961V8.97315H275.769H274.648H273.529H273.523H272.407H271.282H270.161H269.036H267.915H266.79H265.669H264.544H263.422V7.85284V6.72986V5.60956V4.48657V3.36359V2.24329H264.544H265.669H266.79H267.915H269.036H270.161H271.282H272.407H273.529H274.653H275.775H276.894H276.899H278.018H279.14V1.12298V0H278.018H276.899Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M231.061 -6.4373e-05L231.061 2.28124L238.893 2.28124V17.9462H241.175V2.28124L249.007 2.28124V-6.51575e-05L241.175 -6.4463e-05H238.893L231.061 -6.4373e-05Z" fill="white"/>
<rect x="351.594" width="2.28129" height="17.9463" fill="white"/>
<path d="M426.351 0H428.633V15.9692H442.32V17.9463H426.351V0Z" fill="white"/>
<path d="M307.065 18L302.952 10.9179H305.201L309.314 18H307.065Z" fill="white"/>
<path d="M307.06 0.0133919H291.307V17.9978H293.557V11.252H309.311V0H307.06V0.0133919ZM307.06 9.01229H293.557V2.26111H307.06V9.01229Z" fill="white"/>
<path d="M411.934 0.0133919H396.18V17.9978H398.43V11.252H411.934V17.9978H414.184V0H411.934V0.0133919ZM411.934 9.01229H398.43V2.26111H411.934V9.01229Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1,8 +1,25 @@
import nox
test_locations = [
"openbb_platform/tests",
"openbb_platform/core",
"openbb_platform/providers",
"openbb_platform/extensions",
]
@nox.session(python=["3.8", "3.9", "3.10", "3.11"])
@nox.session(python=["3.9", "3.10", "3.11"])
def tests(session):
session.install("./openbb_sdk")
session.install("poetry", "toml")
session.run(
"python",
"./openbb_platform/dev_install.py",
"-e",
"all",
external=True,
)
session.install("pytest")
session.run("pytest", "openbb_sdk/sdk/core/tests")
session.install("pytest-cov")
session.run(
"pytest", *test_locations, "--cov=openbb_platform/", "-m", "not integration"
)

884
openbb_platform/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,884 @@
# Contributing to the OpenBB Platform
<!-- markdownlint-disable MD033 MD024 -->
- [Contributing to the OpenBB Platform](#contributing-to-the-openbb-platform)
- [Introduction](#introduction)
- [Quick look into the OpenBB Platform](#quick-look-into-the-openbb-platform)
- [What is the Standardization Framework?](#what-is-the-standardization-framework)
- [Standardization Caveats](#standardization-caveats)
- [Standard QueryParams Example](#standard-queryparams-example)
- [Standard Data Example](#standard-data-example)
- [What is an extension?](#what-is-an-extension)
- [Types of extensions](#types-of-extensions)
- [Dependency Management](#dependency-management)
- [High-Level Overview](#high-level-overview)
- [Core Dependency Management](#core-dependency-management)
- [Installation](#installation)
- [Using Poetry](#using-poetry)
- [Core and Extensions](#core-and-extensions)
- [Installation](#installation-1)
- [Dependency Management with Poetry](#dependency-management-with-poetry)
- [Developer Guidelines](#developer-guidelines)
- [Expectations for Developers](#expectations-for-developers)
- [How to build OpenBB extensions?](#how-to-build-openbb-extensions)
- [Building Extensions: Best Practices](#building-extensions-best-practices)
- [How to add a new data point?](#how-to-add-a-new-data-point)
- [Identify which type of data you want to add](#identify-which-type-of-data-you-want-to-add)
- [Check if the standard model exists](#check-if-the-standard-model-exists)
- [Create Query Parameters model](#create-query-parameters-model)
- [Create Data Output model](#create-data-output-model)
- [Build the Fetcher](#build-the-fetcher)
- [Make the provider visible](#make-the-provider-visible)
- [How to add custom data sources?](#how-to-add-custom-data-sources)
- [OpenBB Platform commands](#openbb-platform-commands)
- [Architectural considerations](#architectural-considerations)
- [Important classes](#important-classes)
- [Import statements](#import-statements)
- [The TET pattern](#the-tet-pattern)
- [Data processing commands](#data-processing-commands)
- [Python Interface](#python-interface)
- [API Interface](#api-interface)
- [Contributor Guidelines](#contributor-guidelines)
- [Expectations for Contributors](#expectations-for-contributors)
- [Quality Assurance](#quality-assurance)
- [Unit tests](#unit-tests)
- [Integration tests](#integration-tests)
- [Import time](#import-time)
- [Sharing your extension](#sharing-your-extension)
- [Publish your extension to PyPI](#publish-your-extension-to-pypi)
- [Setup](#setup)
- [Release](#release)
- [Publish](#publish)
- [Manage extensions](#manage-extensions)
- [Add an extension as a dependency](#add-an-extension-as-a-dependency)
- [Write code and commit](#write-code-and-commit)
- [How to create a PR?](#how-to-create-a-pr)
- [Branch Naming Conventions](#branch-naming-conventions)
## Introduction
This document provides guidelines for contributing to the OpenBB Platform.
Throughout this document, we will be differentiating between two types of contributors: Developers and Contributors.
1. **Developers**: Those who are building new features or extensions for the OpenBB Platform or leveraging the OpenBB Platform.
2. **Contributors**: Those who contribute to the existing codebase, by opening a [Pull Request](#how-to-create-a-pr) thus giving back to the community.
**Why is this distinction important?**
The OpenBB Platform is designed as a foundation for further development. We anticipate a wide range of creative use cases for it. Some use cases may be highly specific or detail-oriented, solving particular problems that may not necessarily fit within the OpenBB Platform Github repository. This is entirely acceptable and even encouraged. This document provides a comprehensive guide on how to build your own extensions, add new data points, and more.
The **Developer** role, as defined in this document, can be thought of as the foundational role. Developers are those who use the OpenBB Platform as is or build upon it.
Conversely, the **Contributor** role refers to those who enhance the OpenBB Platform codebase (either by directly adding to the OpenBB Platform or by extending the [extension repository](/openbb_platform/extensions/)). Contributors are willing to go the extra mile, spending additional time on quality assurance, testing, or collaborating with the OpenBB development team to ensure adherence to standards, thereby giving back to the community.
### Quick look into the OpenBB Platform
The OpenBB Platform is built by the Open-Source community and is characterized by its core and extensions. The core handles data integration and standardization, while the extensions enable customization and advanced functionalities. The OpenBB Platform is designed to be used both from a Python interface and a REST API.
The REST API is built on top of FastAPI and can be started by running the following command from the root:
```bash
uvicorn openbb_platform.platform.core.openbb_core.api.rest_api:app --host 0.0.0.0 --port 8000 --reload
```
The Python interfaces we provide to users is the `openbb` python package.
The code you will find in this package is generated from a script and it is just a wrapper around the `openbb-core` and any installed extensions.
When the user runs `import openbb`, `from openbb import obb` or other variants, the script that generates the packaged code is triggered. It detects if there are new extensions installed in the environment and rebuilds the packaged code accordingly. If new extensions are not found, it just uses the current packaged version.
When you are developing chances are you want to manually trigger the package rebuild.
You can do that with:
```python
python -c "import openbb; openbb.build()"
```
The Python interface can be imported with:
```python
from openbb import obb
```
This document will take you through two types of contributions:
1. Building a custom extension
2. Contributing directly to the OpenBB Platform
Before moving forward, please take a look at the high-level view of the OpenBB Platform architecture. We will go over each bit in this document.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/OpenBB-finance/OpenBBTerminal/assets/74266147/c9a5a92a-28b6-4257-aefc-deaebe635c6a">
<img alt="OpenBB Platform High-Level Architecture" src="https://github.com/OpenBB-finance/OpenBBTerminal/assets/74266147/c9a5a92a-28b6-4257-aefc-deaebe635c6a">
</picture>
#### What is the Standardization Framework?
The Standardization Framework is a set of tools and guidelines that enable the user to query and obtain data in a consistent way across multiple providers.
Each data model should inherit from a [standard data](platform/provider/openbb_core/provider/standard_models) model that is already defined inside the OpenBB Platform. All standard models are created and maintained by the OpenBB team.
Usage of these models will unlock a set of perks that are only available to standardized data, namely:
- Can query and output data in a standardized way.
- Can expect extensions that follow standardization to work out-of-the-box.
- Can expect transparently defined schemas for the data that is returned by the API.
- Can expect consistent data types and validation.
- Will work seamlessly with other providers that use the same standard model.
The standard models are defined under the `/OpenBBTerminal/openbb_platform/platform/core/provider/openbb_core/provider/standard_models/` directory.
They define the [`QueryParams`](platform/provider/openbb_core/provider/abstract/query_params.py) and [`Data`](platform/provider/openbb_core/provider/abstract/data.py) models, which are used to query and output data. They are pydantic and you can leverage all the pydantic features such as validators.
##### Standardization Caveats
The standardization framework is a very powerful tool, but it has some caveats that you should be aware of:
- We standardize fields that are shared between two or more providers. If there is a third provider that doesn't share the same fields, we will declare it as an `Optional` field.
- When mapping the column names from a provider-specific model to the standard model, the CamelCase to snake_case conversion is done automatically. If the column names are not the same, you'll need to manually map them. (e.g. `o` -> `open`)
- The standard models are created and maintained by the OpenBB team. If you want to add a new field to a standard model, you'll need to open a PR to the OpenBB Platform.
##### Standard QueryParams Example
```python
class EquityHistoricalQueryParams(QueryParams):
"""Equity Historical end of day Query."""
symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", ""))
start_date: Optional[date] = Field(
description=QUERY_DESCRIPTIONS.get("start_date", ""), default=None
)
end_date: Optional[date] = Field(
description=QUERY_DESCRIPTIONS.get("end_date", ""), default=None
)
```
The `QueryParams` is an abstract class that just tells us that we are dealing with query parameters
The OpenBB Platform dynamically knows where the standard models begin in the inheritance tree, so you don't need to worry about it.
##### Standard Data Example
```python
class EquityHistoricalData(Data):
"""Equity Historical end of day price Data."""
date: datetime = Field(description=DATA_DESCRIPTIONS.get("date", ""))
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
close: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("close", ""))
volume: float = Field(description=DATA_DESCRIPTIONS.get("volume", ""))
vwap: Optional[PositiveFloat] = Field(description=DATA_DESCRIPTIONS.get("vwap", ""), default=None)
```
The `Data` class is an abstract class that tells us the expected output data. Here we can see a `vwap` field that is `Optional`. This is because not all providers share this field while it is shared between two or more providers.
#### What is an extension?
An extension adds functionality to the OpenBB Platform. It can be a new data source, a new command, a new visualization, etc.
##### Types of extensions
We primarily have 3 types of extensions:
1. OpenBB Extensions - built and maintained by the OpenBB team (e.g. `openbb-equity`)
2. Community Extensions - built by anyone and primarily maintained by OpenBB (e.g. `openbb-yfinance`)
3. Independent Extensions - built and maintained independently by anyone
If your extension is of high quality and you think that it would be a good community extension, you can open a PR to the OpenBB Platform repository and we'll review it.
We encourage independent extensions to be shared with the community by publishing them to PyPI.
## Dependency Management
### High-Level Overview
- **Provider**: The base package with no dependencies on other `openbb` packages.
- **Core**: Depends on the Provider and serves as the main infrastructural package.
- **Extensions**: Utility packages that leverage Core's infrastructure. Each extension is its own package.
- **Providers**: Utility packages extending functionality to different providers, where each provider is its own package.
### Core Dependency Management
#### Installation
- **pip**: `pip install -e OpenBBTerminal/openbb_platform/platform/core`
- **poetry**: `poetry install OpenBBTerminal/openbb_platform/platform/core`
#### Using Poetry
Ensure you're in a fresh conda environment before adjusting dependencies.
- **Add a Dependency**: `poetry add <my-dependency>`
- **Update Dependencies**:
- All: `poetry update`
- Specific: `poetry update <my-dependency>`
- **Remove a Dependency**: `poetry remove <my-dependency>`
### Core and Extensions
#### Installation
For development setup, use the provided script to install all extensions and their dependencies:
- `python dev_install.py [-e|--extras]`
> **Note**: If developing an extension, avoid installing all extensions to prevent unnecessary overhead.
#### Dependency Management with Poetry
- **Add Platform Extension**: `poetry add openbb-extension-name [--dev]`
- **Resolve Conflicts**: Adjust versions in `pyproject.toml` if notified by Poetry.
- **Lock Dependencies**: `poetry lock`
- **Update Platform**: `poetry update openbb-platform`
- **Documentation**: Maintain `pyproject.toml` and `poetry.lock` for a clear record of dependencies.
## Developer Guidelines
### Expectations for Developers
1. Use Cases:
- Ensure that your extensions or features align with the broader goals of the application.
- Understand that the OpenBB Platform is designed to be foundational; build in a way that complements and doesn't conflict with its core functionalities.
2. Documentation:
- Provide clear and comprehensive documentation for any new feature or extension you develop.
3. Code Quality:
- Adhere to the coding standards and conventions of the OpenBB Platform.
- Ensure your code is maintainable, well-organized, and commented where necessary.
4. Testing:
- Thoroughly test any new feature or extension to ensure it works as expected.
5. Performance:
- Ensure that your extensions or features do not adversely affect the performance of the OpenBB Platform.
- Optimize for scalability, especially if you anticipate high demand for your feature.
6. Collaboration:
- Engage with the OpenBB community to gather feedback on your developments.
### How to build OpenBB extensions?
We have a Cookiecutter template that will help you get started. It serves as a jumpstart for your extension development, so you can focus on the data and not on the boilerplate.
Please refer to the [Cookiecutter template](https://github.com/OpenBB-finance/openbb-cookiecutter) and follow the instructions there.
This document will walk you through the steps of adding a new extension to the OpenBB Platform.
The high level steps are:
- Generate the extension structure
- Install your dependencies
- Install your new package
- Use your extension (either from Python or the API interface)
- QA your extension
- Share your extension with the community
### Building Extensions: Best Practices
1. **Review Platform Dependencies**: Before adding any dependency, ensure it aligns with the Platform's existing dependencies.
2. **Use Loose Versioning**: If possible, specify a range to maintain compatibility. E.g., `>=1.4,<1.5`.
3. **Testing**: Test your extension with the Platform's core to avoid conflicts. Both unit and integration tests are recommended.
4. **Document Dependencies**: Use `pyproject.toml` and `poetry.lock` for clear, up-to-date records.
### How to add a new data point?
In this section, we'll be adding a new data point to the OpenBB Platform. We will add a new provider with an existing [standard data](platform/provider/openbb_core/provider/standard_models) model.
#### Identify which type of data you want to add
In this example, we'll be adding OHLC stock data that is used by the `obb.equity.price.historical` command.
Note that, if no command exists for your data, we need to add one under the right router.
Each router is categorized under different extensions (equity, currency, crypto, etc.).
#### Check if the standard model exists
Given the fact that there's already an endpoint for OHLCV stock data, we can check if the standard exists.
In this case, it's `EquityHistorical` which can be found in `/OpenBBTerminal/openbb_platform/platform/core/provider/openbb_core/provider/standard_models/equity_historical`.
If the standard model doesn't exist:
- you won't need to inherit from it in the next steps.
- all your provider query parameters will be under the `**kwargs` in the python interface.
- it might not work out-of-the box with other extensions that follow standardization e.g. the `charting` extension
##### Create Query Parameters model
Query Parameters are the parameters that are passed to the API endpoint in order to make the request.
For the `EquityHistorical` example, this would look like the following:
```python
class <ProviderName>EquityHistoricalQueryParams(EquityHistoricalQueryParams):
"""<ProviderName> Equity Historical Query.
Source: https://www.<provider_name>.co/documentation/
"""
# provider specific query parameters if any
```
##### Create Data Output model
The data output is the data that is returned by the API endpoint.
For the `EquityHistorical` example, this would look like the following:
```python
class <ProviderName>EquityHistoricalData(EquityHistoricalData):
"""<ProviderName> Equity Historical Data.
Source: https://www.<provider_name>.co/documentation/
"""
# provider specific data output fields if any
```
> Note that, since `EquityHistoricalData` inherits from pydantic's `BaseModel`, we can leverage validators to perform additional checks on the output model. A very good example of this, would be to transform a string date into a datetime object.
##### Build the Fetcher
The `Fetcher` class is responsible for making the request to the API endpoint and providing the output.
It will receive the query parameters, and it will return the output while leveraging the pydantic model schemas.
For the `EquityHistorical` example, this would look like the following:
```python
class <ProviderName>EquityHistoricalFetcher(
Fetcher[
<ProviderName>EquityHistoricalQueryParams,
List[<ProviderName>EquityHistoricalData],
]
):
"""Transform the query, extract and transform the data."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> <ProviderName>EquityHistoricalQueryParams:
"""Transform the query parameters."""
return <ProviderName>EquityHistoricalQueryParams(**transformed_params)
@staticmethod
def extract_data(
query: <ProviderName>EquityHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> dict:
"""Return the raw data from the endpoint."""
obtained_data = my_request(query, credentials, **kwargs)
return obtained_data
@staticmethod
def transform_data(
query: <ProviderName>EquityHistoricalQueryParams,
data: dict,
**kwargs: Any,
) -> List[<ProviderName>EquityHistoricalData]:
"""Transform the data to the standard format."""
return [<ProviderName>EquityHistoricalData.model_validate(d) for d in data]
```
> Make sure that you're following the TET pattern when building a `Fetcher` - **Transform, Extract, Transform**. See more on this [here](#the-tet-pattern).
By default the credentials declared on each `Provider` are required. This means that before a query is executed, we check that all the credentials are present and if not an exception is raised. If you want to make credentials optional on a given fetcher, even though they are declared on the `Provider`, you can add `require_credentials=False` to the `Fetcher` class. See the following example:
```python
class <ProviderName>EquityHistoricalFetcher(
Fetcher[
<ProviderName>EquityHistoricalQueryParams,
List[<ProviderName>EquityHistoricalData],
]
):
"""Transform the query, extract and transform the data."""
require_credentials = False
...
```
#### Make the provider visible
In order to make the new provider visible to the OpenBB Platform, you'll need to add it to the `__init__.py` file of the `providers/<provider_name>/openbb_<provider_name>/` folder.
```python
"""<Provider Name> Provider module."""
from openbb_core.provider.abstract.provider import Provider
from openbb_<provider_name>.models.equity_historical import <ProviderName>EquityHistoricalFetcher
<provider_name>_provider = Provider(
name="<provider_name>",
website="<URL to the provider website>",
description="Provider description goes here",
credentials=["api_key"],
fetcher_dict={
"EquityHistorical": <ProviderName>EquityHistoricalFetcher,
},
)
```
If the provider does not require any credentials, you can remove that parameter. On the other hand, if it requires more than 2 items to authenticate, you can add a list of all the required items to the `credentials` list.
After running `pip install .` on `openbb_platform/providers/<provider_name>` your provider should be ready for usage, both from the Python interface and the API.
### How to add custom data sources?
You will get your data either from a CSV file, local database or from an API endpoint.
If you don't want or don't need to partake in the data standardization framework, you have the option to add all the logic straight inside the router file. This is usually the case when you are returning custom data from your local CSV file, or similar. Keep in mind that we also serve the REST API and that you shouldn't send non-serializable objects as a response (e.g. a pandas dataframe).
Saying that, we highly recommend following the standardization framework, as it will make your life easier in the long run and unlock a set of features that are only available to standardized data.
When standardizing, all data is defined using two different pydantic models:
1. Define the [query parameters](platform/provider/openbb_core/provider/abstract/query_params.py) model.
2. Define the resulting [data schema](platform/provider/openbb_core/provider/abstract/data.py) model.
> The models can be entirely custom, or inherit from the OpenBB standardized models.
> They enforce a safe and consistent data structure, validation and type checking.
We call this the ***Know-Your-Data*** principle.
After you've defined both models, you'll need to define a `Fetcher` class which contains three methods:
1. `transform_query` - transforms the query parameters to the format of the API endpoint.
2. `extract_data` - makes the request to the API endpoint and returns the raw data.
3. `transform_data` - transforms the raw data into the defined data model.
> Note that the `Fetcher` should inherit from the [`Fetcher`](platform/provider/openbb_core/provider/abstract/fetcher.py) class, which is a generic class that receives the query parameters and the data model as type parameters.
After finalizing your models, you need to make them visible to the Openbb Platform. This is done by adding the `Fetcher` to the `__init__.py` file of the `<your_package_name>/<your_module_name>` folder as part of the [`Provider`](platform/provider/openbb_core/provider/abstract/provider.py).
Any command, that uses the `Fetcher` class you've just defined, will be calling the `transform_query`, `extract_data` and `transform_data` methods under the hood in order to get the data and output it do the end user.
If you're not sure what's a command and why is it even using the `Fetcher` class, follow along!
#### OpenBB Platform commands
The OpenBB Platform will enable you to query and output your data in a very simple way.
> Any Platform endpoint will be available both from a Python interface and the API.
The command definition on the Platform follows [FastAPI](https://fastapi.tiangolo.com/) conventions, meaning that you'll be creating **endpoints**.
The Cookiecutter template generates for you a `router.py` file with a set of examples that you can follow, namely:
- Perform a simple `GET` and `POST` request - without worrying on any custom data definition.
- Using a custom data definition so you get your data the exact way you want it.
You can expect the following endpoint structure when using a `Fetcher` to serve the data:
```python
@router.command(model="Example")
async def model_example( # create an async endpoint
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""Example Data."""
return await OBBject.from_query(Query(**locals()))
```
Let's break it down:
- `@router.command(...)` - this tells the OpenBB Platform that this is a command.
- `model="Example"` - this is the name of the `Fetcher` dictionary key that you've defined in the `__init__.py` file of the `<your_package_name>/<your_module_name>` folder.
- `cc: CommandContext` - this contains a set of user and system settings that is useful during the execution of the command - eg. api keys.
- `provider_choices: ProviderChoices` - all the providers that implement the `Example` `Fetcher`.
- `standard_params: StandardParams` - standardized parameters that are common to all providers that implement the `Example` `Fetcher`.
- `extra_params: ExtraParams` - it contains the provider specific arguments that are not standardized.
You only need to change the `model` parameter to the name of the `Fetcher` dictionary key and everything else will be handled by the OpenBB Platform.
### Architectural considerations
#### Important classes
#### Import statements
```python
# The `Data` class
from openbb_core.provider.abstract.data import Data
# The `QueryParams` class
from openbb_core.provider.abstract.query_params import QueryParams
# The `Fetcher` class
from openbb_core.provider.abstract.fetcher import Fetcher
# The `OBBject` class
from openbb_core.app.model.obbject import OBBject
# The `Router` class
from openbb_core.app.router import Router
```
#### The TET pattern
The TET pattern is a pattern that we use to build the `Fetcher` classes. It stands for **Transform, Extract, Transform**.
As the OpenBB Platform has its own standardization framework and the data fetcher are a very important part of it, we need to ensure that the data is transformed and extracted in a consistent way, to help us do that, we came up with the **TET** pattern, which helps us build and ship faster as we have a clear structure on how to build the `Fetcher` classes.
1. Transform - `transform_query(params: Dict[str, Any])`: transforms the query parameters. Given a `params` dictionary this method should return the transformed query parameters as a [`QueryParams`](openbb_platform/platform/provider/openbb_core/provider/abstract/query_params.py) child so that we can leverage the pydantic model schemas and validation into the next step. This might also be the place do perform some transformations on any given parameter, i.e., if you want to transform an empty date into a `datetime.now().date()`.
2. Extract - `extract_data(query: ExampleQueryParams,credentials: Optional[Dict[str, str]],**kwargs: Any,) -> Dict`: makes the request to the API endpoint and returns the raw data. Given the transformed query parameters, the credentials and any other extra arguments, this method should return the raw data as a dictionary.
3. Transform - `transform_data(query: ExampleQueryParams, data: Dict, **kwargs: Any) -> List[ExampleHistoricalData]`: transforms the raw data into the defined data model. Given the transformed query parameters (might be useful for some filtering), the raw data and any other extra arguments, this method should return the transformed data as a list of [`Data`](openbb_platform/platform/provider/openbb_core/provider/abstract/data.py) children.
#### Data processing commands
The data processing commands are commands that are used to process the data that may or may not come from the OpenBB Platform.
In order to create a data processing framework general enough to be used by any extension, we've created a special abstract class called [`Data`](/openbb_platform/platform/provider/openbb_core/provider/abstract/data.py) which **all** standardized (and consequently its child classes) will inherit from.
Why is this important?
So that we can ensure that all `OBBject.results` will share a common ground on which we can apply out-of-the-box data processing commands, such as the `ta`, `qa` or the `econometrics` menus.
But what's really the `Data` class?
It's a pydantic model that inherits from the `BaseModel` and can contain any given number of extra fields. In practice, it looks as follows:
```python
>>> res = obb.equity.price.historical("AAPL")
>>> res.results[0]
AVEquityHistoricalData(date=2023-11-03 00:00:00, open=174.24, high=176.82, low=173.35, close=176.65, volume=79829246.0, vwap=None, adj_close=None, dividend_amount=None, split_coefficient=None)
```
> The `AVEquityHistoricalData` class, is a child class of the `Data` class.
Note how we've indexed to get only the first element of the `results` list (which represents a single row, if we want to think about it as a tabular output). This simply means that we are getting a `List` of `AVEquityHistoricalData` from the `obb.equity.price.historical` command. Or, we can also say that that's equivalent to `List[Data]`!
This is very powerful, as we can now apply any data processing command to the `results` list, without worrying about the underlying data structure.
That's why, on data processing commands (such as the `ta` menu) we will find on its function signature the following:
```python
def ema(
self,
data: Union[List[Data], pandas.DataFrame],
target: str = "close",
index: str = "date",
length: int = 50,
offset: int = 0,
chart: bool = False,
) -> OBBject[List[Data]]:
...
```
> Note that `data` can actually be a different type, but we'll focus on the `List[Data]` case for now.
Does that mean that I can only use the data processing commands if I instantiate a class that inherits from `Data`?
Not at all! Consider the following example:
```python
>>> from openbb_core.provider.abstract.data import Data
>>> my_data_item_1 = {"open": 1, "high": 2, "low": 3, "close": 4, "volume": 5, "date": "2020-01-01"}
>>> my_data_item_1_as_data = Data.model_validate(my_data_item_1)
>>> my_data_item_1_as_data
Data(open=1, high=2, low=3, close=4, volume=5, date=2020-01-01)
```
This means that the `Data` class is cleaver enough to understand that you are passing a dictionary and it will try to validate it for you.
In other words, if you're using data that doesn't come from the OpenBBPlatform, you only need to ensure it's parsable by the `Data` class and you'll be able to use the data processing commands.
In other words, imagine you have a dataframe that you want to use with the `ta` menu. You can do the following:
```python
>>> res = obb.equity.price.historical("AAPL")
>>> my_df = res.to_dataframe() # yes, you can convert your OBBject.results into a dataframe out-of-the-box!
>>> my_records = df.to_dict(orient="records")
>>> obb.ta.ema(data=my_record)
OBBject
results: [{'close': 77.62, 'close_EMA_50': None}, {'close': 80.25, 'close_EMA_50': ... # this is a `List[Data]` yet again
```
> Note that that for this example we've used the `OBBject.to_dataframe()` method to have an example dataframe, but it could be any other dataframe that you have.
##### Python Interface
When using the OpenBB Platform on a Python Interface, docstrings and type hints are your best friends as it provides plenty of context on how to use the commands.
Looking at an example on the `ta` menu:
```python
def ema(
self,
data: Union[List[Data], pandas.DataFrame],
target: str = "close",
index: str = "date",
length: int = 50,
offset: int = 0,
chart: bool = False,
) -> OBBject[List[Data]]:
...
```
We can easily deduct that the `ema` command accept data in the formats of `List[Data]` or `pandas.DataFrame`.
> Note that other types might be added in the future.
##### API Interface
When using the OpenBB Platform on a API Interface, the types are a bit more limited than on the Python one, as, for example, we can't use `pandas.DataFrame` as a type. However the same principles apply for what `Data` means, i.e., any given data processing command, which are characterized as POST endpoints on the API, will accept data as a list of records on the **request body**, i.e.:
```json
[
{
"open": 80,
"high": 80.69,
"low": 77.37,
"close": 77.62,
"volume": 2487300
}
...
]
```
## Contributor Guidelines
The Contributor Guidelines are intended to be a continuation of the [Developer Guidelines](#developer-guidelines). They are not a replacement, but rather an expansion, focusing specifically on those who seek to directly enhance the OpenBB Platform's codebase. It's crucial for Contributors to be familiar with both sets of guidelines to ensure a harmonious and productive engagement with the OpenBB Platform.
There are many ways to contribute to the OpenBB Platform. You can add a [new data point](#how-to-add-a-new-data-point), add a [new command](#openbb-platform-commands), add a [new visualization](/openbb_platform/extensions/charting/README.md), add a [new extension](#how-to-build-openbb-extensions), fix a bug, improve or create documentation, etc.
### Expectations for Contributors
1. Use Cases:
- Ensure that your contributions directly enhance the OpenBB Platform's functionality or extension ecosystem.
2. Documentation:
- All code contributions should come with relevant documentation, including the purpose of the contribution, how it works, and any changes it makes to existing functionalities.
- Update any existing documentation if your contribution alters the behavior of the OpenBB Platform.
3. Code Quality:
- Your code should adhere strictly to the OpenBB Platform's coding standards and conventions.
- Ensure clarity, maintainability, and proper organization in your code.
4. Testing:
- All contributions must be thoroughly tested to avoid introducing bugs to the OpenBB Platform.
- Contributions should include relevant automated tests (unit and integration), and any new feature should come with its test cases.
5. Performance:
- Your contributions should be optimized for performance and should not degrade the overall efficiency of the OpenBB Platform.
- Address any potential bottlenecks and ensure scalability.
6. Collaboration:
- Engage actively with the OpenBB development team to ensure that your contributions align with the platform's roadmap and standards.
- Welcome feedback and be open to making revisions based on reviews and suggestions from the community.
### Quality Assurance
We are strong believers in the Quality Assurance (QA) process and we want to make sure that all the extensions that are added to the OpenBB Platform are of high quality. To ensure this, we have a set of QA tools that you can use to test your extension.
Primarily, we have tools that semi-automate the creation of unit and integration tests.
> The QA tools are still in development and we are constantly improving them.
#### Unit tests
Each `Fetcher` comes equipped with a `test` method that will ensure that it is implemented correctly and that it is returning the expected data. It also ensures that all types are correct and that the data is valid.
To create unit tests for your Fetchers, you can run the following command:
```bash
python openbb_platform/providers/tests/utils/unit_tests_generator.py
```
> Note that you should be running this file from the root of the repository.
> Note that the `tests` folder must exist in order to generate the tests.
The automatic unit test generation will add unit tests for all the fetchers available in a given provider.
To record the unit tests, you can run the following command:
```bash
pytest <path_to_the_unit_test_file> --record=all
```
> Note that sometimes manual intervention is needed. For example, adjusting out-of-top level imports or adding specific arguments for a given fetcher.
#### Integration tests
The integration tests are a bit more complex than the unit tests, as we want to test both the Python interface and the API interface. For this, we have two scripts that will help you generate the integration tests.
To generate the integration tests for the Python interface, you can run the following command:
```bash
python openbb_platform/extensions/tests/utils/integration_tests_generator.py
```
To generate the integration tests for the API interface, you can run the following command:
```bash
python openbb_platform/extensions/tests/utils/integration_tests_api_generator.py
```
When testing the API interface, you'll need to run the OpenBB Platform locally before running the tests. To do so, you can run the following command:
```bash
uvicorn openbb_platform.platform.core.openbb_core.api.rest_api:app --host 0.0.0.0 --port 8000 --reload
```
These automated tests are a great way to reduce the amount of code you need to write, but they are not a replacement for manual testing and might require tweaking. That's why we have unit tests that test the generated integration tests to ensure they cover all providers and parameters.
To run the tests we can do:
- Unit tests only:
```bash
pytest openbb_platform -m "not integration"
```
- Integration tests only:
```bash
pytest openbb_platform -m integration
```
- Both integration and unit tests:
```bash
pytest openbb_platform
```
#### Import time
We aim to have a short import time for the package. To measure that we use `tuna`.
- <https://pypi.org/project/tuna/>
To visualize the import time breakdown by module and find potential bottlenecks, run the
following commands from `openbb_platform` directory:
```bash
pip install tuna
python -X importtime openbb/__init__.py 2> import.log
tuna import.log
```
### Sharing your extension
We encourage you to share your extension with the community. You can do that by publishing it to PyPI.
#### Publish your extension to PyPI
To publish your extension to PyPI, you'll need to have a PyPI account and a PyPI API token.
##### Setup
Create an account and get an API token from <https://pypi.org/manage/account/token/>
Store the token with
```bash
poetry config pypi-token.pypi pypi-YYYYYYYY
```
##### Release
`cd` into the directory where your extension `pyproject.toml` lives and make sure that the `pyproject.toml` specifies the version tag you want to release and run.
```bash
poetry build
```
This will create a `/dist` folder in the directory, which will contain the `.whl` and `tar.gz` files matching the version to release.
If you want to test your package locally you can do it with
```bash
pip install dist/openbb_[FILE_NAME].whl
```
##### Publish
To publish your package to PyPI run:
```bash
poetry publish
```
Now, you can pip install your package from PyPI with:
```bash
pip install openbb-some_ext
```
### Manage extensions
To install an extension hosted on PyPI, use the `pip install <extension>` command.
To install an extension that is developed locally, ensure that it contains a `pyproject.toml` file and then use the `pip install <extension>` command.
> To install the extension in editable mode using pip, add the `-e` argument.
Alternatively, for local extensions, you can add this line in the `LOCAL_DEPS` variable in `dev_install.py` file:
```toml
# If this is a community dependency, add this under "Community dependencies",
# with additional argument optional = true
openbb-extension = { path = "<relative-path-to-the-extension>", develop = true }
```
Now you can use the `python dev_install.py [-e]` command to install the local extension.
#### Add an extension as a dependency
To add the `openbb-qa` extension as a dependency, you'll need to add it to the `pyproject.toml` file:
```toml
[tool.poetry.dependencies]
openbb-qa = "^0.0.0a2"
```
Then you can follow the same process as above to install the extension.
### Write code and commit
#### How to create a PR?
To create a PR to the OpenBB Platform, you'll need to fork the repository and create a new branch.
1. Create your Feature Branch, e.g. `git checkout -b feature/AmazingFeature`
2. Check the files you have touched using `git status`
3. Stage the files you want to commit, e.g.
`git add openbb_platform/platform/core/openbb_core/app/constants.py`.
Note: **DON'T** add any files with personal information.
4. Write a concise commit message under 50 characters, e.g. `git commit -m "meaningful commit message"`. If your PR
solves an issue raised by a user, you may specify such an issue by adding #ISSUE_NUMBER to the commit message, so that
these get linked. Note: If you installed pre-commit hooks and one of the formatters re-formats your code, you'll need
to go back to step 3 to add these.
##### Branch Naming Conventions
The accepted branch naming conventions are:
- `feature/feature-name`
- `hotfix/hotfix-name`
These branches can only have PRs pointing to the `develop` branch.

16
openbb_platform/EXTENSIONS.md vendored Normal file
View File

@@ -0,0 +1,16 @@
# Extensions for OpenBB Platform
| Extension | Package Name | Description | Maintainer |
| --- | --- | --- | --- |
| [Equity](./extensions/equity/README.md) | `openbb-equity` | Historical pricing data, fundamentals, options, sector and industry, and overall due diligence. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Economy](./extensions/economy/README.md) | `openbb-economy` | Global macroeconomic data. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Index](./extensions/index/README.md) | `openbb-index` | Market index data. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [News](./extensions/news/README.md) | `openbb-news` | News articles. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Crypto](./extensions/crypto/README.md) | `openbb-crypto` | Cryptocurrency historical pricing data. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Fixed Income](./extensions/fixed_income/README.md) | `openbb-fixed-income` | Central bank decisions, yield curves, government bonds and corporate bonds data. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Currency](./extensions/currency/README.md) | `openbb-currency` | Historical pricing data, foreign exchanges, quotes and forward rates for currency pairs. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Charting](./extensions/charting/README.md) | `openbb-charting` | Charting Manager extension for OpenBB. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Quantitative Analysis](./extensions/qa/README.md) | `openbb-qa` | Statistical and computational techniques for financial data. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Technical Analysis](./extensions/ta/README.md) | `openbb-ta` | Tools and techniques for financial market analysis. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
<!-- Add your Extension above this line -->

11
openbb_platform/PROVIDERS.md vendored Normal file
View File

@@ -0,0 +1,11 @@
# Data Provider Integrations for OpenBB
| Provider | URL | Package name | Description | Maintainer |
| --- | --- | --- | --- | --- |
| [FMP](./providers/fmp/README.md) | https://financialmodelingprep.com/ | `openbb-fmp` | Access all stocks discounted cash flow statements, market price, stock markets news, and learn more about Financial Modeling. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Polygon](./providers/polygon/README.md) | https://polygon.io/ | `openbb-polygon` | Free stock data APIs. Real time and historical data, unlimited usage, tick level and aggregate granularity, in standardized JSON and CSV formats. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Benzinga](./providers/benzinga/README.md) | https://www.benzinga.com/ | `openbb-benzinga` | Stock Market Quotes, Business News, Financial News, Trading Ideas, and Stock Research by Professionals. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [FRED](./providers/fred/README.md) | https://fred.stlouisfed.org/ | `openbb-fred` | Download, graph, and track 823000 economic time series from 114 sources. | [@OpenBB-Finance](https://github.com/OpenBB-finance) |
| [Ultima](./providers/ultima/README.md) | https://ultimainsights.ai | `openbb-ultima` | Ultima Insights harnesses the power of LLMs + GPT to present relevant news to investors, often before it appears on platforms like Bloomberg. Ultima aims to provide timely and significant information for its users. | [@Ultima-Insights](https://github.com/Ultima-Insights)
<!-- Add your Data Provider integration above this line -->

146
openbb_platform/README.md vendored Normal file
View File

@@ -0,0 +1,146 @@
# OpenBB Platform
[![Downloads](https://static.pepy.tech/badge/openbb)](https://pepy.tech/project/openbb)
[![LatestRelease](https://badge.fury.io/py/openbb.svg)](https://github.com/OpenBB-finance/OpenBBTerminal)
| OpenBB is committed to build the future of investment research by focusing on an open source infrastructure accessible to everyone, everywhere. |
|:--:|
| ![OpenBBLogo](https://user-images.githubusercontent.com/25267873/218899768-1f0964b8-326c-4f35-af6f-ea0946ac970b.png) |
| Check our website at [openbb.co](www.openbb.co) |
## Overview
The OpenBB Platform provides a convenient way to access raw financial data from multiple data providers. The package comes with a ready to use REST API - this allows developers from any language to easily create applications on top of OpenBB Platform.
## Installation
The command below provides access to the core functionalities behind the OpenBB Platform.
```bash
pip install openbb
```
This will install the following data providers:
| Extension Name | Description | Installation Command | Minimum Subscription Type Required |
|----------------|-------------|----------------------|------------------------------------|
| openbb-benzinga | [Benzinga](https://www.benzinga.com/apis/en-ca/) data connector | pip install openbb-benzinga | Paid |
| openbb-biztoc | [Biztoc](https://api.biztoc.com/#biztoc-default) News data connector | pip install openbb-biztoc | Free |
| openbb-ecb | [ECB](https://data.ecb.europa.eu/) data connector | pip install openbb-ecb | None |
| openbb-finra | [FINRA](https://www.finra.org/finra-data) data connector | pip install openbb-finra | None / Free |
| openbb-fmp | [FMP](https://site.financialmodelingprep.com/developer/) data connector | pip install openbb-fmp | Free |
| openbb-fred | [FRED](https://fred.stlouisfed.org/) data connector | pip install openbb-fred | Free |
| openbb-intrinio | [Intrinio](https://intrinio.com/pricing) data connector | pip install openbb-intrinio | Paid |
| openbb-oecd | [OECD](https://data.oecd.org/) data connector | pip install openbb-oecd | Free |
| openbb-polygon | [Polygon](https://polygon.io/) data connector | pip install openbb-polygon | Free |
| openbb-sec | [SEC](https://www.sec.gov/edgar/sec-api-documentation) data connector | pip install openbb-sec | None |
| openbb-tiingo | [Tiingo](https://www.tiingo.com/about/pricing) data connector | pip install openbb-tiingo | Free |
| openbb-tradingeconomics | [TradingEconomics](https://tradingeconomics.com/api) data connector | pip install openbb-tradingeconomics | Paid |
| openbb-ultima | [Ultima Insights](https://ultimainsights.ai/openbb) data connector | pip install openbb-ultima | Paid |
To install extensions that expand the core functionalities specify the extension name or use `all` to install all.
```bash
# Install a single extension, e.g. openbb-charting and yahoo finance
pip install openbb[charting]
pip install openbb-yfinance
```
Alternatively, you can install all extensions at once.
```bash
pip install openbb[all]
```
> Note: These instruction are specific to v4. For installation instructions and documentation for v3 go to our [website](https://docs.openbb.co/sdk).
## Python
```python
>>> from openbb import obb
>>> output = obb.equity.price.historical("AAPL")
>>> df = output.to_dataframe()
>>> df.head()
open high low ... change_percent label change_over_time
date ...
2022-09-19 149.31 154.56 149.10 ... 3.46000 September 19, 22 0.034600
2022-09-20 153.40 158.08 153.08 ... 2.28000 September 20, 22 0.022800
2022-09-21 157.34 158.74 153.60 ... -2.30000 September 21, 22 -0.023000
2022-09-22 152.38 154.47 150.91 ... 0.23625 September 22, 22 0.002363
2022-09-23 151.19 151.47 148.56 ... -0.50268 September 23, 22 -0.005027
[5 rows x 12 columns]
```
## API keys
To fully leverage the OpenBB Platform you need to get some API keys to connect with data providers. Here are the 3 options on where to set them:
1. OpenBB Hub
2. Runtime
3. Local file
### 1. OpenBB Hub
Set your keys at [OpenBB Hub](https://my.openbb.co/app/sdk/api-keys) and get your personal access token from https://my.openbb.co/app/sdk/pat to connect with your account.
```python
>>> from openbb import obb
>>> openbb.account.login(pat="OPENBB_PAT")
>>> # Persist changes in OpenBB Hub
>>> obb.account.save()
```
### 2. Runtime
```python
>>> from openbb import obb
>>> obb.user.credentials.fmp_api_key = "REPLACE_ME"
>>> obb.user.credentials.polygon_api_key = "REPLACE_ME"
>>> # Persist changes in ~/.openbb_platform/user_settings.json
>>> obb.account.save()
```
### 3. Local file
You can specify the keys directly in the `~/.openbb_platform/user_settings.json` file.
Populate this file with the following template and replace the values with your keys:
```json
{
"credentials": {
"fmp_api_key": "REPLACE_ME",
"polygon_api_key": "REPLACE_ME",
"benzinga_api_key": "REPLACE_ME",
"fred_api_key": "REPLACE_ME"
}
}
```
## REST API
The OpenBB Platform comes with a ready to use REST API built with FastAPI. Start the application using this command:
```bash
uvicorn openbb_core.api.rest_api:app --host 0.0.0.0 --port 8000 --reload
```
Check `openbb-core` [README](https://pypi.org/project/openbb-core/) for additional info.
## Install for development
To develop the OpenBB Platform you need to have the following:
- Git
- Python 3.8 or higher
- Virtual Environment with `poetry` and `toml` packages installed
- To install these packages activate your virtual environment and run `pip install poetry toml`
How to install the platform in editable mode?
1. Activate your virtual environment
1. Navigate into the `openbb_platform` folder
1. Run `python dev_install.py` to install the packages in editable mode

57
openbb_platform/core/README.md vendored Normal file
View File

@@ -0,0 +1,57 @@
# OpenBB Platform - Core
## Overview
The Core extension serves as the foundational component of the OpenBB Platform. It encapsulates essential functionalities and serves as an infrastructural base for other extensions. This extension is vital for maintaining the integrity and standardization of the platform.
## Key Features
- **Standardized Data Model** (`Data` Class): A flexible and dynamic Pydantic model capable of handling various data structures.
- **Standardized Query Params** (`QueryParams` Class): A Pydantic model for handling querying to different providers.
- **Dynamic Field Support**: Enables handling of undefined fields, providing versatility in data processing.
- **Robust Data Validation**: Utilizes Pydantic's validation features to ensure data integrity.
- **API Routing Mechanism** (`Router` Class): Simplifies the process of defining API routes and endpoints - out of the box Python and Web endpoints.
## Getting Started
### Prerequisites
- Python 3.8 or higher.
- Familiarity with FastAPI and Pydantic.
### Installation
Installing through pip:
```bash
pip install openbb-core
```
> Note that, the openbb-core is an infrastructural component of the OpenBB Platform. It is not intended to be used as a standalone package.
### Usage
The Core extension is used as the basis for building and integrating new data sources, providers, and extensions into the OpenBB Platform. It provides the necessary classes and structures for standardizing and handling data.
### Contributing
We welcome contributions! If you're looking to contribute, please:
- Follow the existing coding standards and conventions.
- Write clear, documented code.
- Ensure your code does not negatively impact performance.
- Test your contributions thoroughly.
Please refer to our [Contributing Guidelines](https://docs.openbb.co/platform/development).
### Collaboration
Engage with the development team and the community. Be open to feedback and collaborative discussions.
### Support
For support, questions, or more information, please visit [OpenBB Platform Documentation](https://docs.openbb.co/platform/development).
### License
This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/OpenBB-finance/OpenBBTerminal/blob/main/LICENSE) file for details.

View File

@@ -0,0 +1,90 @@
import contextlib
import sys
import pytest
with contextlib.suppress(ImportError):
import polars as pl
with contextlib.suppress(ImportError):
import pandas as pd
with contextlib.suppress(ImportError):
import numpy as np
with contextlib.suppress(ImportError):
from openbb_charting.core.openbb_figure import OpenBBFigure
# pylint: disable=inconsistent-return-statements
@pytest.fixture(scope="session")
def obb(pytestconfig):
"""Fixture to setup obb."""
if pytestconfig.getoption("markexpr") != "not integration":
import openbb # pylint: disable=import-outside-toplevel
return openbb.obb
# pylint: disable=redefined-outer-name
@pytest.mark.skipif("pandas" not in sys.modules, reason="pandas not installed")
@pytest.mark.integration
def test_to_dataframe(obb):
"""Test obbject to dataframe."""
stocks_df = obb.equity.price.historical("AAPL", provider="fmp").to_dataframe()
assert isinstance(stocks_df, pd.DataFrame)
@pytest.mark.skipif("polars" not in sys.modules, reason="polars not installed")
@pytest.mark.integration
def test_to_polars(obb):
"""Test obbject to polars."""
crypto_pl = obb.crypto.price.historical("BTC-USD", provider="fmp").to_polars()
assert isinstance(crypto_pl, pl.DataFrame)
@pytest.mark.skipif("numpy" not in sys.modules, reason="numpy not installed")
@pytest.mark.integration
def test_to_numpy(obb):
"""Test obbject to numpy array."""
cpi_np = obb.economy.cpi(
countries=["portugal", "spain", "switzerland"], frequency="annual"
).to_numpy()
assert isinstance(cpi_np, np.ndarray)
@pytest.mark.integration
def test_to_dict(obb):
"""Test obbject to dict."""
fed_dict = obb.fixedincome.rate.effr(start_date="2010-01-01").to_dict()
assert isinstance(fed_dict, dict)
@pytest.mark.skipif(
"openbb_charting" not in sys.modules, reason="openbb_charting not installed"
)
@pytest.mark.integration
def test_to_chart(obb):
"""Test obbject to chart."""
stocks_chart = obb.equity.price.historical("AAPL", provider="fmp").to_chart()
assert isinstance(stocks_chart, OpenBBFigure)
@pytest.mark.skipif(
"openbb_charting" not in sys.modules, reason="openbb_charting not installed"
)
@pytest.mark.integration
def test_show(obb):
"""Test obbject to chart."""
stocks_data = obb.equity.price.historical("AAPL", provider="fmp", chart=True)
assert isinstance(stocks_data.chart.fig, OpenBBFigure)
assert stocks_data.chart.fig.show() is None

View File

@@ -0,0 +1,18 @@
from typing import List, Optional
from fastapi import APIRouter, FastAPI
class AppLoader:
"""App loader."""
@staticmethod
def from_routers(
app: FastAPI, routers: List[Optional[APIRouter]], prefix: str
) -> FastAPI:
"""Load routers to app."""
for router in routers:
if router:
app.include_router(router=router, prefix=prefix)
return app

View File

@@ -0,0 +1,56 @@
import secrets
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from openbb_core.app.model.user_settings import UserSettings
from openbb_core.app.service.user_service import UserService
from openbb_core.env import Env
from typing_extensions import Annotated
security = HTTPBasic() if Env().API_AUTH else lambda: None
async def authenticate_user(
credentials: Annotated[Optional[HTTPBasicCredentials], Depends(security)]
):
"""Authenticate the user."""
if credentials:
username = Env().API_USERNAME
password = Env().API_PASSWORD
is_correct_username = False
is_correct_password = False
if username is not None and password is not None:
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = username.encode("utf8")
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = password.encode("utf8")
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Basic"},
)
async def get_user_service() -> UserService:
"""Get user service."""
return UserService()
async def get_user_settings(
_: Annotated[None, Depends(authenticate_user)],
user_service: Annotated[UserService, Depends(get_user_service)],
) -> UserSettings:
"""Get user settings."""
return user_service.default_user_settings

View File

@@ -0,0 +1,21 @@
"""Coverage dependency."""
from fastapi import Depends
from openbb_core.app.provider_interface import ProviderInterface
from openbb_core.app.router import CommandMap
from openbb_core.app.service.auth_service import AuthService
from typing_extensions import Annotated
async def get_command_map(
_: Annotated[None, Depends(AuthService().auth_hook)]
) -> CommandMap:
"""Get command map."""
return CommandMap()
async def get_provider_interface(
_: Annotated[None, Depends(AuthService().auth_hook)]
) -> ProviderInterface:
"""Get provider interface."""
return ProviderInterface()

View File

@@ -0,0 +1,20 @@
"""System dependency."""
from fastapi import Depends
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.service.auth_service import AuthService
from openbb_core.app.service.system_service import SystemService
from typing_extensions import Annotated
async def get_system_service() -> SystemService:
"""Get system service."""
return SystemService()
async def get_system_settings(
_: Annotated[None, Depends(AuthService().auth_hook)],
system_service: Annotated[SystemService, Depends(get_system_service)],
) -> SystemSettings:
"""Get system settings."""
return system_service.system_settings

View File

@@ -0,0 +1,114 @@
"""REST API for the OpenBB Platform."""
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from openbb_core.api.app_loader import AppLoader
from openbb_core.api.router.commands import router as router_commands
from openbb_core.api.router.coverage import router as router_coverage
from openbb_core.api.router.system import router as router_system
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.service.auth_service import AuthService
from openbb_core.app.service.system_service import SystemService
from openbb_core.env import Env
logger = logging.getLogger("uvicorn.error")
system = SystemService().system_settings
@asynccontextmanager
async def lifespan(_: FastAPI):
"""Startup event."""
auth = "ENABLED" if Env().API_AUTH else "DISABLED"
banner = rf"""
███╗
█████████████████╔══█████████████████╗ OpenBB Platform v{system.version}
███╔══════════███║ ███╔══════════███║
█████████████████║ █████████████████║ Authentication: {auth}
╚═════════════███║ ███╔═════════════╝
██████████████║ ██████████████╗
███╔═══════███║ ███╔═══════███║
██████████████║ ██████████████║
╚═════════════╝ ╚═════════════╝
Investment research for everyone, anywhere.
https://my.openbb.co/app/platform
"""
logger.info(banner)
yield
app = FastAPI(
title=system.api_settings.title,
description=system.api_settings.description,
version=system.api_settings.version,
terms_of_service=system.api_settings.terms_of_service,
contact={
"name": system.api_settings.contact_name,
"url": system.api_settings.contact_url,
"email": system.api_settings.contact_email,
},
license_info={
"name": system.api_settings.license_name,
"url": system.api_settings.license_url,
},
servers=[
{
"url": s.url,
"description": s.description,
}
for s in system.api_settings.servers
],
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,
)
AppLoader.from_routers(
app=app,
routers=[AuthService().router, router_system, router_coverage, router_commands]
if Env().DEV_MODE
else [router_commands],
prefix=system.api_settings.prefix,
)
@app.exception_handler(Exception)
async def api_exception_handler(_: Request, exc: Exception):
"""Exception handler for all other exceptions."""
return JSONResponse(
status_code=404,
content={
"detail": str(exc),
"error_kind": exc.__class__.__name__,
},
)
@app.exception_handler(OpenBBError)
async def openbb_exception_handler(_: Request, exc: OpenBBError):
"""Exception handler for OpenBB errors."""
openbb_error = exc.original
status_code = 400 if "No results" in str(openbb_error) else 500
return JSONResponse(
status_code=status_code,
content={
"detail": str(openbb_error),
"error_kind": openbb_error.__class__.__name__,
},
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("openbb_core.api.rest_api:app", reload=True)

View File

@@ -0,0 +1,181 @@
import inspect
from functools import partial, wraps
from inspect import Parameter, Signature, signature
from typing import Any, Callable, Dict, Tuple, TypeVar
from fastapi import APIRouter, Depends
from fastapi.routing import APIRoute
from openbb_core.app.charting_service import ChartingService
from openbb_core.app.command_runner import CommandRunner
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.model.user_settings import UserSettings
from openbb_core.app.router import RouterLoader
from openbb_core.app.service.auth_service import AuthService
from openbb_core.app.service.system_service import SystemService
from openbb_core.app.service.user_service import UserService
from openbb_core.env import Env
from pydantic import BaseModel
from typing_extensions import Annotated, ParamSpec
T = TypeVar("T")
P = ParamSpec("P")
router = APIRouter(prefix="")
def build_new_annotation_map(sig: Signature) -> Dict[str, Any]:
"""Build new annotation map."""
annotation_map = {}
parameter_list = sig.parameters.values()
for parameter in parameter_list:
annotation_map[parameter.name] = parameter.annotation
annotation_map["return"] = sig.return_annotation
return annotation_map
def build_new_signature(path: str, func: Callable) -> Signature:
"""Build new function signature."""
sig = signature(func)
parameter_list = sig.parameters.values()
return_annotation = sig.return_annotation
new_parameter_list = []
for parameter in parameter_list:
if parameter.name == "cc" and parameter.annotation == CommandContext:
continue
new_parameter_list.append(
Parameter(
parameter.name,
kind=parameter.kind,
default=parameter.default,
annotation=parameter.annotation,
)
)
if (
path.replace("/", "_")[1:]
in ChartingService.get_implemented_charting_functions()
):
new_parameter_list.append(
Parameter(
"chart",
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=False,
annotation=bool,
)
)
if Env().API_AUTH:
new_parameter_list.append(
Parameter(
"__authenticated_user_settings",
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=UserSettings(),
annotation=Annotated[
UserSettings, Depends(AuthService().user_settings_hook)
],
)
)
return Signature(
parameters=new_parameter_list,
return_annotation=return_annotation,
)
def validate_output(c_out: OBBject) -> OBBject:
"""
Validate OBBject object.
Checks against the OBBject schema and removes fields that contain the
`exclude_from_api` extra `pydantic.Field` kwarg.
Note that the modification to the `OBBject` object is done in-place.
Parameters
----------
c_out : OBBject
OBBject object to validate.
Returns
-------
OBBject
Validated OBBject object.
"""
def is_model(type_):
return inspect.isclass(type_) and issubclass(type_, BaseModel)
def exclude_fields_from_api(key: str, value: Any):
type_ = type(value)
field = c_out.model_fields.get(key, None)
json_schema_extra = field.json_schema_extra if field else None
# case where 1st layer field needs to be excluded
if json_schema_extra and json_schema_extra.get("exclude_from_api", None):
delattr(c_out, key)
# if it's a model with nested fields
elif is_model(type_):
for field_name, field in type_.__fields__.items():
if field.json_schema_extra and field.json_schema_extra.get(
"exclude_from_api", None
):
delattr(value, field_name)
# if it's a yet a nested model we need to go deeper in the recursion
elif is_model(field.annotation):
exclude_fields_from_api(field_name, getattr(value, field_name))
for k, v in c_out.model_copy():
exclude_fields_from_api(k, v)
return c_out.model_dump()
def build_api_wrapper(
command_runner: CommandRunner,
route: APIRoute,
) -> Callable:
"""Build API wrapper for a command."""
func: Callable = route.endpoint # type: ignore
path: str = route.path # type: ignore
new_signature = build_new_signature(path=path, func=func)
new_annotations_map = build_new_annotation_map(sig=new_signature)
func.__signature__ = new_signature # type: ignore
func.__annotations__ = new_annotations_map
@wraps(wrapped=func)
async def wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]):
user_settings: UserSettings = UserSettings.model_validate(
kwargs.pop(
"__authenticated_user_settings",
UserService.read_default_user_settings(),
)
)
execute = partial(command_runner.run, path, user_settings)
output: OBBject = await execute(*args, **kwargs)
output = validate_output(output)
return output
return wrapper
def add_command_map(command_runner: CommandRunner, api_router: APIRouter) -> None:
"""Add command map to the API router."""
plugins_router = RouterLoader.from_extensions()
for route in plugins_router.api_router.routes:
route.endpoint = build_api_wrapper(command_runner=command_runner, route=route) # type: ignore # noqa
api_router.include_router(router=plugins_router.api_router)
system_settings = SystemService(logging_sub_app="api").system_settings
command_runner_instance = CommandRunner(system_settings=system_settings)
add_command_map(command_runner=command_runner_instance, api_router=router)

View File

@@ -0,0 +1,36 @@
"""Coverage API router."""
from fastapi import APIRouter, Depends
from openbb_core.api.dependency.coverage import get_command_map, get_provider_interface
from openbb_core.app.provider_interface import ProviderInterface
from openbb_core.app.router import CommandMap
from typing_extensions import Annotated
router = APIRouter(prefix="/coverage", tags=["Coverage"])
@router.get("/command_model")
async def get_commands_model_map(
command_map: Annotated[CommandMap, Depends(get_command_map)],
provider_interface: Annotated[ProviderInterface, Depends(get_provider_interface)],
):
"""Get the command to model mapping."""
return {
command: provider_interface.map[command_map.commands_model[command][0]]
for command in command_map.commands_model
}
@router.get("/providers")
async def get_provider_coverage(
command_map: Annotated[CommandMap, Depends(get_command_map)]
):
"""Get provider coverage."""
return command_map.provider_coverage
@router.get("/commands")
async def get_command_coverage(
command_map: Annotated[CommandMap, Depends(get_command_map)]
):
"""Get command coverage."""
return command_map.command_coverage

View File

@@ -0,0 +1 @@
"""The init of the coverage helpers."""

View File

@@ -0,0 +1,119 @@
"""Coverage API router helper functions."""
from inspect import _empty, signature
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type
from openbb_core.app.provider_interface import ProviderInterface
from pydantic import BaseModel, Field, create_model
if TYPE_CHECKING:
from openbb_core.app.static.app_factory import BaseApp
provider_interface = ProviderInterface()
def get_route_callable(app: "BaseApp", route: str) -> Callable:
"""Get the callable for a route."""
# TODO: Add return typing Optional[Callable] to this function. First need to
# figure how to do that starting from "BaseApp" and account for the possibility
# of a route not existing. Then remove the type: ignore from the function.
split_route = route.replace(".", "/").split("/")[1:]
return_callable = app
for route_path in split_route:
return_callable = getattr(return_callable, route_path)
return return_callable # type: ignore
def signature_to_fields(app: "BaseApp", route: str) -> Dict[str, Tuple[Any, Field]]: # type: ignore
"""Convert a command signature to pydantic fields."""
return_callable = get_route_callable(app, route)
sig = signature(return_callable)
fields = {}
for name, param in sig.parameters.items():
if name not in ["kwargs", "args"]:
type_annotation = (
param.annotation if param.annotation is not _empty else Any
)
description = (
param.annotation.__metadata__[0].description
if hasattr(param.annotation, "__metadata__")
else None
)
fields[name] = (
type_annotation,
Field(..., title="openbb", description=description),
)
return fields
def dataclass_to_fields(model_name: str) -> Dict[str, Tuple[Any, Field]]: # type: ignore
"""Convert a dataclass to pydantic fields."""
dataclass = provider_interface.params[model_name]["extra"]
fields = {}
for name, field in dataclass.__dataclass_fields__.items():
type_annotation = field.default.annotation if field.default is not None else Any # type: ignore
description = field.default.description if field.default is not None else None # type: ignore
title = field.default.title if field.default is not None else None # type: ignore
fields[name] = (
type_annotation,
Field(..., title=title, description=description),
)
return fields
def create_combined_model(
model_name: str,
*field_sets: Dict[str, Tuple[Any, Field]], # type: ignore
filter_by_provider: Optional[str] = None,
) -> Type[BaseModel]:
"""Create a combined pydantic model."""
combined_fields = {}
for fields in field_sets:
for name, (type_annotation, field) in fields.items():
if (
filter_by_provider is None
or "openbb" in field.title # type: ignore
or (filter_by_provider in field.title) # type: ignore
):
combined_fields[name] = (type_annotation, field)
model = create_model(model_name, **combined_fields) # type: ignore
# # Clean up the metadata
for field in model.model_fields.values():
if hasattr(field, "metadata"):
field.metadata = None # type: ignore
return model
def get_route_schema_map(
app: "BaseApp",
command_model_map: Dict[str, str],
filter_by_provider: Optional[str] = None,
) -> Dict[str, Dict[str, Any]]:
"""Get the route schema map."""
route_schema_map = {}
for route, model in command_model_map.items():
input_model = create_combined_model(
route,
signature_to_fields(app, route),
dataclass_to_fields(model),
filter_by_provider=filter_by_provider,
)
output_model = provider_interface.return_schema[model]
return_callable = get_route_callable(app, route)
route_schema_map[route] = {
"input": input_model,
"output": output_model,
"callable": return_callable,
}
return route_schema_map

View File

@@ -0,0 +1,15 @@
"""System router."""
from fastapi import APIRouter, Depends
from openbb_core.api.dependency.system import get_system_settings
from openbb_core.app.model.system_settings import SystemSettings
from typing_extensions import Annotated
router = APIRouter(prefix="/system", tags=["System"])
@router.get("")
async def get_system_model(
system_settings: Annotated[SystemSettings, Depends(get_system_settings)],
):
"""Get system model."""
return system_settings

View File

@@ -0,0 +1,18 @@
"""OpenBB API Account Router."""
from fastapi import APIRouter, Depends
from openbb_core.api.auth.user import authenticate_user, get_user_settings
from openbb_core.app.model.user_settings import UserSettings
from typing_extensions import Annotated
router = APIRouter(prefix="/user", tags=["User"])
auth_hook = authenticate_user
user_settings_hook = get_user_settings
@router.get("/me")
async def read_user_settings(
user_settings: Annotated[UserSettings, Depends(get_user_settings)]
):
"""Read current user settings."""
return user_settings

View File

@@ -0,0 +1,307 @@
from importlib import import_module
from inspect import getmembers, getsource, isfunction
from typing import Callable, List, Optional, Tuple, TypeVar
from importlib_metadata import entry_points
from openbb_core.app.model.abstract.singleton import SingletonMeta
from openbb_core.app.model.charts.chart import Chart, ChartFormat
from openbb_core.app.model.charts.charting_settings import ChartingSettings
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.model.user_settings import UserSettings
from openbb_core.env import Env
T = TypeVar("T")
POETRY_PLUGIN = "openbb_core_extension"
# this is needed because static assets and api endpoints are built before any user is instantiated
EXTENSION_NAME = Env().CHARTING_EXTENSION
class ChartingServiceError(Exception):
pass
class ChartingService(metaclass=SingletonMeta):
"""
Charting service class.
It is responsible for retrieving and executing the charting function, corresponding
to a given route, from the user's preferred charting extension.
Parameters
----------
user_settings : UserSettings
User settings.
system_settings : SystemSettings
System settings.
Attributes
----------
_charting_extension : str
Charting extension name, which is retrieved from the user preferences.
_charting_extension_installed : bool
Either charting extension is installed or not.
Raises
------
ChartingServiceError
If charting extension is not installed.
"""
def __init__(
self,
user_settings: Optional[UserSettings] = None,
system_settings: Optional[SystemSettings] = None,
) -> None:
# Although the __init__ method states that both the user_settings and the system_settings
# are optional, they are actually required for the first initialization of the ChartingService.
# This is because the ChartingService is a singleton and it is initialized only once.
# We want to be able to call the ChartingService from other parts of the code without having
# to pass the user_settings and the system_settings every time.
if not user_settings or not system_settings:
raise ChartingServiceError(
"User settings and system settings must be provided."
)
self._charting_settings = ChartingSettings(user_settings, system_settings)
self._charting_extension = self._check_and_get_charting_extension_name(
user_settings.preferences.charting_extension
)
self._charting_extension_installed = self._check_charting_extension_installed(
self._charting_extension
)
@property
def charting_settings(self) -> ChartingSettings:
return self._charting_settings
@charting_settings.setter
def charting_settings(self, value: Tuple[SystemSettings, UserSettings]):
system_settings, user_settings = value
self._charting_settings = ChartingSettings(
user_settings=user_settings,
system_settings=system_settings,
)
@staticmethod
def _check_and_get_charting_extension_name(
user_preferences_charting_extension: str,
):
"""
Checks if the charting extension defined on user preferences is the same as the one defined in the env file.
"""
if user_preferences_charting_extension != EXTENSION_NAME:
raise ChartingServiceError(
f"The charting extension defined on user preferences must be the same as the one defined in the env file."
f"diff: {user_preferences_charting_extension} != {EXTENSION_NAME}"
)
return user_preferences_charting_extension
@staticmethod
def _check_charting_extension_installed(
charting_extension: str, plugin: str = POETRY_PLUGIN
) -> bool:
"""
Checks if charting extension is installed.
Given a charting extension name, it checks if it is installed under the given plugin.
Parameters
----------
charting_extension : str
Charting extension name.
plugin : Optional[str]
Plugin name.
Returns
-------
bool
Either charting extension is installed or not.
"""
extensions = [ext.name for ext in entry_points(group=plugin)]
return charting_extension in extensions
@staticmethod
def _get_extension_router(
extension_name: str, plugin: Optional[str] = POETRY_PLUGIN
):
"""
Get the module of the given extension.
"""
entry_points_ = entry_points(group=plugin)
entry_point = next(
(ep for ep in entry_points_ if ep.name == extension_name), None
)
if entry_point is None:
raise ChartingServiceError(
f"Extension '{extension_name}' is not installed."
)
return import_module(entry_point.module)
@staticmethod
def _handle_backend(charting_extension: str, charting_settings: ChartingSettings):
"""
Handles the backend of the given charting extension.
This function that the module expose in its root (__init__.py) the following functions:
- `create_backend(charting_settings: ChartingSettings)`
- `get_backend()`
Parameters
----------
charting_extension : str
Charting extension name.
charting_settings : ChartingSettings
Charting settings.
"""
# Dynamically import the backend module
backend_module = import_module(charting_extension)
create_backend_func = getattr(backend_module, "create_backend")
get_backend_func = getattr(backend_module, "get_backend")
create_backend_func(charting_settings=charting_settings)
get_backend_func().start(debug=charting_settings.debug_mode)
@classmethod
def _get_chart_format(cls, extension_name: str) -> ChartFormat:
"""
Given an extension name, it returns the chart format.
The module must contain the `CHART_FORMAT` attribute.
"""
module = cls._get_extension_router(extension_name)
return getattr(module, "CHART_FORMAT")
@classmethod
def _get_chart_function(cls, extension_name: str, route: str) -> Callable:
"""
Given an extension name and a route, it returns the chart function.
The module must contain the given route.
"""
adjusted_route = route.replace("/", "_")[1:]
module = cls._get_extension_router(extension_name)
return getattr(module, adjusted_route)
@classmethod
def get_implemented_charting_functions(
cls, extension_name: str = EXTENSION_NAME
) -> List[str]:
"""
Given an extension name, it returns the implemented charting functions from its router.
"""
implemented_functions = []
try:
if module := cls._get_extension_router(extension_name):
module_name = module.__name__
for name, obj in getmembers(module, isfunction):
if (
obj.__module__ == module_name
and not name.startswith("_")
and "NotImplementedError" not in getsource(obj)
):
implemented_functions.append(name)
except ChartingServiceError:
pass
return implemented_functions
def to_chart(self, **kwargs) -> Chart:
"""
Returns the chart object.
Parameters
----------
**kwargs
Keyword arguments to be passed to the charting extension.
Returns
-------
Chart
Chart object.
Raises
------
ChartingServiceError
If charting extension is not installed.
Exception
If the charting extension module does not contain the `to_chart` function.
"""
if not self._charting_extension_installed:
raise ChartingServiceError(
f"Charting extension `{self._charting_extension}` is not installed"
)
self._handle_backend(self._charting_extension, self._charting_settings)
# Dynamically import the charting module
backend_module = import_module(self._charting_extension)
# Get the `to_chart` function from the charting module
to_chart_func = getattr(backend_module, "to_chart")
# Add the charting settings to the kwargs
kwargs["charting_settings"] = self._charting_settings
fig, content = to_chart_func(**kwargs)
return Chart(
content=content,
format=self._get_chart_format(self._charting_extension),
fig=fig,
)
def chart(
self,
user_settings: UserSettings,
system_settings: SystemSettings,
route: str,
obbject_item: T,
**kwargs,
) -> Chart:
"""
If the charting extension is not installed, an error is raised.
Otherwise, a charting function will be retrieved and executed from the user's preferred charting extension.
This function assumes that, in order to successfully retrieve the charting function,
the charting extension uses the following naming convention to convert routes into charting functions:
- Route: `/stocks/load`
- Charting function: `stocks_load()`
- Route: `/ta/ema`
- Charting function: `ta_ema()`
Note that the route should be in its original format, since it will be converted inside this function.
Parameters
----------
user_settings : UserSettings
User settings.
route : str
Route name, example: `/stocks/load`.
obbject_item
Command output item.
Returns
-------
Chart
Chart object.
"""
self._charting_settings = ChartingSettings(user_settings, system_settings)
self._charting_extension = user_settings.preferences.charting_extension
self._charting_extension_installed = self._check_charting_extension_installed(
self._charting_extension
)
if not self._charting_extension_installed:
raise ChartingServiceError(
f"Charting extension `{self._charting_extension}` is not installed"
)
self._handle_backend(self._charting_extension, self._charting_settings)
kwargs["obbject_item"] = obbject_item
kwargs["charting_settings"] = self._charting_settings
charting_function = self._get_chart_function(self._charting_extension, route)
fig, content = charting_function(**kwargs)
return Chart(
content=content,
format=self._get_chart_format(self._charting_extension),
fig=fig,
)

View File

@@ -0,0 +1,446 @@
import warnings
from contextlib import nullcontext
from copy import deepcopy
from datetime import datetime
from inspect import Parameter, signature
from sys import exc_info
from time import perf_counter_ns
from typing import Any, Callable, ContextManager, Dict, List, Optional, Tuple, Union
from pydantic import ConfigDict, create_model
from openbb_core.app.charting_service import ChartingService
from openbb_core.app.logs.logging_service import LoggingService
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.abstract.warning import cast_warning
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.metadata import Metadata
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.model.user_settings import UserSettings
from openbb_core.app.provider_interface import ProviderInterface
from openbb_core.app.router import CommandMap
from openbb_core.app.service.system_service import SystemService
from openbb_core.app.service.user_service import UserService
from openbb_core.env import Env
from openbb_core.provider.utils.helpers import maybe_coroutine, run_async
class ExecutionContext:
def __init__(
self,
command_map: CommandMap,
route: str,
system_settings: SystemSettings,
user_settings: UserSettings,
) -> None:
self.command_map = command_map
self.route = route
self.system_settings = system_settings
self.user_settings = user_settings
class ParametersBuilder:
"""Build parameters for a function."""
@staticmethod
def get_polished_parameter_list(func: Callable) -> List[Parameter]:
"""Get the signature parameters values as a list."""
sig = signature(func)
parameter_list = list(sig.parameters.values())
return parameter_list
@staticmethod
def get_polished_func(func: Callable) -> Callable:
"""Remove __authenticated_user_settings from the function signature and annotations."""
func = deepcopy(func)
sig = signature(func)
parameter_map = dict(sig.parameters)
if "__authenticated_user_settings" in parameter_map:
parameter_map.pop("__authenticated_user_settings")
parameter_list = list(parameter_map.values())
new_signature = signature(func).replace(parameters=parameter_list)
func.__signature__ = new_signature # type: ignore
func.__annotations__ = parameter_map
return func
@classmethod
def merge_args_and_kwargs(
cls,
func: Callable,
args: Tuple[Any],
kwargs: Dict[str, Any],
) -> Dict[str, Any]:
"""Merge args and kwargs into a single dict."""
args = deepcopy(args)
kwargs = deepcopy(kwargs)
parameter_list = cls.get_polished_parameter_list(func=func)
parameter_map = {}
for index, parameter in enumerate(parameter_list):
if index < len(args):
parameter_map[parameter.name] = args[index]
elif parameter.name in kwargs:
parameter_map[parameter.name] = kwargs[parameter.name]
elif parameter.default is not parameter.empty:
parameter_map[parameter.name] = parameter.default
else:
parameter_map[parameter.name] = None
return parameter_map
@staticmethod
def update_command_context(
func: Callable,
kwargs: Dict[str, Any],
system_settings: SystemSettings,
user_settings: UserSettings,
) -> Dict[str, Any]:
"""Update the command context with the available user and system settings."""
argcount = func.__code__.co_argcount
if "cc" in func.__code__.co_varnames[:argcount]:
kwargs["cc"] = CommandContext(
user_settings=user_settings,
system_settings=system_settings,
)
return kwargs
@staticmethod
def update_provider_choices(
func: Callable,
command_coverage: Dict[str, List[str]],
route: str,
kwargs: Dict[str, Any],
route_default: Optional[Dict[str, Optional[str]]],
) -> Dict[str, Any]:
"""Update the provider choices with the available providers and set default provider."""
def _needs_provider(func: Callable) -> bool:
"""Check if the function needs a provider."""
parameters = signature(func).parameters.keys()
return "provider_choices" in parameters
def _has_provider(kwargs: Dict[str, Any]) -> bool:
"""Check if the kwargs already have a provider."""
provider_choices = kwargs.get("provider_choices", None)
if isinstance(provider_choices, dict): # when in python
return provider_choices.get("provider", None) is not None
if isinstance(provider_choices, object): # when running as fastapi
return getattr(provider_choices, "provider", None) is not None
return False
def _get_first_provider() -> Optional[str]:
"""Get the first available provider."""
available_providers = ProviderInterface().available_providers
return available_providers[0] if available_providers else None
def _get_default_provider(
command_coverage: Dict[str, List[str]],
route_default: Optional[Dict[str, Optional[str]]],
) -> Optional[str]:
"""Get the default provider for the given route.
Either pick it from the user defaults or from the command coverage."""
cmd_cov_given_route = command_coverage.get(route, None)
command_cov_provider = (
cmd_cov_given_route[0] if cmd_cov_given_route else None
)
if route_default:
return route_default.get("provider", None) or command_cov_provider # type: ignore
return command_cov_provider
if not _has_provider(kwargs) and _needs_provider(func):
provider = (
_get_default_provider(
command_coverage,
route_default,
)
if route in command_coverage
else _get_first_provider()
)
kwargs["provider_choices"] = {"provider": provider}
return kwargs
@staticmethod
def validate_kwargs(
func: Callable,
kwargs: Dict[str, Any],
) -> Dict[str, Any]:
"""Validate kwargs and if possible coerce to the correct type"""
config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
sig = signature(func)
fields = {
n: (
p.annotation,
... if p.default is Parameter.empty else p.default,
)
for n, p in sig.parameters.items()
}
ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore
model = ValidationModel(**kwargs)
result = dict(model)
return result
@classmethod
def build(
cls,
args: Tuple[Any],
execution_context: ExecutionContext,
func: Callable,
route: str,
kwargs: Dict[str, Any],
) -> Dict[str, Any]:
func = cls.get_polished_func(func=func)
system_settings = execution_context.system_settings
user_settings = execution_context.user_settings
command_map = execution_context.command_map
kwargs = cls.merge_args_and_kwargs(
func=func,
args=args,
kwargs=kwargs,
)
kwargs = cls.update_command_context(
func=func,
kwargs=kwargs,
system_settings=system_settings,
user_settings=user_settings,
)
kwargs = cls.update_provider_choices(
func=func,
command_coverage=command_map.command_coverage,
route=route,
kwargs=kwargs,
route_default=user_settings.defaults.routes.get(route, None),
)
kwargs = cls.validate_kwargs(func=func, kwargs=kwargs)
return kwargs
class StaticCommandRunner:
@classmethod
async def _command(cls, func: Callable, kwargs: Dict[str, Any]) -> OBBject:
"""Run a command and return the output"""
context_manager: Union[warnings.catch_warnings, ContextManager[None]] = (
warnings.catch_warnings(record=True)
if not Env().DEBUG_MODE
else nullcontext()
)
with context_manager as warning_list:
obbject = await maybe_coroutine(func, **kwargs)
obbject.provider = getattr(
kwargs.get("provider_choices", None), "provider", None
)
if warning_list:
obbject.warnings = list(map(cast_warning, warning_list))
return obbject
@classmethod
def _chart(
cls,
obbject: OBBject,
user_settings: UserSettings,
system_settings: SystemSettings,
route: str,
**kwargs,
) -> None:
"""Create a chart from the command output."""
cs = ChartingService(
user_settings=user_settings, system_settings=system_settings
)
obbject.chart = cs.chart(
user_settings=user_settings,
system_settings=system_settings,
route=route,
obbject_item=obbject.results,
**kwargs,
)
@classmethod
async def _execute_func(
cls,
route: str,
args: Tuple[Any],
execution_context: ExecutionContext,
func: Callable,
kwargs: Dict[str, Any],
) -> OBBject:
"""Execute a function and return the output"""
user_settings = execution_context.user_settings
system_settings = execution_context.system_settings
# If we're on Jupyter we need to pop here because we will lose "chart" after
# ParametersBuilder.build. This needs to be fixed in a way that chart is
# added to the function signature and shared for jupyter and api
# We can check in the router decorator if the given function has a chart
# in the charting extension then we add it there. This way we can remove
# the chart parameter from the commands.py and package_builder, it will be
# added to the function signature in the router decorator
chart = kwargs.pop("chart", False)
kwargs = ParametersBuilder.build(
args=args,
execution_context=execution_context,
func=func,
route=route,
kwargs=kwargs,
)
# If we're on the api we need to remove "chart" here because the parameter is added on
# commands.py and the function signature does not expect "chart"
kwargs.pop("chart", None)
try:
obbject = await cls._command(
func=func,
kwargs=kwargs,
)
if chart and obbject.results:
cls._chart(
obbject=obbject,
user_settings=user_settings,
system_settings=system_settings,
route=route,
**kwargs,
)
except Exception as e:
raise OpenBBError(e) from e
finally:
ls = LoggingService(
user_settings=user_settings, system_settings=system_settings
)
ls.log(
user_settings=user_settings,
system_settings=system_settings,
route=route,
func=func,
kwargs=kwargs,
exec_info=exc_info(),
)
return obbject
@classmethod
async def run(
cls,
execution_context: ExecutionContext,
/,
*args,
**kwargs,
) -> OBBject:
timestamp = datetime.now()
start_ns = perf_counter_ns()
command_map = execution_context.command_map
route = execution_context.route
if func := command_map.get_command(route=route):
obbject = await cls._execute_func(
route=route,
args=args, # type: ignore
execution_context=execution_context,
func=func,
kwargs=kwargs,
)
else:
raise AttributeError(f"Invalid command : route={route}")
duration = perf_counter_ns() - start_ns
if execution_context.user_settings.preferences.metadata:
try:
obbject.extra["metadata"] = Metadata(
arguments=kwargs,
duration=duration,
route=route,
timestamp=timestamp,
)
except Exception as e:
if Env().DEBUG_MODE:
raise OpenBBError(e) from e
return obbject
class CommandRunner:
def __init__(
self,
command_map: Optional[CommandMap] = None,
system_settings: Optional[SystemSettings] = None,
user_settings: Optional[UserSettings] = None,
) -> None:
self._command_map = command_map or CommandMap()
self._system_settings = system_settings or SystemService().system_settings
self._user_settings = user_settings or UserService.read_default_user_settings()
self._logging_service = LoggingService(
system_settings=self._system_settings, user_settings=self._user_settings
)
self._charting_service = ChartingService(
system_settings=self._system_settings, user_settings=self._user_settings
)
@property
def command_map(self) -> CommandMap:
return self._command_map
@property
def system_settings(self) -> SystemSettings:
return self._system_settings
@property
def user_settings(self) -> UserSettings:
return self._user_settings
@user_settings.setter
def user_settings(self, user_settings: UserSettings) -> None:
self._user_settings = user_settings
async def run(
self,
route: str,
user_settings: Optional[UserSettings] = None,
/,
*args,
**kwargs,
) -> OBBject:
"""Run a command and return the OBBject as output."""
self._user_settings = user_settings or self._user_settings
execution_context = ExecutionContext(
command_map=self._command_map,
route=route,
system_settings=self._system_settings,
user_settings=self._user_settings,
)
return await StaticCommandRunner.run(execution_context, *args, **kwargs)
def sync_run(
self,
route: str,
user_settings: Optional[UserSettings] = None,
/,
*args,
**kwargs,
) -> OBBject:
"""Run a command and return the OBBject as output."""
return run_async(self.run, route, user_settings, *args, **kwargs)

View File

@@ -0,0 +1,7 @@
"""Constants for the OpenBB Platform."""
from pathlib import Path
HOME_DIRECTORY = Path.home()
OPENBB_DIRECTORY = Path(HOME_DIRECTORY, ".openbb_platform")
USER_SETTINGS_PATH = Path(OPENBB_DIRECTORY, "user_settings.json")
SYSTEM_SETTINGS_PATH = Path(OPENBB_DIRECTORY, "system_settings.json")

View File

@@ -0,0 +1,181 @@
# IMPORTATION STANDARD
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"""
DATEFORMAT = "%Y-%m-%dT%H:%M:%S%z"
LOGFORMAT = "%(asctime)s|%(name)s|%(funcName)s|%(lineno)s|%(message)s"
LOGPREFIXFORMAT = (
"%(levelname)s|%(appName)s|%(commitHash)s|%(appId)s|%(sessionId)s|%(userId)s|"
)
@staticmethod
def calculate_level_name(record: logging.LogRecord) -> str:
if record.exc_text:
level_name = "X"
elif record.levelname:
level_name = record.levelname[0]
else:
level_name = "U"
return level_name
@staticmethod
def extract_log_extra(record: logging.LogRecord):
log_extra = dict()
if hasattr(record, "func_name_override"):
record.funcName = record.func_name_override # type: ignore
record.lineno = 0
if hasattr(record, "session_id"):
log_extra["sessionId"] = record.session_id # type: ignore
return log_extra
@staticmethod
def mock_ipv4(text: str) -> str:
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
replacement = " FILTERED_IP "
text_mocked = re.sub(pattern, replacement, text)
return text_mocked
@staticmethod
def mock_email(text: str) -> str:
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)
return text_mocked
@staticmethod
def mock_password(text: str) -> str:
pattern = r'("password": ")[^"]+'
replacement = r"\1 FILTERED_PASSWORD "
text_mocked = re.sub(pattern, replacement, text)
return text_mocked
@staticmethod
def mock_flair(text: str) -> str:
pattern = r'("FLAIR": "\[)(.*?)\]'
replacement = r"\1 FILTERED_FLAIR ]"
text_mocked = re.sub(pattern, replacement, text)
return text_mocked
@staticmethod
def mock_home_directory(text: str) -> str:
user_home_directory = str(Path.home().as_posix())
text_mocked = text.replace("\\", "/").replace(
user_home_directory, "MOCKING_USER_PATH"
)
return text_mocked
@staticmethod
def filter_special_tags(text: str) -> str:
text_filtered = text.replace("\n", " MOCKING_BREAKLINE ")
text_filtered = text_filtered.replace("'Traceback", "Traceback")
return text_filtered
@classmethod
def filter_piis(cls, text: str) -> str:
text_filtered = cls.mock_ipv4(text=text)
text_filtered = cls.mock_email(text=text_filtered)
text_filtered = cls.mock_password(text=text_filtered)
text_filtered = cls.mock_home_directory(text=text_filtered)
text_filtered = cls.mock_flair(text=text_filtered)
return text_filtered
@classmethod
def filter_log_line(cls, text: str):
text_filtered = cls.filter_special_tags(text=text)
text_filtered = cls.filter_piis(text=text_filtered)
return text_filtered
# OVERRIDE
def __init__(
self,
settings: LoggingSettings,
style="%",
validate=True,
) -> None:
super().__init__(
fmt=self.LOGFORMAT,
datefmt=self.DATEFORMAT,
style=style,
validate=validate,
)
self.settings = settings
@property
def settings(self) -> LoggingSettings:
return deepcopy(self.__settings)
@settings.setter
def settings(self, settings: LoggingSettings) -> None:
self.__settings = settings
# OVERRIDE
def formatException(self, ei) -> str:
"""Exception formatting handler
Parameters
----------
ei : logging._SysExcInfoType
Exception to be logged
Returns
----------
str
Formatted exception
"""
result = super().formatException(ei)
return repr(result)
# OVERRIDE
def format(self, record: logging.LogRecord) -> str:
"""Log formatter
Parameters
----------
record : logging.LogRecord
Logging record
Returns
----------
str
Formatted_log message
"""
level_name = self.calculate_level_name(record=record)
log_prefix_content = {
"appName": self.settings.app_name,
"levelname": level_name,
"appId": self.settings.app_id,
"sessionId": self.settings.session_id,
"commitHash": "unknown-commit",
"userId": self.settings.user_id,
}
log_extra = self.extract_log_extra(record=record)
log_prefix_content = {**log_prefix_content, **log_extra}
log_prefix = self.LOGPREFIXFORMAT % log_prefix_content
record.msg = record.msg.replace("|", "-MOCK_PIPE-")
log_line = super().format(record)
log_line = self.filter_log_line(text=log_line)
log_line_full = log_prefix + log_line
return log_line_full

View File

@@ -0,0 +1,79 @@
# IMPORTATION STANDARD
from copy import deepcopy
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
# IMPORTATION THIRDPARTY
# IMPORTATION INTERNAL
from openbb_core.app.logs.models.logging_settings import LoggingSettings
from openbb_core.app.logs.utils.expired_files import (
get_expired_file_list,
get_timestamp_from_x_days,
remove_file_list,
)
ARCHIVES_FOLDER_NAME = "archives"
TMP_FOLDER_NAME = "tmp"
class PathTrackingFileHandler(TimedRotatingFileHandler):
@staticmethod
def build_log_file_path(settings: LoggingSettings) -> Path:
app_name = settings.app_name
directory = settings.user_logs_directory
session_id = settings.session_id
path = directory.absolute().joinpath(f"{app_name}_{session_id}")
return path
def clean_expired_files(self, before_timestamp: float):
"""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
expired_logs_file_list = get_expired_file_list(
directory=logs_dir,
before_timestamp=before_timestamp,
)
expired_archives_file_list = get_expired_file_list(
directory=archives_directory,
before_timestamp=before_timestamp,
)
expired_tmp_file_list = get_expired_file_list(
directory=tmp_directory,
before_timestamp=before_timestamp,
)
remove_file_list(file_list=expired_logs_file_list)
remove_file_list(file_list=expired_archives_file_list)
remove_file_list(file_list=expired_tmp_file_list)
@property
def settings(self) -> LoggingSettings:
return deepcopy(self.__settings)
@settings.setter
def settings(self, settings: LoggingSettings) -> None:
self.__settings = settings
# OVERRIDE
def __init__(
self,
settings: LoggingSettings,
*args,
**kwargs,
) -> None:
# SETUP PARENT CLASS
filename = str(self.build_log_file_path(settings=settings))
frequency = settings.frequency
kwargs["when"] = frequency
super().__init__(filename, *args, **kwargs)
self.suffix += ".log"
# SETUP CURRENT CLASS
self.__settings = settings
self.clean_expired_files(before_timestamp=get_timestamp_from_x_days(x=5))

View File

@@ -0,0 +1,130 @@
# IMPORT STANDARD
import json
import logging
import re
import warnings
from copy import deepcopy
from enum import Enum
from typing import Any, Dict
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
from openbb_core.app.logs.models.logging_settings import LoggingSettings
from openbb_core.env import Env
from posthog import Posthog
openbb_posthog = Posthog(
"phc_6FXLqu4uW9yxfyN8DpPdgzCdlYXOmIWdMGh6GnBgJLX", # pragma: allowlist secret
host="https://app.posthog.com",
)
class PosthogHandler(logging.Handler):
"""Posthog Handler"""
class AllowedEvents(Enum):
"""Allowed Posthog Events"""
log_startup = "log_startup"
log_cmd = "log_cmd"
log_error = "log_error"
log_warning = "log_warning"
def __init__(self, settings: LoggingSettings):
super().__init__()
self._settings = settings
self.logged_in = False
@property
def settings(self) -> LoggingSettings:
return deepcopy(self._settings)
@settings.setter
def settings(self, settings: LoggingSettings) -> None:
self._settings = settings
def emit(self, record: logging.LogRecord):
try:
self.send(record=record)
except Exception:
self.handleError(record)
def log_to_dict(self, log_info: str) -> dict:
"""Log to dict"""
log_regex = r"(STARTUP|CMD|ERROR): (.*)"
log_dict: Dict[str, Any] = {}
for log in re.findall(log_regex, log_info):
log_dict[log[0]] = json.loads(log[1])
return log_dict
def send(self, record: logging.LogRecord):
"""Send log record to Posthog"""
level_name = logging.getLevelName(record.levelno)
log_line = FormatterWithExceptions.filter_log_line(text=record.getMessage())
log_extra = self.extract_log_extra(record=record)
log_extra.update(dict(level=level_name, message=log_line))
event_name = f"log_{level_name.lower()}"
if log_dict := self.log_to_dict(log_info=log_line):
event_name = f"log_{list(log_dict.keys())[0].lower()}"
log_dict = log_dict.get("STARTUP", log_dict)
log_extra = {**log_extra, **log_dict}
log_extra.pop("message", None)
if re.match(r"^(QUEUE|START|END|INPUT:)", log_line) and not log_dict:
return
if event_name not in [e.value for e in self.AllowedEvents]:
return
if not self.logged_in and self._settings.user_id:
self.logged_in = True
openbb_posthog.identify(
self._settings.user_id,
{
"email": self._settings.user_email,
"primaryUsage": self._settings.user_primary_usage,
},
)
openbb_posthog.alias(self._settings.user_id, self._settings.app_id)
result = openbb_posthog.capture(
self._settings.app_id,
event_name,
properties=log_extra,
)
if Env().DEBUG_MODE:
warnings.warn(result)
def extract_log_extra(self, record: logging.LogRecord) -> Dict[str, Any]:
"""Extract log extra from record"""
log_extra: Dict[str, Any] = {
"appName": self._settings.app_name,
"subAppName": self._settings.sub_app_name,
"appId": self._settings.app_id,
"sessionId": self._settings.session_id,
"platform": self._settings.platform,
"pythonVersion": self._settings.python_version,
"obbPlatformVersion": self._settings.platform_version,
}
if self._settings.user_id:
log_extra["userId"] = self._settings.user_id
if hasattr(record, "extra"):
log_extra = {**log_extra, **record.extra} # type: ignore
if record.exc_info:
log_extra["exception"] = {
"type": str(record.exc_info[0]),
"value": str(record.exc_info[1]),
}
return log_extra

View File

@@ -0,0 +1,71 @@
# IMPORT STANDARD
import logging
import sys
# IMPORT THIRD-PARTY
# IMPORT INTERNAL
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
from openbb_core.app.logs.handlers.path_tracking_file_handler import (
PathTrackingFileHandler,
)
from openbb_core.app.logs.handlers.posthog_handler import PosthogHandler
from openbb_core.app.logs.models.logging_settings import LoggingSettings
class HandlersManager:
def __init__(self, settings: LoggingSettings):
self._handlers = settings.handler_list
self._settings = settings
for handler_type in self._handlers:
if handler_type == "stdout":
self._add_stdout_handler()
elif handler_type == "stderr":
self._add_stderr_handler()
elif handler_type == "noop":
self._add_noop_handler()
elif handler_type == "file":
self._add_file_handler()
elif handler_type == "posthog":
self._add_posthog_handler()
else:
logging.getLogger().debug("Unknown log handler.")
def _add_posthog_handler(self):
handler = PosthogHandler(settings=self._settings)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_stdout_handler(self):
handler = logging.StreamHandler(sys.stdout)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_stderr_handler(self):
handler = logging.StreamHandler(sys.stderr)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_noop_handler(self):
handler = logging.NullHandler()
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def _add_file_handler(self):
handler = PathTrackingFileHandler(settings=self._settings)
formatter = FormatterWithExceptions(settings=self._settings)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def update_handlers(self, settings: LoggingSettings):
logger = logging.getLogger()
for hdlr in logger.handlers:
if isinstance(hdlr, (PathTrackingFileHandler, PosthogHandler)):
hdlr.settings = settings
hdlr.formatter.settings = settings # type: ignore

View File

@@ -0,0 +1,243 @@
import json
import logging
from enum import Enum
from types import TracebackType
from typing import Any, Callable, Dict, Optional, Tuple, Type, cast
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
FormatterWithExceptions,
)
from openbb_core.app.logs.handlers_manager import HandlersManager
from openbb_core.app.logs.models.logging_settings import LoggingSettings
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.model.abstract.singleton import SingletonMeta
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.model.user_settings import UserSettings
from pydantic_core import to_jsonable_python
class LoggingService(metaclass=SingletonMeta):
"""
Logging Manager class responsible for managing logging settings and handling logs.
Attributes
----------
_user_settings : Optional[UserSettings]
User Settings object.
_system_settings : Optional[SystemSettings]
System Settings object.
_logging_settings : LoggingSettings
LoggingSettings object containing the current logging settings.
_handlers_manager : HandlersManager
HandlersManager object managing logging handlers.
Methods
-------
__init__(system_settings, user_settings)
Logging Manager Constructor.
log(user_settings, system_settings, obbject, route, func, kwargs)
Log command output and relevant information.
logging_settings
Property to access the current logging settings.
logging_settings.setter
Setter method to update the logging settings.
_setup_handlers()
Setup Logging Handlers.
_log_startup()
Log startup information.
"""
def __init__(
self,
system_settings: SystemSettings,
user_settings: UserSettings,
) -> None:
"""
Logging Manager Constructor
Sets up the logging settings and handlers and then logs the startup information.
Parameters
----------
system_settings : SystemSettings
System Settings, by default None
user_settings : UserSettings
User Settings, by default None
"""
self._user_settings = user_settings
self._system_settings = system_settings
self._logging_settings = LoggingSettings(
user_settings=self._user_settings,
system_settings=self._system_settings,
)
self._handlers_manager = self._setup_handlers()
self._log_startup()
@property
def logging_settings(self) -> LoggingSettings:
"""
Current logging settings.
Returns
-------
LoggingSettings
LoggingSettings object containing the current logging settings.
"""
return self._logging_settings
@logging_settings.setter
def logging_settings(self, value: Tuple[SystemSettings, UserSettings]):
"""
Setter for updating the logging settings.
Parameters
----------
value : Tuple[SystemSettings, UserSettings]
Tuple containing updated SystemSettings and UserSettings.
"""
system_settings, user_settings = value
self._logging_settings = LoggingSettings(
user_settings=user_settings,
system_settings=system_settings,
)
def _setup_handlers(self) -> HandlersManager:
"""
Setup Logging Handlers.
Returns
-------
HandlersManager
Handlers Manager object.
"""
logger = logging.getLogger(__name__)
logging.basicConfig(
level=self._logging_settings.verbosity,
format=FormatterWithExceptions.LOGFORMAT,
datefmt=FormatterWithExceptions.DATEFORMAT,
handlers=[],
force=True,
)
handlers_manager = HandlersManager(settings=self._logging_settings)
logger.info("Logging configuration finished")
logger.info("Logging set to %s", self._logging_settings.handler_list)
logger.info("Verbosity set to %s", self._logging_settings.verbosity)
logger.info(
"LOGFORMAT: %s%s",
FormatterWithExceptions.LOGPREFIXFORMAT.replace("|", "-"),
FormatterWithExceptions.LOGFORMAT.replace("|", "-"),
)
return handlers_manager
def _log_startup(self, route: Optional[str] = None) -> None:
"""
Log startup information.
"""
def check_credentials_defined(credentials: Dict[str, Any]):
class CredentialsDefinition(Enum):
defined = "defined"
undefined = "undefined"
return {
c: CredentialsDefinition.defined.value
if credentials[c]
else CredentialsDefinition.undefined.value
for c in credentials
}
logger = logging.getLogger(__name__)
logger.info(
"STARTUP: %s ",
json.dumps(
{
"route": route,
"PREFERENCES": self._user_settings.preferences,
"KEYS": check_credentials_defined(
self._user_settings.credentials.model_dump()
if self._user_settings.credentials
else {}
),
"SYSTEM": self._system_settings,
},
default=to_jsonable_python,
),
)
def log(
self,
user_settings: UserSettings,
system_settings: SystemSettings,
route: str,
func: Callable,
kwargs: Dict[str, Any],
exec_info: Optional[
Tuple[Type[BaseException], BaseException, Optional[TracebackType]]
] = None,
):
"""
Log command output and relevant information.
Parameters
----------
user_settings : UserSettings
User Settings object.
system_settings : SystemSettings
System Settings object.
route : str
Route for the command.
func : Callable
Callable representing the executed function.
kwargs : Dict[str, Any]
Keyword arguments passed to the function.
exec_info : Optional[Tuple[Type[BaseException], BaseException, Optional[TracebackType]]], optional
Exception information, by default None
"""
self._user_settings = user_settings
self._system_settings = system_settings
self._logging_settings = LoggingSettings(
user_settings=self._user_settings,
system_settings=self._system_settings,
)
self._handlers_manager.update_handlers(self._logging_settings)
if "login" in route:
self._log_startup(route)
else:
logger = logging.getLogger(__name__)
# Remove CommandContext if any
kwargs.pop("cc", None)
# Truncate kwargs if too long
kwargs = {k: str(v)[:100] for k, v in kwargs.items()}
# Get execution info
openbb_error = cast(
Optional[OpenBBError], exec_info[1] if exec_info else None
)
# Construct message
message_label = "ERROR" if openbb_error else "CMD"
log_message = json.dumps(
{
"route": route,
"input": kwargs,
"error": str(openbb_error.original) if openbb_error else None,
},
default=to_jsonable_python,
)
log_message = f"{message_label}: {log_message}"
log_level = logger.error if openbb_error else logger.info
log_level(
log_message,
extra={"func_name_override": func.__name__},
exc_info=exec_info,
)

View File

@@ -0,0 +1,50 @@
from pathlib import Path
from typing import List, Optional
from openbb_core.app.logs.utils.utils import get_app_id, get_log_dir, get_session_id
from openbb_core.app.model.system_settings import SystemSettings
from openbb_core.app.model.user_settings import UserSettings
# pylint: disable=too-many-instance-attributes
class LoggingSettings:
def __init__(
self,
user_settings: Optional[UserSettings] = None,
system_settings: Optional[SystemSettings] = None,
):
user_settings = user_settings or UserSettings()
system_settings = system_settings or SystemSettings()
user_data_directory = (
str(Path.home() / "OpenBBUserData")
if not user_settings.preferences
else user_settings.preferences.data_directory
)
hub_session = (
user_settings.profile.hub_session if user_settings.profile else None
)
if hub_session:
user_id = hub_session.user_uuid
user_email = hub_session.email
user_primary_usage = hub_session.primary_usage
else:
user_id, user_email, user_primary_usage = None, None, None
# System
self.app_name: str = system_settings.logging_app_name
self.sub_app_name: str = system_settings.logging_sub_app
self.app_id: str = get_app_id(user_data_directory)
self.session_id: str = get_session_id()
self.frequency: str = system_settings.logging_frequency
self.handler_list: List[str] = system_settings.logging_handlers
self.rolling_clock: bool = system_settings.logging_rolling_clock
self.verbosity: int = system_settings.logging_verbosity
self.platform: str = system_settings.platform
self.python_version: str = system_settings.python_version
self.platform_version: str = system_settings.version
# User
self.user_id: Optional[str] = user_id
self.user_logs_directory: Path = get_log_dir(user_data_directory)
self.user_email: Optional[str] = user_email
self.user_primary_usage: Optional[str] = user_primary_usage

View File

@@ -0,0 +1,30 @@
# IMPORTATION STANDARD
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:
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]:
expired_files = []
if directory.is_dir(): # Check if the directory exists and is a directory
for file in directory.iterdir():
if file.is_file() and file.lstat().st_mtime < before_timestamp:
expired_files.append(file)
return expired_files
def remove_file_list(file_list: List[Path]):
for file in file_list:
with contextlib.suppress(PermissionError):
file.unlink(missing_ok=True)

View File

@@ -0,0 +1,72 @@
# IMPORT STANDARD
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."""
session_id = str(uuid.uuid4()) + "-" + str(int(time.time()))
return session_id
def get_app_id(contextual_user_data_directory: str) -> str:
"""Get UUID of the current installation."""
try:
app_id = get_log_dir(contextual_user_data_directory).stem
except OSError as e:
if e.errno == 30:
warnings.warn("Please move the application into a writable location.")
warnings.warn(
"Note for macOS users: copy `OpenBB Terminal` folder outside the DMG."
)
raise e
except Exception as e:
raise e
return app_id
def get_log_dir(contextual_user_data_directory: str) -> PosixPath:
"""Retrieve application's log directory."""
log_dir = create_log_dir_if_not_exists(contextual_user_data_directory)
logging_uuid = create_log_uuid_if_not_exists(log_dir)
uuid_log_dir = create_uuid_dir_if_not_exists(log_dir, logging_uuid)
return uuid_log_dir
def create_log_dir_if_not_exists(contextual_user_data_directory: str) -> Path:
log_dir = Path(contextual_user_data_directory).joinpath("logs").absolute()
if not log_dir.is_dir():
log_dir.mkdir(parents=True, exist_ok=True)
return log_dir
def create_log_uuid_if_not_exists(log_dir: Path) -> str:
log_id = get_log_id(log_dir)
if not log_id.is_file():
logging_id = f"{uuid.uuid4()}"
log_id.write_text(logging_id, encoding="utf-8")
else:
logging_id = log_id.read_text(encoding="utf-8").rstrip()
return logging_id
def get_log_id(log_dir):
return (log_dir / ".logid").absolute()
def create_uuid_dir_if_not_exists(log_dir, logging_id) -> PosixPath:
uuid_log_dir = (log_dir / logging_id).absolute()
if not uuid_log_dir.is_dir():
uuid_log_dir.mkdir(parents=True, exist_ok=True)
return uuid_log_dir

View File

@@ -0,0 +1,9 @@
from typing import Optional, Union
class OpenBBError(Exception):
"""OpenBB Error."""
def __init__(self, original: Optional[Union[str, Exception]] = None):
self.original = original
super().__init__(str(original))

View File

@@ -0,0 +1,3 @@
from pydantic import BaseModel
Results = BaseModel

View File

@@ -0,0 +1,15 @@
from typing import Dict, Generic, TypeVar
T = TypeVar("T")
class SingletonMeta(type, Generic[T]):
# TODO : check if we want to update this to be thread safe
_instances: Dict[T, T] = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]

View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel, Field
from uuid_extensions import uuid7str
class Tagged(BaseModel):
id: str = Field(default_factory=uuid7str, alias="_id")

View File

@@ -0,0 +1,19 @@
from warnings import WarningMessage
from pydantic import BaseModel
class Warning_(BaseModel):
category: str
message: str
def cast_warning(w: WarningMessage) -> Warning_:
return Warning_(
category=w.category.__name__,
message=str(w.message),
)
class OpenBBWarning(Warning):
pass

Some files were not shown because too many files have changed in this diff Show More