mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
[Feature] openbb-cookiecutter: Extension Template (#7261)
* add openbb-cookiecutter to repo * typo * review items * some fixes * missing annotation * duplicate ignore in pylintrc * protected access --------- Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com> Co-authored-by: deeleeramone <>
This commit is contained in:
1
.codespell.skip
vendored
1
.codespell.skip
vendored
@@ -17,6 +17,7 @@
|
|||||||
./build/pyinstaller
|
./build/pyinstaller
|
||||||
./**/node_modules
|
./**/node_modules
|
||||||
./frontend-components/**
|
./frontend-components/**
|
||||||
|
./cookiecutter/**
|
||||||
./openbb_platform/providers/econdb/openbb_econdb/utils/helpers.py
|
./openbb_platform/providers/econdb/openbb_econdb/utils/helpers.py
|
||||||
./openbb_platform/core/openbb/package/**
|
./openbb_platform/core/openbb/package/**
|
||||||
./openbb_platform/providers/imf/openbb_imf/utils/constants.py
|
./openbb_platform/providers/imf/openbb_imf/utils/constants.py
|
||||||
|
|||||||
1
.coveragerc
vendored
1
.coveragerc
vendored
@@ -4,4 +4,5 @@ omit =
|
|||||||
tests/*
|
tests/*
|
||||||
**/tests/**
|
**/tests/**
|
||||||
**/package/**
|
**/package/**
|
||||||
|
cookiecutter/**
|
||||||
source = .
|
source = .
|
||||||
|
|||||||
8
.pylintrc
vendored
8
.pylintrc
vendored
@@ -1,14 +1,12 @@
|
|||||||
[MASTER]
|
[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
|
# 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
|
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||||
# run arbitrary code
|
# run arbitrary code
|
||||||
extension-pkg-allow-list=math,binascii
|
extension-pkg-allow-list=math,binascii
|
||||||
|
|
||||||
# Add files or directories to the blacklist. They should be base names, not
|
|
||||||
# paths.
|
|
||||||
ignore=CVS
|
|
||||||
|
|
||||||
# Add files or directories matching the regex patterns to the blacklist. The
|
# Add files or directories matching the regex patterns to the blacklist. The
|
||||||
# regex matches against base names, not paths.
|
# regex matches against base names, not paths.
|
||||||
ignore-patterns=
|
ignore-patterns=
|
||||||
|
|||||||
5
cookiecutter/MANIFEST.in
vendored
Normal file
5
cookiecutter/MANIFEST.in
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include README.md
|
||||||
|
include LICENSE
|
||||||
|
recursive-include openbb_cookiecutter/template *
|
||||||
|
recursive-exclude openbb_cookiecutter/template *.pyc
|
||||||
|
recursive-exclude openbb_cookiecutter/template __pycache__
|
||||||
72
cookiecutter/README.md
vendored
Normal file
72
cookiecutter/README.md
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# OpenBB ODP Extensions Cookiecutter
|
||||||
|
|
||||||
|
[Cookiecutter](https://cookiecutter.readthedocs.io/en/1.7.2/) is a command-line utility that creates projects from templates.
|
||||||
|
|
||||||
|
This extension is a simple template for setting up new OpenBB Python Package extensions and projects.
|
||||||
|
|
||||||
|
## Template Structure
|
||||||
|
|
||||||
|
The Cookiecutter template prompts the user for information to use in the `pyproject.toml` file, and then generates a project based on that information.
|
||||||
|
All fields are optional.
|
||||||
|
|
||||||
|
- Your Name
|
||||||
|
- Your Email
|
||||||
|
- Project Name
|
||||||
|
- Project Tag (some-distributable-package)
|
||||||
|
- Package Name ("include" code folder name - "some_package")
|
||||||
|
- Provider Name - name of the provider for the entry point - i.e, 'fmp'
|
||||||
|
- Router Name - name of the router path - i.e. `obb.{some_package}`
|
||||||
|
- OBBject Name - name of the OBBject accessor namespace.
|
||||||
|
|
||||||
|
The template will generate all extension types as a single, installable Python project.
|
||||||
|
You likely won't always use all in tandem, just delete the unwanted folders and entrypoints.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Install in a Python environment from PyPI with:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install openbb-cookiecutter
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, with `uvx`:
|
||||||
|
|
||||||
|
```
|
||||||
|
uvx openbb-cookiecutter
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate the current working directory to the desired output location and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
openbb-cookiecutter
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter values or press `enter` to continue with the default.
|
||||||
|
|
||||||
|
3. Create a new Python environment for the project.
|
||||||
|
|
||||||
|
4. Navigate into the generated folder and install with:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Python static files will be generated on first import, or trigger with `openbb-build`.
|
||||||
|
|
||||||
|
6. Import the Python package or start the API and use like any other OpenBB application.
|
||||||
|
|
||||||
|
7. Modify the business logic and get started building!
|
||||||
|
|
||||||
|
See the developer documentation [here](https://docs.openbb.co/python/developer).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions to this template! Please feel free to open an issue or submit a pull request with your improvements.
|
||||||
|
|
||||||
|
## Contacts
|
||||||
|
|
||||||
|
If you have any questions about the cookiecutter or anything OpenBB, feel free to email us at `support@openbb.co`
|
||||||
|
|
||||||
|
If you want to say hi, or are interested in partnering with us, feel free to reach us at `hello@openbb.co`
|
||||||
|
|
||||||
|
Any of our social media platforms: [openbb.co/links](https://openbb.co/links)
|
||||||
8
cookiecutter/cookiecutter.json
vendored
Normal file
8
cookiecutter/cookiecutter.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"full_name": "Super Quant",
|
||||||
|
"email": "super@duper.quant",
|
||||||
|
"project_name": "Super Quant",
|
||||||
|
"project_tag": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
|
||||||
|
"package_name": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}",
|
||||||
|
"_template": "{% now 'utc', '%Y%m%d%H%M%S' %}"
|
||||||
|
}
|
||||||
10
cookiecutter/openbb_cookiecutter/__init__.py
Normal file
10
cookiecutter/openbb_cookiecutter/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""OpenBB Cookiecutter Template."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
__version__ = "0.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_path() -> Path:
|
||||||
|
"""Return the path to the cookiecutter template directory."""
|
||||||
|
return Path(__file__).parent / "template"
|
||||||
76
cookiecutter/openbb_cookiecutter/cli.py
Normal file
76
cookiecutter/openbb_cookiecutter/cli.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""CLI for OpenBB Cookiecutter template."""
|
||||||
|
|
||||||
|
# pylint: disable=W0718
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from cookiecutter.main import cookiecutter
|
||||||
|
|
||||||
|
from . import get_template_path
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list | None = None) -> int:
|
||||||
|
"""Run the OpenBB cookiecutter template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
argv: Command line arguments (defaults to sys.argv[1:])
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Exit code (0 for success, non-zero for error)
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate an OpenBB Platform extension from template"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output-dir",
|
||||||
|
default=".",
|
||||||
|
help="Where to output the generated project (default: current directory)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-input",
|
||||||
|
action="store_true",
|
||||||
|
help="Do not prompt for parameters and use defaults",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--overwrite-if-exists", action="store_true", help="Overwrite if exists"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--extra-context",
|
||||||
|
action="append",
|
||||||
|
metavar="KEY=VALUE",
|
||||||
|
help="Extra context variables (can be used multiple times)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Build extra context from arguments
|
||||||
|
extra_context = {}
|
||||||
|
if args.extra_context:
|
||||||
|
for item in args.extra_context:
|
||||||
|
if "=" not in item:
|
||||||
|
print(f"Error: extra-context must be in KEY=VALUE format: {item}")
|
||||||
|
return 1
|
||||||
|
key, value = item.split("=", 1)
|
||||||
|
extra_context[key] = value
|
||||||
|
|
||||||
|
# Get the bundled template path
|
||||||
|
template_path = get_template_path()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cookiecutter(
|
||||||
|
str(template_path),
|
||||||
|
output_dir=args.output_dir,
|
||||||
|
no_input=args.no_input,
|
||||||
|
overwrite_if_exists=args.overwrite_if_exists,
|
||||||
|
extra_context=extra_context if extra_context else None,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr) # noqa
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
11
cookiecutter/openbb_cookiecutter/template/cookiecutter.json
vendored
Normal file
11
cookiecutter/openbb_cookiecutter/template/cookiecutter.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"full_name": "Hello World",
|
||||||
|
"email": "hello@world.com",
|
||||||
|
"project_name": "OpenBB Python Extension Template",
|
||||||
|
"project_tag": "extension-template",
|
||||||
|
"package_name": "extension_template",
|
||||||
|
"provider_name": "template",
|
||||||
|
"router_name": "template",
|
||||||
|
"obbject_name": "template",
|
||||||
|
"_template": "{% now 'utc', '%Y%m%d%H%M%S' %}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""OpenBB Platform Extension post-generation script."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$"
|
||||||
|
|
||||||
|
MODULE_NAME = "{{ cookiecutter.package_name }}"
|
||||||
|
PROVIDER_NAME = "{{ cookiecutter.provider_name }}" or ""
|
||||||
|
ROUTER_NAME = "{{ cookiecutter.router_name }}" or ""
|
||||||
|
OBBJECT_NAME = "{{ cookiecutter.obbject_name }}" or ""
|
||||||
|
|
||||||
|
if not re.match(MODULE_REGEX, MODULE_NAME):
|
||||||
|
print(f"ERROR: {MODULE_NAME} is not a valid Python package name.")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if PROVIDER_NAME and not re.match(MODULE_REGEX, PROVIDER_NAME):
|
||||||
|
print(f"ERROR: {PROVIDER_NAME} should be in lower snakecase.")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if ROUTER_NAME and not re.match(MODULE_REGEX, ROUTER_NAME):
|
||||||
|
print(f"ERROR: {ROUTER_NAME} should be in lower snakecase.")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if OBBJECT_NAME and not re.match(MODULE_REGEX, OBBJECT_NAME):
|
||||||
|
print(f"ERROR: {OBBJECT_NAME} should be in lower snakecase.")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""OpenBB Platform Extension pre-generation script."""
|
||||||
|
|
||||||
|
BANNER = """
|
||||||
|
|
||||||
|
|
||||||
|
One of us ❤️
|
||||||
|
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
___ ____ ____
|
||||||
|
/ _ \\ _ __ ___ _ __ | __ )| __ )
|
||||||
|
| | | | '_ \\ / _ \\ '_ \\| _ \\| _
|
||||||
|
| |_| | |_) | __/ | | | |_) | |_) |
|
||||||
|
\\___/| .__/ \\___|_| |_|____/|____/
|
||||||
|
|_|
|
||||||
|
@@@
|
||||||
|
@@@
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@@ @@@ @@@ @@@
|
||||||
|
@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@@ @@@
|
||||||
|
%%%%%%%%%%%%%%%%%@@@ @@@%%%%%%%%%%%%%%%%%
|
||||||
|
@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@@ @@@ @@@ @@@
|
||||||
|
@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@
|
||||||
|
|
||||||
|
Investment research for everyone, anywhere.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
print(BANNER)
|
||||||
7
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.coveragerc
vendored
Normal file
7
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.coveragerc
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[coverage:run]
|
||||||
|
omit =
|
||||||
|
env/*
|
||||||
|
tests/*
|
||||||
|
**/tests/**
|
||||||
|
**/package/**
|
||||||
|
source = .
|
||||||
170
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.gitignore
vendored
Normal file
170
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.gitignore
vendored
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
# OpenBB Platform extension specifig paths to ignore
|
||||||
|
{{cookiecutter.package_name}}/package/
|
||||||
|
|
||||||
|
# Bellow this line is a default .gitignore for Python projects.
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# VS Code project settings
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
148
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.secrets.baseline
vendored
Normal file
148
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/.secrets.baseline
vendored
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"version": "1.4.0",
|
||||||
|
"plugins_used": [
|
||||||
|
{
|
||||||
|
"name": "ArtifactoryDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AWSKeyDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AzureStorageKeyDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Base64HighEntropyString",
|
||||||
|
"limit": 4.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BasicAuthDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CloudantDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DiscordBotTokenDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GitHubTokenDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HexHighEntropyString",
|
||||||
|
"limit": 3.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IbmCloudIamDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IbmCosHmacDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JwtTokenDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "KeywordDetector",
|
||||||
|
"keyword_exclude": "example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MailchimpDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NpmDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PrivateKeyDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SendGridDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SlackDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SoftlayerDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SquareOAuthDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "StripeDetector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TwilioKeyDetector"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filters_used": [
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
|
||||||
|
"min_level": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_lock_file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_sequential_string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_swagger_file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.heuristic.is_templated_secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.regex.should_exclude_file",
|
||||||
|
"pattern": [
|
||||||
|
"test*.py"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.regex.should_exclude_line",
|
||||||
|
"pattern": [
|
||||||
|
".*example.*",
|
||||||
|
".*_api_key.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "detect_secrets.filters.regex.should_exclude_secret",
|
||||||
|
"pattern": [
|
||||||
|
"example",
|
||||||
|
"REPLACE_ME",
|
||||||
|
"PASSWORD",
|
||||||
|
"PASS",
|
||||||
|
"my_email",
|
||||||
|
"my_password",
|
||||||
|
"my_pat",
|
||||||
|
"fmp",
|
||||||
|
"other_key",
|
||||||
|
"polygon",
|
||||||
|
"fred",
|
||||||
|
"benzinga",
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRiMjEyZDdhZj",
|
||||||
|
"c2MWI0ZTNlOGNjZGM3OWQ5Zjk4YWM5In0.eyJhY2Nlc3NfdG9rZW4iOiJ0",
|
||||||
|
"b2tlbiIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJ1dWlkIjoidXVpZCIsInV",
|
||||||
|
"zZXJuYW1lIjoidXNlcm5hbWUiLCJlbWFpbCI6ImVtYWlsIiwicHJpbWFyeV9",
|
||||||
|
"1c2FnZSI6InByaW1hcnlfdXNhZ2UifQ.FAtE8-a1a-313Zoa6dREIxGZOHaW9",
|
||||||
|
"-JLZnFzyJ6dlHBZnkjQT2tfaaefxnTdAlSmToQwxGykvuatmI7L0wztPQ"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"results": {},
|
||||||
|
"generated_at": "2023-12-14T18:07:51Z"
|
||||||
|
}
|
||||||
29
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/README.md
vendored
Normal file
29
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/README.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# OpenBB ODP Extensions Cookiecutter Template
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This is the generated cookiecutter template for the OpenBB Python Package.
|
||||||
|
It is used to help you create a new extension that can be integrated into the existing structure
|
||||||
|
|
||||||
|
With it you can:
|
||||||
|
|
||||||
|
- Create a new extension
|
||||||
|
- Build custom commands
|
||||||
|
- Interact with the standardization framework
|
||||||
|
- Build custom services and applications on top of the framework
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
We recommend you check out the files in the following order:
|
||||||
|
|
||||||
|
* `{{cookiecutter.package_name}}/routers/{{cookiecutter.router_name}}.py`
|
||||||
|
* `{{cookiecutter.package_name}}/prvoviders/{{cookiecutter.provider_name}}/models/example.py`
|
||||||
|
* `{{cookiecutter.package_name}}/providers/{{cookiecutter.provider_name}}/__init__.py`
|
||||||
|
* `{{cookiecutter.package_name}}/obbject/{{cookiecutter.obbject_name}}/__init__.py`
|
||||||
|
* `{{cookiecutter.package_name}}/routers/{{cookiecutter.router_name}}_views.py`
|
||||||
|
|
||||||
|
Check out the developer [documentation](https://docs.openbb.co/python/developer) for more information on getting started making OpenBB extensions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🦋 Made with [openbb cookiecutter](https://github.com/openbb-finance/OpenBB/cookiecutter).
|
||||||
34
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/pyproject.toml
vendored
Normal file
34
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/pyproject.toml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "{{ cookiecutter.project_tag }}"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "{{ cookiecutter.project_name }}"
|
||||||
|
authors = ["{{ cookiecutter.full_name }} <{{ cookiecutter.email }}>"]
|
||||||
|
readme = "README.md"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
packages = [{ include = "{{ cookiecutter.package_name }}" }]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.10,<3.14"
|
||||||
|
openbb-core = "*"
|
||||||
|
openbb-platform-api = "*"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
openbb-devtools = { version = "*" }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."openbb_core_extension"]
|
||||||
|
{{ cookiecutter.router_name }} = "{{ cookiecutter.package_name }}.routers.{{ cookiecutter.router_name }}:router"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."openbb_charting_extension"]
|
||||||
|
{{ cookiecutter.router_name }} = "{{ cookiecutter.package_name }}.routers.{{ cookiecutter.router_name }}_views:{{cookiecutter.router_name.replace('_', ' ').title().replace(' ', '').replace('"', '')}}Views"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."openbb_provider_extension"]
|
||||||
|
{{ cookiecutter.provider_name }} = "{{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}:{{ cookiecutter.provider_name }}_provider"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."openbb_obbject_extension"]
|
||||||
|
to_string = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:ext"
|
||||||
|
{{ cookiecutter.obbject_name }} = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:class_ext"
|
||||||
|
nonblocking_plugin = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:nonblocking_plugin"
|
||||||
7
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/pytest.ini
vendored
Normal file
7
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/pytest.ini
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = -p no:warnings
|
||||||
|
markers =
|
||||||
|
linux: tests that are not stable on Windows
|
||||||
|
integration: OpenBB Platform integration test marker
|
||||||
|
testpaths =
|
||||||
|
tests
|
||||||
48
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/ruff.toml
vendored
Normal file
48
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/ruff.toml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
line-length = 122
|
||||||
|
target-version = "py310"
|
||||||
|
fix = true
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"F",
|
||||||
|
"Q",
|
||||||
|
"S",
|
||||||
|
"UP",
|
||||||
|
"I",
|
||||||
|
"PLC",
|
||||||
|
"PLE",
|
||||||
|
"PLR",
|
||||||
|
"PLW",
|
||||||
|
"SIM",
|
||||||
|
"T20",
|
||||||
|
]
|
||||||
|
# These ignores should be seen as temporary solutions to problems that will NEED fixed
|
||||||
|
ignore = ["PLR2004", "PLR0913", "PLR0915", "PLC0415", "E402"]
|
||||||
|
|
||||||
|
[lint.per-file-ignores]
|
||||||
|
"**/tests/*" = ["S101"]
|
||||||
|
"*init*.py" = ["F401"]
|
||||||
|
"website/*" = ["T201", "PLR0915"]
|
||||||
|
"*integration/*" = ["S101"]
|
||||||
|
|
||||||
|
[lint.isort]
|
||||||
|
combine-as-imports = true
|
||||||
|
force-wrap-aliases = true
|
||||||
|
|
||||||
|
[lint.pylint]
|
||||||
|
max-args = 8
|
||||||
|
max-branches = 26
|
||||||
|
max-returns = 9
|
||||||
|
max-statements = 30
|
||||||
|
|
||||||
|
[lint.pydocstyle]
|
||||||
|
convention = "numpy"
|
||||||
|
|
||||||
|
[lint.flake8-import-conventions.aliases]
|
||||||
|
"matplotlib.pyplot" = "plt"
|
||||||
|
numpy = "np"
|
||||||
|
pandas = "pd"
|
||||||
|
seaborn = "sns"
|
||||||
|
openbb = "obb"
|
||||||
0
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/tests/.gitkeep
vendored
Normal file
0
cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/tests/.gitkeep
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Root configuration for pytest."""
|
||||||
|
|
||||||
|
# flake8: noqa: S101
|
||||||
|
# pylint: disable=unused-argument,unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest # noqa: F401
|
||||||
|
|
||||||
|
ROOT_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure():
|
||||||
|
"""Set environment variables for testing."""
|
||||||
|
os.environ["OPENBB_AUTO_BUILD"] = "true"
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
"""Modify test collection to ensure cleanup-dependent tests run first."""
|
||||||
|
# Find tests that should run early (checking clean state)
|
||||||
|
early_tests: list = []
|
||||||
|
other_tests: list = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
# Tests that check repository state should run first
|
||||||
|
if item.get_closest_marker("order"):
|
||||||
|
early_tests.append(item)
|
||||||
|
else:
|
||||||
|
other_tests.append(item)
|
||||||
|
|
||||||
|
# Sort early tests by their order marker if present
|
||||||
|
early_tests.sort(
|
||||||
|
key=lambda x: (
|
||||||
|
getattr(x.get_closest_marker("order"), "args", [999])[0]
|
||||||
|
if x.get_closest_marker("order")
|
||||||
|
else 999
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reorder: early tests first, then others
|
||||||
|
items[:] = early_tests + other_tests
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""OBBject Extensions module."""
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""{{ cookiecutter.package_name }} OBBject Extension - {{ cookiecutter.obbject_name }}"""
|
||||||
|
|
||||||
|
# pylint: disable=W0613,R0903
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from openbb_core.app.model.extension import Extension
|
||||||
|
from openbb_core.app.model.obbject import OBBject
|
||||||
|
|
||||||
|
# Extensions are registered as OBBject accessors.
|
||||||
|
# It can be a class, or it can be a callable method.
|
||||||
|
ext = Extension(
|
||||||
|
name="to_string",
|
||||||
|
description="An OBBject extension that converts the results to a string representation.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# If it is a function, no parameters will be accepted.
|
||||||
|
# The function will execute like a property method.
|
||||||
|
# The accessor is called when the namespace is entered.
|
||||||
|
@ext.obbject_accessor
|
||||||
|
def to_string(obbject, **kwargs) -> str:
|
||||||
|
"""OBBject accessor providing a "to_string" method."""
|
||||||
|
return obbject.model_dump_json(exclude_none=True, exclude_unset=True, include="results")
|
||||||
|
|
||||||
|
# We ignore this OpenBBWarning: Skipping '{{ cookiecutter.obbject_name }}', name already in user.
|
||||||
|
|
||||||
|
class_ext = Extension(
|
||||||
|
name="{{ cookiecutter.obbject_name }}",
|
||||||
|
description="An OBBject extension with namespace."
|
||||||
|
)
|
||||||
|
|
||||||
|
@class_ext.obbject_accessor
|
||||||
|
class OBBjectExtension:
|
||||||
|
"""OBBject Extension Template."""
|
||||||
|
|
||||||
|
def __init__(self, obbject: OBBject):
|
||||||
|
"""Initialize the extension."""
|
||||||
|
self._obbject = obbject
|
||||||
|
|
||||||
|
def hello_world(self, **kwargs):
|
||||||
|
"""Say hello from the OBBject extension."""
|
||||||
|
print(f"Hello from the OBBject instance! \n\n{repr(self._obbject)}") # noqa
|
||||||
|
|
||||||
|
|
||||||
|
nonblocking_plugin = Extension(
|
||||||
|
name="nonblocking_plugin",
|
||||||
|
description="An on-command-output plugin simulating an extensive task performed in a separate thread.",
|
||||||
|
on_command_output=True, # Must be set as True
|
||||||
|
command_output_paths=["/{{cookiecutter.router_name}}/candles"],
|
||||||
|
immutable=True, # Set to `True` for parallel processing.
|
||||||
|
results_only=False, # Use this as a flag to return only the "results" portion of the OBBject.
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _expensive_operation_worker(serialized_obbject: dict):
|
||||||
|
"""Simulate a long-running task without blocking the caller."""
|
||||||
|
working_copy = OBBject(**serialized_obbject)
|
||||||
|
print("\nThis is the deserialized OBBject in the non-blocking thread.")
|
||||||
|
print(working_copy.__repr__())
|
||||||
|
for i in range(10):
|
||||||
|
print(str(i) + " seconds remaining...")
|
||||||
|
time.sleep(1)
|
||||||
|
print("Expensive operation is now complete.")
|
||||||
|
|
||||||
|
|
||||||
|
@nonblocking_plugin.obbject_accessor
|
||||||
|
def empty_plugin_function(obbject): # This can also be an async function.
|
||||||
|
"""Simulated on_commnd_output function that executes an expensive task
|
||||||
|
in a non-blocking thread."""
|
||||||
|
print(
|
||||||
|
"Serializing the obbject and passing to a new thread.\n"
|
||||||
|
f"Command executed: {obbject.extra['metadata']}\n"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"Simulating an expensive task that is non-blocking and allows the function to return."
|
||||||
|
)
|
||||||
|
threading.Thread(
|
||||||
|
target=_expensive_operation_worker,
|
||||||
|
args=(obbject.model_dump(),),
|
||||||
|
name="empty-plugin-expensive-operation",
|
||||||
|
daemon=False,
|
||||||
|
).start()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Providers Module"""
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""{{ cookiecutter.package_name }} OpenBB Platform Provider."""
|
||||||
|
|
||||||
|
from openbb_core.provider.abstract.provider import Provider
|
||||||
|
from {{cookiecutter.package_name}}.providers.{{cookiecutter.provider_name}}.models.example import ExampleFetcher
|
||||||
|
from {{cookiecutter.package_name}}.providers.{{cookiecutter.provider_name}}.models.ohlc_example import {{cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '')}}EquityHistoricalFetcher
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{{cookiecutter.provider_name}}_provider = Provider(
|
||||||
|
name="{{cookiecutter.provider_name}}",
|
||||||
|
description="Data provider for {{cookiecutter.project_name}}.",
|
||||||
|
# Only add 'credentials' if they are needed.
|
||||||
|
# For multiple login details, list them all here.
|
||||||
|
# credentials=["api_key"],
|
||||||
|
website="https://{{cookiecutter.project_tag}}.com",
|
||||||
|
# Here, we list out the fetchers showing what our provider can get.
|
||||||
|
# The dictionary key is the fetcher's name, used in the `../routers/router.py`.
|
||||||
|
fetcher_dict={
|
||||||
|
"EquityHistorical": {{cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '')}}EquityHistoricalFetcher,
|
||||||
|
"Example": ExampleFetcher,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Example Data Integration.
|
||||||
|
|
||||||
|
The OpenBB Platform gives developers easy tools for integration.
|
||||||
|
|
||||||
|
To use it, developers should:
|
||||||
|
1. Define the request/query parameters.
|
||||||
|
2. Define the resulting data schema.
|
||||||
|
3. Define how to fetch raw data.
|
||||||
|
|
||||||
|
First 2 steps make sure developers really get to know their data.
|
||||||
|
This is called the "Know Your Data" principle.
|
||||||
|
|
||||||
|
Note: The format of the QueryParams and Data is defined by a pydantic model that can
|
||||||
|
be entirely custom, or inherit from the OpenBB standardized models.
|
||||||
|
|
||||||
|
This file shows an example of how to integrate data from a provider.
|
||||||
|
"""
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
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 pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleQueryParams(QueryParams):
|
||||||
|
"""Example provider query.
|
||||||
|
|
||||||
|
This is the definition of our query parameters that are specific to this provider.
|
||||||
|
We use this class to create our own parameters that will provided as input to the
|
||||||
|
command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
symbol: str = Field(description="Symbol to query.")
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleData(Data):
|
||||||
|
"""Sample provider data.
|
||||||
|
|
||||||
|
The fields are displayed as-is in the output of the command. In this case, its the
|
||||||
|
Open, High, Low, Close and Volume data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
o: float = Field(description="Open price.")
|
||||||
|
h: float = Field(description="High price.")
|
||||||
|
l: float = Field(description="Low price.")
|
||||||
|
c: float = Field(description="Close price.")
|
||||||
|
v: float = Field(description="Volume.")
|
||||||
|
d: str = Field(description="Date")
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleFetcher(
|
||||||
|
Fetcher[
|
||||||
|
ExampleQueryParams,
|
||||||
|
list[ExampleData],
|
||||||
|
]
|
||||||
|
):
|
||||||
|
"""Example Fetcher class.
|
||||||
|
|
||||||
|
This class is responsible for the actual data retrieval.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_query(params: dict[str, Any]) -> ExampleQueryParams:
|
||||||
|
"""Define example transform_query.
|
||||||
|
|
||||||
|
Here we can pre-process the query parameters and add any extra parameters that
|
||||||
|
will be used inside the extract_data method.
|
||||||
|
"""
|
||||||
|
return ExampleQueryParams(**params)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_data(
|
||||||
|
query: ExampleQueryParams,
|
||||||
|
credentials: dict[str, str] | None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Define example extract_data.
|
||||||
|
|
||||||
|
Here we make the actual request to the data provider and receive the raw data.
|
||||||
|
If you said your Provider class needs credentials you can get them here.
|
||||||
|
"""
|
||||||
|
api_key = (
|
||||||
|
credentials.get("{{cookiecutter.package_name}}_api_key")
|
||||||
|
if credentials
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Here we mock an example_response for brevity.
|
||||||
|
example_response = [
|
||||||
|
{
|
||||||
|
"o": 2,
|
||||||
|
"h": 5,
|
||||||
|
"l": 1,
|
||||||
|
"c": 4,
|
||||||
|
"v": 5,
|
||||||
|
"d": "August 23, 2023",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"o": 4,
|
||||||
|
"h": 7,
|
||||||
|
"l": 3,
|
||||||
|
"c": 6,
|
||||||
|
"v": 10,
|
||||||
|
"d": "August 24, 2023",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return example_response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_data(
|
||||||
|
query: ExampleQueryParams, data: list[dict], **kwargs: Any
|
||||||
|
) -> list[ExampleData]:
|
||||||
|
"""Define example transform_data.
|
||||||
|
|
||||||
|
Right now, we're converting the data to fit our desired format.
|
||||||
|
You can apply other transformations to it here.
|
||||||
|
"""
|
||||||
|
return [ExampleData(**d) for d in data]
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Example Data Integration With Standard Model.
|
||||||
|
|
||||||
|
This file shows an example of how to integrate this provider with ends available to other providers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||||
|
from openbb_core.provider.standard_models.equity_historical import (
|
||||||
|
EquityHistoricalData,
|
||||||
|
EquityHistoricalQueryParams,
|
||||||
|
)
|
||||||
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams(EquityHistoricalQueryParams):
|
||||||
|
"""Example provider query.
|
||||||
|
|
||||||
|
The standard model here comes with parameters for symbol, start_date, and end_date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
custom_param: str | None = Field(
|
||||||
|
default=None, description="Some optional parameter"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalData(EquityHistoricalData):
|
||||||
|
"""Sample provider data.
|
||||||
|
|
||||||
|
The standard model has these fields,
|
||||||
|
so we use __alias_dict__ to map them.
|
||||||
|
We only need to add fields not in the inherited model, or to override.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__alias_dict__ = {
|
||||||
|
"date": "d",
|
||||||
|
"open": "o",
|
||||||
|
"high": "h",
|
||||||
|
"low": "l",
|
||||||
|
"close": "c",
|
||||||
|
"volume": "v",
|
||||||
|
"custom_field": "f",
|
||||||
|
}
|
||||||
|
custom_field: str | None = Field(default=None, description="Some optional field")
|
||||||
|
|
||||||
|
@field_validator("custom_field", mode="before", check_fields=False)
|
||||||
|
@classmethod
|
||||||
|
def _validate_custom_field(cls, v):
|
||||||
|
"""Validate the custom field."""
|
||||||
|
return v if v else "Data validator replaced None."
|
||||||
|
|
||||||
|
|
||||||
|
class {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalFetcher(
|
||||||
|
Fetcher[
|
||||||
|
{{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams,
|
||||||
|
list[{{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalData],
|
||||||
|
]
|
||||||
|
):
|
||||||
|
"""Example Fetcher class.
|
||||||
|
|
||||||
|
This class is responsible for the actual data retrieval.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_query(params: dict[str, Any]) -> {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams:
|
||||||
|
"""Define example transform_query.
|
||||||
|
|
||||||
|
Here we can pre-process the query parameters and add any extra parameters that
|
||||||
|
will be used inside the extract_data method.
|
||||||
|
"""
|
||||||
|
return {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams(**params)
|
||||||
|
|
||||||
|
# Note the use of async here. Make the Fetcher async with this small change.
|
||||||
|
@staticmethod
|
||||||
|
async def aextract_data(
|
||||||
|
query: {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams,
|
||||||
|
credentials: dict[str, str] | None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Define example extract_data.
|
||||||
|
|
||||||
|
Here we make the actual request to the data provider and receive the raw data.
|
||||||
|
If you said your Provider class needs credentials you can get them here.
|
||||||
|
"""
|
||||||
|
api_key = (
|
||||||
|
credentials.get("{{ cookiecutter.provider_name }}_api_key") if credentials else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Here we mock an example_response for brevity.
|
||||||
|
# Show model validation by only returning one row of custom_field
|
||||||
|
# Show model validation by only returning one row of custom_field
|
||||||
|
example_response = [
|
||||||
|
{
|
||||||
|
"o": 2,
|
||||||
|
"h": 5,
|
||||||
|
"l": 1,
|
||||||
|
"c": 4,
|
||||||
|
"v": 5,
|
||||||
|
"d": "August 23, 2023",
|
||||||
|
"f": query.custom_param,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"o": 4,
|
||||||
|
"h": 7,
|
||||||
|
"l": 3,
|
||||||
|
"c": 6,
|
||||||
|
"v": 10,
|
||||||
|
"d": "August 24, 2023",
|
||||||
|
"f": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return example_response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_data(
|
||||||
|
query: {{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalQueryParams,
|
||||||
|
data: list[dict],
|
||||||
|
**kwargs: Any
|
||||||
|
) -> list[{{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalData]:
|
||||||
|
"""Define example transform_data.
|
||||||
|
|
||||||
|
Right now, we're converting the data to fit our desired format.
|
||||||
|
You can apply other transformations to it here.
|
||||||
|
"""
|
||||||
|
return [{{cookiecutter.provider_name.replace('_', ' ').capitalize().replace(' ', '')}}EquityHistoricalData.model_validate(d) for d in data]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""{{ cookiecutter.provider_name}} utilities module."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""{{ cookiecutter.provider_name}} helper functions."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""{{ cookiecutter.project_name}} routers module."""
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Router dependency injections."""
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from fastapi import Depends
|
||||||
|
from openbb_core.provider.utils.helpers import get_requests_session
|
||||||
|
|
||||||
|
Session = Annotated[requests.Session, Depends(get_requests_session)]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""{{cookiecutter.router_name}} router command example."""
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
from openbb_core.app.model.command_context import CommandContext
|
||||||
|
from openbb_core.app.model.example import APIEx, PythonEx
|
||||||
|
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
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# Example dependency injection yielding a configured requests.Session object.
|
||||||
|
from {{cookiecutter.package_name}}.routers.depends import Session
|
||||||
|
|
||||||
|
# The prefix extension's prefix is determined by the `pyproject.toml` EntryPoint assignment.
|
||||||
|
# Assign a prefix only if this is a sub-router.
|
||||||
|
router = Router(prefix="")
|
||||||
|
|
||||||
|
|
||||||
|
@router.command(
|
||||||
|
methods=["GET"],
|
||||||
|
examples=[
|
||||||
|
PythonEx(
|
||||||
|
description="Here is an example for using this endpoint.",
|
||||||
|
code=[
|
||||||
|
"obb.{{ cookiecutter.router_name }}.get_example(symbol='AAPL')",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
async def get_example(session: Session, symbol: str = "AAPL") -> OBBject[dict]:
|
||||||
|
"""Get options data."""
|
||||||
|
url = f"https://www.cboe.com/education/tools/trade-optimizer/symbol-info?symbol={symbol}"
|
||||||
|
response = session.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return OBBject(results=data["details"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.command(methods=["POST"])
|
||||||
|
async def post_example(
|
||||||
|
data: BaseModel, # These are body parameters.
|
||||||
|
flag: bool = False, # These are query parameters.
|
||||||
|
) -> OBBject[dict]:
|
||||||
|
"""Calculate mid and spread."""
|
||||||
|
|
||||||
|
bid = getattr(data, "bid_col", 0)
|
||||||
|
ask = getattr(data, "ask_col", 0)
|
||||||
|
mid = (bid + ask) / 2
|
||||||
|
spread = ask - bid
|
||||||
|
|
||||||
|
return OBBject(results={"mid": mid, "spread": spread, "flag": flag})
|
||||||
|
|
||||||
|
|
||||||
|
@router.command(model="Example")
|
||||||
|
async def model_example(
|
||||||
|
cc: CommandContext,
|
||||||
|
provider_choices: ProviderChoices,
|
||||||
|
standard_params: StandardParams,
|
||||||
|
extra_params: ExtraParams,
|
||||||
|
) -> OBBject[BaseModel]:
|
||||||
|
"""Example Data."""
|
||||||
|
return await OBBject.from_query(Query(**locals()))
|
||||||
|
|
||||||
|
|
||||||
|
# If you had another provider installed that mapped to this model - i.e, `openbb-fmp`
|
||||||
|
# they will be added to this endpoint.
|
||||||
|
@router.command(model="EquityHistorical")
|
||||||
|
async def candles(
|
||||||
|
cc: CommandContext,
|
||||||
|
provider_choices: ProviderChoices,
|
||||||
|
standard_params: StandardParams,
|
||||||
|
extra_params: ExtraParams,
|
||||||
|
) -> OBBject: # results type is inferred from Fetcher annotations.
|
||||||
|
"""Example Data."""
|
||||||
|
return await OBBject.from_query(Query(**locals()))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Views for the {{ cookiecutter.router_name }} Extension."""
|
||||||
|
|
||||||
|
# flake8: noqa: PLR0912
|
||||||
|
# pylint: disable=import-outside-toplevel,too-few-public-methods
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from openbb_charting.core.openbb_figure import OpenBBFigure # `openbb-charting` is Optional for the user.
|
||||||
|
|
||||||
|
|
||||||
|
# You can make charts that are returned when you map a function
|
||||||
|
# route in lower_snake_case to this class using static methods.
|
||||||
|
# You can return whatever format you like, but it must be JSON-serializable.
|
||||||
|
# The returned tuple object gets added to the response object from the application.
|
||||||
|
# The charting extension itself is accessible under the `charting` namespace.
|
||||||
|
# While the finished chart is under the `chart` object of the OBBject response output.
|
||||||
|
# The application will check if the user has `openbb-charting` installed on run.
|
||||||
|
# If not, the views are not added to the application.
|
||||||
|
|
||||||
|
|
||||||
|
class {{cookiecutter.router_name.replace("_", " ").title().replace(" ", "")}}Views:
|
||||||
|
"""{{ cookiecutter.router_name }} Views."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def {{cookiecutter.router_name}}_candles(**kwargs) -> tuple["OpenBBFigure", dict[str, Any]]:
|
||||||
|
"""Create a chart that will return to the API as a JSON-encoded string."""
|
||||||
|
# Keep imports here so they are imported only at function run.
|
||||||
|
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||||
|
|
||||||
|
data = kwargs["obbject_item"] # This is where the data will always be.
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
fig = OpenBBFigure()
|
||||||
|
|
||||||
|
fig.add_bar(x=[d.date for d in data], y=[d.high for d in data])
|
||||||
|
content = fig.show(external=True).to_plotly_json()
|
||||||
|
# fig should be the binary Python object of the chart
|
||||||
|
# content should be a JSON-serialized version ready for the frontend to render.
|
||||||
|
return fig, content
|
||||||
24
cookiecutter/pyproject.toml
vendored
Normal file
24
cookiecutter/pyproject.toml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "openbb-cookiecutter"
|
||||||
|
version = "0.4.0"
|
||||||
|
description = "Extensions template for the OpenBB Python Package."
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
authors = ["OpenBB Team <hello@openbb.co>"]
|
||||||
|
packages = [{ include = "openbb_cookiecutter" }]
|
||||||
|
readme = "README.md"
|
||||||
|
homepage = "https://openbb.co"
|
||||||
|
repository = "https://github.com/OpenBB-finance/openbb-cookiecutter"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.10,<3.14"
|
||||||
|
cookiecutter = "^2.6.0"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
openbb-cookiecutter = "openbb_cookiecutter.cli:main"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."cookiecutter.templates"]
|
||||||
|
openbb = "openbb_cookiecutter"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Command runner module."""
|
"""Command runner module."""
|
||||||
|
|
||||||
# pylint: disable=R0903
|
# pylint: disable=R0903
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import asdict, is_dataclass
|
from dataclasses import asdict, is_dataclass
|
||||||
@@ -15,6 +14,7 @@ from warnings import catch_warnings, showwarning, warn
|
|||||||
from openbb_core.app.extension_loader import ExtensionLoader
|
from openbb_core.app.extension_loader import ExtensionLoader
|
||||||
from openbb_core.app.model.abstract.error import OpenBBError
|
from openbb_core.app.model.abstract.error import OpenBBError
|
||||||
from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
|
from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
|
||||||
|
from openbb_core.app.model.extension import CachedAccessor
|
||||||
from openbb_core.app.model.metadata import Metadata
|
from openbb_core.app.model.metadata import Metadata
|
||||||
from openbb_core.app.model.obbject import OBBject
|
from openbb_core.app.model.obbject import OBBject
|
||||||
from openbb_core.app.provider_interface import ExtraParams
|
from openbb_core.app.provider_interface import ExtraParams
|
||||||
@@ -448,9 +448,9 @@ class StaticCommandRunner:
|
|||||||
raise OpenBBError(e) from e
|
raise OpenBBError(e) from e
|
||||||
warn(str(e), OpenBBWarning)
|
warn(str(e), OpenBBWarning)
|
||||||
|
|
||||||
|
if isinstance(obbject, OBBject):
|
||||||
try:
|
try:
|
||||||
cls._trigger_command_output_callbacks(route, obbject)
|
cls._trigger_command_output_callbacks(route, obbject)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if Env().DEBUG_MODE:
|
if Env().DEBUG_MODE:
|
||||||
raise OpenBBError(e) from e
|
raise OpenBBError(e) from e
|
||||||
@@ -463,7 +463,8 @@ class StaticCommandRunner:
|
|||||||
"""Trigger command output callbacks for extensions."""
|
"""Trigger command output callbacks for extensions."""
|
||||||
loader = ExtensionLoader()
|
loader = ExtensionLoader()
|
||||||
callbacks = loader.on_command_output_callbacks
|
callbacks = loader.on_command_output_callbacks
|
||||||
results_only = False
|
if not callbacks:
|
||||||
|
return
|
||||||
|
|
||||||
# For each extension registered for all routes or the specific route,
|
# For each extension registered for all routes or the specific route,
|
||||||
# we call its accessor on the OBBject.
|
# we call its accessor on the OBBject.
|
||||||
@@ -473,53 +474,93 @@ class StaticCommandRunner:
|
|||||||
# mutates the OBBject so we can pass this information to the interface.
|
# mutates the OBBject so we can pass this information to the interface.
|
||||||
# We also set the _results_only attribute to True if any extension
|
# We also set the _results_only attribute to True if any extension
|
||||||
# indicates that only results should be returned.
|
# indicates that only results should be returned.
|
||||||
if "*" in callbacks:
|
results_only = False
|
||||||
for ext in callbacks["*"]:
|
executed_keys: set[str] = set()
|
||||||
if ext.results_only is True:
|
ordered_extensions: list = []
|
||||||
results_only = True
|
all_on_command_output_exts: list = []
|
||||||
if ext.immutable is True:
|
|
||||||
if hasattr(obbject, ext.name):
|
|
||||||
obbject_copy = deepcopy(obbject)
|
|
||||||
accessor = getattr(obbject_copy, ext.name)
|
|
||||||
if iscoroutinefunction(accessor):
|
|
||||||
run_async(accessor)
|
|
||||||
elif callable(accessor):
|
|
||||||
accessor()
|
|
||||||
elif ext.immutable is False:
|
|
||||||
if ext.results_only is True:
|
|
||||||
results_only = True
|
|
||||||
if hasattr(obbject, ext.name):
|
|
||||||
accessor = getattr(obbject, ext.name)
|
|
||||||
if iscoroutinefunction(accessor):
|
|
||||||
run_async(accessor)
|
|
||||||
elif callable(accessor):
|
|
||||||
accessor()
|
|
||||||
setattr(obbject, "_extension_modified", True)
|
|
||||||
|
|
||||||
if route in callbacks:
|
def _extension_key(ext) -> str:
|
||||||
for ext in callbacks[route]:
|
if key := getattr(ext, "identifier", None):
|
||||||
|
return str(key)
|
||||||
|
if path := getattr(ext, "import_path", None):
|
||||||
|
return f"{path}:{getattr(ext, 'name', id(ext))}"
|
||||||
|
return str(getattr(ext, "name", id(ext)))
|
||||||
|
|
||||||
|
def _clone_for_immutable(source: OBBject) -> OBBject | None:
|
||||||
|
try:
|
||||||
|
new_source = source.model_copy()
|
||||||
|
new_source = OBBject.model_validate(source.model_dump())
|
||||||
|
return source.model_validate(new_source)
|
||||||
|
except Exception as e:
|
||||||
|
warn(
|
||||||
|
"Skipped immutable callback because the OBBject "
|
||||||
|
f"could not be duplicated. {e}",
|
||||||
|
OpenBBWarning,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for ext_list in callbacks.values():
|
||||||
|
all_on_command_output_exts.extend(ext_list)
|
||||||
|
|
||||||
|
for ext in callbacks.get("*", []):
|
||||||
|
key = _extension_key(ext)
|
||||||
|
if key not in executed_keys:
|
||||||
|
executed_keys.add(key)
|
||||||
|
ordered_extensions.append(ext)
|
||||||
|
|
||||||
|
for ext in callbacks.get(route, []):
|
||||||
|
key = _extension_key(ext)
|
||||||
|
if key not in executed_keys:
|
||||||
|
executed_keys.add(key)
|
||||||
|
ordered_extensions.append(ext)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for ext in ordered_extensions:
|
||||||
if ext.results_only is True:
|
if ext.results_only is True:
|
||||||
results_only = True
|
results_only = True
|
||||||
|
|
||||||
if ext.immutable is True:
|
if ext.command_output_paths and route not in ext.command_output_paths:
|
||||||
if hasattr(obbject, ext.name):
|
continue
|
||||||
obbject_copy = deepcopy(obbject)
|
|
||||||
accessor = getattr(obbject_copy, ext.name)
|
accessors: set = getattr(type(obbject), "accessors", set())
|
||||||
if iscoroutinefunction(accessor):
|
if ext.name not in accessors:
|
||||||
run_async(accessor)
|
continue
|
||||||
elif callable(accessor):
|
|
||||||
accessor()
|
descriptor = type(obbject).__dict__.get(ext.name)
|
||||||
elif ext.immutable is False and hasattr(obbject, ext.name):
|
if not isinstance(descriptor, CachedAccessor):
|
||||||
accessor = getattr(obbject, ext.name)
|
continue
|
||||||
if iscoroutinefunction(accessor):
|
|
||||||
run_async(accessor)
|
factory = descriptor._accessor # type: ignore # pylint: disable=W0212
|
||||||
elif callable(accessor):
|
|
||||||
accessor()
|
target = _clone_for_immutable(obbject) if ext.immutable else obbject
|
||||||
setattr(obbject, "_extension_modified", True)
|
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if iscoroutinefunction(factory):
|
||||||
|
run_async(factory, target)
|
||||||
|
else:
|
||||||
|
result = factory(target)
|
||||||
|
if callable(result):
|
||||||
|
result()
|
||||||
|
|
||||||
|
if ext.immutable is False:
|
||||||
|
object.__setattr__(obbject, "_extension_modified", True)
|
||||||
|
|
||||||
if results_only is True:
|
if results_only is True:
|
||||||
setattr(obbject, "_results_only", True)
|
object.__setattr__(obbject, "_results_only", True)
|
||||||
setattr(obbject, "_extension_modified", True)
|
object.__setattr__(obbject, "_extension_modified", True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise OpenBBError(e) from e
|
||||||
|
|
||||||
|
for ext in all_on_command_output_exts:
|
||||||
|
if ext.name in type(obbject).__dict__:
|
||||||
|
object.__setattr__(
|
||||||
|
obbject,
|
||||||
|
ext.name,
|
||||||
|
"Accessor is not callable outside of function execution.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandRunner:
|
class CommandRunner:
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ class CredentialsLoader:
|
|||||||
**self.format_credentials(additional), # type: ignore
|
**self.format_credentials(additional), # type: ignore
|
||||||
)
|
)
|
||||||
model._env_defaults = env_overrides # type: ignore # pylint: disable=W0212
|
model._env_defaults = env_overrides # type: ignore # pylint: disable=W0212
|
||||||
|
model.origins = self.credentials
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from openbb_core.app.command_runner import (
|
|||||||
)
|
)
|
||||||
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
||||||
from openbb_core.app.model.command_context import CommandContext
|
from openbb_core.app.model.command_context import CommandContext
|
||||||
from openbb_core.app.model.extension import Extension
|
from openbb_core.app.model.extension import CachedAccessor, Extension
|
||||||
from openbb_core.app.model.obbject import OBBject
|
from openbb_core.app.model.obbject import OBBject
|
||||||
from openbb_core.app.model.system_settings import SystemSettings
|
from openbb_core.app.model.system_settings import SystemSettings
|
||||||
from openbb_core.app.model.user_settings import UserSettings
|
from openbb_core.app.model.user_settings import UserSettings
|
||||||
@@ -495,7 +495,13 @@ def test_extension_mutable_modifies_original_and_sets_extension_modified_and_rou
|
|||||||
if isinstance(getattr(self, "results", None), list):
|
if isinstance(getattr(self, "results", None), list):
|
||||||
self.results.append("modified_by_mut")
|
self.results.append("modified_by_mut")
|
||||||
|
|
||||||
monkeypatch.setattr(OBBject, ext.name, mut_accessor, raising=False)
|
monkeypatch.setattr(
|
||||||
|
"openbb_core.app.model.obbject.OBBject.accessors",
|
||||||
|
OBBject.accessors | {ext.name},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
OBBject, ext.name, CachedAccessor(ext.name, mut_accessor), raising=False
|
||||||
|
)
|
||||||
|
|
||||||
# register the extension only for "mock/route"
|
# register the extension only for "mock/route"
|
||||||
fake_loader = SimpleNamespace(on_command_output_callbacks={"mock/route": [ext]})
|
fake_loader = SimpleNamespace(on_command_output_callbacks={"mock/route": [ext]})
|
||||||
@@ -535,7 +541,13 @@ def test_results_only_flag_sets_attribute_and_accessor_runs(monkeypatch):
|
|||||||
def ro_accessor(self):
|
def ro_accessor(self):
|
||||||
called["hit"] = True
|
called["hit"] = True
|
||||||
|
|
||||||
monkeypatch.setattr(OBBject, ext.name, ro_accessor, raising=False)
|
monkeypatch.setattr(
|
||||||
|
"openbb_core.app.model.obbject.OBBject.accessors",
|
||||||
|
OBBject.accessors | {ext.name},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
OBBject, ext.name, CachedAccessor(ext.name, ro_accessor), raising=False
|
||||||
|
)
|
||||||
|
|
||||||
fake_loader = SimpleNamespace(on_command_output_callbacks={"*": [ext]})
|
fake_loader = SimpleNamespace(on_command_output_callbacks={"*": [ext]})
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ class OpenBBFigure(go.Figure):
|
|||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if fig:
|
if fig:
|
||||||
self.__dict__ = fig.__dict__
|
self.__dict__ = (
|
||||||
|
go.Figure(fig).__dict__ if isinstance(fig, dict) else fig.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
self._charting_settings: ChartingSettings | None = kwargs.pop(
|
self._charting_settings: ChartingSettings | None = kwargs.pop(
|
||||||
"charting_settings", None
|
"charting_settings", None
|
||||||
|
|||||||
1
ruff.toml
vendored
1
ruff.toml
vendored
@@ -5,6 +5,7 @@
|
|||||||
exclude = [
|
exclude = [
|
||||||
"^openbb_platform/platform/core/openbb_core/app/static/package/.*",
|
"^openbb_platform/platform/core/openbb_core/app/static/package/.*",
|
||||||
"^openbb_platform/core/openbb/package/.*",
|
"^openbb_platform/core/openbb/package/.*",
|
||||||
|
"^cookiecutter/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
line-length = 122
|
line-length = 122
|
||||||
|
|||||||
Reference in New Issue
Block a user