mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-06 01:12:27 +00:00
Merge remote-tracking branch 'OpenBB-finance/develop' into feature/argparse_translator
This commit is contained in:
45
.codespell.ignore
vendored
Normal file
45
.codespell.ignore
vendored
Normal 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
16
.codespell.skip
vendored
Normal 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
2
.coveragerc
vendored
@@ -2,4 +2,6 @@
|
||||
omit =
|
||||
env/*
|
||||
tests/*
|
||||
**/tests/**
|
||||
**/package/**
|
||||
source = .
|
||||
|
||||
73
.github/pull_request_template.md
vendored
73
.github/pull_request_template.md
vendored
@@ -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>
|
||||
|
||||
6
.github/workflows/branch-name-check.yml
vendored
6
.github/workflows/branch-name-check.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/build-release.yml
vendored
3
.github/workflows/build-release.yml
vendored
@@ -10,8 +10,7 @@ env:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
- main
|
||||
- release/v3/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
4
.github/workflows/docker-build.yml
vendored
4
.github/workflows/docker-build.yml
vendored
@@ -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')
|
||||
|
||||
17
.github/workflows/gh-pages.yml
vendored
17
.github/workflows/gh-pages.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/labels-PR.yml
vendored
2
.github/workflows/labels-PR.yml
vendored
@@ -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
|
||||
|
||||
37
.github/workflows/linting.yml
vendored
37
.github/workflows/linting.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/nightly-build.yml
vendored
9
.github/workflows/nightly-build.yml
vendored
@@ -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 }}
|
||||
120
.github/workflows/platform-api-integration-test.yml
vendored
Normal file
120
.github/workflows/platform-api-integration-test.yml
vendored
Normal 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
46
.github/workflows/platform-core.yml
vendored
Normal 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 }}
|
||||
73
.github/workflows/pypi-nightly.yml
vendored
73
.github/workflows/pypi-nightly.yml
vendored
@@ -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 }}
|
||||
|
||||
3
.github/workflows/pypi.yml
vendored
3
.github/workflows/pypi.yml
vendored
@@ -3,8 +3,7 @@ name: Deploy to PyPI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
- main
|
||||
- release/v3/*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
68
.github/workflows/test_pypi_platform.yml
vendored
Normal file
68
.github/workflows/test_pypi_platform.yml
vendored
Normal 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
10
.gitignore
vendored
@@ -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/*
|
||||
|
||||
13
.pre-commit-config.yaml
vendored
13
.pre-commit-config.yaml
vendored
@@ -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
11
.secrets.baseline
vendored
@@ -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
39
CONTRIBUTING.md
vendored
@@ -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: → `_view.py` :art: &nbs
|
||||
| **File ** | **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: → `_view.py` :art: &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
85
README.md
vendored
@@ -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]
|
||||
|
||||
[](https://github.com/OpenBB-finance/OpenBBTerminal/releases)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://www.tickgit.com/browse?repo=github.com/OpenBB-finance/OpenBBTerminal&branch=main)
|
||||
|
||||

|
||||
[](https://twitter.com/openbb_finance)
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
|
||||
[](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. |
|
||||
|:--:|
|
||||
| [](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>
|
||||
[](https://twitter.com/openbb_finance)
|
||||
[](https://github.com/OpenBB-finance/OpenBBTerminal/releases)
|
||||

|
||||
[](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>
|
||||
  ·   <a href="https://github.com/OpenBB-finance/OpenBBTerminal/tree/master/CONTRIBUTING.md"><strong>CONTRIBUTING</strong></a>   ·  
|
||||
<a href="https://docs.openbb.co/terminal">
|
||||
<strong>SEE FEATURES »</strong></a>
|
||||
<a href="https://docs.openbb.co/terminal/installation">
|
||||
<strong>≪ INSTALL</strong>
|
||||
</a>
|
||||
  ·  
|
||||
<a href="https://my.openbb.co/app/terminal/data">
|
||||
<strong>DATA</strong>
|
||||
</a>
|
||||
  ·  
|
||||
<a href="https://my.openbb.co/app/terminal/features">
|
||||
<strong>SEE FEATURES</strong>
|
||||
</a>
|
||||
  ·  
|
||||
<a href="https://github.com/OpenBB-finance/OpenBBTerminal/tree/master/CONTRIBUTING.md">
|
||||
<strong>CONTRIBUTING »</strong>
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
14
build/README.md
vendored
14
build/README.md
vendored
@@ -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.
|
||||
|
||||
|
||||
4
build/docker/api.dockerfile
vendored
4
build/docker/api.dockerfile
vendored
@@ -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
|
||||
|
||||
2
build/docker/compose.env
vendored
2
build/docker/compose.env
vendored
@@ -1,2 +1,2 @@
|
||||
OPENBBTERMINAL_DOCKER_REGISTRY="ghcr.io"
|
||||
OPENBBTERMINAL_DOCKER_RELEASE_VERSION="3.2.1"
|
||||
OPENBBTERMINAL_DOCKER_RELEASE_VERSION="3.2.4"
|
||||
|
||||
8
build/nsis/setup.nsi
vendored
8
build/nsis/setup.nsi
vendored
@@ -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"
|
||||
|
||||
;--------------------------------
|
||||
|
||||
1
build/pyinstaller/.env
vendored
1
build/pyinstaller/.env
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
1
build/pyinstaller/macOS/build-macos.sh
vendored
1
build/pyinstaller/macOS/build-macos.sh
vendored
@@ -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() {
|
||||
|
||||
@@ -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;"> $ <span style="color: #abb0b0">wso2am-2.6.0 [command]</span></code>
|
||||
</div>
|
||||
<div style="font-family: Helvetica; padding-left: 10px;" align="left">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
57
build/pyinstaller/terminal.spec
vendored
57
build/pyinstaller/terminal.spec
vendored
@@ -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,
|
||||
)
|
||||
|
||||
8
build/pyinstaller/version.rc
vendored
8
build/pyinstaller/version.rc
vendored
@@ -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
51
build/pypi/openbb_platform/PUBLISH.md
vendored
Normal 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).
|
||||
85
build/pypi/openbb_platform/nightly.py
Normal file
85
build/pypi/openbb_platform/nightly.py
Normal 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()
|
||||
29
build/pypi/openbb_platform/poetry_update.py
Normal file
29
build/pypi/openbb_platform/poetry_update.py
Normal 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)
|
||||
106
build/pypi/openbb_platform/publish.py
Normal file
106
build/pypi/openbb_platform/publish.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
84
examples/README.md
vendored
@@ -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.
|
||||
|
||||
97770
examples/copperToGoldRatio.ipynb
vendored
97770
examples/copperToGoldRatio.ipynb
vendored
File diff suppressed because it is too large
Load Diff
1059
examples/financialStatements.ipynb
vendored
Normal file
1059
examples/financialStatements.ipynb
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1409
examples/findSymbols.ipynb
vendored
Normal file
1409
examples/findSymbols.ipynb
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2125
examples/googleColab.ipynb
vendored
Normal file
2125
examples/googleColab.ipynb
vendored
Normal file
File diff suppressed because one or more lines are too long
29579
examples/loadHistoricalPriceData.ipynb
vendored
29579
examples/loadHistoricalPriceData.ipynb
vendored
File diff suppressed because it is too large
Load Diff
79
examples/v3/README.md
vendored
Normal file
79
examples/v3/README.md
vendored
Normal 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
95896
examples/v3/copperToGoldRatio.ipynb
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
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
6441
examples/v3/volumeAtPrice.ipynb
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
17
images/openbb-logo-dark.svg
vendored
Normal 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
17
images/openbb-logo-light.svg
vendored
Normal 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 |
23
noxfile.py
23
noxfile.py
@@ -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
884
openbb_platform/CONTRIBUTING.md
vendored
Normal 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
16
openbb_platform/EXTENSIONS.md
vendored
Normal 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
11
openbb_platform/PROVIDERS.md
vendored
Normal 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
146
openbb_platform/README.md
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
# OpenBB Platform
|
||||
|
||||
[](https://pepy.tech/project/openbb)
|
||||
[](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. |
|
||||
|:--:|
|
||||
|  |
|
||||
| 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
57
openbb_platform/core/README.md
vendored
Normal 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.
|
||||
90
openbb_platform/core/integration/test_obbject.py
Normal file
90
openbb_platform/core/integration/test_obbject.py
Normal 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
|
||||
0
openbb_platform/core/openbb_core/__init__.py
Normal file
0
openbb_platform/core/openbb_core/__init__.py
Normal file
18
openbb_platform/core/openbb_core/api/app_loader.py
Normal file
18
openbb_platform/core/openbb_core/api/app_loader.py
Normal 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
|
||||
56
openbb_platform/core/openbb_core/api/auth/user.py
Normal file
56
openbb_platform/core/openbb_core/api/auth/user.py
Normal 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
|
||||
21
openbb_platform/core/openbb_core/api/dependency/coverage.py
Normal file
21
openbb_platform/core/openbb_core/api/dependency/coverage.py
Normal 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()
|
||||
20
openbb_platform/core/openbb_core/api/dependency/system.py
Normal file
20
openbb_platform/core/openbb_core/api/dependency/system.py
Normal 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
|
||||
114
openbb_platform/core/openbb_core/api/rest_api.py
Normal file
114
openbb_platform/core/openbb_core/api/rest_api.py
Normal 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)
|
||||
181
openbb_platform/core/openbb_core/api/router/commands.py
Normal file
181
openbb_platform/core/openbb_core/api/router/commands.py
Normal 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)
|
||||
36
openbb_platform/core/openbb_core/api/router/coverage.py
Normal file
36
openbb_platform/core/openbb_core/api/router/coverage.py
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
"""The init of the coverage helpers."""
|
||||
@@ -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
|
||||
15
openbb_platform/core/openbb_core/api/router/system.py
Normal file
15
openbb_platform/core/openbb_core/api/router/system.py
Normal 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
|
||||
18
openbb_platform/core/openbb_core/api/router/user.py
Normal file
18
openbb_platform/core/openbb_core/api/router/user.py
Normal 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
|
||||
0
openbb_platform/core/openbb_core/app/__init__.py
Normal file
0
openbb_platform/core/openbb_core/app/__init__.py
Normal file
307
openbb_platform/core/openbb_core/app/charting_service.py
Normal file
307
openbb_platform/core/openbb_core/app/charting_service.py
Normal 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,
|
||||
)
|
||||
446
openbb_platform/core/openbb_core/app/command_runner.py
Normal file
446
openbb_platform/core/openbb_core/app/command_runner.py
Normal 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)
|
||||
7
openbb_platform/core/openbb_core/app/constants.py
Normal file
7
openbb_platform/core/openbb_core/app/constants.py
Normal 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")
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
243
openbb_platform/core/openbb_core/app/logs/logging_service.py
Normal file
243
openbb_platform/core/openbb_core/app/logs/logging_service.py
Normal 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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
72
openbb_platform/core/openbb_core/app/logs/utils/utils.py
Normal file
72
openbb_platform/core/openbb_core/app/logs/utils/utils.py
Normal 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
|
||||
@@ -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))
|
||||
@@ -0,0 +1,3 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
Results = BaseModel
|
||||
@@ -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]
|
||||
@@ -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")
|
||||
@@ -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
Reference in New Issue
Block a user