[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:
Danglewood
2026-04-28 17:11:43 -07:00
committed by GitHub
parent a08d5d75d0
commit 7041362aa1
318 changed files with 17023 additions and 11503 deletions

View File

@@ -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]

View File

@@ -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.

View File

@@ -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
View 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

View File

@@ -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
View File

@@ -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

View File

@@ -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()):

View File

@@ -1,7 +1,6 @@
"""Root configuration for pytest."""
# flake8: noqa: S101
# pylint: disable=unused-argument,unused-import
import os
from pathlib import Path

View File

@@ -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).

View 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

View 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

View File

@@ -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

View File

@@ -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()

View File

@@ -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": [],

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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_)

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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

View File

@@ -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."""

View File

@@ -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":

View File

@@ -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

View File

@@ -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

View File

@@ -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"]:

View File

@@ -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 "")

View File

@@ -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):

View File

@@ -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):

View File

@@ -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

View File

@@ -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(),

View File

@@ -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

View File

@@ -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,

View File

@@ -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]

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}."

View File

@@ -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")

View File

@@ -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

View 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)

View File

@@ -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

View File

@@ -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}"

View File

@@ -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

View File

@@ -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)}"
)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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]]]]

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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