mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
[Feature] OpenBB V5: Initial openbb-core Refactor (#7482)
* initial refactor * ruff does not dictate style * unresolved imports in test action * fix general linting * resolve pandas imports for actions run * httpx in test workflow * pytest-asyncio in test workflow * pandas.Index(...) * max version for windows CI is 3.14 * remove temp refactor files that shouldn't have been committed: * use ruff format instead of black * formatting and typing * tab space helper * test fixtures.. * ty check
This commit is contained in:
@@ -23,7 +23,9 @@ def fetch_pr_details(owner: str, repo: str, pr_number: str, github_token: str) -
|
||||
return {}
|
||||
|
||||
|
||||
def parse_and_fetch_pr_details(markdown_text: str, owner: str, repo: str, github_token: str) -> dict[str, str]:
|
||||
def parse_and_fetch_pr_details(
|
||||
markdown_text: str, owner: str, repo: str, github_token: str
|
||||
) -> dict[str, str]:
|
||||
"""Parse the markdown text and fetch details of PRs mentioned in the text."""
|
||||
sections = re.split(r"\n## ", markdown_text)
|
||||
categories: dict[str, str] = {}
|
||||
@@ -46,7 +48,9 @@ def parse_and_fetch_pr_details(markdown_text: str, owner: str, repo: str, github
|
||||
"body": re.sub(r"\s+", " ", pr_details["body"].strip()).strip(),
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error("Failed to fetch PR details for PR #%s: %s", pr_number, e)
|
||||
logging.error(
|
||||
"Failed to fetch PR details for PR #%s: %s", pr_number, e
|
||||
)
|
||||
if category_name in categories:
|
||||
categories[category_name].append(pr_info) # type: ignore
|
||||
else:
|
||||
@@ -55,7 +59,9 @@ def parse_and_fetch_pr_details(markdown_text: str, owner: str, repo: str, github
|
||||
return categories
|
||||
|
||||
|
||||
def insert_summary_into_markdown(markdown_text: str, category_name: str, summary: str) -> str:
|
||||
def insert_summary_into_markdown(
|
||||
markdown_text: str, category_name: str, summary: str
|
||||
) -> str:
|
||||
"""Insert a summary into the markdown text directly under the specified category name."""
|
||||
marker = f"## {category_name}"
|
||||
if marker in markdown_text:
|
||||
@@ -66,7 +72,12 @@ def insert_summary_into_markdown(markdown_text: str, category_name: str, summary
|
||||
if newline_pos != -1:
|
||||
# Insert the summary right after the newline that follows the category name
|
||||
# Ensuring it's on a new line and followed by two newlines before any subsequent content
|
||||
updated_markdown = markdown_text[: newline_pos + 1] + "\n" + summary + markdown_text[newline_pos + 1 :]
|
||||
updated_markdown = (
|
||||
markdown_text[: newline_pos + 1]
|
||||
+ "\n"
|
||||
+ summary
|
||||
+ markdown_text[newline_pos + 1 :]
|
||||
)
|
||||
else:
|
||||
# If there's no newline (e.g., end of file), just append the summary
|
||||
updated_markdown = markdown_text + "\n\n" + summary + "\n"
|
||||
@@ -78,7 +89,7 @@ def insert_summary_into_markdown(markdown_text: str, category_name: str, summary
|
||||
|
||||
def summarize_text_with_openai(text: str, openai_api_key: str) -> str:
|
||||
"""Summarize text using OpenAI's GPT model."""
|
||||
from openai import OpenAI # pylint: disable=C0415
|
||||
from openai import OpenAI
|
||||
|
||||
openai = OpenAI(api_key=openai_api_key)
|
||||
response = openai.chat.completions.create(
|
||||
@@ -86,7 +97,7 @@ def summarize_text_with_openai(text: str, openai_api_key: str) -> str:
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Summarize the following text in a concise way to describe what happened in the new release. This will be used on top of the changelog to provide a high-level overview of the changes. Make sure it is well-written, concise, structured and that it captures the essence of the text. It should read like a concise story.", # noqa: E501 # pylint: disable=C0301
|
||||
"content": "Summarize the following text in a concise way to describe what happened in the new release. This will be used on top of the changelog to provide a high-level overview of the changes. Make sure it is well-written, concise, structured and that it captures the essence of the text. It should read like a concise story.", # noqa: E501
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
@@ -128,11 +139,13 @@ def summarize_changelog_v2(
|
||||
aggregated_text = "\n".join(
|
||||
[
|
||||
f"- {pr['title']}: {re.sub(pattern, '', pr['body'])}" # type: ignore
|
||||
for pr in categories[category_of_interest] # type: ignore
|
||||
for pr in categories[category_of_interest]
|
||||
]
|
||||
)
|
||||
summary = summarize_text_with_openai(aggregated_text, openai_api_key)
|
||||
updated_markdown = insert_summary_into_markdown(updated_markdown, category_of_interest, summary)
|
||||
updated_markdown = insert_summary_into_markdown(
|
||||
updated_markdown, category_of_interest, summary
|
||||
)
|
||||
|
||||
with open(changelog_v2, "w") as file:
|
||||
logging.info("Writing updated file: %s", changelog_v2)
|
||||
@@ -141,7 +154,9 @@ def summarize_changelog_v2(
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
logging.error("Usage: python summarize_changelog.py <github_token> <openai_api_key>")
|
||||
logging.error(
|
||||
"Usage: python summarize_changelog.py <github_token> <openai_api_key>"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
token = sys.argv[1]
|
||||
|
||||
9
.github/workflows/README.md
vendored
9
.github/workflows/README.md
vendored
@@ -73,13 +73,10 @@ The first job, `code-linting`, runs on an Ubuntu machine and performs several li
|
||||
- Checking out the code from the repository
|
||||
- Setting up Python 3.10
|
||||
- Installing a number of Python packages necessary for the linting tasks
|
||||
- Running `bandit` to check for security vulnerabilities
|
||||
- Running `black` to check the code formatting
|
||||
- Running `codespell` to check the spelling of comments, strings, and variable names
|
||||
- Running `ruff` to check the use of Python
|
||||
- Running `pylint` to perform static analysis of the code
|
||||
- Running `mypy` to check the type annotations
|
||||
- Running `pydocstyle` to check the docstrings
|
||||
- Running `black` to check the code formatting
|
||||
- Running `ruff` to lint the code (replaces pylint, isort, flake8, and pydocstyle)
|
||||
- Running `ty` to check the type annotations
|
||||
|
||||
The second job, `markdown-link-check`, runs on an Ubuntu machine and performs linting of the markdown files in the repository. It uses a Docker container `avtodev/markdown-lint` to perform the linting.
|
||||
|
||||
|
||||
10
.github/workflows/general-linting.yml
vendored
10
.github/workflows/general-linting.yml
vendored
@@ -59,16 +59,16 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-linting-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- run: |
|
||||
pip install openbb-devtools
|
||||
pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil types-setuptools types-six
|
||||
pip install ./openbb_platform/extensions/devtools
|
||||
- name: Install openbb-core (so ty can resolve runtime deps)
|
||||
run: pip install -e "./openbb_platform/core[pandas]" numpy
|
||||
- run: codespell --ignore-words=.codespell.ignore --skip="$(tr '\n' ',' < .codespell.skip | sed 's/,$//')" --quiet-level=2
|
||||
- run: |
|
||||
# Run linters for openbb_platform | cli
|
||||
if [ -n "${{ env.diff_files }}" ]; then
|
||||
black --diff --check ${{ env.diff_files }}
|
||||
mypy ${{ env.diff_files }} --ignore-missing-imports --scripts-are-modules --check-untyped-defs
|
||||
pylint ${{ env.diff_files }}
|
||||
ruff format --check ${{ env.diff_files }}
|
||||
ruff check ${{ env.diff_files }}
|
||||
ty check ${{ env.diff_files }}
|
||||
else
|
||||
echo "No Python files changed in openbb_platform | cli"
|
||||
fi
|
||||
|
||||
87
.github/workflows/test-openbb-core.yml
vendored
Normal file
87
.github/workflows/test-openbb-core.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: 🧱 openbb-core
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- v5
|
||||
paths:
|
||||
- 'openbb_platform/core/**'
|
||||
- 'openbb_platform/extensions/devtools/**'
|
||||
- '.github/workflows/test-openbb-core.yml'
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- v5
|
||||
- 'feature/**'
|
||||
- 'hotfix/**'
|
||||
- 'release/**'
|
||||
paths:
|
||||
- 'openbb_platform/core/**'
|
||||
- 'openbb_platform/extensions/devtools/**'
|
||||
- '.github/workflows/test-openbb-core.yml'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/core
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (${{ matrix.os }} / Python ${{ matrix.python_version }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Linux: full sweep across every supported Python.
|
||||
- { os: ubuntu-latest, python_version: "3.10" }
|
||||
- { os: ubuntu-latest, python_version: "3.11" }
|
||||
- { os: ubuntu-latest, python_version: "3.12" }
|
||||
- { os: ubuntu-latest, python_version: "3.13" }
|
||||
- { os: ubuntu-latest, python_version: "3.14" }
|
||||
- { os: ubuntu-latest, python_version: "3.15" }
|
||||
# macOS and Windows: min and max supported.
|
||||
- { os: macos-latest, python_version: "3.10" }
|
||||
- { os: macos-latest, python_version: "3.15" }
|
||||
- { os: windows-latest, python_version: "3.10" }
|
||||
- { os: windows-latest, python_version: "3.14" }
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Install Python ${{ matrix.python_version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
cache: "pip"
|
||||
|
||||
- name: Install openbb-core (with pandas extra) and test deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[pandas]"
|
||||
pip install pytest pytest-cov pytest-asyncio httpx
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
pytest tests --cov=openbb_core --cov-report=term-missing --cov-report=xml -m "not integration"
|
||||
|
||||
- name: Upload coverage artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openbb-core-coverage-${{ matrix.os }}-py${{ matrix.python_version }}
|
||||
path: openbb_platform/core/coverage.xml
|
||||
if-no-files-found: ignore
|
||||
23
.pre-commit-config.yaml
vendored
23
.pre-commit-config.yaml
vendored
@@ -42,19 +42,13 @@ repos:
|
||||
"--skip=./**/tests/**,./**/test_*.py,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml,build/pyinstaller/*,./website/config.toml",
|
||||
"-x=.github/workflows/general-linting.yml"
|
||||
]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.15.0"
|
||||
- repo: https://github.com/astral-sh/ty-pre-commit
|
||||
rev: 0.0.1a14
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
description: ""
|
||||
entry: mypy
|
||||
language: python
|
||||
"types_or": [python, pyi]
|
||||
args: ["--ignore-missing-imports", "--scripts-are-modules", "--check-untyped-defs"]
|
||||
additional_dependencies: ["types-requests", "types-setuptools", "types-python-dateutil", "types-pytz"]
|
||||
require_serial: true
|
||||
exclude: 'test_.*\.py'
|
||||
- id: ty
|
||||
name: ty (static type check)
|
||||
args: ["check"]
|
||||
exclude: 'test_.*\.py|tests/.*\.py'
|
||||
- repo: https://github.com/kynan/nbstripout
|
||||
rev: 0.8.1
|
||||
hooks:
|
||||
@@ -62,11 +56,6 @@ repos:
|
||||
name: Strip notebooks output
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
- id: check-generated-files
|
||||
name: Check for generated files
|
||||
entry: bash
|
||||
|
||||
397
.pylintrc
vendored
397
.pylintrc
vendored
@@ -1,397 +0,0 @@
|
||||
[MASTER]
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS,cookiecutter
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-allow-list=math,binascii
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
# jobs=1
|
||||
jobs=2
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call
|
||||
# disable=too-many-instance-attributes,len-as-condition,too-few-public-methods,anomalous-backslash-in-string,no-else-return,simplifiable-if-statement,too-many-arguments,duplicate-code,no-name-in-module,no-member,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,missing-docstring,invalid-name,bad-whitespace,consider-using-enumerate,unexpected-keyword-arg
|
||||
|
||||
disable=useless-return,missing-docstring,invalid-name,import-error,broad-except,unnecessary-lambda,missing-timeout,duplicate-code,consider-using-generator,unbalanced-tuple-unpacking,use-list-literal,no-name-in-module,use-sequence-for-iteration,use-dict-literal,unspecified-encoding,unsubscriptable-object,broad-exception-raised,possibly-used-before-assignment
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
msg-template='{path} {line}: {msg_id} {msg} ({symbol})'
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio).You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=9
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
# notes=FIXME,XXX,TODO
|
||||
notes=FIXME,XXX
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
# expected-line-ending-format=
|
||||
expected-line-ending-format=LF
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=140
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
# class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
class-rgx=[A-Z_][a-zA-Z0-9_]+$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
# good-names=i,j,k,ex,Run,_
|
||||
good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=8
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
# max-attributes=7
|
||||
max-attributes=11
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=25
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=40
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=30
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=9
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=120
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=0
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=builtins.BaseException,
|
||||
builtins.Exception
|
||||
@@ -1,7 +1,7 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import toml
|
||||
|
||||
@@ -9,8 +9,8 @@ PLATFORM_PATH = Path(__file__).parent.parent.parent.parent.resolve() / "openbb_p
|
||||
PYPROJECT = PLATFORM_PATH / "pyproject.toml"
|
||||
|
||||
SUB_PACKAGES = {}
|
||||
DEPENDENCIES: Dict[str, Any] = {}
|
||||
PLUGINS: Dict[str, Dict] = {
|
||||
DEPENDENCIES: dict[str, Any] = {}
|
||||
PLUGINS: dict[str, dict] = {
|
||||
"openbb_core_extension": {},
|
||||
"openbb_provider_extension": {},
|
||||
"openbb_obbject_extension": {},
|
||||
@@ -19,7 +19,7 @@ CMD = [sys.executable, "-m", "poetry", "build"]
|
||||
|
||||
|
||||
PYPROJECT_TOML = toml.load(PYPROJECT)
|
||||
POETRY_DICT: Dict[str, dict] = PYPROJECT_TOML["tool"]["poetry"]
|
||||
POETRY_DICT: dict[str, dict] = PYPROJECT_TOML["tool"]["poetry"]
|
||||
POETRY_DICT.pop("extras", None)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ def gather_metadata(sub_path: str):
|
||||
"""
|
||||
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"]
|
||||
poetry_dict: dict[str, dict] = pyproject_toml["tool"]["poetry"]
|
||||
package_name = poetry_dict["packages"][0]["include"]
|
||||
|
||||
for extension in list(PLUGINS.keys()):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Root configuration for pytest."""
|
||||
|
||||
# flake8: noqa: S101
|
||||
# pylint: disable=unused-argument,unused-import
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
190
openbb_platform/core/README.md
vendored
190
openbb_platform/core/README.md
vendored
@@ -1,55 +1,183 @@
|
||||
# Open Data Platform by OpenBB
|
||||
# openbb-core
|
||||
|
||||
Open Data Platform by OpenBB (ODP) is the open-source toolset that helps data engineers integrate proprietary, licensed, and public data sources into downstream applications like AI copilots and research dashboards.
|
||||
`openbb-core` is the runtime that turns a collection of installed Python
|
||||
packages into the OpenBB Platform. By itself it ships **no data, no providers,
|
||||
and no commands**. Its job is to:
|
||||
|
||||
ODP operates as the "connect once, consume everywhere" infrastructure layer that consolidates and exposes data to multiple surfaces at once: Python environments for quants, OpenBB Workspace and Excel for analysts, MCP servers for AI agents, and REST APIs for other applications.
|
||||
1. Discover provider, command, and result-type extensions installed in the
|
||||
current Python environment via setuptools entry points.
|
||||
2. Stitch them together into a single typed Python client (`from openbb import
|
||||
obb`) and an equivalent FastAPI REST API.
|
||||
3. Execute calls against those extensions, normalise the output through shared
|
||||
Pydantic models, and return a uniform `OBBject` result wrapper.
|
||||
|
||||
## Overview
|
||||
If you are an end user, you install `openbb` (which depends on this package)
|
||||
and one or more provider extensions. If you are an extension author, you
|
||||
depend on `openbb-core` and register one of the entry points below.
|
||||
|
||||
The Core extension is used as the basis for building and integrating Open Data Platform Python packages.
|
||||
It provides the necessary classes and structures for standardizing and handling data.
|
||||
It is also responsible for generating a REST API and Python package static assets,
|
||||
which operate independently and interface with various consumption vehicles.
|
||||
## Entry points
|
||||
|
||||
Typically, this library will be used as a project dependency, and extended.
|
||||
Extensions plug into the runtime through three setuptools entry-point groups
|
||||
defined in [`openbb_core/app/extension_loader.py`](openbb_core/app/extension_loader.py).
|
||||
At import time, `ExtensionLoader` enumerates each group and the rest of the
|
||||
platform consumes the results.
|
||||
|
||||
Go to the [documentation](https://docs.openbb.co/python/developer) for information on getting started.
|
||||
### `openbb_core_extension`
|
||||
|
||||
### Prerequisites
|
||||
A `Router` to mount under the platform. Each entry point is loaded as an
|
||||
`openbb_core.app.router.Router` instance and merged into the root router. This
|
||||
is how command surfaces such as `obb.equity`, `obb.economy`, `obb.derivatives`
|
||||
get added.
|
||||
|
||||
- Python >=3.10,<3.14
|
||||
- Familiarity with FastAPI and Pydantic.
|
||||
|
||||
### Installation
|
||||
|
||||
Installing through pip:
|
||||
|
||||
```sh
|
||||
pip install openbb-core
|
||||
```toml
|
||||
# pyproject.toml of an extension
|
||||
[project.entry-points."openbb_core_extension"]
|
||||
equity = "openbb_equity.equity_router:router"
|
||||
```
|
||||
|
||||
> Note that, the openbb-core is an infrastructural component of the OpenBB Platform. It is not intended to be used as a standalone package.
|
||||
### `openbb_provider_extension`
|
||||
|
||||
### Build
|
||||
A `Provider` describing a data source — its credentials, the standard models
|
||||
it implements, and the `Fetcher` classes that satisfy them. Routers do not
|
||||
hard-code providers; they declare a standard model, and at call time the
|
||||
runtime picks a `Fetcher` from any installed provider that implements it.
|
||||
|
||||
Build the Python application, with installed extensions, by running:
|
||||
```toml
|
||||
[project.entry-points."openbb_provider_extension"]
|
||||
yfinance = "openbb_yfinance:yfinance_provider"
|
||||
```
|
||||
|
||||
### `openbb_obbject_extension`
|
||||
|
||||
An `Extension` (see [`openbb_core/app/model/extension.py`](openbb_core/app/model/extension.py))
|
||||
attached to the `OBBject` result wrapper. There are two flavours, configured
|
||||
on the same `Extension` instance:
|
||||
|
||||
**1. Accessor extensions** (default — `on_command_output=False`)
|
||||
|
||||
Add a namespace attribute on every `OBBject`, pandas-style. The entry-point
|
||||
name becomes the attribute name. Used for opt-in transformations the caller
|
||||
explicitly invokes:
|
||||
|
||||
```python
|
||||
result = obb.equity.price.historical("AAPL")
|
||||
result.charting.to_chart() # accessor registered by openbb_charting
|
||||
```
|
||||
|
||||
```toml
|
||||
[project.entry-points."openbb_obbject_extension"]
|
||||
charting = "openbb_charting:ext" # ext = Extension(name="charting")
|
||||
```
|
||||
|
||||
**2. `on_command_output` callbacks** (post-execution hooks)
|
||||
|
||||
Set `on_command_output=True` to make the extension run automatically on the
|
||||
`OBBject` returned by every command, or only by specific routes via
|
||||
`command_output_paths`. `ExtensionLoader._register_command_output_callbacks`
|
||||
indexes them by path (with `"*"` meaning "all commands") and the API layer
|
||||
([`openbb_core/api/router/commands.py`](openbb_core/api/router/commands.py))
|
||||
applies them after the command runs.
|
||||
|
||||
Three flags govern their behaviour:
|
||||
|
||||
- `on_command_output: bool` — enable the post-execution hook.
|
||||
- `command_output_paths: list[str] | None` — restrict to specific endpoint
|
||||
paths; `None` means every command.
|
||||
- `immutable: bool = True` — if `False`, the hook may modify `OBBject.results`
|
||||
in place. Mutable hooks additionally require
|
||||
`system_settings.allow_mutable_extensions=True`.
|
||||
- `results_only: bool = False` — return just `OBBject.results` over the REST
|
||||
API instead of the full envelope.
|
||||
|
||||
```toml
|
||||
[project.entry-points."openbb_obbject_extension"]
|
||||
my_hook = "openbb_my_hook:ext"
|
||||
# ext = Extension(
|
||||
# name="my_hook",
|
||||
# on_command_output=True,
|
||||
# command_output_paths=["/equity/price/historical"],
|
||||
# immutable=False,
|
||||
# )
|
||||
```
|
||||
|
||||
Because these hooks run on every matching response, they are gated by an
|
||||
explicit opt-in: `system_settings.allow_on_command_output` (env:
|
||||
`OPENBB_ALLOW_ON_COMMAND_OUTPUT`) must be `True`, or instantiating the
|
||||
`Extension` will raise. Mutating hooks need
|
||||
`allow_mutable_extensions` (env: `OPENBB_ALLOW_MUTABLE_EXTENSIONS`) on top of
|
||||
that. Treat `on_command_output` extensions as code that runs against every
|
||||
result — only install them from sources you trust.
|
||||
|
||||
## Console script: `openbb-build`
|
||||
|
||||
`openbb-core` installs one console script:
|
||||
|
||||
```sh
|
||||
openbb-build
|
||||
```
|
||||
|
||||
## Key Features
|
||||
It is defined in [`openbb_core/build.py`](openbb_core/build.py) (`openbb-build
|
||||
= openbb_core.build:main`). It imports the `openbb` package in a subprocess,
|
||||
which triggers static-client generation when the generated package is missing
|
||||
or stale, and falls back to calling `openbb.build()` explicitly if the import
|
||||
did not rebuild. Run it after installing or upgrading any extension to
|
||||
regenerate `openbb/package/` and `reference.json`.
|
||||
|
||||
- **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.
|
||||
## What lives inside the package
|
||||
|
||||
- **`openbb_core.provider`** — base classes (`Data`, `QueryParams`, `Provider`,
|
||||
`Fetcher`) and the registry that turns `openbb_provider_extension` entries
|
||||
into a queryable map of `(standard_model, provider) -> Fetcher`.
|
||||
- **`openbb_core.provider.standard_models`** — the shared Pydantic schemas
|
||||
(equity prices, options chains, CPI, treasury rates, etc.) that every
|
||||
provider maps onto. This is what makes "swap providers without changing
|
||||
caller code" possible.
|
||||
- **`openbb_core.app.router.Router`** — a thin wrapper around FastAPI's
|
||||
`APIRouter` used by `openbb_core_extension` packages to declare commands.
|
||||
- **`openbb_core.app.command_runner`** — the execution path used by the
|
||||
generated Python client: resolves provider choice, runs the matching
|
||||
`Fetcher`, applies post-processing hooks, and returns an `OBBject`.
|
||||
- **`openbb_core.app.static.package_builder`** — walks the merged router and
|
||||
emits the typed `openbb/package/` modules plus `reference.json`. This is
|
||||
what `openbb-build` ultimately drives.
|
||||
- **`openbb_core.api.rest_api`** — the FastAPI app (`app`) built from the same
|
||||
routers, with auth hooks and OpenAPI generation. Serve it with any ASGI
|
||||
server.
|
||||
- **`openbb_core.app.extension_loader`** — the entry-point discovery layer
|
||||
described above.
|
||||
- **`openbb_core.app.service`** — system settings, user settings, credentials,
|
||||
and auth services shared by the runtime and the REST API.
|
||||
- **`openbb_core.app.logs`** — structured logging with rotating file handlers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python `>=3.10,<4`
|
||||
- For extension authors: working knowledge of FastAPI and Pydantic v2.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pip install openbb-core
|
||||
```
|
||||
|
||||
You normally do not install `openbb-core` directly — `openbb` and provider
|
||||
extensions pull it in. Install it explicitly only when authoring an extension.
|
||||
|
||||
## Running the REST API
|
||||
|
||||
```sh
|
||||
uvicorn openbb_core.api.rest_api:app --reload
|
||||
```
|
||||
|
||||
The same router graph that backs `from openbb import obb` is exposed over
|
||||
HTTP, with OpenAPI available at `/docs`.
|
||||
|
||||
## Bugs
|
||||
|
||||
Report bugs on [Github](https://github.com/OpenBB-finance/OpenBB/issues/new/choose) by opening a new issue, or commenting on an already open one, with all the details.
|
||||
Report bugs on
|
||||
[GitHub](https://github.com/OpenBB-finance/OpenBB/issues/new/choose).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPL-3.0 License - see the [LICENSE.md](https://github.com/OpenBB-finance/OpenBB/blob/main/LICENSE) file for details.
|
||||
AGPL-3.0-only. See
|
||||
[LICENSE](https://github.com/OpenBB-finance/OpenBB/blob/main/LICENSE).
|
||||
|
||||
321
openbb_platform/core/integration/conftest.py
Normal file
321
openbb_platform/core/integration/conftest.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""Session-scoped fixture that builds & installs a synthetic OpenBB extension.
|
||||
|
||||
This is the integration substrate for tests that need a *real* ``from
|
||||
openbb import obb`` to expose a real command. Rather than depending on
|
||||
the developer having any of the production extensions
|
||||
(``openbb-fixedincome``, ``openbb-equity``, ``openbb-fmp``, …)
|
||||
installed, the fixture writes a self-contained extension distribution
|
||||
to disk, ``pip install``s it into the current interpreter, and runs
|
||||
``openbb-build`` so the static package under ``openbb/package/`` is
|
||||
regenerated with that extension's command(s) baked in.
|
||||
|
||||
Because the static package is wired via entry points discovered at
|
||||
*interpreter start*, every test that imports ``openbb`` after the build
|
||||
must do so in a fresh ``python`` subprocess. The fixture exposes a
|
||||
helper, ``run_in_obb``, that runs an inline snippet of code in a child
|
||||
interpreter with the freshly-built package on its path.
|
||||
|
||||
Cleanup: after the session, the synthetic distribution is uninstalled
|
||||
and the generated ``openbb/package/`` and ``openbb/assets/`` directories
|
||||
are restored to their pre-test state.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
CORE_DIR = Path(__file__).resolve().parents[1]
|
||||
OPENBB_DIR = CORE_DIR / "openbb"
|
||||
PACKAGE_DIR = OPENBB_DIR / "package"
|
||||
ASSETS_DIR = OPENBB_DIR / "assets"
|
||||
|
||||
EXT_NAME = "openbb-fake-integration"
|
||||
EXT_PKG = "openbb_fake_integration"
|
||||
|
||||
|
||||
PYPROJECT = """\
|
||||
[project]
|
||||
name = "{ext_name}"
|
||||
version = "0.0.1"
|
||||
requires-python = ">=3.10,<4"
|
||||
dependencies = ["openbb-core"]
|
||||
|
||||
[project.entry-points."openbb_core_extension"]
|
||||
fake_integration = "{pkg}.router:router"
|
||||
|
||||
[project.entry-points."openbb_provider_extension"]
|
||||
fake_integration = "{pkg}:fake_integration_provider"
|
||||
|
||||
[project.entry-points."openbb_obbject_extension"]
|
||||
fake_accessor = "{pkg}.accessor:ext"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["{pkg}"]
|
||||
"""
|
||||
|
||||
INIT_PY = '''\
|
||||
"""Synthetic provider for OpenBB integration tests."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.provider import Provider
|
||||
from openbb_core.provider.standard_models.available_indicators import (
|
||||
AvailableIndicatorsData,
|
||||
AvailableIndicesQueryParams,
|
||||
)
|
||||
|
||||
|
||||
class _FakeIntegrationData(AvailableIndicatorsData):
|
||||
"""Provider-specific data for the fake_integration provider."""
|
||||
|
||||
extra: str | None = None
|
||||
|
||||
|
||||
class _FakeIntegrationFetcher(
|
||||
Fetcher[AvailableIndicesQueryParams, list[_FakeIntegrationData]]
|
||||
):
|
||||
"""Deterministic fetcher: returns one row regardless of input."""
|
||||
|
||||
require_credentials = False
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> AvailableIndicesQueryParams:
|
||||
return AvailableIndicesQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
def extract_data(
|
||||
query: AvailableIndicesQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict[str, Any]]:
|
||||
return [{"symbol": "FAKE", "extra": "ok"}]
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: AvailableIndicesQueryParams,
|
||||
data: list[dict[str, Any]],
|
||||
**kwargs,
|
||||
) -> list[_FakeIntegrationData]:
|
||||
return [_FakeIntegrationData(**row) for row in data]
|
||||
|
||||
|
||||
fake_integration_provider = Provider(
|
||||
name="fake_integration",
|
||||
description="Synthetic provider used by the openbb-core integration suite.",
|
||||
website="https://example.invalid",
|
||||
credentials=None,
|
||||
fetcher_dict={"AvailableIndicators": _FakeIntegrationFetcher},
|
||||
)
|
||||
'''
|
||||
|
||||
ROUTER_PY = '''\
|
||||
"""Synthetic router for OpenBB integration tests."""
|
||||
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.app.provider_interface import (
|
||||
ExtraParams,
|
||||
ProviderChoices,
|
||||
StandardParams,
|
||||
)
|
||||
from openbb_core.app.query import Query
|
||||
from openbb_core.app.router import Router
|
||||
|
||||
router = Router(prefix="", description="Fake integration extension.")
|
||||
|
||||
|
||||
@router.command(model="AvailableIndicators")
|
||||
async def list_indicators(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Return synthetic indicator rows for integration testing."""
|
||||
return await OBBject.from_query(Query(**locals()))
|
||||
'''
|
||||
|
||||
|
||||
ACCESSOR_PY = '''\
|
||||
"""Synthetic OBBject accessor extension.
|
||||
|
||||
Demonstrates the ``openbb_obbject_extension`` entry-point group.
|
||||
``Extension.obbject_accessor`` registers ``FakeAccessor`` as
|
||||
``OBBject.fake_accessor``, so any ``OBBject`` instance can call
|
||||
``result.fake_accessor.upper_symbols()`` (the typical pandas-style
|
||||
accessor pattern).
|
||||
"""
|
||||
|
||||
from openbb_core.app.model.extension import Extension
|
||||
|
||||
ext = Extension(name="fake_accessor", description="Fake accessor for integration tests.")
|
||||
|
||||
|
||||
@ext.obbject_accessor
|
||||
class FakeAccessor:
|
||||
"""Custom accessor exposed as ``OBBject.fake_accessor``."""
|
||||
|
||||
def __init__(self, obbject):
|
||||
"""Hold a reference to the wrapped OBBject (pandas-accessor pattern)."""
|
||||
self._obbject = obbject
|
||||
|
||||
def upper_symbols(self) -> list[str]:
|
||||
"""Return the symbol field of every row, uppercased."""
|
||||
out = []
|
||||
for row in self._obbject.results or []:
|
||||
sym = (
|
||||
row.symbol
|
||||
if hasattr(row, "symbol")
|
||||
else (row.get("symbol") if isinstance(row, dict) else None)
|
||||
)
|
||||
if sym is not None:
|
||||
out.append(str(sym).upper())
|
||||
return out
|
||||
|
||||
def row_count(self) -> int:
|
||||
"""Return the number of result rows."""
|
||||
return len(self._obbject.results or [])
|
||||
'''
|
||||
|
||||
|
||||
def _snapshot_dir(src: Path, dst: Path) -> None:
|
||||
if src.exists():
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
|
||||
|
||||
def _restore_dir(src: Path, dst: Path) -> None:
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
if src.exists():
|
||||
shutil.copytree(src, dst)
|
||||
|
||||
|
||||
def _pip(*args: str) -> None:
|
||||
subprocess.run( # noqa: S603
|
||||
[sys.executable, "-m", "pip", *args],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _run_openbb_build() -> None:
|
||||
"""Invoke ``openbb-build`` in a fresh subprocess.
|
||||
|
||||
A child process is required because ``ExtensionLoader``/``RouterLoader``
|
||||
cache entry points at module load time; the parent process cannot
|
||||
discover the freshly-installed distribution without a restart.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["OPENBB_AUTO_BUILD"] = "true"
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", "import openbb"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fake_extension_installed(tmp_path_factory):
|
||||
"""Build, install, openbb-build, yield, then uninstall and restore."""
|
||||
work = tmp_path_factory.mktemp("fake_ext_dist")
|
||||
pkg_dir = work / EXT_PKG
|
||||
pkg_dir.mkdir()
|
||||
(pkg_dir / "__init__.py").write_text(INIT_PY, encoding="utf-8")
|
||||
(pkg_dir / "router.py").write_text(ROUTER_PY, encoding="utf-8")
|
||||
(pkg_dir / "accessor.py").write_text(ACCESSOR_PY, encoding="utf-8")
|
||||
(work / "pyproject.toml").write_text(
|
||||
PYPROJECT.format(ext_name=EXT_NAME, pkg=EXT_PKG), encoding="utf-8"
|
||||
)
|
||||
|
||||
snapshot_root = tmp_path_factory.mktemp("openbb_snapshot")
|
||||
pkg_snapshot = snapshot_root / "package"
|
||||
assets_snapshot = snapshot_root / "assets"
|
||||
_snapshot_dir(PACKAGE_DIR, pkg_snapshot)
|
||||
_snapshot_dir(ASSETS_DIR, assets_snapshot)
|
||||
|
||||
try:
|
||||
_pip("install", "--no-deps", "-e", str(work))
|
||||
# The parent pytest interpreter processed `.pth` files at startup, so
|
||||
# the editable install isn't visible to `import` here even though
|
||||
# `importlib.metadata.entry_points()` will return the new entry point.
|
||||
# Add the source dir to sys.path so subsequent unit tests in this same
|
||||
# process that instantiate `ExtensionLoader` can `ep.load()` it.
|
||||
if str(work) not in sys.path:
|
||||
sys.path.insert(0, str(work))
|
||||
_run_openbb_build()
|
||||
|
||||
ext_pkg_module = PACKAGE_DIR / "fake_integration.py"
|
||||
if not ext_pkg_module.exists():
|
||||
pytest.fail(
|
||||
"openbb-build did not emit fake_integration.py; "
|
||||
f"contents of {PACKAGE_DIR}: "
|
||||
f"{sorted(p.name for p in PACKAGE_DIR.glob('*.py'))}"
|
||||
)
|
||||
|
||||
yield {
|
||||
"ext_name": EXT_NAME,
|
||||
"ext_pkg": EXT_PKG,
|
||||
"package_dir": PACKAGE_DIR,
|
||||
"assets_dir": ASSETS_DIR,
|
||||
}
|
||||
finally:
|
||||
with contextlib.suppress(subprocess.CalledProcessError):
|
||||
_pip("uninstall", "-y", EXT_NAME)
|
||||
with contextlib.suppress(ValueError):
|
||||
sys.path.remove(str(work))
|
||||
# Drop any cached imports of the synthetic package so later tests
|
||||
# that re-instantiate `ExtensionLoader` don't see a phantom module.
|
||||
for mod_name in [m for m in sys.modules if m.startswith(EXT_PKG)]:
|
||||
del sys.modules[mod_name]
|
||||
_restore_dir(pkg_snapshot, PACKAGE_DIR)
|
||||
_restore_dir(assets_snapshot, ASSETS_DIR)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def run_in_obb(fake_extension_installed): # noqa: ARG001
|
||||
"""Run an inline snippet under a fresh ``python`` subprocess that imports ``openbb``.
|
||||
|
||||
Returns a callable ``run(snippet: str) -> dict``. The snippet must
|
||||
assign a JSON-serializable value to a variable called ``RESULT``;
|
||||
the wrapper ``json.dumps`` it to stdout and the helper parses it
|
||||
back into Python.
|
||||
"""
|
||||
|
||||
def _run(snippet: str) -> dict:
|
||||
wrapper = textwrap.dedent("""
|
||||
import json, sys
|
||||
from openbb import obb # noqa: F401
|
||||
{snippet}
|
||||
sys.stdout.write(json.dumps(RESULT))
|
||||
""").format(snippet=textwrap.indent(snippet, ""))
|
||||
proc = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-c", wrapper],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "OPENBB_AUTO_BUILD": "false"},
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"obb subprocess failed (rc={proc.returncode})\n"
|
||||
f"--- stdout ---\n{proc.stdout}\n"
|
||||
f"--- stderr ---\n{proc.stderr}"
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
return _run
|
||||
195
openbb_platform/core/integration/test_built_package.py
Normal file
195
openbb_platform/core/integration/test_built_package.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""End-to-end integration tests using a real built ``openbb`` static package.
|
||||
|
||||
These tests rely on the ``fake_extension_installed`` / ``run_in_obb``
|
||||
fixtures defined in ``conftest.py``. The fixtures:
|
||||
|
||||
1. Write a synthetic OpenBB extension distribution to a temp dir.
|
||||
2. ``pip install`` it as ``openbb-fake-integration`` so its
|
||||
``openbb_core_extension`` and ``openbb_provider_extension`` entry
|
||||
points are visible to ``importlib.metadata``.
|
||||
3. Spawn a subprocess that imports ``openbb`` with
|
||||
``OPENBB_AUTO_BUILD=true``, which runs the real ``PackageBuilder``
|
||||
and emits ``openbb/package/fake_integration.py``.
|
||||
4. After the session, uninstall the distribution and restore the
|
||||
pre-test contents of ``openbb/package`` and ``openbb/assets``.
|
||||
|
||||
The tests below then run small snippets in a fresh interpreter and
|
||||
verify ``obb.fake_integration.list_indicators(...)`` is reachable on
|
||||
the freshly-built static package.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def test_built_package_emits_fake_integration_module(fake_extension_installed):
|
||||
"""``openbb-build`` writes a real ``fake_integration.py`` for the synthetic extension."""
|
||||
package_dir = fake_extension_installed["package_dir"]
|
||||
module = package_dir / "fake_integration.py"
|
||||
assert module.exists()
|
||||
source = module.read_text(encoding="utf-8")
|
||||
assert "list_indicators" in source
|
||||
assert "AUTO-GENERATED" in source.upper() or "auto-generated" in source
|
||||
|
||||
|
||||
def test_obb_exposes_fake_integration_namespace(run_in_obb):
|
||||
"""``from openbb import obb`` exposes the freshly-built ``fake_integration`` namespace."""
|
||||
snippet = (
|
||||
"RESULT = {"
|
||||
" 'has_namespace': hasattr(obb, 'fake_integration'),"
|
||||
" 'has_command': hasattr(getattr(obb, 'fake_integration', None),"
|
||||
" 'list_indicators'),"
|
||||
"}"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["has_namespace"], (
|
||||
"obb.fake_integration is missing — generated package was not picked up"
|
||||
)
|
||||
assert result["has_command"], "obb.fake_integration.list_indicators is missing"
|
||||
|
||||
|
||||
def test_obb_command_dispatches_through_real_runner(run_in_obb):
|
||||
"""Calling the generated method runs through the real ``CommandRunner`` and synthetic provider."""
|
||||
snippet = (
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"rows = [r.model_dump() if hasattr(r, 'model_dump') else dict(r) "
|
||||
" for r in out.results]\n"
|
||||
"RESULT = {'rows': rows}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert len(result["rows"]) == 1
|
||||
row = result["rows"][0]
|
||||
assert row["symbol"] == "FAKE"
|
||||
assert row["extra"] == "ok"
|
||||
|
||||
|
||||
def test_obb_coverage_reports_fake_integration_provider(run_in_obb):
|
||||
"""The ``Coverage`` view on a real built ``obb`` includes the synthetic provider."""
|
||||
snippet = (
|
||||
"RESULT = {"
|
||||
" 'providers': sorted(obb.coverage.providers.keys()),"
|
||||
" 'commands': sorted(obb.coverage.commands.keys()),"
|
||||
"}"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert "fake_integration" in result["providers"]
|
||||
assert any("list_indicators" in cmd for cmd in result["commands"]), (
|
||||
f"expected list_indicators in commands, got {result['commands']!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OBBject built-in conversion methods (require pandas)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
pandas_required = pytest.mark.requires_pandas
|
||||
|
||||
|
||||
@pandas_required
|
||||
def test_obbject_to_dict_returns_real_records(run_in_obb):
|
||||
"""``OBBject.to_dict(orient='records')`` round-trips real provider data."""
|
||||
snippet = (
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"RESULT = {'records': out.to_dict(orient='records')}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert isinstance(result["records"], list)
|
||||
assert len(result["records"]) == 1
|
||||
assert result["records"][0]["symbol"] == "FAKE"
|
||||
assert result["records"][0]["extra"] == "ok"
|
||||
|
||||
|
||||
@pandas_required
|
||||
def test_obbject_to_dataframe_via_inline_check(run_in_obb):
|
||||
"""``OBBject.to_dataframe()`` produces a real ``pandas.DataFrame``.
|
||||
|
||||
The DataFrame itself can't cross the JSON boundary, so the check is
|
||||
performed in the child interpreter and only the boolean is returned.
|
||||
"""
|
||||
snippet = (
|
||||
"import pandas as pd\n"
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"df = out.to_dataframe()\n"
|
||||
"RESULT = {"
|
||||
" 'is_dataframe': isinstance(df, pd.DataFrame),"
|
||||
" 'columns': sorted(df.columns.tolist()),"
|
||||
" 'symbol_value': df['symbol'].iloc[0],"
|
||||
"}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["is_dataframe"] is True
|
||||
assert "symbol" in result["columns"]
|
||||
assert "extra" in result["columns"]
|
||||
assert result["symbol_value"] == "FAKE"
|
||||
|
||||
|
||||
@pandas_required
|
||||
def test_obbject_to_llm_emits_json_string(run_in_obb):
|
||||
"""``OBBject.to_llm()`` returns a JSON-encoded string of the records."""
|
||||
snippet = (
|
||||
"import json\n"
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"llm = out.to_llm()\n"
|
||||
"RESULT = {"
|
||||
" 'is_str': isinstance(llm, str),"
|
||||
" 'parsed': json.loads(llm),"
|
||||
"}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["is_str"] is True
|
||||
assert isinstance(result["parsed"], list)
|
||||
assert result["parsed"][0]["symbol"] == "FAKE"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OBBject extension (accessor) — openbb_obbject_extension entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_fake_accessor_is_registered_on_obbject_class(run_in_obb):
|
||||
"""The ``openbb_obbject_extension`` entry point registers the accessor on ``OBBject``.
|
||||
|
||||
``Extension.obbject_accessor`` adds the accessor name to
|
||||
``OBBject.accessors`` and sets the ``CachedAccessor`` descriptor as
|
||||
a class attribute. Both must be true on ``import openbb``.
|
||||
"""
|
||||
snippet = (
|
||||
"from openbb_core.app.model.obbject import OBBject\n"
|
||||
"RESULT = {"
|
||||
" 'in_accessors': 'fake_accessor' in OBBject.accessors,"
|
||||
" 'has_descriptor': hasattr(OBBject, 'fake_accessor'),"
|
||||
"}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["in_accessors"], (
|
||||
"fake_accessor was not added to OBBject.accessors — openbb_obbject_extension entry point did not load"
|
||||
)
|
||||
assert result["has_descriptor"], "OBBject has no fake_accessor descriptor"
|
||||
|
||||
|
||||
def test_fake_accessor_methods_run_against_real_obbject(run_in_obb):
|
||||
"""The custom accessor's methods execute against a real ``OBBject`` instance."""
|
||||
snippet = (
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"RESULT = {"
|
||||
" 'upper': out.fake_accessor.upper_symbols(),"
|
||||
" 'count': out.fake_accessor.row_count(),"
|
||||
"}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["upper"] == ["FAKE"]
|
||||
assert result["count"] == 1
|
||||
|
||||
|
||||
def test_fake_accessor_instance_is_cached_per_obbject(run_in_obb):
|
||||
"""``CachedAccessor`` caches the accessor instance on the OBBject after first access."""
|
||||
snippet = (
|
||||
"out = obb.fake_integration.list_indicators(provider='fake_integration')\n"
|
||||
"first = out.fake_accessor\n"
|
||||
"second = out.fake_accessor\n"
|
||||
"RESULT = {'identical': first is second}\n"
|
||||
)
|
||||
result = run_in_obb(snippet)
|
||||
assert result["identical"] is True
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Test the OBBject."""
|
||||
|
||||
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 or "polars-lts-cpu" 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(
|
||||
country=["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.ameribor(start_date="2020-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."""
|
||||
|
||||
res = obb.equity.price.historical("AAPL", provider="fmp")
|
||||
res.charting.to_chart(render=False)
|
||||
assert isinstance(res.chart.fig, 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
|
||||
@@ -40,11 +40,10 @@ _PackageBuilder(_this_dir).auto_build()
|
||||
_ReferenceLoader(_this_dir)
|
||||
|
||||
try:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb.package.__extensions__ import Extensions as _Extensions # type: ignore
|
||||
|
||||
obb: Union[_BaseApp, _Extensions] = _create_app(_Extensions) # type: ignore
|
||||
obb: Union[_BaseApp, _Extensions] = _create_app(_Extensions)
|
||||
sdk = obb
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
print("Failed to import extensions. Are any installed?")
|
||||
obb = sdk = _create_app() # type: ignore
|
||||
obb = sdk = _create_app()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"openbb": "1.6.8core",
|
||||
"openbb": "1.7.0core",
|
||||
"info": {
|
||||
"title": "OpenBB Platform (Python)",
|
||||
"description": "Investment research for everyone, anywhere.",
|
||||
"core": "1.6.8",
|
||||
"core": "1.7.0",
|
||||
"extensions": {
|
||||
"openbb_core_extension": [],
|
||||
"openbb_provider_extension": [],
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openbb_core.api.exception_handlers import ExceptionHandlers
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.router import RouterLoader
|
||||
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
||||
from pydantic import ValidationError
|
||||
|
||||
|
||||
class AppLoader:
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Annotated
|
||||
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Exception handlers module."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
@@ -9,10 +7,11 @@ from typing import Any
|
||||
from fastapi import Request
|
||||
from fastapi.exceptions import ResponseValidationError
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
||||
from pydantic import ValidationError
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
@@ -48,11 +47,11 @@ class ExceptionHandlers:
|
||||
# Required parameters are missing and is not handled by ValidationError.
|
||||
if isinstance(errors, Iterable):
|
||||
for err in errors:
|
||||
if err.get("type") == "missing":
|
||||
if err.get("type") == "missing": # ty: ignore[unresolved-attribute]
|
||||
return await ExceptionHandlers._handle(
|
||||
exception=error,
|
||||
status_code=422,
|
||||
detail={**err},
|
||||
detail={**err}, # ty: ignore[invalid-argument-type]
|
||||
)
|
||||
return await ExceptionHandlers._handle(
|
||||
exception=error,
|
||||
@@ -93,7 +92,9 @@ class ExceptionHandlers:
|
||||
except Exception:
|
||||
errors = error.errors if hasattr(error, "errors") else error
|
||||
all_in_query = all(
|
||||
loc in query_params for err in errors for loc in err.get("loc", ())
|
||||
loc in query_params
|
||||
for err in errors # ty: ignore[not-iterable]
|
||||
for loc in err.get("loc", ())
|
||||
)
|
||||
if "QueryParams" in error.title and all_in_query:
|
||||
detail = [
|
||||
@@ -101,7 +102,7 @@ class ExceptionHandlers:
|
||||
**{k: v for k, v in err.items() if k != "ctx"},
|
||||
"loc": ("query",) + err.get("loc", ()),
|
||||
}
|
||||
for err in errors
|
||||
for err in errors # ty: ignore[not-iterable]
|
||||
]
|
||||
return await ExceptionHandlers._handle(
|
||||
exception=error,
|
||||
|
||||
@@ -5,6 +5,7 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
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
|
||||
@@ -89,7 +90,6 @@ AppLoader.add_exception_handlers(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import uvicorn
|
||||
|
||||
# This initializes the OpenBB environment variables so they can be read before uvicorn is run.
|
||||
|
||||
@@ -11,6 +11,9 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.params import Depends as DependsParam
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from openbb_core.app.command_runner import CommandRunner
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
@@ -20,17 +23,13 @@ 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.app.utils_optional import is_installed
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.utils.helpers import to_snake_case
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
try:
|
||||
from openbb_charting import Charting
|
||||
|
||||
CHARTING_INSTALLED = True
|
||||
except ImportError:
|
||||
CHARTING_INSTALLED = False
|
||||
CHARTING_INSTALLED = is_installed("openbb_charting")
|
||||
if CHARTING_INSTALLED:
|
||||
from openbb_charting import Charting # ty: ignore[unresolved-import]
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
@@ -214,8 +213,8 @@ def build_api_wrapper(
|
||||
route: APIRoute,
|
||||
) -> Callable:
|
||||
"""Build API wrapper for a command."""
|
||||
func: Callable = route.endpoint # type: ignore
|
||||
path: str = route.path # type: ignore
|
||||
func: Callable = route.endpoint
|
||||
path: str = route.path
|
||||
original_signature = signature(func)
|
||||
has_var_kwargs = any(
|
||||
param.kind == Parameter.VAR_KEYWORD
|
||||
@@ -235,7 +234,7 @@ def build_api_wrapper(
|
||||
route.response_model = None
|
||||
|
||||
@wraps(wrapped=func)
|
||||
async def wrapper( # pylint: disable=R0914,R0912 # noqa: PLR0912
|
||||
async def wrapper( # noqa: PLR0912
|
||||
*args: tuple[Any], **kwargs: dict[str, Any]
|
||||
) -> OBBject | JSONResponse:
|
||||
user_settings: UserSettings = UserSettings.model_validate(
|
||||
@@ -329,7 +328,7 @@ def build_api_wrapper(
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(output), status_code=200
|
||||
)
|
||||
except Exception as exc: # pylint: disable=W0703
|
||||
except Exception as exc:
|
||||
raise OpenBBError(
|
||||
f"Error serializing output for an extension-modified endpoint {path}: {exc}",
|
||||
) from exc
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
from typing import Annotated
|
||||
|
||||
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
|
||||
@@ -39,9 +40,7 @@ async def get_commands_model_map(
|
||||
data_fields = data.get("fields", {})
|
||||
|
||||
for field, field_info in query_fields.items():
|
||||
attributes = (
|
||||
field_info._attributes_set # pylint: disable=protected-access
|
||||
)
|
||||
attributes = field_info._attributes_set
|
||||
if attributes.get("annotation"):
|
||||
_annotation = str(attributes.get("annotation"))
|
||||
attributes["annotation"] = _annotation
|
||||
@@ -51,9 +50,7 @@ async def get_commands_model_map(
|
||||
new_command[provider]["QueryParams"]["docstring"] = query.get("docstring")
|
||||
|
||||
for field, field_info in data_fields.items():
|
||||
attributes = (
|
||||
field_info._attributes_set # pylint: disable=protected-access
|
||||
)
|
||||
attributes = field_info._attributes_set
|
||||
if attributes.get("annotation"):
|
||||
_annotation = str(attributes.get("annotation"))
|
||||
attributes["annotation"] = _annotation
|
||||
|
||||
@@ -4,9 +4,10 @@ from collections.abc import Callable
|
||||
from inspect import _empty, signature
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_core.app.static.app_factory import BaseApp
|
||||
|
||||
@@ -80,17 +81,17 @@ def create_combined_model(
|
||||
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
|
||||
or "openbb" in field.title
|
||||
or (filter_by_provider in field.title)
|
||||
):
|
||||
combined_fields[name] = (type_annotation, field)
|
||||
|
||||
model = create_model(model_name, **combined_fields) # type: ignore
|
||||
model = create_model(model_name, **combined_fields)
|
||||
|
||||
# # Clean up the metadata
|
||||
for field in model.model_fields.values():
|
||||
if hasattr(field, "metadata"):
|
||||
field.metadata = None # type: ignore
|
||||
field.metadata = None
|
||||
|
||||
return model
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from openbb_core.api.dependency.system import get_system_settings
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Command runner module."""
|
||||
|
||||
# pylint: disable=R0903
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, is_dataclass
|
||||
@@ -12,6 +11,8 @@ from typing import TYPE_CHECKING, Any, Optional
|
||||
from warnings import catch_warnings, showwarning, warn
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel, ConfigDict, create_model
|
||||
|
||||
from openbb_core.app.extension_loader import ExtensionLoader
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
|
||||
@@ -22,10 +23,10 @@ from openbb_core.app.provider_interface import ExtraParams
|
||||
from openbb_core.app.static.package_builder import PathHandler
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.utils.helpers import maybe_coroutine, run_async, to_snake_case
|
||||
from pydantic import BaseModel, ConfigDict, create_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.app.router import CommandMap
|
||||
@@ -131,11 +132,12 @@ class ParametersBuilder:
|
||||
user_settings: "UserSettings",
|
||||
) -> dict[str, Any]:
|
||||
"""Update the command context with the available user and system settings."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
|
||||
argcount = func.__code__.co_argcount
|
||||
if "cc" in func.__code__.co_varnames[:argcount]:
|
||||
argcount = func.__code__.co_argcount # ty: ignore[unresolved-attribute]
|
||||
if (
|
||||
"cc" in func.__code__.co_varnames[:argcount] # ty: ignore[unresolved-attribute]
|
||||
):
|
||||
kwargs["cc"] = CommandContext(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
@@ -157,7 +159,7 @@ class ParametersBuilder:
|
||||
if is_dataclass(annotation) and any(
|
||||
t is ExtraParams for t in getattr(annotation, "__bases__", [])
|
||||
):
|
||||
valid = asdict(annotation()) # type: ignore
|
||||
valid = asdict(annotation())
|
||||
for p in extra_params:
|
||||
if "chart_params" in p:
|
||||
continue
|
||||
@@ -173,7 +175,7 @@ class ParametersBuilder:
|
||||
try:
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
return asdict(obj) if is_dataclass(obj) else dict(obj) # type: ignore
|
||||
return asdict(obj) if is_dataclass(obj) else dict(obj)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@@ -195,7 +197,6 @@ class ParametersBuilder:
|
||||
fields[name] = (annotation, default)
|
||||
# We allow extra fields to return with model with 'cc: CommandContext'
|
||||
config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
||||
# pylint: disable=C0103
|
||||
ValidationModel = create_model(func.__name__, __config__=config, **fields) # type: ignore
|
||||
# Validate and coerce
|
||||
model = ValidationModel(**kwargs)
|
||||
@@ -205,7 +206,6 @@ class ParametersBuilder:
|
||||
)
|
||||
return dict(model)
|
||||
|
||||
# pylint: disable=R0913
|
||||
@classmethod
|
||||
def build(
|
||||
cls,
|
||||
@@ -236,7 +236,6 @@ class ParametersBuilder:
|
||||
return kwargs
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class StaticCommandRunner:
|
||||
"""Static Command Runner."""
|
||||
|
||||
@@ -245,7 +244,7 @@ class StaticCommandRunner:
|
||||
cls,
|
||||
func: Callable,
|
||||
kwargs: dict[str, Any],
|
||||
show_warnings: bool = True, # pylint: disable=unused-argument # type: ignore
|
||||
show_warnings: bool = True,
|
||||
) -> OBBject:
|
||||
"""Run a command and return the output."""
|
||||
obbject = await maybe_coroutine(func, **kwargs)
|
||||
@@ -289,8 +288,8 @@ class StaticCommandRunner:
|
||||
if chart_params:
|
||||
kwargs.update(chart_params)
|
||||
|
||||
obbject.charting.show(render=False, **kwargs) # type: ignore[attr-defined]
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
obbject.charting.show(render=False, **kwargs) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
|
||||
except Exception as e:
|
||||
if Env().DEBUG_MODE:
|
||||
raise OpenBBError(e) from e
|
||||
warn(str(e), OpenBBWarning)
|
||||
@@ -303,9 +302,8 @@ class StaticCommandRunner:
|
||||
return params.__dict__
|
||||
return params
|
||||
|
||||
# pylint: disable=R0913, R0914
|
||||
@classmethod
|
||||
async def _execute_func( # pylint: disable=too-many-positional-arguments
|
||||
async def _execute_func(
|
||||
cls,
|
||||
route: str,
|
||||
args: tuple[Any, ...],
|
||||
@@ -363,17 +361,13 @@ class StaticCommandRunner:
|
||||
# In this case, we avoid accessing those attributes.
|
||||
if isinstance(obbject, OBBject):
|
||||
# This section prepares the obbject to pass to the charting service.
|
||||
obbject._route = route # pylint: disable=protected-access
|
||||
obbject._route = route
|
||||
std_params = cls._extract_params(kwargs, "standard_params") or (
|
||||
kwargs if "data" in kwargs else {}
|
||||
)
|
||||
extra_params = cls._extract_params(kwargs, "extra_params") or kwargs
|
||||
obbject._standard_params = ( # pylint: disable=protected-access
|
||||
std_params
|
||||
)
|
||||
obbject._extra_params = ( # pylint: disable=protected-access
|
||||
extra_params
|
||||
)
|
||||
obbject._standard_params = std_params
|
||||
obbject._extra_params = extra_params
|
||||
if chart and obbject.results:
|
||||
if "extra_params" not in kwargs_copy:
|
||||
kwargs_copy["extra_params"] = {}
|
||||
@@ -398,7 +392,7 @@ class StaticCommandRunner:
|
||||
obbject.warnings = []
|
||||
for w in raised_warnings:
|
||||
if isinstance(obbject, OBBject):
|
||||
obbject.warnings.append(cast_warning(w)) # type: ignore
|
||||
obbject.warnings.append(cast_warning(w))
|
||||
if user_settings.preferences.show_warnings:
|
||||
showwarning(
|
||||
message=w.message,
|
||||
@@ -410,7 +404,6 @@ class StaticCommandRunner:
|
||||
)
|
||||
|
||||
if system_settings.logging_suppress is False:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.logs.logging_service import LoggingService
|
||||
|
||||
ls = LoggingService(system_settings, user_settings)
|
||||
@@ -426,7 +419,6 @@ class StaticCommandRunner:
|
||||
|
||||
return obbject
|
||||
|
||||
# pylint: disable=W0718
|
||||
@classmethod
|
||||
async def run(
|
||||
cls,
|
||||
@@ -445,7 +437,7 @@ class StaticCommandRunner:
|
||||
if func := command_map.get_command(route=route):
|
||||
obbject = await cls._execute_func(
|
||||
route=route,
|
||||
args=args, # type: ignore
|
||||
args=args,
|
||||
execution_context=execution_context,
|
||||
func=func,
|
||||
kwargs=kwargs,
|
||||
@@ -480,9 +472,7 @@ class StaticCommandRunner:
|
||||
dependency_param_names.add(dep_name)
|
||||
|
||||
for dep_key in dependency_param_names:
|
||||
_ = obbject._extra_params.pop( # type:ignore # pylint: disable=W0212
|
||||
dep_key, None
|
||||
)
|
||||
_ = obbject._extra_params.pop(dep_key, None) # type: ignore
|
||||
|
||||
meta = getattr(obbject.extra.get("metadata"), "arguments", {})
|
||||
|
||||
@@ -569,8 +559,7 @@ class StaticCommandRunner:
|
||||
return source.model_validate(new_source)
|
||||
except Exception as e:
|
||||
warn(
|
||||
"Skipped immutable callback because the OBBject "
|
||||
f"could not be duplicated. {e}",
|
||||
f"Skipped immutable callback because the OBBject could not be duplicated. {e}",
|
||||
OpenBBWarning,
|
||||
)
|
||||
return None
|
||||
@@ -606,7 +595,7 @@ class StaticCommandRunner:
|
||||
if not isinstance(descriptor, CachedAccessor):
|
||||
continue
|
||||
|
||||
factory = descriptor._accessor # type: ignore # pylint: disable=W0212
|
||||
factory = descriptor._accessor
|
||||
|
||||
target = _clone_for_immutable(obbject) if ext.immutable else obbject
|
||||
|
||||
@@ -649,7 +638,6 @@ class CommandRunner:
|
||||
user_settings: Optional["UserSettings"] = None,
|
||||
) -> None:
|
||||
"""Initialize the command runner."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
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
|
||||
@@ -660,7 +648,6 @@ class CommandRunner:
|
||||
|
||||
def init_logging_service(self) -> None:
|
||||
"""Initialize the logging service."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.logs.logging_service import LoggingService
|
||||
|
||||
_ = LoggingService(
|
||||
@@ -686,7 +673,6 @@ class CommandRunner:
|
||||
def user_settings(self, user_settings: "UserSettings") -> None:
|
||||
self._user_settings = user_settings
|
||||
|
||||
# pylint: disable=W1113
|
||||
async def run(
|
||||
self,
|
||||
route: str,
|
||||
@@ -696,7 +682,6 @@ class CommandRunner:
|
||||
**kwargs,
|
||||
) -> OBBject:
|
||||
"""Run a command and return the OBBject as output."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
self._user_settings = user_settings or self._user_settings
|
||||
|
||||
@@ -709,7 +694,6 @@ class CommandRunner:
|
||||
|
||||
return await StaticCommandRunner.run(execution_context, *args, **kwargs)
|
||||
|
||||
# pylint: disable=W1113
|
||||
def sync_run(
|
||||
self,
|
||||
route: str,
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from importlib_metadata import EntryPoint, EntryPoints, entry_points
|
||||
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.extension import Extension
|
||||
|
||||
@@ -162,7 +163,6 @@ class ExtensionLoader(metaclass=SingletonMeta):
|
||||
|
||||
def load_core(eps: EntryPoints) -> dict[str, "Router"]:
|
||||
"""Return a dictionary of core objects."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.router import Router
|
||||
|
||||
entries: dict[str, Router] = {}
|
||||
@@ -183,7 +183,6 @@ class ExtensionLoader(metaclass=SingletonMeta):
|
||||
|
||||
Keys are entry point names and values are instances of the Provider class.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.abstract.provider import Provider
|
||||
|
||||
entries: dict = {}
|
||||
@@ -200,4 +199,4 @@ class ExtensionLoader(metaclass=SingletonMeta):
|
||||
OpenBBGroups.core: load_core,
|
||||
OpenBBGroups.provider: load_provider,
|
||||
}
|
||||
return func[group](entry_points_) # type: ignore
|
||||
return func[group](entry_points_)
|
||||
|
||||
@@ -36,14 +36,13 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
record.lineno = 0
|
||||
|
||||
if hasattr(record, "session_id"):
|
||||
log_extra["sessionId"] = record.session_id # type: ignore
|
||||
log_extra["sessionId"] = record.session_id
|
||||
|
||||
return log_extra
|
||||
|
||||
@staticmethod
|
||||
def mock_ipv4(text: str) -> str:
|
||||
"""Mock IPv4 addresses in the text."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import re
|
||||
|
||||
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
|
||||
@@ -55,7 +54,6 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
@staticmethod
|
||||
def mock_email(text: str) -> str:
|
||||
"""Mock email addresses in the text."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import re
|
||||
|
||||
pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
||||
@@ -67,7 +65,6 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
@staticmethod
|
||||
def mock_password(text: str) -> str:
|
||||
"""Mock passwords in the text."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import re
|
||||
|
||||
pattern = r'("password": ")[^"]+'
|
||||
@@ -78,7 +75,6 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
@staticmethod
|
||||
def mock_flair(text: str) -> str:
|
||||
"""Mock flair in the text."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import re
|
||||
|
||||
pattern = r'("FLAIR": "\[)(.*?)\]'
|
||||
@@ -90,7 +86,6 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
@staticmethod
|
||||
def mock_home_directory(text: str) -> str:
|
||||
"""Mock home directory in the text."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pathlib import Path
|
||||
|
||||
user_home_directory = str(Path.home().as_posix())
|
||||
@@ -146,7 +141,6 @@ class FormatterWithExceptions(logging.Formatter):
|
||||
@property
|
||||
def settings(self) -> LoggingSettings:
|
||||
"""Get the settings."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from copy import deepcopy
|
||||
|
||||
return deepcopy(self.__settings)
|
||||
|
||||
@@ -7,6 +7,9 @@ from enum import Enum
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic_core import to_jsonable_python
|
||||
|
||||
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
|
||||
FormatterWithExceptions,
|
||||
)
|
||||
@@ -15,8 +18,6 @@ from openbb_core.app.logs.models.logging_settings import LoggingSettings
|
||||
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 import BaseModel
|
||||
from pydantic_core import to_jsonable_python
|
||||
|
||||
|
||||
class DummyProvider(BaseModel):
|
||||
@@ -195,7 +196,6 @@ class LoggingService(metaclass=SingletonMeta):
|
||||
),
|
||||
)
|
||||
|
||||
# pylint: disable=R0917
|
||||
def log(
|
||||
self,
|
||||
user_settings: UserSettings,
|
||||
@@ -277,6 +277,8 @@ class LoggingService(metaclass=SingletonMeta):
|
||||
log_level = self._logger.error if error else self._logger.info
|
||||
log_level(
|
||||
log_message,
|
||||
extra={"func_name_override": func.__name__},
|
||||
extra={
|
||||
"func_name_override": func.__name__ # ty: ignore[unresolved-attribute]
|
||||
},
|
||||
exc_info=exec_info,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ 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:
|
||||
"""Logging settings."""
|
||||
|
||||
@@ -27,7 +26,9 @@ class LoggingSettings:
|
||||
else user_settings.preferences.data_directory
|
||||
)
|
||||
hub_session = (
|
||||
user_settings.profile.hub_session if user_settings.profile else None
|
||||
user_settings.profile.hub_session # ty: ignore[unresolved-attribute]
|
||||
if user_settings.profile # ty: ignore[unresolved-attribute]
|
||||
else None
|
||||
)
|
||||
if hub_session:
|
||||
user_id = hub_session.user_uuid
|
||||
|
||||
@@ -11,7 +11,6 @@ if TYPE_CHECKING:
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class ChartingSettings:
|
||||
"""Charting settings."""
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Command Context."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CommandContext(BaseModel):
|
||||
|
||||
@@ -7,11 +7,6 @@ import warnings
|
||||
from pathlib import Path
|
||||
from typing import Annotated, ClassVar, Optional
|
||||
|
||||
from openbb_core.app.constants import USER_SETTINGS_PATH
|
||||
from openbb_core.app.extension_loader import ExtensionLoader
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
from openbb_core.env import Env
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
@@ -21,6 +16,12 @@ from pydantic import (
|
||||
)
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
|
||||
from openbb_core.app.constants import USER_SETTINGS_PATH
|
||||
from openbb_core.app.extension_loader import ExtensionLoader
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
from openbb_core.env import Env
|
||||
|
||||
|
||||
class LoadingError(Exception):
|
||||
"""Error loading extension."""
|
||||
@@ -158,12 +159,12 @@ class CredentialsLoader:
|
||||
if key in additional and additional[key] not in (None, "")
|
||||
}
|
||||
|
||||
model = create_model(
|
||||
model = create_model( # ty: ignore[no-matching-overload]
|
||||
"Credentials",
|
||||
__config__=ConfigDict(validate_assignment=True, populate_by_name=True),
|
||||
**self.format_credentials(additional), # type: ignore
|
||||
**self.format_credentials(additional),
|
||||
)
|
||||
model._env_defaults = env_overrides # type: ignore # pylint: disable=W0212
|
||||
model._env_defaults = env_overrides
|
||||
model.origins = self.credentials
|
||||
|
||||
return model
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from typing import Any
|
||||
from warnings import warn
|
||||
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||
|
||||
|
||||
class Defaults(BaseModel):
|
||||
"""Defaults."""
|
||||
|
||||
@@ -62,10 +62,7 @@ class APIEx(Example):
|
||||
@staticmethod
|
||||
def _unpack_type(type_: type) -> set:
|
||||
"""Unpack types from types, example Union[List[str], int] -> {typing._GenericAlias, int}."""
|
||||
if (
|
||||
hasattr(type_, "__args__")
|
||||
and type(type_) is not _GenericAlias # pylint: disable=C0123
|
||||
):
|
||||
if hasattr(type_, "__args__") and type(type_) is not _GenericAlias:
|
||||
return set().union(*map(APIEx._unpack_type, type_.__args__)) # type: ignore
|
||||
return {type_} if isinstance(type_, type) else {type(type_)}
|
||||
|
||||
@@ -135,10 +132,14 @@ class APIEx(Example):
|
||||
for k, v in sample.items():
|
||||
if k == "date":
|
||||
obs[k] = (
|
||||
datetime.strptime(v, "%Y-%m-%d") + timedelta(days=i)
|
||||
datetime.strptime(
|
||||
v, # ty: ignore[invalid-argument-type]
|
||||
"%Y-%m-%d",
|
||||
)
|
||||
+ timedelta(days=i)
|
||||
).strftime("%Y-%m-%d")
|
||||
else:
|
||||
obs[k] = round(v * s, 2)
|
||||
obs[k] = round(v * s, 2) # ty: ignore[unsupported-operator]
|
||||
result.append(obs)
|
||||
return result
|
||||
if dataset == "panel":
|
||||
|
||||
@@ -11,7 +11,6 @@ class Extension:
|
||||
See https://docs.openbb.co/developer/extension_types/obbject for more information.
|
||||
"""
|
||||
|
||||
# pylint: disable=R0917
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -41,7 +40,6 @@ class Extension:
|
||||
results_only : bool, optional
|
||||
Whether the extension returns only the results instead of the OBBject, by default False.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
|
||||
self.name = name
|
||||
@@ -97,7 +95,6 @@ class Extension:
|
||||
@property
|
||||
def obbject_accessor(self) -> Callable:
|
||||
"""Extend an OBBject, inspired by pandas."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
|
||||
class OpenBBField(FieldInfo):
|
||||
class OpenBBField(FieldInfo): # ty: ignore[subclass-of-final-class]
|
||||
"""Custom field for OpenBB."""
|
||||
|
||||
def __repr__(self):
|
||||
@@ -24,5 +24,5 @@ class OpenBBField(FieldInfo):
|
||||
def choices(self) -> list[Any] | None:
|
||||
"""Custom choices."""
|
||||
if self.json_schema_extra:
|
||||
return self.json_schema_extra.get("choices") # type: ignore[union-attr,return-value]
|
||||
return self.json_schema_extra.get("choices") # type: ignore[union-attr,return-value] # ty: ignore[unresolved-attribute]
|
||||
return None
|
||||
|
||||
@@ -4,9 +4,10 @@ from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
|
||||
|
||||
class Metadata(BaseModel):
|
||||
"""Metadata of a command execution."""
|
||||
@@ -38,10 +39,13 @@ class Metadata(BaseModel):
|
||||
containing the type and the columns. If the type is not one of the previous, the
|
||||
value is kept or trimmed to 80 characters.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from inspect import isclass # noqa
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
|
||||
from openbb_core.app.utils_optional import require_optional
|
||||
|
||||
np, pd = require_optional("numpy", "pandas")
|
||||
ndarray = np.ndarray # type: ignore[union-attr]
|
||||
DataFrame, Series = pd.DataFrame, pd.Series # type: ignore[union-attr]
|
||||
|
||||
arguments: dict[str, Any] = {}
|
||||
for item in ["provider_choices", "standard_params", "extra_params"]:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""The OBBject."""
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
||||
|
||||
from collections.abc import Callable, Hashable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -12,13 +10,14 @@ from typing import (
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.tagged import Tagged
|
||||
from openbb_core.app.model.abstract.warning import Warning_
|
||||
from openbb_core.app.model.charts.chart import Chart
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
@@ -44,7 +43,7 @@ class OBBject(Tagged, Generic[T]):
|
||||
default=None,
|
||||
description="Serializable results.",
|
||||
)
|
||||
provider: str | None = Field( # type: ignore
|
||||
provider: str | None = Field(
|
||||
default=None,
|
||||
description="Provider name.",
|
||||
)
|
||||
@@ -156,9 +155,11 @@ class OBBject(Tagged, Generic[T]):
|
||||
DataFrame
|
||||
Pandas DataFrame.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import DataFrame, Series, concat # noqa
|
||||
from openbb_core.app.utils import basemodel_to_df # noqa
|
||||
from openbb_core.app.utils_optional import require_optional
|
||||
|
||||
pd = require_optional("pandas")
|
||||
DataFrame, Series, concat = pd.DataFrame, pd.Series, pd.concat # type: ignore[union-attr]
|
||||
|
||||
def is_list_of_basemodel(items: list[T] | T) -> bool:
|
||||
return isinstance(items, list) and all(
|
||||
@@ -178,9 +179,7 @@ class OBBject(Tagged, Generic[T]):
|
||||
|
||||
# BaseModel
|
||||
if isinstance(res, BaseModel):
|
||||
res_dict = res.model_dump( # pylint: disable=no-member
|
||||
exclude_unset=True, exclude_none=True
|
||||
)
|
||||
res_dict = res.model_dump(exclude_unset=True, exclude_none=True)
|
||||
# Model is serialized as a dict[str, list] or list[dict]
|
||||
if (
|
||||
(
|
||||
@@ -218,7 +217,10 @@ class OBBject(Tagged, Generic[T]):
|
||||
for k, v in r.items():
|
||||
# Dict[str, List[BaseModel]]
|
||||
if is_list_of_basemodel(v):
|
||||
dict_of_df[k] = basemodel_to_df(v, index)
|
||||
dict_of_df[k] = basemodel_to_df(
|
||||
v,
|
||||
index,
|
||||
)
|
||||
sort_columns = False
|
||||
# Dict[str, Any]
|
||||
else:
|
||||
@@ -228,13 +230,14 @@ class OBBject(Tagged, Generic[T]):
|
||||
|
||||
# List[BaseModel]
|
||||
elif is_list_of_basemodel(res):
|
||||
dt: list[Data] | Data = res # type: ignore
|
||||
r = dt[0] if isinstance(dt, list) and len(dt) == 1 else None # type: ignore
|
||||
dt: list[Data] | Data = res
|
||||
r = dt[0] if isinstance(dt, list) and len(dt) == 1 else None
|
||||
if r and all(
|
||||
prop.get("type") == "array" for prop in r.model_json_schema()["properties"].values() # type: ignore
|
||||
prop.get("type") == "array"
|
||||
for prop in r.model_json_schema()["properties"].values()
|
||||
):
|
||||
sort_columns = False
|
||||
df = DataFrame(r.model_dump(exclude_unset=True, exclude_none=True)) # type: ignore
|
||||
df = DataFrame(r.model_dump(exclude_unset=True, exclude_none=True))
|
||||
else:
|
||||
df = basemodel_to_df(dt, index)
|
||||
sort_columns = False
|
||||
@@ -284,16 +287,12 @@ class OBBject(Tagged, Generic[T]):
|
||||
|
||||
return df
|
||||
|
||||
def to_polars(self) -> "PolarsDataFrame": # type: ignore
|
||||
def to_polars(self) -> "PolarsDataFrame":
|
||||
"""Convert results field to polars dataframe."""
|
||||
try:
|
||||
from polars import from_pandas # type: ignore # pylint: disable=import-outside-toplevel
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Please install polars: `pip install polars pyarrow` to use this method."
|
||||
) from exc
|
||||
from openbb_core.app.utils_optional import require_optional
|
||||
|
||||
return from_pandas(self.to_dataframe(index=None))
|
||||
polars = require_optional("polars")
|
||||
return polars.from_pandas(self.to_dataframe(index=None)) # type: ignore[union-attr]
|
||||
|
||||
def to_numpy(self) -> "ndarray":
|
||||
"""Convert results field to numpy array."""
|
||||
@@ -321,10 +320,7 @@ class OBBject(Tagged, Generic[T]):
|
||||
if (
|
||||
orient == "list"
|
||||
and isinstance(self.results, dict)
|
||||
and all(
|
||||
isinstance(value, dict)
|
||||
for value in self.results.values() # pylint: disable=no-member
|
||||
)
|
||||
and all(isinstance(value, dict) for value in self.results.values())
|
||||
):
|
||||
df = df.T
|
||||
results: dict | list = df.to_dict(orient=orient)
|
||||
@@ -334,13 +330,13 @@ class OBBject(Tagged, Generic[T]):
|
||||
|
||||
return results
|
||||
|
||||
def to_llm(self) -> dict[Hashable, Any] | list[dict[Hashable, Any]]:
|
||||
def to_llm(self) -> str:
|
||||
"""Convert results field to an LLM compatible output.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[Dict[Hashable, Any], List[Dict[Hashable, Any]]]
|
||||
Dictionary of lists or list of dictionaries if orient is "records".
|
||||
str
|
||||
JSON-encoded string of the records (``orient="records"``).
|
||||
"""
|
||||
df = self.to_dataframe(index=None)
|
||||
|
||||
@@ -350,11 +346,10 @@ class OBBject(Tagged, Generic[T]):
|
||||
date_unit="s",
|
||||
)
|
||||
|
||||
return results # type: ignore
|
||||
return results or "[]"
|
||||
|
||||
def show(self, **kwargs: Any) -> None:
|
||||
"""Display chart."""
|
||||
# pylint: disable=no-member
|
||||
if not self.chart or not self.chart.fig:
|
||||
raise OpenBBError("Chart not found.")
|
||||
kwargs.setdefault("command_location", self._route or "")
|
||||
|
||||
@@ -5,6 +5,8 @@ import platform as pl # I do this so that the import doesn't conflict with the
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from openbb_core.app.constants import (
|
||||
HOME_DIRECTORY,
|
||||
OPENBB_DIRECTORY,
|
||||
@@ -16,7 +18,6 @@ from openbb_core.app.model.api_settings import APISettings
|
||||
from openbb_core.app.model.python_settings import PythonSettings
|
||||
from openbb_core.app.version import CORE_VERSION, VERSION
|
||||
from openbb_core.env import Env
|
||||
from pydantic import ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class SystemSettings(Tagged):
|
||||
|
||||
@@ -4,12 +4,13 @@ import json
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.app.constants import USER_SETTINGS_PATH
|
||||
from openbb_core.app.model.abstract.tagged import Tagged
|
||||
from openbb_core.app.model.credentials import Credentials
|
||||
from openbb_core.app.model.defaults import Defaults
|
||||
from openbb_core.app.model.preferences import Preferences
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class UserSettings(Tagged):
|
||||
|
||||
@@ -14,11 +14,6 @@ from typing import (
|
||||
)
|
||||
|
||||
from fastapi import Body, Query
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.provider.query_executor import QueryExecutor
|
||||
from openbb_core.provider.registry_map import MapType, RegistryMap
|
||||
from openbb_core.provider.utils.helpers import to_snake_case
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
@@ -30,6 +25,12 @@ from pydantic import (
|
||||
)
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.provider.query_executor import QueryExecutor
|
||||
from openbb_core.provider.registry_map import MapType, RegistryMap
|
||||
from openbb_core.provider.utils.helpers import to_snake_case
|
||||
|
||||
TupleFieldType = tuple[str, type | None, Any | None]
|
||||
|
||||
|
||||
@@ -169,7 +170,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
|
||||
def create_executor(self) -> QueryExecutor:
|
||||
"""Get query executor."""
|
||||
return self._query_executor(self._registry_map.registry) # type: ignore[operator]
|
||||
return self._query_executor(self._registry_map.registry) # type: ignore[operator] # ty: ignore[call-non-callable]
|
||||
|
||||
@staticmethod
|
||||
def _merge_fields(
|
||||
@@ -235,8 +236,8 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
json_schema_extra=json_schema_extra,
|
||||
)
|
||||
|
||||
merged_type: type | None = (
|
||||
Union[curr_type, inc_type] if curr_type != inc_type else curr_type # type: ignore[assignment] # noqa
|
||||
merged_type: type | None = ( # ty: ignore[invalid-assignment]
|
||||
Union[curr_type, inc_type] if curr_type != inc_type else curr_type # type: ignore[assignment] # noqa # ty: ignore[invalid-type-form]
|
||||
)
|
||||
|
||||
return DataclassField(curr_name, merged_type, merged_default)
|
||||
@@ -259,11 +260,14 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
for p, v in extra.items(): # type: ignore
|
||||
if isinstance(v, dict) and v.get("multiple_items_allowed"):
|
||||
providers.append(p)
|
||||
choices[p] = {"multiple_items_allowed": True, "choices": v.get("choices")} # type: ignore
|
||||
choices[p] = {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": v.get("choices"),
|
||||
}
|
||||
elif isinstance(v, list) and "multiple_items_allowed" in v:
|
||||
# For backwards compatibility, before this was a list
|
||||
providers.append(p)
|
||||
choices[p] = {"multiple_items_allowed": True, "choices": None} # type: ignore
|
||||
choices[p] = {"multiple_items_allowed": True, "choices": None}
|
||||
elif isinstance(v, dict) and v.get("choices"):
|
||||
choices[p] = {
|
||||
"multiple_items_allowed": False,
|
||||
@@ -326,12 +330,12 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
|
||||
if (
|
||||
hasattr(annotation, "__name__")
|
||||
and annotation.__name__ in ["Dict", "dict", "Data"] # type: ignore
|
||||
and annotation.__name__ in ["Dict", "dict", "Data"]
|
||||
or field.kw_only is True
|
||||
):
|
||||
return DataclassField(
|
||||
new_name,
|
||||
annotation,
|
||||
annotation, # ty: ignore[invalid-argument-type]
|
||||
Body(
|
||||
default=default,
|
||||
title=provider_name,
|
||||
@@ -346,7 +350,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
# up in the swagger, it's a fastapi limitation
|
||||
return DataclassField(
|
||||
new_name,
|
||||
annotation,
|
||||
annotation, # ty: ignore[invalid-argument-type]
|
||||
Query(
|
||||
default=default,
|
||||
title=provider_name,
|
||||
@@ -358,7 +362,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
if provider_name:
|
||||
return DataclassField(
|
||||
new_name,
|
||||
annotation,
|
||||
annotation, # ty: ignore[invalid-argument-type]
|
||||
Field(
|
||||
default=default or None,
|
||||
title=provider_name,
|
||||
@@ -367,7 +371,11 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
),
|
||||
)
|
||||
|
||||
return DataclassField(new_name, annotation, default)
|
||||
return DataclassField(
|
||||
new_name,
|
||||
annotation, # ty: ignore[invalid-argument-type]
|
||||
default,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _extract_params(
|
||||
@@ -559,7 +567,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
if "openbb" in choices:
|
||||
choices.remove("openbb")
|
||||
|
||||
result[model_name] = make_dataclass( # type: ignore
|
||||
result[model_name] = make_dataclass(
|
||||
cls_name=model_name,
|
||||
fields=[
|
||||
(
|
||||
@@ -619,12 +627,12 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
"standard": create_model( # type: ignore
|
||||
model_name,
|
||||
__base__=StandardData,
|
||||
**self._fields_to_pydantic(list(standard.values())), # type: ignore
|
||||
**self._fields_to_pydantic(list(standard.values())),
|
||||
),
|
||||
"extra": create_model(
|
||||
"extra": create_model( # ty: ignore[no-matching-overload]
|
||||
model_name,
|
||||
__base__=ExtraData,
|
||||
**self._fields_to_pydantic(list(extra.values())), # type: ignore
|
||||
**self._fields_to_pydantic(list(extra.values())),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -663,7 +671,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
result[model_name] = create_model( # type: ignore
|
||||
model_name,
|
||||
__config__=model_config,
|
||||
**fields_dict, # type: ignore
|
||||
**fields_dict,
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -732,7 +740,7 @@ class ProviderInterface(metaclass=SingletonMeta):
|
||||
for name, models in original_models.items():
|
||||
outer = {model["results_type"] for model in models.values()}
|
||||
inner = self._get_annotated_union(models)
|
||||
full = Union[tuple((o[inner] if o else inner) for o in outer)] # type: ignore # noqa
|
||||
full = Union[tuple((o[inner] if o else inner) for o in outer)] # noqa
|
||||
annotations[name] = create_model(
|
||||
f"OBBject_{name}",
|
||||
__base__=OBBject[full], # type: ignore
|
||||
|
||||
@@ -67,12 +67,17 @@ class Query:
|
||||
"""Execute the query."""
|
||||
standard_dict = asdict(self.standard_params)
|
||||
extra_dict = (
|
||||
self.filter_extra_params(self.extra_params, self.provider) if self.extra_params else {} # type: ignore
|
||||
self.filter_extra_params(
|
||||
self.extra_params,
|
||||
self.provider, # ty: ignore[invalid-argument-type]
|
||||
)
|
||||
if self.extra_params
|
||||
else {}
|
||||
)
|
||||
query_executor = self.provider_interface.create_executor()
|
||||
|
||||
return await query_executor.execute(
|
||||
provider_name=self.provider,
|
||||
provider_name=self.provider, # ty: ignore[invalid-argument-type]
|
||||
model_name=self.name,
|
||||
params={**standard_dict, **extra_dict},
|
||||
credentials=self.cc.user_settings.credentials.model_dump(),
|
||||
|
||||
@@ -15,6 +15,9 @@ from typing import (
|
||||
)
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from openbb_core.app.deprecation import DeprecationSummary, OpenBBDeprecationWarning
|
||||
from openbb_core.app.extension_loader import ExtensionLoader
|
||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||
@@ -27,8 +30,6 @@ from openbb_core.app.provider_interface import (
|
||||
StandardParams,
|
||||
)
|
||||
from openbb_core.env import Env
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
@@ -119,13 +120,16 @@ class Router:
|
||||
kwargs["operation_id"] = kwargs.get(
|
||||
"operation_id", SignatureInspector.get_operation_id(func)
|
||||
)
|
||||
kwargs["path"] = kwargs.get("path", f"/{func.__name__}")
|
||||
kwargs["path"] = kwargs.get(
|
||||
"path",
|
||||
f"/{func.__name__}", # ty: ignore[unresolved-attribute]
|
||||
)
|
||||
kwargs["endpoint"] = func
|
||||
kwargs["methods"] = kwargs.get("methods", ["GET"])
|
||||
kwargs["response_model"] = (
|
||||
kwargs.get(
|
||||
"response_model",
|
||||
func.__annotations__["return"], # type: ignore
|
||||
func.__annotations__["return"],
|
||||
)
|
||||
if not no_validate
|
||||
else func.__annotations__["return"]
|
||||
@@ -243,7 +247,7 @@ class SignatureInspector:
|
||||
if model not in provider_interface.models:
|
||||
if Env().DEBUG_MODE:
|
||||
warnings.warn(
|
||||
message=f"\nSkipping api route '/{func.__name__}'.\n"
|
||||
message=f"\nSkipping api route '/{func.__name__}'.\n" # ty: ignore[unresolved-attribute]
|
||||
f"Model '{model}' not found.\n\n"
|
||||
"Check available models in ProviderInterface().models",
|
||||
category=OpenBBWarning,
|
||||
@@ -326,12 +330,12 @@ class SignatureInspector:
|
||||
for k, v in expected.items():
|
||||
if k not in func.__annotations__:
|
||||
raise AttributeError(
|
||||
f"Invalid signature: '{func.__name__}'. Missing '{k}' parameter."
|
||||
f"Invalid signature: '{func.__name__}'. Missing '{k}' parameter." # ty: ignore[unresolved-attribute]
|
||||
)
|
||||
|
||||
if func.__annotations__[k] != v:
|
||||
raise TypeError(
|
||||
f"Invalid signature: '{func.__name__}'. '{k}' parameter must be of type '{v.__name__}'."
|
||||
f"Invalid signature: '{func.__name__}'. '{k}' parameter must be of type '{v.__name__}'." # ty: ignore[unresolved-attribute]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -339,7 +343,7 @@ class SignatureInspector:
|
||||
func: Callable[P, OBBject], arg: str, callable_: Any
|
||||
) -> Callable[P, OBBject]:
|
||||
"""Annotate function with dependency injection."""
|
||||
func.__annotations__[arg] = Annotated[callable_, Depends()] # type: ignore
|
||||
func.__annotations__[arg] = Annotated[callable_, Depends()]
|
||||
return func
|
||||
|
||||
@staticmethod
|
||||
@@ -368,7 +372,7 @@ class SignatureInspector:
|
||||
"""Get operation id."""
|
||||
operation_id = [
|
||||
t.replace("_router", "").replace("openbb_", "")
|
||||
for t in func.__module__.split(".") + [func.__name__]
|
||||
for t in func.__module__.split(".") + [func.__name__] # ty: ignore[unresolved-attribute]
|
||||
]
|
||||
cleaned_id = sep.join({c: "" for c in operation_id if c}.keys())
|
||||
return cleaned_id
|
||||
@@ -452,7 +456,7 @@ class CommandMap:
|
||||
coverage_map[provider] = []
|
||||
if hasattr(route, "path"):
|
||||
rp = (
|
||||
route.path # type: ignore
|
||||
route.path
|
||||
if sep is None
|
||||
else route.path.replace("/", sep) # type: ignore
|
||||
)
|
||||
@@ -481,7 +485,7 @@ class CommandMap:
|
||||
|
||||
if hasattr(route, "path"):
|
||||
rp = route.path if sep is None else route.path.replace("/", sep) # type: ignore
|
||||
if route.path not in coverage_map: # type: ignore
|
||||
if route.path not in coverage_map:
|
||||
coverage_map[rp] = []
|
||||
coverage_map[rp] = providers
|
||||
return coverage_map
|
||||
@@ -498,7 +502,7 @@ class CommandMap:
|
||||
model = openapi_extra.get("model", None)
|
||||
if model and hasattr(route, "path"):
|
||||
rp = route.path if sep is None else route.path.replace("/", sep) # type: ignore
|
||||
if route.path not in coverage_map: # type: ignore
|
||||
if route.path not in coverage_map:
|
||||
coverage_map[rp] = []
|
||||
coverage_map[rp] = model
|
||||
return coverage_map
|
||||
|
||||
@@ -6,6 +6,7 @@ from importlib import import_module
|
||||
from types import ModuleType
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from openbb_core.api.router.user import (
|
||||
auth_hook as default_auth_hook,
|
||||
router as default_router,
|
||||
|
||||
@@ -51,13 +51,12 @@ class BaseApp:
|
||||
return self._reference
|
||||
|
||||
|
||||
def create_app(extensions: E | None = None) -> type[BaseApp]: # type: ignore
|
||||
def create_app(extensions: E | None = None) -> type[BaseApp]:
|
||||
"""Create the app."""
|
||||
|
||||
class App(BaseApp, extensions or object): # type: ignore[misc]
|
||||
class App(BaseApp, extensions or object): # type: ignore[misc] # ty: ignore[shadowed-type-variable, unsupported-base]
|
||||
def __repr__(self) -> str:
|
||||
# pylint: disable=E1101
|
||||
ext_doc = extensions.__doc__ if extensions else ""
|
||||
return BASE_DOC + (ext_doc or "")
|
||||
|
||||
return App(command_runner=CommandRunner()) # type: ignore[call-arg]
|
||||
return App(command_runner=CommandRunner()) # type: ignore[call-arg] # ty: ignore[invalid-return-type]
|
||||
|
||||
@@ -13,7 +13,6 @@ class Container:
|
||||
|
||||
def __init__(self, command_runner: "CommandRunner") -> None:
|
||||
"""Initialize the container."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
self._command_runner = command_runner
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
"""Package Builder Class.
|
||||
|
||||
This package was split from the original ``package_builder.py`` module into
|
||||
smaller submodules. The public API is preserved: every name that was importable
|
||||
from ``openbb_core.app.static.package_builder`` continues to be importable from
|
||||
this package.
|
||||
"""
|
||||
|
||||
# Re-export module-level helpers and singletons so that callers that patch via
|
||||
# dotted paths (e.g. ``monkeypatch.setattr("openbb_core.app.static.package_builder.X", ...)``)
|
||||
# continue to work unchanged.
|
||||
# Re-export common symbols from the original module-level namespace so that
|
||||
# downstream code (and tests) which used to import them via
|
||||
# ``openbb_core.app.static.package_builder.X`` continues to work.
|
||||
from inspect import Parameter, _empty, isclass, signature # noqa: E402, F401
|
||||
from typing import ( # noqa: E402, F401
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from typing_extensions import _AnnotatedAlias # noqa: E402, F401
|
||||
|
||||
from openbb_core.app.static.package_builder.builder import ( # noqa: E402, F401
|
||||
CHARTING_INSTALLED,
|
||||
CORE_VERSION,
|
||||
VERSION,
|
||||
Console,
|
||||
DataProcessingSupportedTypes,
|
||||
Env,
|
||||
ExtensionLoader,
|
||||
Linters,
|
||||
OpenBBGroups,
|
||||
PackageBuilder,
|
||||
contextlib,
|
||||
dumps,
|
||||
entry_points,
|
||||
load,
|
||||
os,
|
||||
shutil,
|
||||
signal,
|
||||
sys,
|
||||
traceback,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.class_definition import ClassDefinition
|
||||
from openbb_core.app.static.package_builder.docstring_generator import ( # noqa: E402, F401
|
||||
DocstringGenerator,
|
||||
SystemService,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.file_lock import FileLock
|
||||
from openbb_core.app.static.package_builder.import_definition import ImportDefinition
|
||||
from openbb_core.app.static.package_builder.method_definition import MethodDefinition
|
||||
from openbb_core.app.static.package_builder.module_builder import ModuleBuilder
|
||||
|
||||
# Module-level constants / aliases that were defined in the original file.
|
||||
from openbb_core.app.static.package_builder.path_handler import ( # noqa: E402
|
||||
TAB,
|
||||
PathHandler,
|
||||
RouterLoader,
|
||||
create_indent,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.reference_generator import (
|
||||
ReferenceGenerator,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CHARTING_INSTALLED",
|
||||
"CORE_VERSION",
|
||||
"ClassDefinition",
|
||||
"Console",
|
||||
"DocstringGenerator",
|
||||
"Env",
|
||||
"ExtensionLoader",
|
||||
"FileLock",
|
||||
"ImportDefinition",
|
||||
"Linters",
|
||||
"MethodDefinition",
|
||||
"ModuleBuilder",
|
||||
"OpenBBGroups",
|
||||
"PackageBuilder",
|
||||
"Parameter",
|
||||
"PathHandler",
|
||||
"ReferenceGenerator",
|
||||
"RouterLoader",
|
||||
"SystemService",
|
||||
"TAB",
|
||||
"VERSION",
|
||||
"create_indent",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Shared indentation helpers for package_builder submodules."""
|
||||
|
||||
TAB = " "
|
||||
|
||||
|
||||
def create_indent(n: int) -> str:
|
||||
"""Create n indentation space."""
|
||||
return TAB * n
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Top-level PackageBuilder coordinator."""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
from json import dumps, load
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from importlib_metadata import entry_points
|
||||
|
||||
from openbb_core.app.extension_loader import ExtensionLoader, OpenBBGroups
|
||||
from openbb_core.app.static.utils.console import Console
|
||||
from openbb_core.app.static.utils.linters import Linters
|
||||
from openbb_core.app.version import CORE_VERSION, VERSION
|
||||
from openbb_core.env import Env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.file_lock import FileLock
|
||||
from openbb_core.app.static.package_builder.module_builder import ModuleBuilder
|
||||
from openbb_core.app.static.package_builder.path_handler import PathHandler
|
||||
from openbb_core.app.static.package_builder.reference_generator import (
|
||||
ReferenceGenerator,
|
||||
)
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
"""Build the extension package for the Platform."""
|
||||
|
||||
def __init__(
|
||||
self, directory: Path | None = None, lint: bool = True, verbose: bool = False
|
||||
) -> None:
|
||||
"""Initialize the package builder."""
|
||||
self.directory = directory or Path(__file__).parent
|
||||
self.lint = lint
|
||||
self.verbose = verbose
|
||||
self.console = Console(verbose)
|
||||
self.route_map = PathHandler.build_route_map()
|
||||
self.path_list = PathHandler.build_path_list(route_map=self.route_map)
|
||||
self._lock_path = self.directory / ".build.lock"
|
||||
|
||||
def auto_build(self) -> None:
|
||||
"""Trigger build if there are differences between built and installed extensions."""
|
||||
if Env().AUTO_BUILD:
|
||||
reference = PackageBuilder._read(
|
||||
self.directory / "assets" / "reference.json"
|
||||
)
|
||||
ext_map = reference.get("info", {}).get("extensions", {})
|
||||
add, remove = PackageBuilder._diff(ext_map)
|
||||
if add:
|
||||
a = ", ".join(sorted(add))
|
||||
print(f"Extensions to add: {a}") # noqa: T201
|
||||
|
||||
if remove:
|
||||
r = ", ".join(sorted(remove))
|
||||
print(f"Extensions to remove: {r}") # noqa: T201
|
||||
|
||||
if add or remove:
|
||||
print("\nBuilding...") # noqa: T201
|
||||
self.build()
|
||||
|
||||
def build(
|
||||
self,
|
||||
modules: str | list[str] | None = None,
|
||||
) -> None:
|
||||
"""Build the extensions for the Platform."""
|
||||
self._lock_path.touch(exist_ok=True)
|
||||
|
||||
# Open lock file and acquire exclusive lock
|
||||
with open(self._lock_path, "w", encoding="utf-8") as lock_file:
|
||||
file_lock = FileLock(lock_file)
|
||||
try:
|
||||
# Get exclusive lock on file
|
||||
file_lock.acquire(blocking=False)
|
||||
|
||||
# Write PID to lock file for debugging
|
||||
lock_file.seek(0)
|
||||
lock_file.truncate()
|
||||
lock_file.write(str(os.getpid()))
|
||||
lock_file.flush()
|
||||
|
||||
# Signal handler for SIGTERM
|
||||
def _handle_term(signum, _):
|
||||
self._clean(modules)
|
||||
sys.exit(signum)
|
||||
|
||||
if hasattr(signal, "SIGTERM"):
|
||||
original_sigterm = signal.getsignal(signal.SIGTERM)
|
||||
signal.signal(signal.SIGTERM, _handle_term)
|
||||
|
||||
try:
|
||||
self._clean(modules)
|
||||
ext_map = self._get_extension_map()
|
||||
self._save_modules(modules, ext_map)
|
||||
self._save_reference_file(ext_map)
|
||||
self._save_package()
|
||||
if self.lint:
|
||||
self._run_linters()
|
||||
except BaseException as e:
|
||||
if not isinstance(e, (KeyboardInterrupt, SystemExit)):
|
||||
self.console.error("\nBuild failed!")
|
||||
self.console.error(f"Error: {e}")
|
||||
self.console.error(traceback.format_exc())
|
||||
self.console.error("\nInstruction:")
|
||||
self.console.error(
|
||||
"Set OPENBB_DEBUG_MODE='true' environment variable and run "
|
||||
"'openbb-build' again to see verbose output."
|
||||
)
|
||||
self._clean(modules)
|
||||
raise
|
||||
finally:
|
||||
if hasattr(signal, "SIGTERM"):
|
||||
signal.signal(signal.SIGTERM, original_sigterm)
|
||||
except BlockingIOError:
|
||||
raise RuntimeError( # noqa
|
||||
f"Another build process is running and has locked {self._lock_path}"
|
||||
)
|
||||
finally:
|
||||
# Release the file lock, suppressing any exceptions during cleanup
|
||||
with contextlib.suppress(Exception):
|
||||
file_lock.release()
|
||||
|
||||
def _clean(self, modules: str | list[str] | None = None) -> None:
|
||||
"""Delete the assets and package folder or modules before building."""
|
||||
shutil.rmtree(self.directory / "assets", ignore_errors=True)
|
||||
if modules:
|
||||
for module in modules:
|
||||
module_path = self.directory / "package" / f"{module}.py"
|
||||
if module_path.exists():
|
||||
module_path.unlink()
|
||||
else:
|
||||
shutil.rmtree(self.directory / "package", ignore_errors=True)
|
||||
|
||||
def _get_extension_map(self) -> dict[str, list[str]]:
|
||||
"""Get map of extensions available at build time."""
|
||||
el = ExtensionLoader()
|
||||
og = OpenBBGroups.groups()
|
||||
ext_map: dict[str, list[str]] = {}
|
||||
|
||||
for group, entry_point in zip(og, el.entry_points):
|
||||
ext_map[group] = [
|
||||
f"{e.name}@{getattr(e.dist, 'version', '')}" for e in entry_point
|
||||
]
|
||||
return ext_map
|
||||
|
||||
def _save_modules(
|
||||
self,
|
||||
modules: str | list[str] | None = None,
|
||||
ext_map: dict[str, list[str]] | None = None,
|
||||
):
|
||||
"""Save the modules."""
|
||||
self.console.log("\nWriting modules...")
|
||||
|
||||
if not self.path_list:
|
||||
self.console.log("\nThere is nothing to write.")
|
||||
return
|
||||
|
||||
MAX_LEN = max([len(path) for path in self.path_list if path != "/"])
|
||||
|
||||
_path_list = (
|
||||
[path for path in self.path_list if path in modules]
|
||||
if modules
|
||||
else self.path_list
|
||||
)
|
||||
|
||||
for path in _path_list:
|
||||
route = PathHandler.get_route(path, self.route_map)
|
||||
# Only create a module if this path doesn't have a direct route
|
||||
# This prevents creating sub-router modules for paths like /empty/also_empty
|
||||
# when the actual route is /empty/also_empty/{param}
|
||||
if route is None:
|
||||
code = ModuleBuilder.build(path, ext_map)
|
||||
name = PathHandler.build_module_name(path)
|
||||
self.console.log(f"({path})", end=" " * (MAX_LEN - len(path)))
|
||||
self._write(code, name)
|
||||
|
||||
def _save_package(self):
|
||||
"""Save the package."""
|
||||
self.console.log("\nWriting package __init__...")
|
||||
code = '""" Autogenerated OpenBB module."""\n'
|
||||
code += "### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###"
|
||||
self._write(code=code, name="__init__")
|
||||
|
||||
def _save_reference_file(self, ext_map: dict[str, list[str]] | None = None):
|
||||
"""Save the reference.json file."""
|
||||
self.console.log("\nWriting reference file...")
|
||||
code = dumps(
|
||||
obj={
|
||||
"openbb": VERSION.replace("dev", ""),
|
||||
"info": {
|
||||
"title": "OpenBB Platform (Python)",
|
||||
"description": "Investment research for everyone, anywhere.",
|
||||
"core": CORE_VERSION.replace("dev", ""),
|
||||
"extensions": ext_map,
|
||||
},
|
||||
"paths": ReferenceGenerator.get_paths(self.route_map),
|
||||
"routers": ReferenceGenerator.get_routers(self.route_map),
|
||||
},
|
||||
indent=4,
|
||||
)
|
||||
self._write(code=code, name="reference", extension="json", folder="assets")
|
||||
|
||||
def _run_linters(self):
|
||||
"""Run the linters."""
|
||||
self.console.log("\nRunning linters...")
|
||||
linters = Linters(self.directory / "package", self.verbose)
|
||||
linters.black()
|
||||
linters.ruff()
|
||||
|
||||
def _write(
|
||||
self, code: str, name: str, extension: str = "py", folder: str = "package"
|
||||
) -> None:
|
||||
"""Write the module to the package."""
|
||||
package_folder = self.directory / folder
|
||||
package_path = package_folder / f"{name}.{extension}"
|
||||
package_folder.mkdir(exist_ok=True)
|
||||
self.console.log(str(package_path))
|
||||
|
||||
with package_path.open("w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(code.replace("typing.", "").replace("List", "list"))
|
||||
|
||||
@staticmethod
|
||||
def _read(path: Path) -> dict:
|
||||
"""Get content from folder."""
|
||||
try:
|
||||
with open(Path(path)) as fp:
|
||||
content = load(fp)
|
||||
except Exception:
|
||||
content = {}
|
||||
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
def _diff(ext_map: dict[str, list[str]]) -> tuple[set[str], set[str]]:
|
||||
"""Check differences between built and installed extensions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ext_map: Dict[str, List[str]]
|
||||
Dictionary containing the extensions.
|
||||
Example:
|
||||
{
|
||||
"openbb_core_extension": [
|
||||
"commodity@1.0.1",
|
||||
...
|
||||
],
|
||||
"openbb_provider_extension": [
|
||||
"benzinga@1.1.3",
|
||||
...
|
||||
],
|
||||
"openbb_obbject_extension": [
|
||||
"openbb_charting@1.0.0",
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Set[str], Set[str]]
|
||||
First element: set of installed extensions that are not in the package.
|
||||
Second element: set of extensions in the package that are not installed.
|
||||
"""
|
||||
add: set[str] = set()
|
||||
remove: set[str] = set()
|
||||
groups = OpenBBGroups.groups()
|
||||
|
||||
for g in groups:
|
||||
built = set(ext_map.get(g, {}))
|
||||
installed = set(
|
||||
f"{e.name}@{getattr(e.dist, 'version', '')}"
|
||||
for e in entry_points(group=g)
|
||||
)
|
||||
add = add.union(installed - built)
|
||||
remove = remove.union(built - installed)
|
||||
|
||||
return add, remove
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Class definition emitter for the static package."""
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.method_definition import MethodDefinition
|
||||
from openbb_core.app.static.package_builder.path_handler import PathHandler
|
||||
|
||||
|
||||
class ClassDefinition:
|
||||
"""Build the class definition for the Platform."""
|
||||
|
||||
@staticmethod
|
||||
def build(path: str, ext_map: dict[str, list[str]] | None = None) -> str:
|
||||
"""Build the class definition."""
|
||||
class_name = PathHandler.build_module_class(path=path)
|
||||
code = f"class {class_name}(Container):\n"
|
||||
route_map = PathHandler.build_route_map()
|
||||
path_list = PathHandler.build_path_list(route_map)
|
||||
child_path_list = sorted(
|
||||
PathHandler.get_child_path_list(
|
||||
path,
|
||||
path_list,
|
||||
)
|
||||
)
|
||||
doc = f' """{path}\n' if path else ' # fmt: off\n """\nRouters:\n'
|
||||
methods = ""
|
||||
|
||||
for c in child_path_list:
|
||||
route = PathHandler.get_route(c, route_map)
|
||||
has_subroutes = any(r.startswith(c + "/") and r != c for r in route_map)
|
||||
|
||||
if route is None:
|
||||
if has_subroutes:
|
||||
doc += " /" if path else " /"
|
||||
doc += c.split("/")[-1] + "\n"
|
||||
methods += MethodDefinition.build_class_loader_method(path=c)
|
||||
continue
|
||||
|
||||
route_methods = getattr(route, "methods", None)
|
||||
is_command_route = (
|
||||
route
|
||||
and hasattr(route, "endpoint")
|
||||
and callable(route.endpoint)
|
||||
and isinstance(route_methods, set)
|
||||
and route_methods
|
||||
)
|
||||
|
||||
if (path == "" and is_command_route) or "." in path:
|
||||
continue
|
||||
|
||||
if is_command_route:
|
||||
doc += f" {route.name}\n"
|
||||
methods += MethodDefinition.build_command_method(
|
||||
path=route.path,
|
||||
func=route.endpoint,
|
||||
model_name=(
|
||||
route.openapi_extra.get("model", None)
|
||||
if hasattr(route, "openapi_extra")
|
||||
and getattr(route, "openapi_extra", None) is not None
|
||||
else None
|
||||
),
|
||||
examples=(
|
||||
route.openapi_extra.get("examples", [])
|
||||
if hasattr(route, "openapi_extra")
|
||||
and getattr(route, "openapi_extra", None) is not None
|
||||
else []
|
||||
),
|
||||
)
|
||||
continue
|
||||
|
||||
if has_subroutes:
|
||||
# This is a sub-router path - create a property
|
||||
doc += " /" if path else " /"
|
||||
doc += c.split("/")[-1] + "\n"
|
||||
methods += MethodDefinition.build_class_loader_method(path=c)
|
||||
|
||||
if not path:
|
||||
if ext_map:
|
||||
doc += "\n"
|
||||
doc += "Extensions:\n"
|
||||
doc += "\n".join(
|
||||
[f" - {ext}" for ext in ext_map.get("openbb_core_extension", [])]
|
||||
)
|
||||
doc += "\n\n"
|
||||
doc += "\n".join(
|
||||
[
|
||||
f" - {ext}"
|
||||
for ext in ext_map.get("openbb_provider_extension", [])
|
||||
]
|
||||
)
|
||||
doc += ' """\n'
|
||||
doc += " # fmt: on\n"
|
||||
else:
|
||||
doc += ' """\n'
|
||||
|
||||
code += doc + "\n"
|
||||
code += " def __repr__(self) -> str:\n"
|
||||
code += ' return self.__doc__ or ""\n'
|
||||
code += methods
|
||||
|
||||
return code
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
"""Cross-platform file lock used by the package builder."""
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
|
||||
|
||||
class FileLock:
|
||||
"""Simple cross-platform file lock wrapper used only for this module."""
|
||||
|
||||
def __init__(self, file_obj):
|
||||
"""Initialize the file lock."""
|
||||
self._file = file_obj
|
||||
|
||||
def acquire(self, blocking: bool = True) -> None:
|
||||
"""Acquire the file lock."""
|
||||
if _HAS_FCNTL:
|
||||
flags = fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)
|
||||
fcntl.flock(self._file.fileno(), flags)
|
||||
else: # Windows via msvcrt
|
||||
mode = msvcrt.LK_LOCK if blocking else msvcrt.LK_NBLCK # type: ignore
|
||||
try:
|
||||
# lock 1 byte at file start; file.seek(0) to ensure position
|
||||
self._file.seek(0)
|
||||
msvcrt.locking(self._file.fileno(), mode, 1) # type: ignore
|
||||
except OSError as exc: # pragma: no cover - platform specific
|
||||
# Normalize to BlockingIOError for parity with fcntl non-blocking
|
||||
raise BlockingIOError from exc
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the file lock."""
|
||||
try:
|
||||
if _HAS_FCNTL:
|
||||
fcntl.flock(self._file.fileno(), fcntl.LOCK_UN)
|
||||
else:
|
||||
try:
|
||||
self._file.seek(0)
|
||||
msvcrt.locking(self._file.fileno(), msvcrt.LK_UNLCK, 1) # type: ignore
|
||||
except OSError:
|
||||
# If unlocking fails on Windows, ignore - file will be closed soon
|
||||
pass
|
||||
except Exception: # noqa
|
||||
pass
|
||||
@@ -0,0 +1,337 @@
|
||||
"""Import statement emitter for the static package."""
|
||||
|
||||
import typing as typing_module
|
||||
from inspect import _empty, signature
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
TypeVar,
|
||||
get_args,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from typing_extensions import _AnnotatedAlias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.path_handler import PathHandler
|
||||
|
||||
|
||||
class ImportDefinition:
|
||||
"""Build the import definition for the Platform."""
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_type_name(type_name: str) -> str:
|
||||
"""Normalize a raw type name extracted from annotations."""
|
||||
sanitized = type_name.strip().replace('"', "").replace("'", "")
|
||||
sanitized = sanitized.replace("typing.", "").replace("typing_extensions.", "")
|
||||
sanitized = sanitized.split("[", 1)[0]
|
||||
sanitized = sanitized.split("(", 1)[0]
|
||||
return sanitized
|
||||
|
||||
@staticmethod
|
||||
def filter_hint_type_list(hint_type_list: list[type]) -> list[type]:
|
||||
"""Filter the hint type list."""
|
||||
new_hint_type_list = []
|
||||
primitive_types = {int, float, str, bool, list, dict, tuple, set}
|
||||
|
||||
for hint_type in hint_type_list:
|
||||
# Skip primitive types and empty types
|
||||
# Check for _empty first (doesn't require hashing)
|
||||
if hint_type == _empty:
|
||||
continue
|
||||
|
||||
# Skip Depends objects (they're not types we need to import)
|
||||
if (
|
||||
hasattr(hint_type, "__class__")
|
||||
and "Depends" in hint_type.__class__.__name__
|
||||
):
|
||||
continue
|
||||
|
||||
# Skip Annotated types that contain Depends in their metadata
|
||||
if isinstance(hint_type, _AnnotatedAlias):
|
||||
has_depends = False
|
||||
if hasattr(hint_type, "__metadata__"):
|
||||
for meta in hint_type.__metadata__:
|
||||
if (
|
||||
hasattr(meta, "__class__")
|
||||
and "Depends" in meta.__class__.__name__
|
||||
):
|
||||
has_depends = True
|
||||
break
|
||||
if has_depends:
|
||||
continue
|
||||
|
||||
# Now safe to check against primitive_types set
|
||||
try:
|
||||
if hint_type in primitive_types:
|
||||
continue
|
||||
except TypeError:
|
||||
# If somehow we still get an unhashable type, skip it
|
||||
continue
|
||||
|
||||
# Only include types that have a module and are not builtins
|
||||
if (
|
||||
hasattr(hint_type, "__module__") and hint_type.__module__ != "builtins"
|
||||
) or (isinstance(hint_type, str)):
|
||||
new_hint_type_list.append(hint_type)
|
||||
|
||||
# Deduplicate without using set() to handle unhashable types
|
||||
deduplicated: list = []
|
||||
for hint_type in new_hint_type_list:
|
||||
is_duplicate = False
|
||||
for existing in deduplicated:
|
||||
try:
|
||||
if hint_type == existing:
|
||||
is_duplicate = True
|
||||
break
|
||||
except TypeError:
|
||||
# If comparison fails, compare by identity
|
||||
if id(hint_type) == id(existing):
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if not is_duplicate:
|
||||
deduplicated.append(hint_type)
|
||||
|
||||
return deduplicated
|
||||
|
||||
@classmethod
|
||||
def get_function_hint_type_list(cls, route) -> list[type]:
|
||||
"""Get the hint type list from the function."""
|
||||
|
||||
no_validate = (getattr(route, "openapi_extra", None) or {}).get("no_validate")
|
||||
|
||||
func = route.endpoint
|
||||
sig = signature(func)
|
||||
if no_validate is True:
|
||||
route.response_model = None
|
||||
|
||||
parameter_map = sig.parameters
|
||||
return_type = (
|
||||
sig.return_annotation if not no_validate else route.response_model or Any
|
||||
)
|
||||
|
||||
hint_type_list: list = []
|
||||
|
||||
for parameter in parameter_map.values():
|
||||
hint_type_list.append(parameter.annotation)
|
||||
|
||||
# Extract dependencies from Annotated metadata
|
||||
if isinstance(parameter.annotation, _AnnotatedAlias):
|
||||
for meta in parameter.annotation.__metadata__:
|
||||
# Check if this is a Depends object
|
||||
if hasattr(meta, "dependency"):
|
||||
# Add the dependency function to hint_type_list
|
||||
hint_type_list.append(meta.dependency)
|
||||
|
||||
if return_type:
|
||||
hint_type = (
|
||||
get_args(get_type_hints(return_type)["results"])[0]
|
||||
if hasattr(return_type, "__class__")
|
||||
and hasattr(return_type.__class__, "__name__")
|
||||
and "OBBject" in getattr(return_type.__class__, "__name__", "")
|
||||
else return_type
|
||||
)
|
||||
hint_type_list.append(hint_type)
|
||||
|
||||
hint_type_list = cls.filter_hint_type_list(hint_type_list)
|
||||
|
||||
return hint_type_list
|
||||
|
||||
@classmethod
|
||||
def get_path_hint_type_list(cls, path: str) -> list[type]:
|
||||
"""Get the hint type list from the path."""
|
||||
route_map = PathHandler.build_route_map()
|
||||
path_list = PathHandler.build_path_list(route_map=route_map)
|
||||
child_path_list = PathHandler.get_child_path_list(
|
||||
path=path, path_list=path_list
|
||||
)
|
||||
hint_type_list = []
|
||||
for child_path in child_path_list:
|
||||
route = PathHandler.get_route(path=child_path, route_map=route_map)
|
||||
if route:
|
||||
if getattr(route, "deprecated", None):
|
||||
hint_type_list.append(type(route.summary.metadata))
|
||||
function_hint_type_list = cls.get_function_hint_type_list(route=route)
|
||||
hint_type_list.extend(function_hint_type_list)
|
||||
|
||||
for dependency in PathHandler.get_router_dependencies(path):
|
||||
dependency_func = getattr(dependency, "dependency", None)
|
||||
if callable(dependency_func):
|
||||
hint_type_list.append(dependency_func)
|
||||
|
||||
hint_type_list = [
|
||||
d
|
||||
for d in list(set(hint_type_list))
|
||||
if d not in [int, list, str, dict, float, set, bool, tuple]
|
||||
]
|
||||
return hint_type_list
|
||||
|
||||
@classmethod
|
||||
def build(cls, path: str) -> str:
|
||||
"""Build the import definition."""
|
||||
hint_type_list = cls.get_path_hint_type_list(path=path)
|
||||
code = "from openbb_core.app.static.container import Container"
|
||||
code += "\nfrom openbb_core.app.model.obbject import OBBject"
|
||||
|
||||
# These imports were not detected before build, so we add them manually and
|
||||
# ruff --fix the resulting code to remove unused imports.
|
||||
# TODO: Find a better way to handle this. This is a temporary solution.
|
||||
code += "\nimport openbb_core.provider"
|
||||
code += "\nfrom openbb_core.provider.abstract.data import Data"
|
||||
code += "\nimport pandas"
|
||||
code += "\nfrom pandas import DataFrame, Series"
|
||||
code += "\nimport numpy"
|
||||
code += "\nfrom numpy import ndarray"
|
||||
code += "\nimport datetime"
|
||||
code += "\nfrom datetime import date"
|
||||
code += "\nimport pydantic"
|
||||
code += "\nfrom pydantic import BaseModel"
|
||||
code += "\nfrom inspect import Parameter"
|
||||
code += "\nimport typing"
|
||||
code += "\nfrom typing import TYPE_CHECKING, Annotated, ForwardRef, Union, Optional, Literal, Any"
|
||||
code += "\nfrom annotated_types import Ge, Le, Gt, Lt"
|
||||
code += "\nfrom warnings import warn, simplefilter"
|
||||
code += "\nfrom openbb_core.app.static.utils.decorators import exception_handler, validate\n"
|
||||
code += "\nfrom openbb_core.app.static.utils.filters import filter_inputs\n"
|
||||
code += "\nfrom openbb_core.app.deprecation import OpenBBDeprecationWarning\n"
|
||||
code += "\nfrom openbb_core.app.model.field import OpenBBField"
|
||||
code += "\nfrom fastapi import Depends"
|
||||
|
||||
module_list = [
|
||||
hint_type.__module__
|
||||
for hint_type in hint_type_list
|
||||
if hasattr(hint_type, "__module__")
|
||||
]
|
||||
module_list = list(set(module_list))
|
||||
module_list.sort()
|
||||
|
||||
code += "\n"
|
||||
for module in module_list:
|
||||
code += f"import {module}\n"
|
||||
|
||||
# Group types by module and capture the return types for the imports.
|
||||
module_types: dict = {}
|
||||
for hint_type in hint_type_list:
|
||||
if hasattr(hint_type, "__module__") and hint_type.__module__ != "builtins":
|
||||
module = hint_type.__module__
|
||||
|
||||
if hasattr(hint_type, "__origin__"):
|
||||
type_name = (
|
||||
hint_type.__origin__.__name__
|
||||
if hasattr(hint_type.__origin__, "__name__")
|
||||
else str(hint_type.__origin__)
|
||||
)
|
||||
else:
|
||||
raw_type_name = getattr(
|
||||
hint_type,
|
||||
"__name__",
|
||||
str(hint_type).rsplit(".", maxsplit=1)[-1],
|
||||
)
|
||||
type_name = (
|
||||
raw_type_name.split("[")[0]
|
||||
if "[" in raw_type_name
|
||||
else raw_type_name
|
||||
)
|
||||
|
||||
type_name_str = str(type_name)
|
||||
if type_name_str.startswith("typing.Optional"):
|
||||
continue
|
||||
if "|" in type_name_str:
|
||||
continue
|
||||
|
||||
sanitized_name = cls._sanitize_type_name(type_name_str)
|
||||
if not sanitized_name:
|
||||
continue
|
||||
if (
|
||||
module == "typing" and sanitized_name in dir(__builtins__)
|
||||
) or sanitized_name in {
|
||||
"Dict",
|
||||
"List",
|
||||
"int",
|
||||
"float",
|
||||
"str",
|
||||
"dict",
|
||||
"list",
|
||||
"set",
|
||||
"bool",
|
||||
"tuple",
|
||||
}:
|
||||
continue
|
||||
if not (
|
||||
sanitized_name == "TYPE_CHECKING" or sanitized_name.isidentifier()
|
||||
):
|
||||
continue
|
||||
|
||||
if module not in module_types:
|
||||
module_types[module] = set()
|
||||
|
||||
module_types[module].add(sanitized_name)
|
||||
|
||||
# Generate from-import statements for modules with specific types
|
||||
for module, types in sorted(module_types.items()):
|
||||
if module == "types":
|
||||
continue
|
||||
_types = types
|
||||
if module == "typing":
|
||||
_types = {t for t in types if hasattr(typing_module, t)}
|
||||
if not _types:
|
||||
continue
|
||||
|
||||
if len(_types) == 1:
|
||||
type_name = next(iter(_types))
|
||||
code += f"\nfrom {module} import {type_name}"
|
||||
else:
|
||||
import_types = [
|
||||
d
|
||||
for d in sorted(_types)
|
||||
if d
|
||||
not in [
|
||||
"Dict",
|
||||
"List",
|
||||
"int",
|
||||
"float",
|
||||
"str",
|
||||
"dict",
|
||||
"list",
|
||||
"set",
|
||||
]
|
||||
]
|
||||
if import_types:
|
||||
code += f"\nfrom {module} import ("
|
||||
for type_name in import_types:
|
||||
code += f"\n {type_name},"
|
||||
code += "\n)"
|
||||
code += "\n"
|
||||
|
||||
return code + "\n"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
"""Module-level emitter that wires imports and the class definition together."""
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
from openbb_core.app.static.package_builder.class_definition import ClassDefinition
|
||||
from openbb_core.app.static.package_builder.import_definition import ImportDefinition
|
||||
|
||||
|
||||
class ModuleBuilder:
|
||||
"""Build the module for the Platform."""
|
||||
|
||||
@staticmethod
|
||||
def build(path: str, ext_map: dict[str, list[str]] | None = None) -> str:
|
||||
"""Build the module."""
|
||||
code = f'"""Autogenerated OpenBB {path} Module."""\n\n'
|
||||
code += "### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###\n\n"
|
||||
code += ImportDefinition.build(path)
|
||||
code += ClassDefinition.build(path, ext_map)
|
||||
|
||||
return code
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Path / route handler helpers used during package generation."""
|
||||
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
from openbb_core.app.router import RouterLoader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray # noqa
|
||||
from pandas import DataFrame, Series # noqa
|
||||
from openbb_core.provider.abstract.data import Data # noqa
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
CHARTING_INSTALLED = find_spec("openbb_charting") is not None
|
||||
|
||||
try:
|
||||
_HAS_FCNTL = True
|
||||
except Exception: # noqa
|
||||
_HAS_FCNTL = False
|
||||
import msvcrt # noqa
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
dict,
|
||||
"DataFrame",
|
||||
list["DataFrame"],
|
||||
"Series",
|
||||
list["Series"],
|
||||
"ndarray",
|
||||
"Data",
|
||||
)
|
||||
|
||||
from openbb_core.app.static.package_builder._indent import ( # noqa: F401
|
||||
TAB,
|
||||
create_indent,
|
||||
)
|
||||
|
||||
|
||||
class PathHandler:
|
||||
"""Handle the paths for the Platform."""
|
||||
|
||||
@staticmethod
|
||||
def get_router_dependencies(path: str) -> list:
|
||||
"""Collect APIRouter dependencies for the path and its parents."""
|
||||
router = RouterLoader.from_extensions()
|
||||
segments = [
|
||||
segment
|
||||
for segment in path.split("/")
|
||||
if segment and not segment.startswith("{")
|
||||
]
|
||||
candidate_paths = ["/"]
|
||||
current = ""
|
||||
for segment in segments:
|
||||
current = f"{current}/{segment}" if current else f"/{segment}"
|
||||
candidate_paths.append(current)
|
||||
|
||||
dependencies: list = []
|
||||
seen: set = set()
|
||||
|
||||
for candidate in candidate_paths:
|
||||
try:
|
||||
api_router = router.get_attr(candidate, "api_router")
|
||||
except Exception: # pragma: no cover
|
||||
api_router = None
|
||||
if not api_router:
|
||||
continue
|
||||
for dependency in getattr(api_router, "dependencies", []) or []:
|
||||
dependency_func = getattr(dependency, "dependency", None)
|
||||
if callable(dependency_func) and dependency_func not in seen:
|
||||
dependencies.append(dependency)
|
||||
seen.add(dependency_func)
|
||||
return dependencies
|
||||
|
||||
@staticmethod
|
||||
def build_route_map() -> dict[str, BaseRoute]:
|
||||
"""Build the route map."""
|
||||
router = RouterLoader.from_extensions()
|
||||
route_map = {
|
||||
route.path: route
|
||||
for route in router.api_router.routes
|
||||
if isinstance(route, APIRoute)
|
||||
and "." not in str(route.path)
|
||||
and getattr(route, "include_in_schema", True)
|
||||
}
|
||||
|
||||
# Also include routes directly registered on _api_router instances
|
||||
# We need to traverse the router tree to find all _api_router instances
|
||||
def collect_api_router_routes(router_obj, collected_routes):
|
||||
"""Recursively collect routes from _api_router instances."""
|
||||
if hasattr(router_obj, "_api_router"):
|
||||
for inner_route in router_obj._api_router.routes:
|
||||
if (
|
||||
isinstance(inner_route, APIRoute)
|
||||
and getattr(inner_route, "include_in_schema", True)
|
||||
and (inner_route.path not in collected_routes)
|
||||
):
|
||||
collected_routes[inner_route.path] = inner_route
|
||||
|
||||
# Check if this router has sub-routers
|
||||
if hasattr(router_obj, "api_router") and hasattr(
|
||||
router_obj.api_router, "routes"
|
||||
):
|
||||
for route in router_obj.api_router.routes:
|
||||
if not isinstance(route, APIRoute):
|
||||
continue
|
||||
endpoint = getattr(route, "endpoint", None)
|
||||
if endpoint and hasattr(endpoint, "__self__"):
|
||||
collect_api_router_routes(endpoint.__self__, collected_routes)
|
||||
|
||||
collect_api_router_routes(router, route_map)
|
||||
|
||||
return route_map # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def build_path_list(route_map: dict[str, BaseRoute]) -> list[str]:
|
||||
"""Build the path list."""
|
||||
path_list = []
|
||||
for route_path in route_map:
|
||||
if route_path not in path_list:
|
||||
path_list.append(route_path)
|
||||
|
||||
sub_path_list = route_path.split("/")
|
||||
|
||||
for length in range(len(sub_path_list)):
|
||||
sub_path = "/".join(sub_path_list[:length])
|
||||
if sub_path not in path_list:
|
||||
# Don't add paths that only exist as part of parameterized routes
|
||||
has_direct_route = sub_path in route_map
|
||||
# A child route is non-parameterized if the next segment doesn't start with {
|
||||
has_real_children = False
|
||||
for r in route_map:
|
||||
if r.startswith(sub_path + "/"):
|
||||
remainder = r[len(sub_path) + 1 :]
|
||||
next_segment = (
|
||||
remainder.split("/")[0] if remainder else ""
|
||||
)
|
||||
if next_segment and not next_segment.startswith("{"):
|
||||
has_real_children = True
|
||||
break
|
||||
|
||||
if has_direct_route or has_real_children:
|
||||
path_list.append(sub_path)
|
||||
|
||||
return path_list
|
||||
|
||||
@staticmethod
|
||||
def get_route(path: str, route_map: dict[str, BaseRoute]):
|
||||
"""Get the route from the path."""
|
||||
return route_map.get(path)
|
||||
|
||||
@staticmethod
|
||||
def get_child_path_list(path: str, path_list: list[str]) -> list[str]:
|
||||
"""Get the child path list.
|
||||
|
||||
This returns both sub-router paths AND direct route paths that are children of the given path.
|
||||
For example, for path="/empty", it returns both:
|
||||
- "/empty/sub_router" (a sub-router in path_list)
|
||||
- "/empty/also_empty/{param}" (a direct route from route_map)
|
||||
"""
|
||||
direct_children = []
|
||||
base_depth = path.count("/") if path else 0
|
||||
|
||||
# Get route_map to check for routes that aren't in path_list
|
||||
route_map = PathHandler.build_route_map()
|
||||
|
||||
# First, add children from path_list (these are sub-routers)
|
||||
for p in path_list:
|
||||
if p.startswith(path + "/") if path else p.startswith("/"):
|
||||
p_depth = p.count("/")
|
||||
if p_depth == base_depth + 1:
|
||||
direct_children.append(p)
|
||||
|
||||
# Second, add routes from route_map that are direct children but not in path_list
|
||||
# (these are endpoints with path parameters)
|
||||
for route_path in route_map:
|
||||
if route_path not in direct_children and (
|
||||
route_path.startswith(path + "/")
|
||||
if path
|
||||
else route_path.startswith("/")
|
||||
):
|
||||
# Remove the parent path prefix
|
||||
remainder = route_path[len(path) + 1 :] if path else route_path[1:]
|
||||
|
||||
# Split by "/" and count non-empty segments
|
||||
segments = [s for s in remainder.split("/") if s]
|
||||
if segments:
|
||||
first_non_param_idx = next(
|
||||
(
|
||||
i
|
||||
for i, seg in enumerate(segments)
|
||||
if not seg.startswith("{")
|
||||
),
|
||||
None,
|
||||
)
|
||||
is_direct_child = first_non_param_idx is None or (
|
||||
first_non_param_idx == 0
|
||||
and all(seg.startswith("{") for seg in segments[1:])
|
||||
)
|
||||
if is_direct_child and route_path not in direct_children:
|
||||
direct_children.append(route_path)
|
||||
|
||||
return direct_children
|
||||
|
||||
@staticmethod
|
||||
def clean_path(path: str) -> str:
|
||||
"""Clean the path."""
|
||||
if path.startswith("/"):
|
||||
path = path[1:]
|
||||
return path.replace("-", "_").replace("/", "_")
|
||||
|
||||
@classmethod
|
||||
def build_module_name(cls, path: str) -> str:
|
||||
"""Build the module name."""
|
||||
if not path:
|
||||
return "__extensions__"
|
||||
return cls.clean_path(path=path)
|
||||
|
||||
@classmethod
|
||||
def build_module_class(cls, path: str) -> str:
|
||||
"""Build the module class."""
|
||||
if not path:
|
||||
return "Extensions"
|
||||
return f"ROUTER_{cls.clean_path(path=path)}"
|
||||
|
||||
@staticmethod
|
||||
def extract_path_parameters(path: str) -> list[str]:
|
||||
"""Extract path parameters from a route path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The route path (e.g., "/users/{user_id}/posts/{post_id}")
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
List of path parameter names (e.g., ["user_id", "post_id"])
|
||||
"""
|
||||
# Match parameters in curly braces
|
||||
pattern = r"\{(\w+)\}"
|
||||
return re.findall(pattern, path)
|
||||
|
||||
@staticmethod
|
||||
def get_router_description(path: str) -> str:
|
||||
"""Return the description for a router path."""
|
||||
router = RouterLoader.from_extensions()
|
||||
description = router.get_attr(path or "/", "description")
|
||||
if description:
|
||||
return description
|
||||
clean_path = path or "/"
|
||||
return f"Router for {clean_path}."
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,12 @@ from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, TypeVar, overload
|
||||
|
||||
from pydantic import ValidationError, validate_call
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
||||
from pydantic import ValidationError, validate_call
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
@@ -5,14 +5,15 @@ import json
|
||||
from datetime import time
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.preferences import Preferences
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.utils_optional import require_optional
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from pydantic import ValidationError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from numpy import ndarray
|
||||
from pandas import DataFrame, Series
|
||||
|
||||
@@ -22,8 +23,8 @@ def basemodel_to_df(
|
||||
index: str | None = None,
|
||||
) -> "DataFrame":
|
||||
"""Convert list of BaseModel to a Pandas DataFrame."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import DataFrame, to_datetime
|
||||
pd = require_optional("pandas")
|
||||
DataFrame, to_datetime = pd.DataFrame, pd.to_datetime # type: ignore[union-attr]
|
||||
|
||||
if isinstance(data, list):
|
||||
df = DataFrame(
|
||||
@@ -34,7 +35,8 @@ def basemodel_to_df(
|
||||
df = DataFrame(data.model_dump(exclude_none=True, exclude_unset=True))
|
||||
except ValueError:
|
||||
df = DataFrame(
|
||||
data.model_dump(exclude_none=True, exclude_unset=True), index=["values"]
|
||||
data.model_dump(exclude_none=True, exclude_unset=True),
|
||||
index=pd.Index(["values"]), # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
if "is_multiindex" in df.columns:
|
||||
@@ -62,8 +64,8 @@ def df_to_basemodel(
|
||||
df: Union["DataFrame", "Series"], index: bool = False
|
||||
) -> list[Data]:
|
||||
"""Convert from a Pandas DataFrame to list of BaseModel."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import MultiIndex, Series, to_datetime
|
||||
pd = require_optional("pandas")
|
||||
MultiIndex, Series, to_datetime = pd.MultiIndex, pd.Series, pd.to_datetime # type: ignore[union-attr]
|
||||
|
||||
is_multiindex = isinstance(df.index, MultiIndex)
|
||||
|
||||
@@ -91,8 +93,8 @@ def df_to_basemodel(
|
||||
|
||||
def list_to_basemodel(data_list: list) -> list[Data]:
|
||||
"""Convert a list to a list of BaseModel."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import DataFrame, Series
|
||||
pd = require_optional("pandas")
|
||||
DataFrame, Series = pd.DataFrame, pd.Series # type: ignore[union-attr]
|
||||
|
||||
base_models = []
|
||||
for item in data_list:
|
||||
@@ -129,9 +131,9 @@ def ndarray_to_basemodel(array: "ndarray") -> list[Data]:
|
||||
|
||||
def convert_to_basemodel(data) -> Data | list[Data]:
|
||||
"""Dispatch function to convert different types to BaseModel."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from numpy import ndarray
|
||||
from pandas import DataFrame, Series
|
||||
np, pd = require_optional("numpy", "pandas")
|
||||
ndarray = np.ndarray # type: ignore[union-attr]
|
||||
DataFrame, Series = pd.DataFrame, pd.Series # type: ignore[union-attr]
|
||||
|
||||
if isinstance(data, Data) or issubclass(type(data), Data):
|
||||
return data
|
||||
@@ -158,10 +160,8 @@ def get_target_column(df: "DataFrame", target: str) -> "Series":
|
||||
|
||||
def get_target_columns(df: "DataFrame", target_columns: list[str]) -> "DataFrame":
|
||||
"""Get target columns from time series data."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import DataFrame
|
||||
|
||||
df_result = DataFrame()
|
||||
pd = require_optional("pandas")
|
||||
df_result = pd.DataFrame() # type: ignore[union-attr]
|
||||
for target in target_columns:
|
||||
df_result[target] = get_target_column(df, target).to_frame()
|
||||
return df_result
|
||||
|
||||
89
openbb_platform/core/openbb_core/app/utils_optional.py
Normal file
89
openbb_platform/core/openbb_core/app/utils_optional.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Helpers for handling optional runtime dependencies uniformly."""
|
||||
|
||||
import builtins
|
||||
import sys
|
||||
from importlib.util import find_spec
|
||||
from types import ModuleType
|
||||
from typing import overload
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
|
||||
__all__ = ["is_installed", "require_optional"]
|
||||
|
||||
|
||||
_INSTALL_HINTS: dict[str, str] = {
|
||||
"pandas": "pip install 'openbb-core[pandas]'",
|
||||
"numpy": "pip install 'openbb-core[pandas]'",
|
||||
"polars": "pip install polars pyarrow",
|
||||
"pyarrow": "pip install pyarrow",
|
||||
"openbb_charting": "pip install openbb-charting",
|
||||
"pycountry": "pip install pycountry",
|
||||
"bs4": "pip install beautifulsoup4",
|
||||
}
|
||||
|
||||
|
||||
def _install_hint(module_name: str) -> str:
|
||||
"""Return the install command for *module_name* (top-level package)."""
|
||||
top = module_name.split(".", 1)[0]
|
||||
return _INSTALL_HINTS.get(top, f"pip install {top}")
|
||||
|
||||
|
||||
def _format_error(missing: list[str]) -> str:
|
||||
"""Format a user-facing error for one or more missing optional modules."""
|
||||
if len(missing) == 1:
|
||||
name = missing[0]
|
||||
return (
|
||||
f"Optional dependency '{name}' is required for this feature. "
|
||||
f"Install with: {_install_hint(name)}"
|
||||
)
|
||||
hints = "; ".join(f"{name}: {_install_hint(name)}" for name in missing)
|
||||
names = ", ".join(f"'{name}'" for name in missing)
|
||||
return (
|
||||
f"Optional dependencies {names} are required for this feature. "
|
||||
f"Install with — {hints}"
|
||||
)
|
||||
|
||||
|
||||
def is_installed(module_name: str) -> bool:
|
||||
"""Return True if *module_name* can be imported without actually importing it."""
|
||||
try:
|
||||
return find_spec(module_name) is not None
|
||||
except (ImportError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
@overload
|
||||
def require_optional(module_name: str, /) -> ModuleType: ...
|
||||
@overload
|
||||
def require_optional(
|
||||
module_name: str, /, *module_names: str
|
||||
) -> tuple[ModuleType, ...]: ...
|
||||
def require_optional(*module_names: str) -> ModuleType | tuple[ModuleType, ...]:
|
||||
"""Import optional modules or raise a uniform OpenBBError with install hints.
|
||||
|
||||
Returns the module when called with one name, or a tuple of modules in the
|
||||
same order as the arguments when called with multiple names.
|
||||
"""
|
||||
if not module_names:
|
||||
raise ValueError("require_optional requires at least one module name")
|
||||
|
||||
modules: list[ModuleType] = []
|
||||
missing: list[str] = []
|
||||
for name in module_names:
|
||||
try:
|
||||
# Use ``builtins.__import__`` (rather than ``importlib.import_module``)
|
||||
# so that test suites which monkeypatch ``__import__`` to simulate a
|
||||
# missing optional dependency are honored.
|
||||
builtins.__import__(name)
|
||||
modules.append(sys.modules[name])
|
||||
except ImportError:
|
||||
missing.append(name)
|
||||
|
||||
if missing:
|
||||
# ``from None`` suppresses the chained ``ImportError`` context so the
|
||||
# user sees a single, clean OpenBBError with the install hint.
|
||||
raise OpenBBError(_format_error(missing)) from None
|
||||
|
||||
if len(modules) == 1:
|
||||
return modules[0]
|
||||
return tuple(modules)
|
||||
@@ -32,7 +32,6 @@ def get_package_version(package: str):
|
||||
|
||||
def is_git_repo(path: Path):
|
||||
"""Check if the given directory is a git repository."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Script to build the OpenBB platform static assets."""
|
||||
|
||||
# flake8: noqa: S603
|
||||
# pylint: disable=import-outside-toplevel,unused-import
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -59,23 +58,23 @@ def main():
|
||||
import openbb # noqa
|
||||
|
||||
openbb.build()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
except Exception as e:
|
||||
raise RuntimeError( # noqa
|
||||
"Failed to build the OpenBB platform static assets. \n"
|
||||
f"{e} -> {e.__traceback__.tb_frame.f_code.co_filename}:" # type:ignore # pylint: disable=E1101
|
||||
f"{e.__traceback__.tb_lineno}" # type:ignore
|
||||
f"{e} -> {e.__traceback__.tb_frame.f_code.co_filename}:"
|
||||
f"{e.__traceback__.tb_lineno}"
|
||||
if hasattr(e, "__traceback__")
|
||||
and hasattr(e.__traceback__, "tb_frame") # type:ignore
|
||||
and hasattr(e.__traceback__, "tb_frame")
|
||||
and hasattr(
|
||||
e.__traceback__.tb_frame, # type:ignore
|
||||
e.__traceback__.tb_frame,
|
||||
"f_code",
|
||||
)
|
||||
and hasattr(
|
||||
e.__traceback__.tb_frame.f_code, # type:ignore # pylint: disable=E1101
|
||||
e.__traceback__.tb_frame.f_code,
|
||||
"co_filename",
|
||||
)
|
||||
and hasattr(
|
||||
e.__traceback__, # type:ignore
|
||||
e.__traceback__,
|
||||
"tb_lineno",
|
||||
)
|
||||
else f"Failed to build the OpenBB platform static assets. \n{e}"
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
import dotenv
|
||||
|
||||
from openbb_core.app.constants import OPENBB_DIRECTORY
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Abstract class for the fetcher."""
|
||||
|
||||
# ruff: noqa: S101, E501
|
||||
# pylint: disable=E1101, C0301
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -62,7 +61,7 @@ class Fetcher(Generic[Q, R]):
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
|
||||
if cls.aextract_data != Fetcher.aextract_data:
|
||||
cls.extract_data = cls.aextract_data # type: ignore[method-assign]
|
||||
cls.extract_data = cls.aextract_data # type: ignore[method-assign] # ty: ignore[invalid-assignment]
|
||||
elif cls.extract_data == Fetcher.extract_data:
|
||||
raise NotImplementedError(
|
||||
"Fetcher subclass must implement either extract_data or aextract_data"
|
||||
@@ -87,26 +86,23 @@ class Fetcher(Generic[Q, R]):
|
||||
@classproperty
|
||||
def query_params_type(self) -> Q:
|
||||
"""Get the type of query."""
|
||||
# pylint: disable=E1101
|
||||
return self.__orig_bases__[0].__args__[0] # type: ignore
|
||||
|
||||
@classproperty
|
||||
def return_type(self) -> R:
|
||||
"""Get the type of return."""
|
||||
# pylint: disable=E1101
|
||||
return_type = self.__orig_bases__[0].__args__[1] # type: ignore
|
||||
if get_origin(return_type) is AnnotatedResult:
|
||||
return_type = get_args(return_type)[0]
|
||||
return return_type
|
||||
|
||||
@classproperty
|
||||
def data_type(self) -> D: # type: ignore
|
||||
def data_type(self) -> D:
|
||||
"""Get the type data."""
|
||||
# pylint: disable=E1101
|
||||
return self._get_data_type(self.__orig_bases__[0].__args__[1]) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _get_data_type(data: Any) -> D: # type: ignore
|
||||
def _get_data_type(data: Any) -> D:
|
||||
"""Get the type of the data."""
|
||||
if get_origin(data) is list:
|
||||
data = get_args(data)[0]
|
||||
@@ -135,8 +131,9 @@ class Fetcher(Generic[Q, R]):
|
||||
AssertionError
|
||||
If any of the tests fail.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from pandas import DataFrame
|
||||
from openbb_core.app.utils_optional import require_optional
|
||||
|
||||
DataFrame = require_optional("pandas").DataFrame # type: ignore[union-attr]
|
||||
|
||||
query = cls.transform_query(params=params)
|
||||
data = run_async(
|
||||
@@ -145,18 +142,18 @@ class Fetcher(Generic[Q, R]):
|
||||
result = cls.transform_data(query=query, data=data, **kwargs)
|
||||
|
||||
# Class Assertions
|
||||
assert isinstance(
|
||||
cls.require_credentials, bool
|
||||
), "require_credentials must be a boolean."
|
||||
assert isinstance(cls.require_credentials, bool), (
|
||||
"require_credentials must be a boolean."
|
||||
)
|
||||
|
||||
# Query Assertions
|
||||
assert query, "Query must not be None."
|
||||
assert issubclass(
|
||||
type(query), cls.query_params_type
|
||||
), f"Query type mismatch. Expected: {cls.query_params_type} Got: {type(query)}"
|
||||
assert all(
|
||||
getattr(query, key) == value for key, value in params.items()
|
||||
), f"Query must have the correct values. Expected: {params} Got: {query.__dict__}"
|
||||
assert issubclass(type(query), cls.query_params_type), (
|
||||
f"Query type mismatch. Expected: {cls.query_params_type} Got: {type(query)}"
|
||||
)
|
||||
assert all(getattr(query, key) == value for key, value in params.items()), (
|
||||
f"Query must have the correct values. Expected: {params} Got: {query.__dict__}"
|
||||
)
|
||||
|
||||
# Data Assertions
|
||||
if not isinstance(data, DataFrame):
|
||||
@@ -169,20 +166,24 @@ class Fetcher(Generic[Q, R]):
|
||||
field in data[0]
|
||||
for field in cls.data_type.model_fields
|
||||
if field in data[0]
|
||||
), f"Data must have the correct fields. Expected: {cls.data_type.model_fields} Got: {data[0].__dict__}"
|
||||
), (
|
||||
f"Data must have the correct fields. Expected: {cls.data_type.model_fields} Got: {data[0].__dict__}"
|
||||
)
|
||||
# This makes sure that the data is not transformed yet so that the
|
||||
# pipeline is implemented correctly. We can remove this assertion if we
|
||||
# want to be less strict.
|
||||
assert (
|
||||
issubclass(type(data[0]), cls.data_type) is False
|
||||
), f"Data must not be transformed yet. Expected: {cls.data_type} Got: {type(data[0])}"
|
||||
assert issubclass(type(data[0]), cls.data_type) is False, (
|
||||
f"Data must not be transformed yet. Expected: {cls.data_type} Got: {type(data[0])}"
|
||||
)
|
||||
else:
|
||||
assert all(
|
||||
field in data for field in cls.data_type.model_fields if field in data
|
||||
), f"Data must have the correct fields. Expected: {cls.data_type.model_fields} Got: {data.__dict__}"
|
||||
assert (
|
||||
issubclass(type(data), cls.data_type) is False
|
||||
), f"Data must not be transformed yet. Expected: {cls.data_type} Got: {type(data)}"
|
||||
), (
|
||||
f"Data must have the correct fields. Expected: {cls.data_type.model_fields} Got: {data.__dict__}"
|
||||
)
|
||||
assert issubclass(type(data), cls.data_type) is False, (
|
||||
f"Data must not be transformed yet. Expected: {cls.data_type} Got: {type(data)}"
|
||||
)
|
||||
|
||||
assert len(data) > 0, "Data must not be empty."
|
||||
|
||||
@@ -208,26 +209,34 @@ class Fetcher(Generic[Q, R]):
|
||||
return_type_fields = return_type_args.model_fields
|
||||
return_type = return_type_args
|
||||
|
||||
assert len(transformed_data) > 0, "Transformed data must not be empty." # type: ignore
|
||||
assert len(transformed_data) > 0, "Transformed data must not be empty."
|
||||
assert all(
|
||||
field in transformed_data[0].__dict__ for field in return_type_fields # type: ignore
|
||||
), f"Transformed data must have the correct fields. Expected: {return_type_fields} Got: {transformed_data[0].__dict__}" # type: ignore
|
||||
field in transformed_data[0].__dict__ for field in return_type_fields
|
||||
), (
|
||||
f"Transformed data must have the correct fields. Expected: {return_type_fields} Got: {transformed_data[0].__dict__}"
|
||||
)
|
||||
assert issubclass(
|
||||
type(transformed_data[0]),
|
||||
cls.data_type,
|
||||
), (
|
||||
f"Transformed data must be of the correct type. Expected: {cls.data_type} Got: {type(transformed_data[0])}"
|
||||
)
|
||||
assert issubclass(
|
||||
type(transformed_data[0]),
|
||||
cls.data_type, # type: ignore
|
||||
), f"Transformed data must be of the correct type. Expected: {cls.data_type} Got: {type(transformed_data[0])}" # type: ignore
|
||||
assert issubclass( # type: ignore
|
||||
type(transformed_data[0]), # type: ignore
|
||||
return_type,
|
||||
), f"Transformed data must be of the correct type. Expected: {return_type} Got: {type(transformed_data[0])}" # type: ignore
|
||||
), (
|
||||
f"Transformed data must be of the correct type. Expected: {return_type} Got: {type(transformed_data[0])}"
|
||||
)
|
||||
else:
|
||||
assert all(
|
||||
field in transformed_data.__dict__
|
||||
for field in cls.return_type.model_fields
|
||||
), f"Transformed data must have the correct fields. Expected: {cls.return_type.model_fields} Got: {transformed_data.__dict__}"
|
||||
assert issubclass(
|
||||
type(transformed_data), cls.data_type
|
||||
), f"Transformed data must be of the correct type. Expected: {cls.data_type} Got: {type(transformed_data)}"
|
||||
assert issubclass(
|
||||
type(transformed_data), cls.return_type
|
||||
), f"Transformed data must be of the correct type. Expected: {cls.return_type} Got: {type(transformed_data)}"
|
||||
), (
|
||||
f"Transformed data must have the correct fields. Expected: {cls.return_type.model_fields} Got: {transformed_data.__dict__}"
|
||||
)
|
||||
assert issubclass(type(transformed_data), cls.data_type), (
|
||||
f"Transformed data must be of the correct type. Expected: {cls.data_type} Got: {type(transformed_data)}"
|
||||
)
|
||||
assert issubclass(type(transformed_data), cls.return_type), (
|
||||
f"Transformed data must be of the correct type. Expected: {cls.return_type} Got: {type(transformed_data)}"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
class Provider:
|
||||
"""Serves as provider extension entry point and must be created by each provider."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.provider import Provider
|
||||
from openbb_core.provider.registry import Registry, RegistryLoader
|
||||
from pydantic import SecretStr
|
||||
|
||||
|
||||
class QueryExecutor:
|
||||
|
||||
@@ -5,11 +5,12 @@ from inspect import getfile, isclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, get_origin
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.registry import Registry, RegistryLoader
|
||||
from pydantic import BaseModel
|
||||
|
||||
MapType = dict[str, dict[str, dict[str, dict[str, Any]]]]
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class AmeriborQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data, ForceInt
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class AnalystEstimatesQueryParams(QueryParams):
|
||||
|
||||
@@ -4,9 +4,10 @@ from datetime import (
|
||||
datetime,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class AnalystSearchQueryParams(QueryParams):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Available Indicators Standard Model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class AvailableIndicesQueryParams(QueryParams):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Available Indices Standard Model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class AvailableIndicesQueryParams(QueryParams):
|
||||
|
||||
@@ -4,9 +4,10 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class BalanceOfPaymentsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
|
||||
class BalanceSheetQueryParams(QueryParams):
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class BalanceSheetGrowthQueryParams(QueryParams):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""BLS Search Model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class SearchQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class SeriesQueryParams(QueryParams):
|
||||
|
||||
@@ -5,13 +5,14 @@ from datetime import (
|
||||
)
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class BondIndicesQueryParams(QueryParams):
|
||||
|
||||
@@ -4,9 +4,10 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class BondPricesQueryParams(QueryParams):
|
||||
|
||||
@@ -4,9 +4,10 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class BondReferenceQueryParams(QueryParams):
|
||||
|
||||
@@ -6,13 +6,14 @@ from datetime import (
|
||||
)
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class BondTradesQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CalendarDividendQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CalendarEarningsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CalendarEventsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CalendarIpoQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CalendarSplitsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
|
||||
class CashFlowStatementQueryParams(QueryParams):
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class CashFlowStatementGrowthQueryParams(QueryParams):
|
||||
|
||||
@@ -4,13 +4,14 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CentralBankHoldingsQueryParams(QueryParams):
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Cik Map Standard Model."""
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class CikMapQueryParams(QueryParams):
|
||||
|
||||
@@ -4,13 +4,14 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommercialPaperParams(QueryParams):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Commodity Production Supply & Demand Data Standard Model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommodityPsdDataQueryParams(QueryParams):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Commodity Production Supply & Distribution Report Standard Model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommodityPsdReportQueryParams(QueryParams):
|
||||
|
||||
@@ -4,13 +4,14 @@ from datetime import (
|
||||
date as dateType,
|
||||
)
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommoditySpotPricesQueryParams(QueryParams):
|
||||
|
||||
@@ -5,12 +5,13 @@ from datetime import (
|
||||
)
|
||||
|
||||
from dateutil import parser
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class CompanyFilingsQueryParams(QueryParams):
|
||||
|
||||
@@ -6,13 +6,14 @@ from datetime import (
|
||||
)
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, NonNegativeInt, field_validator
|
||||
|
||||
|
||||
class CompanyNewsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CompareCompanyFactsQueryParams(QueryParams):
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CompositeLeadingIndicatorQueryParams(QueryParams):
|
||||
@@ -26,7 +27,7 @@ class CompositeLeadingIndicatorData(Data):
|
||||
"""Composite Leading Indicator Data."""
|
||||
|
||||
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date"))
|
||||
value: float = Field(
|
||||
value: float | None = Field(
|
||||
default=None,
|
||||
description="CLI value",
|
||||
json_schema_extra={"x-unit_measurement": "index"},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user