mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-13 04:41:24 +00:00
refactor cookiecutter
This commit is contained in:
@@ -222,6 +222,25 @@ def test_translator_handles_basemodel_param_flattens_fields():
|
||||
assert any("data" in opt and "field2" in opt for opt in optstrings)
|
||||
|
||||
|
||||
def test_translator_basemodel_param_skips_variadic_signature_parameters():
|
||||
"""A model allowing extra fields flattens its fields but not its ``**extra_data``."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
class ExtraData(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
field1: str = "a"
|
||||
|
||||
def f(data: ExtraData) -> dict:
|
||||
"""f."""
|
||||
return {}
|
||||
|
||||
t = ArgparseTranslator(f)
|
||||
optstrings = {opt for action in t.parser._actions for opt in action.option_strings}
|
||||
assert any("data" in opt and "field1" in opt for opt in optstrings)
|
||||
assert not any("extra_data" in opt for opt in optstrings)
|
||||
|
||||
|
||||
def test_translator_required_vs_optional_param_routing():
|
||||
"""Required params land in the 'required arguments' group; optional in standard."""
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ class TestShowFigure:
|
||||
):
|
||||
show_figure(FIGURE, "t")
|
||||
open_browser.assert_called_once_with((tmp_path / "chart.html").as_uri())
|
||||
assert "Plotly.newPlot" in (tmp_path / "chart.html").read_text()
|
||||
assert "Plotly.newPlot" in (tmp_path / "chart.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestStripThemeColors:
|
||||
|
||||
5
cookiecutter/MANIFEST.in
vendored
5
cookiecutter/MANIFEST.in
vendored
@@ -1,5 +0,0 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
recursive-include openbb_cookiecutter/template *
|
||||
recursive-exclude openbb_cookiecutter/template *.pyc
|
||||
recursive-exclude openbb_cookiecutter/template __pycache__
|
||||
91
cookiecutter/README.md
vendored
91
cookiecutter/README.md
vendored
@@ -1,72 +1,73 @@
|
||||
# OpenBB ODP Extensions Cookiecutter
|
||||
# OpenBB Extensions Cookiecutter
|
||||
|
||||
[Cookiecutter](https://cookiecutter.readthedocs.io/en/1.7.2/) is a command-line utility that creates projects from templates.
|
||||
A [Cookiecutter](https://cookiecutter.readthedocs.io/) template that generates a new OpenBB Platform extension as an installable Python project.
|
||||
|
||||
This extension is a simple template for setting up new OpenBB Python Package extensions and projects.
|
||||
## Extension types
|
||||
|
||||
## Template Structure
|
||||
Choose one or more types, or `all`:
|
||||
|
||||
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.
|
||||
| Type | Generates |
|
||||
|------|-----------|
|
||||
| `router` | Commands under `obb.<router_name>` |
|
||||
| `provider` | A data provider with an example model and an `EquityHistorical` standard-model implementation |
|
||||
| `charting` | Chart views for the router's commands, used with `openbb-charting` |
|
||||
| `obbject` | Accessors added to every command result |
|
||||
| `on_command_output` | A plugin that runs after a command returns |
|
||||
|
||||
- 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.
|
||||
The generated project follows the same conventions as the extensions in this repository: hatchling packaging, entry points in `[project.entry-points]`, a `dev` dependency group, ruff and ty configuration in `pyproject.toml`, and tests for every generated module.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Install in a Python environment from PyPI with:
|
||||
Run it with [uv](https://docs.astral.sh/uv/) without installing:
|
||||
|
||||
```
|
||||
pip install openbb-cookiecutter
|
||||
```
|
||||
|
||||
Alternatively, with `uvx`:
|
||||
|
||||
```
|
||||
```bash
|
||||
uvx openbb-cookiecutter
|
||||
```
|
||||
|
||||
2. Navigate the current working directory to the desired output location and run:
|
||||
Or install it and run the command:
|
||||
|
||||
```
|
||||
```bash
|
||||
pip install openbb-cookiecutter
|
||||
openbb-cookiecutter
|
||||
```
|
||||
|
||||
Enter values or press `enter` to continue with the default.
|
||||
Enter a value at each prompt or press enter to accept the default. Pass the types directly to skip that prompt:
|
||||
|
||||
3. Create a new Python environment for the project.
|
||||
|
||||
4. Navigate into the generated folder and install with:
|
||||
|
||||
```
|
||||
pip install -e .
|
||||
```bash
|
||||
openbb-cookiecutter -e router provider
|
||||
```
|
||||
|
||||
5. Python static files will be generated on first import, or trigger with `openbb-build`.
|
||||
Generate without prompts, into another directory:
|
||||
|
||||
6. Import the Python package or start the API and use like any other OpenBB application.
|
||||
```bash
|
||||
openbb-cookiecutter --no-input -o ./extensions -e all
|
||||
```
|
||||
|
||||
7. Modify the business logic and get started building!
|
||||
Override any template value with `--extra-context KEY=VALUE`, for example `--extra-context provider_name=my_source`.
|
||||
|
||||
See the developer documentation [here](https://docs.openbb.co/python/developer).
|
||||
The provider and the OBBject extension each register a credentials namespace under their name, so they need different names.
|
||||
|
||||
## Contributing
|
||||
## Working on the generated project
|
||||
|
||||
We welcome contributions to this template! Please feel free to open an issue or submit a pull request with your improvements.
|
||||
The generated `README.md` covers setup. In short, from the project directory:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run openbb-build
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Developing the template
|
||||
|
||||
From this directory:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
The tests generate a project for each extension type and check its files, its entry points, and that it passes ruff and ty.
|
||||
|
||||
## 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)
|
||||
Questions about the template or OpenBB: `support@openbb.co`. Partnerships: `hello@openbb.co`. Social links: [openbb.co/links](https://openbb.co/links).
|
||||
|
||||
9
cookiecutter/cookiecutter.json
vendored
9
cookiecutter/cookiecutter.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"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(' ', '_') }}",
|
||||
"extension_types": "router",
|
||||
"_template": "{% now 'utc', '%Y%m%d%H%M%S' %}"
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
"""OpenBB Cookiecutter Template."""
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.4.0"
|
||||
try:
|
||||
__version__ = version("openbb-cookiecutter")
|
||||
except PackageNotFoundError:
|
||||
__version__ = "0.0.0"
|
||||
|
||||
|
||||
def get_template_path() -> Path:
|
||||
"""Return the path to the cookiecutter template directory."""
|
||||
"""Return the path to the cookiecutter template directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Path
|
||||
The directory holding ``cookiecutter.json`` and the project template.
|
||||
"""
|
||||
return Path(__file__).parent / "template"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""CLI for OpenBB Cookiecutter template."""
|
||||
|
||||
# pylint: disable=W0718
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
@@ -21,6 +19,23 @@ VALID_EXTENSION_TYPES = [
|
||||
|
||||
|
||||
def _parse_extension_types(value: str) -> list[str]:
|
||||
"""Parse a comma-separated extension type list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : str
|
||||
Comma-separated extension types.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
The selected extension types.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If a type is not valid or none is given.
|
||||
"""
|
||||
types = [t.strip() for t in value.split(",") if t.strip()]
|
||||
invalid = [t for t in types if t not in VALID_EXTENSION_TYPES]
|
||||
if invalid:
|
||||
@@ -34,6 +49,18 @@ def _parse_extension_types(value: str) -> list[str]:
|
||||
|
||||
|
||||
def _prompt_context(preset_extension_types: list[str] | None = None) -> dict:
|
||||
"""Prompt for the template context.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
preset_extension_types : list[str] | None
|
||||
Extension types given on the command line, which skip that prompt.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The cookiecutter context.
|
||||
"""
|
||||
context = {}
|
||||
|
||||
context["full_name"] = Prompt.ask(" full_name", default="Hello World")
|
||||
@@ -75,9 +102,9 @@ def _prompt_context(preset_extension_types: list[str] | None = None) -> dict:
|
||||
context["router_name"] = "template"
|
||||
|
||||
if is_all or "obbject" in types or "on_command_output" in types:
|
||||
context["obbject_name"] = Prompt.ask(" obbject_name", default="template")
|
||||
context["obbject_name"] = Prompt.ask(" obbject_name", default="template_ext")
|
||||
else:
|
||||
context["obbject_name"] = "template"
|
||||
context["obbject_name"] = "template_ext"
|
||||
|
||||
return context
|
||||
|
||||
@@ -85,11 +112,15 @@ def _prompt_context(preset_extension_types: list[str] | None = None) -> dict:
|
||||
def main(argv: list | None = None) -> int:
|
||||
"""Run the OpenBB cookiecutter template.
|
||||
|
||||
Args:
|
||||
argv: Command line arguments (defaults to sys.argv[1:])
|
||||
Parameters
|
||||
----------
|
||||
argv : list | None
|
||||
Command line arguments, defaulting to ``sys.argv[1:]``.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, non-zero for error)
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The exit code, 0 on success.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate an OpenBB Platform extension from template"
|
||||
@@ -120,8 +151,9 @@ def main(argv: list | None = None) -> int:
|
||||
nargs="+",
|
||||
choices=VALID_EXTENSION_TYPES,
|
||||
default=None,
|
||||
help="Extension types to include (default: all). "
|
||||
"Choices: router, provider, obbject, on_command_output, charting, all",
|
||||
help="Extension types to include. Prompted when omitted, and 'all' with "
|
||||
"--no-input. Choices: router, provider, obbject, on_command_output, "
|
||||
"charting, all",
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
@@ -155,7 +187,7 @@ def main(argv: list | None = None) -> int:
|
||||
)
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr) # noqa
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"extension_types": "all",
|
||||
"provider_name": "template",
|
||||
"router_name": "template",
|
||||
"obbject_name": "template",
|
||||
"obbject_name": "template_ext",
|
||||
"_template": "{% now 'utc', '%Y%m%d%H%M%S' %}"
|
||||
}
|
||||
@@ -1,100 +1,115 @@
|
||||
"""OpenBB Platform Extension post-generation script."""
|
||||
"""OpenBB Platform extension post-generation script."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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 ""
|
||||
PROVIDER_NAME = "{{ cookiecutter.provider_name }}"
|
||||
ROUTER_NAME = "{{ cookiecutter.router_name }}"
|
||||
OBBJECT_NAME = "{{ cookiecutter.obbject_name }}"
|
||||
EXTENSION_TYPES_RAW = "{{ cookiecutter.extension_types }}"
|
||||
ALL_TYPES = {"router", "provider", "obbject", "on_command_output", "charting"}
|
||||
LOCAL_ARTIFACTS = (".ruff_cache", ".pytest_cache", ".ipynb_checkpoints", "__pycache__", ".DS_Store")
|
||||
|
||||
|
||||
def parse_extension_types(raw: str) -> set[str]:
|
||||
"""Return the selected extension types, expanding ``all``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw : str
|
||||
Comma-separated extension types.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set[str]
|
||||
The selected extension types.
|
||||
"""
|
||||
types = {t.strip().lower() for t in raw.split(",") if t.strip()}
|
||||
if "all" in types:
|
||||
return {"router", "provider", "obbject", "on_command_output", "charting"}
|
||||
return types
|
||||
return set(ALL_TYPES) if "all" in types else types
|
||||
|
||||
|
||||
def remove_path(path: str):
|
||||
if os.path.isdir(path):
|
||||
def remove_path(path: Path) -> None:
|
||||
"""Remove a generated file or directory if it exists.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : Path
|
||||
The file or directory to remove.
|
||||
"""
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
elif os.path.isfile(path):
|
||||
os.remove(path)
|
||||
elif path.exists():
|
||||
path.unlink()
|
||||
|
||||
|
||||
EXTENSION_TYPES = parse_extension_types(EXTENSION_TYPES_RAW)
|
||||
def require_module_name(name: str, label: str) -> None:
|
||||
"""Exit when a name is not a valid Python module name.
|
||||
|
||||
if not re.match(MODULE_REGEX, MODULE_NAME):
|
||||
print(f"ERROR: {MODULE_NAME} is not a valid Python package name.")
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name to check.
|
||||
label : str
|
||||
What the name is used for, shown in the error.
|
||||
"""
|
||||
if not re.match(MODULE_REGEX, name):
|
||||
print(f"ERROR: {label} '{name}' must be a lower snake_case Python name.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
types = parse_extension_types(EXTENSION_TYPES_RAW)
|
||||
has_router = "router" in types
|
||||
has_charting = "charting" in types
|
||||
has_provider = "provider" in types
|
||||
has_obbject = "obbject" in types
|
||||
has_on_command_output = "on_command_output" in types
|
||||
|
||||
require_module_name(MODULE_NAME, "package_name")
|
||||
if has_provider:
|
||||
require_module_name(PROVIDER_NAME, "provider_name")
|
||||
if has_router or has_charting:
|
||||
require_module_name(ROUTER_NAME, "router_name")
|
||||
if has_obbject or has_on_command_output:
|
||||
require_module_name(OBBJECT_NAME, "obbject_name")
|
||||
if has_provider and has_obbject and PROVIDER_NAME == OBBJECT_NAME:
|
||||
print(
|
||||
f"ERROR: provider_name and obbject_name are both '{PROVIDER_NAME}'; "
|
||||
"they share the credentials namespace, so use different names."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
has_router = "router" in EXTENSION_TYPES
|
||||
has_charting = "charting" in EXTENSION_TYPES
|
||||
has_provider = "provider" in EXTENSION_TYPES
|
||||
has_obbject = "obbject" in EXTENSION_TYPES
|
||||
has_on_command_output = "on_command_output" in EXTENSION_TYPES
|
||||
|
||||
if has_provider and PROVIDER_NAME and not re.match(MODULE_REGEX, PROVIDER_NAME):
|
||||
print(f"ERROR: {PROVIDER_NAME} should be in lower snakecase.")
|
||||
sys.exit(1)
|
||||
|
||||
if (
|
||||
(has_router or has_charting)
|
||||
and ROUTER_NAME
|
||||
and not re.match(MODULE_REGEX, ROUTER_NAME)
|
||||
):
|
||||
print(f"ERROR: {ROUTER_NAME} should be in lower snakecase.")
|
||||
sys.exit(1)
|
||||
|
||||
if (
|
||||
(has_obbject or has_on_command_output)
|
||||
and OBBJECT_NAME
|
||||
and not re.match(MODULE_REGEX, OBBJECT_NAME)
|
||||
):
|
||||
print(f"ERROR: {OBBJECT_NAME} should be in lower snakecase.")
|
||||
sys.exit(1)
|
||||
|
||||
routers_dir = os.path.join(MODULE_NAME, "routers")
|
||||
providers_dir = os.path.join(MODULE_NAME, "providers")
|
||||
obbject_dir = os.path.join(MODULE_NAME, "obbject")
|
||||
package = Path(MODULE_NAME)
|
||||
routers = package / "routers"
|
||||
obbject = package / "obbject"
|
||||
tests = Path("tests")
|
||||
|
||||
if not has_router:
|
||||
remove_path(os.path.join(routers_dir, ROUTER_NAME + ".py"))
|
||||
remove_path(os.path.join(routers_dir, "depends.py"))
|
||||
|
||||
remove_path(routers / f"{ROUTER_NAME}.py")
|
||||
remove_path(tests / "test_router.py")
|
||||
if not has_charting:
|
||||
remove_path(os.path.join(routers_dir, ROUTER_NAME + "_views.py"))
|
||||
|
||||
remove_path(routers / f"{ROUTER_NAME}_views.py")
|
||||
remove_path(tests / "test_views.py")
|
||||
if not has_router and not has_charting:
|
||||
remove_path(routers_dir)
|
||||
|
||||
remove_path(routers)
|
||||
if not has_provider:
|
||||
remove_path(providers_dir)
|
||||
|
||||
remove_path(package / "providers")
|
||||
remove_path(tests / "test_provider.py")
|
||||
if not has_obbject:
|
||||
remove_path(tests / "test_obbject.py")
|
||||
if not has_on_command_output:
|
||||
remove_path(obbject / OBBJECT_NAME / "on_command_output.py")
|
||||
remove_path(tests / "test_on_command_output.py")
|
||||
if not has_obbject and not has_on_command_output:
|
||||
remove_path(obbject_dir)
|
||||
remove_path(obbject)
|
||||
|
||||
if (has_obbject or has_on_command_output) and not has_router:
|
||||
try:
|
||||
from importlib.metadata import entry_points
|
||||
for artifact in LOCAL_ARTIFACTS:
|
||||
for path in sorted(Path().rglob(artifact), reverse=True):
|
||||
remove_path(path)
|
||||
|
||||
eps = entry_points()
|
||||
core_eps = (
|
||||
eps.select(group="openbb_core_extension")
|
||||
if hasattr(eps, "select")
|
||||
else eps.get("openbb_core_extension", [])
|
||||
)
|
||||
if not list(core_eps):
|
||||
print(
|
||||
"\n WARNING: No 'openbb_core_extension' entry points found in the environment."
|
||||
"\n The 'obbject' and 'on_command_output' extension types require at least"
|
||||
"\n one router extension to be installed in order to function.\n"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if ruff := shutil.which("ruff"):
|
||||
subprocess.run([ruff, "format", "--quiet", "--no-cache", "."], check=False)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[coverage:run]
|
||||
omit =
|
||||
env/*
|
||||
tests/*
|
||||
**/tests/**
|
||||
**/package/**
|
||||
source = .
|
||||
@@ -1,170 +1,15 @@
|
||||
|
||||
# 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
|
||||
.venv/
|
||||
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/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.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
|
||||
htmlcov/
|
||||
.ipynb_checkpoints/
|
||||
.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
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,41 +1,64 @@
|
||||
# OpenBB ODP Extensions Cookiecutter Template
|
||||
{%- set types = cookiecutter.extension_types.split(',') | map('trim') | list -%}
|
||||
{%- set is_all = 'all' in types -%}
|
||||
{%- set has_router = is_all or 'router' in types -%}
|
||||
{%- set has_provider = is_all or 'provider' in types -%}
|
||||
{%- set has_charting = is_all or 'charting' in types -%}
|
||||
{%- set has_obbject = is_all or 'obbject' in types -%}
|
||||
{%- set has_on_command_output = is_all or 'on_command_output' in types -%}
|
||||
# {{ cookiecutter.project_name }}
|
||||
|
||||
## Introduction
|
||||
An extension for the OpenBB Platform, generated with `openbb-cookiecutter`.
|
||||
|
||||
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
|
||||
## Contents
|
||||
{% if has_router %}
|
||||
- `{{ cookiecutter.package_name }}/routers/{{ cookiecutter.router_name }}.py`: commands under `obb.{{ cookiecutter.router_name }}`.
|
||||
{%- endif %}
|
||||
{%- if has_provider %}
|
||||
- `{{ cookiecutter.package_name }}/providers/{{ cookiecutter.provider_name }}/`: the `{{ cookiecutter.provider_name }}` provider, with an `Example` model and an `EquityHistorical` standard-model implementation.
|
||||
{%- endif %}
|
||||
{%- if has_charting %}
|
||||
- `{{ cookiecutter.package_name }}/routers/{{ cookiecutter.router_name }}_views.py`: charts for `{{ cookiecutter.router_name }}` commands, used when `openbb-charting` is installed.
|
||||
{%- endif %}
|
||||
{%- if has_obbject %}
|
||||
- `{{ cookiecutter.package_name }}/obbject/{{ cookiecutter.obbject_name }}/__init__.py`: the `to_string` and `{{ cookiecutter.obbject_name }}` accessors on every command result.
|
||||
{%- endif %}
|
||||
{%- if has_on_command_output %}
|
||||
- `{{ cookiecutter.package_name }}/obbject/{{ cookiecutter.obbject_name }}/on_command_output.py`: a plugin that runs after `/{{ cookiecutter.router_name }}/candles`.
|
||||
{%- endif %}
|
||||
{%- if has_charting and not has_router %}
|
||||
|
||||
With it you can:
|
||||
The views are named after `{{ cookiecutter.router_name }}` routes, so pair them with a router extension that uses that name.
|
||||
{%- endif %}
|
||||
|
||||
- Create a new extension
|
||||
- Build custom commands
|
||||
- Interact with the standardization framework
|
||||
- Build custom services and applications on top of the framework
|
||||
## Development
|
||||
|
||||
## Getting Started
|
||||
Create an environment with [uv](https://docs.astral.sh/uv/) and install the package in editable mode with its development dependencies:
|
||||
|
||||
We recommend you check out the files in the following order:
|
||||
{% set types = cookiecutter.extension_types.split(',') | map('trim') | list %}
|
||||
{% if 'router' in types or 'all' in types %}
|
||||
* `{{cookiecutter.package_name}}/routers/{{cookiecutter.router_name}}.py`
|
||||
{% endif %}
|
||||
{% if 'provider' in types or 'all' in types %}
|
||||
* `{{cookiecutter.package_name}}/providers/{{cookiecutter.provider_name}}/models/example.py`
|
||||
* `{{cookiecutter.package_name}}/providers/{{cookiecutter.provider_name}}/__init__.py`
|
||||
{% endif %}
|
||||
{% if 'obbject' in types or 'on_command_output' in types or 'all' in types %}
|
||||
* `{{cookiecutter.package_name}}/obbject/{{cookiecutter.obbject_name}}/__init__.py`
|
||||
{% endif %}
|
||||
{% if 'charting' in types or 'all' in types %}
|
||||
* `{{cookiecutter.package_name}}/routers/{{cookiecutter.router_name}}_views.py`
|
||||
{% endif %}
|
||||
{% if 'charting' in types and 'router' not in types and 'all' not in types %}
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
> **Note:** You selected charting without a router. The views file references `{{cookiecutter.router_name}}` naming conventions. You will need to pair this with an existing router extension that uses the same name.
|
||||
{% endif %}
|
||||
Build the `openbb` package so the new commands appear under `obb`:
|
||||
|
||||
Check out the developer [documentation](https://docs.openbb.co/python/developer) for more information on getting started making OpenBB extensions.
|
||||
```bash
|
||||
uv run openbb-build
|
||||
```
|
||||
{%- if has_on_command_output %}
|
||||
|
||||
---
|
||||
On-command-output plugins run on every matching result, so OpenBB loads them only when explicitly allowed:
|
||||
|
||||
🦋 Made with [openbb cookiecutter](https://github.com/openbb-finance/OpenBB/cookiecutter).
|
||||
```bash
|
||||
export OPENBB_ALLOW_ON_COMMAND_OUTPUT=true
|
||||
```
|
||||
{%- endif %}
|
||||
|
||||
Lint, type check, and test:
|
||||
|
||||
```bash
|
||||
uv run ruff format --check .
|
||||
uv run ruff check .
|
||||
uv run ty check {{ cookiecutter.package_name }}
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
Serve the commands over the REST API with `uv run openbb-api`.
|
||||
|
||||
@@ -1,43 +1,102 @@
|
||||
[tool.poetry]
|
||||
{%- set types = cookiecutter.extension_types.split(',') | map('trim') | list -%}
|
||||
{%- set is_all = 'all' in types -%}
|
||||
{%- set has_router = is_all or 'router' in types -%}
|
||||
{%- set has_provider = is_all or 'provider' in types -%}
|
||||
{%- set has_charting = is_all or 'charting' in types -%}
|
||||
{%- set has_obbject = is_all or 'obbject' in types -%}
|
||||
{%- set has_on_command_output = is_all or 'on_command_output' in types -%}
|
||||
{%- set views_class = cookiecutter.router_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
[project]
|
||||
name = "{{ cookiecutter.project_tag }}"
|
||||
version = "0.0.1"
|
||||
description = "{{ cookiecutter.project_name }}"
|
||||
authors = ["{{ cookiecutter.full_name }} <{{ cookiecutter.email }}>"]
|
||||
authors = [{ name = "{{ cookiecutter.full_name }}", email = "{{ cookiecutter.email }}" }]
|
||||
license = { text = "Apache-2.0" }
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
packages = [{ include = "{{ cookiecutter.package_name }}" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4"
|
||||
openbb-core = "*"
|
||||
openbb-platform-api = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
openbb-devtools = { version = "*" }
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
{% set types = cookiecutter.extension_types.split(',') | map('trim') | list %}
|
||||
{% if 'router' in types or 'all' in types %}
|
||||
[tool.poetry.plugins."openbb_core_extension"]
|
||||
requires-python = ">=3.10,<4"
|
||||
dependencies = ["openbb-core[pandas]>=2.0.0"]
|
||||
{% if has_charting %}
|
||||
[project.optional-dependencies]
|
||||
charting = ["openbb-charting>=3.0.0"]
|
||||
{% endif %}
|
||||
{%- if has_router %}
|
||||
[project.entry-points."openbb_core_extension"]
|
||||
{{ cookiecutter.router_name }} = "{{ cookiecutter.package_name }}.routers.{{ cookiecutter.router_name }}:router"
|
||||
{% endif %}
|
||||
{% if 'charting' in types or 'all' in types %}
|
||||
[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"
|
||||
{%- if has_charting %}
|
||||
[project.entry-points."openbb_charting_extension"]
|
||||
{{ cookiecutter.router_name }} = "{{ cookiecutter.package_name }}.routers.{{ cookiecutter.router_name }}_views:{{ views_class }}Views"
|
||||
{% endif %}
|
||||
{% if 'provider' in types or 'all' in types %}
|
||||
[tool.poetry.plugins."openbb_provider_extension"]
|
||||
{%- if has_provider %}
|
||||
[project.entry-points."openbb_provider_extension"]
|
||||
{{ cookiecutter.provider_name }} = "{{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}:{{ cookiecutter.provider_name }}_provider"
|
||||
{% endif %}
|
||||
{% if 'obbject' in types or 'on_command_output' in types or 'all' in types %}
|
||||
[tool.poetry.plugins."openbb_obbject_extension"]
|
||||
{% if 'obbject' in types or 'all' in types %}
|
||||
{%- if has_obbject or has_on_command_output %}
|
||||
[project.entry-points."openbb_obbject_extension"]
|
||||
{%- if has_obbject %}
|
||||
to_string = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:ext"
|
||||
{{ cookiecutter.obbject_name }} = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:class_ext"
|
||||
{%- endif %}
|
||||
{%- if has_on_command_output %}
|
||||
nonblocking_plugin = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}.on_command_output:nonblocking_plugin"
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
{% if 'on_command_output' in types or 'all' in types %}
|
||||
nonblocking_plugin = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:nonblocking_plugin"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"openbb-devtools>=2.0.1",
|
||||
"openbb-platform-api>=2.0.0",
|
||||
{%- if has_charting %}
|
||||
"openbb-charting>=3.0.0",
|
||||
{%- endif %}
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.25"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["{{ cookiecutter.package_name }}"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
fix = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["D", "E", "W", "F", "S", "UP", "I", "PLC", "PLE", "PLR", "PLW", "SIM", "T20"]
|
||||
ignore = [
|
||||
"PLR2004", "PLR0913", "PLR0915", "PLR0917", "PLC0415",
|
||||
"E402", "E501",
|
||||
"D202", "D203", "D205", "D206", "D210", "D212", "D213", "D214",
|
||||
"D215", "D300", "D400", "D404", "D405", "D406", "D407", "D408",
|
||||
"D409", "D410", "D411", "D413", "D415", "D416", "D417",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["S101", "D"]
|
||||
"**/__init__.py" = ["F401"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
combine-as-imports = true
|
||||
force-wrap-aliases = true
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "numpy"
|
||||
|
||||
[tool.ty]
|
||||
[tool.ty.environment]
|
||||
python-version = "3.10"
|
||||
|
||||
[tool.ty.src]
|
||||
include = ["{{ cookiecutter.package_name }}"]
|
||||
exclude = ["tests"]
|
||||
|
||||
[tool.ty.rules]
|
||||
unresolved-import = "warn"
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "@overload"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q --strict-markers"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[pytest]
|
||||
addopts = -p no:warnings
|
||||
markers =
|
||||
linux: tests that are not stable on Windows
|
||||
integration: OpenBB Platform integration test marker
|
||||
testpaths =
|
||||
tests
|
||||
@@ -1,48 +0,0 @@
|
||||
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,0 +1 @@
|
||||
"""{{ cookiecutter.project_name }} tests."""
|
||||
@@ -1,42 +1,5 @@
|
||||
"""Root configuration for pytest."""
|
||||
|
||||
# flake8: noqa: S101
|
||||
# pylint: disable=unused-argument,unused-import
|
||||
"""Shared test configuration."""
|
||||
|
||||
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
|
||||
os.environ.setdefault("OPENBB_ALLOW_ON_COMMAND_OUTPUT", "true")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Tests for the {{ cookiecutter.obbject_name }} OBBject extensions."""
|
||||
|
||||
import json
|
||||
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
import {{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }} # noqa: F401
|
||||
|
||||
|
||||
class TestToString:
|
||||
"""``obbject.to_string`` serializes the results."""
|
||||
|
||||
def test_value(self):
|
||||
obbject = OBBject(results=[{"a": 1}])
|
||||
assert json.loads(obbject.to_string) == {"results": [{"a": 1}]}
|
||||
|
||||
|
||||
class TestNamespace:
|
||||
"""``obbject.{{ cookiecutter.obbject_name }}`` exposes the extension methods."""
|
||||
|
||||
def test_hello_world(self):
|
||||
obbject = OBBject(results=[{"a": 1}], provider="demo")
|
||||
assert obbject.{{ cookiecutter.obbject_name }}.hello_world() == (
|
||||
"Hello from the OBBject returned by demo!"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Tests for the on-command-output plugin."""
|
||||
|
||||
import logging
|
||||
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
from {{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }} import on_command_output
|
||||
|
||||
|
||||
class TestNonblockingPlugin:
|
||||
"""The plugin processes the output on a background thread."""
|
||||
|
||||
def test_configuration(self):
|
||||
plugin = on_command_output.nonblocking_plugin
|
||||
assert plugin.on_command_output is True
|
||||
assert plugin.command_output_paths == ["/{{ cookiecutter.router_name }}/candles"]
|
||||
|
||||
def test_processes_in_background(self, caplog):
|
||||
caplog.set_level(logging.INFO, logger=on_command_output.__name__)
|
||||
obbject = OBBject(results=[{"a": 1}, {"a": 2}], provider="demo")
|
||||
worker = obbject.nonblocking_plugin
|
||||
worker.join(timeout=5)
|
||||
assert not worker.is_alive()
|
||||
assert "Processed 2 rows from demo." in caplog.text
|
||||
@@ -0,0 +1,72 @@
|
||||
{%- set provider_class = cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
"""Tests for the {{ cookiecutter.provider_name }} provider."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from {{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }} import (
|
||||
{{ cookiecutter.provider_name }}_provider,
|
||||
)
|
||||
from {{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}.models.equity_historical import (
|
||||
{{ provider_class }}EquityHistoricalFetcher,
|
||||
)
|
||||
from {{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}.models.example import (
|
||||
ExampleFetcher,
|
||||
)
|
||||
|
||||
|
||||
class TestProvider:
|
||||
"""The provider registers every fetcher."""
|
||||
|
||||
def test_fetcher_dict(self):
|
||||
assert {{ cookiecutter.provider_name }}_provider.fetcher_dict == {
|
||||
"EquityHistorical": {{ provider_class }}EquityHistoricalFetcher,
|
||||
"Example": ExampleFetcher,
|
||||
}
|
||||
|
||||
|
||||
class TestExampleFetcher:
|
||||
"""The example fetcher maps the raw columns onto the data model."""
|
||||
|
||||
def test_fetcher(self):
|
||||
ExampleFetcher.test({"symbol": "AAPL"})
|
||||
|
||||
def test_rows(self):
|
||||
rows = asyncio.run(ExampleFetcher.fetch_data({"symbol": "AAPL"}))
|
||||
assert [row.model_dump() for row in rows] == [
|
||||
{
|
||||
"symbol": "AAPL",
|
||||
"date": "2023-08-23",
|
||||
"open": 2.0,
|
||||
"high": 5.0,
|
||||
"low": 1.0,
|
||||
"close": 4.0,
|
||||
"volume": 5.0,
|
||||
},
|
||||
{
|
||||
"symbol": "AAPL",
|
||||
"date": "2023-08-24",
|
||||
"open": 4.0,
|
||||
"high": 7.0,
|
||||
"low": 3.0,
|
||||
"close": 6.0,
|
||||
"volume": 10.0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestEquityHistoricalFetcher:
|
||||
"""The standard-model fetcher fills the custom field."""
|
||||
|
||||
def test_fetcher(self):
|
||||
{{ provider_class }}EquityHistoricalFetcher.test({"symbol": "AAPL"})
|
||||
|
||||
def test_custom_field(self):
|
||||
rows = asyncio.run(
|
||||
{{ provider_class }}EquityHistoricalFetcher.fetch_data(
|
||||
{"symbol": "AAPL", "custom_param": "x"}
|
||||
)
|
||||
)
|
||||
assert [row.custom_field for row in rows] == [
|
||||
"x",
|
||||
"Data validator replaced None.",
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
{%- set types = cookiecutter.extension_types.split(',') | map('trim') | list -%}
|
||||
{%- set has_provider = 'provider' in types or 'all' in types -%}
|
||||
"""Tests for the {{ cookiecutter.router_name }} router."""
|
||||
|
||||
import asyncio
|
||||
{%- if has_provider %}
|
||||
from unittest.mock import AsyncMock, patch
|
||||
{%- endif %}
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
|
||||
from {{ cookiecutter.package_name }}.routers import {{ cookiecutter.router_name }} as module
|
||||
|
||||
|
||||
class TestGetExample:
|
||||
"""``get_example`` returns the symbol details from the response."""
|
||||
|
||||
def test_rejects_non_object_response(self, monkeypatch):
|
||||
async def fake_request(url):
|
||||
return [{"details": {}}]
|
||||
|
||||
monkeypatch.setattr(module, "amake_request", fake_request)
|
||||
with pytest.raises(OpenBBError):
|
||||
asyncio.run(module.get_example(symbol="AAPL"))
|
||||
|
||||
def test_results(self, monkeypatch):
|
||||
requested: list[str] = []
|
||||
|
||||
async def fake_request(url):
|
||||
requested.append(url)
|
||||
return {"details": {"symbol": "AAPL"}}
|
||||
|
||||
monkeypatch.setattr(module, "amake_request", fake_request)
|
||||
result = asyncio.run(module.get_example(symbol="AAPL"))
|
||||
assert result.results == {"symbol": "AAPL"}
|
||||
assert requested == [f"{module.SYMBOL_INFO_URL}?symbol=AAPL"]
|
||||
|
||||
|
||||
class TestPostExample:
|
||||
"""``post_example`` measures the quote."""
|
||||
|
||||
def test_results(self):
|
||||
result = asyncio.run(
|
||||
module.post_example(module.QuoteQueryParams(bid=1.0, ask=3.0, flag=True))
|
||||
)
|
||||
assert result.results == {"mid": 2.0, "spread": 2.0, "flag": True}
|
||||
{%- if has_provider %}
|
||||
|
||||
|
||||
class TestModelCommands:
|
||||
"""Model commands delegate to ``OBBject.from_query``."""
|
||||
|
||||
@pytest.mark.parametrize("command", [module.model_example, module.candles])
|
||||
def test_delegates_to_query(self, command):
|
||||
sentinel = object()
|
||||
with (
|
||||
patch.object(module, "OBBject") as obbject,
|
||||
patch.object(module, "Query") as query,
|
||||
):
|
||||
obbject.from_query = AsyncMock(return_value=sentinel)
|
||||
result = asyncio.run(
|
||||
command(
|
||||
cc=None,
|
||||
provider_choices=None,
|
||||
standard_params=None,
|
||||
extra_params=None,
|
||||
)
|
||||
)
|
||||
assert result is sentinel
|
||||
query.assert_called_once()
|
||||
{%- endif %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{%- set views_class = cookiecutter.router_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
"""Tests for the {{ cookiecutter.router_name }} charting views."""
|
||||
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("openbb_charting")
|
||||
|
||||
from {{ cookiecutter.package_name }}.routers.{{ cookiecutter.router_name }}_views import (
|
||||
{{ views_class }}Views,
|
||||
)
|
||||
|
||||
|
||||
class TestCandlesView:
|
||||
"""The candles view charts the daily highs."""
|
||||
|
||||
def test_figure(self):
|
||||
rows = [
|
||||
SimpleNamespace(date=date(2023, 8, 23), high=5.0),
|
||||
SimpleNamespace(date=date(2023, 8, 24), high=7.0),
|
||||
]
|
||||
fig, content = {{ views_class }}Views.{{ cookiecutter.router_name }}_candles(obbject_item=rows)
|
||||
assert list(fig.data[0].y) == [5.0, 7.0]
|
||||
assert content["data"][0]["type"] == "bar"
|
||||
@@ -1 +1 @@
|
||||
"""OBBject Extensions module."""
|
||||
"""OBBject extensions."""
|
||||
|
||||
@@ -1,86 +1,63 @@
|
||||
"""{{ cookiecutter.package_name }} OBBject Extension - {{ cookiecutter.obbject_name }}"""
|
||||
{% set types = cookiecutter.extension_types.split(',') | map('trim') | list %}
|
||||
{% set has_obbject = 'obbject' in types or 'all' in types %}
|
||||
{% set has_on_command_output = 'on_command_output' in types or 'all' in types %}
|
||||
# pylint: disable=W0613,R0903
|
||||
{% if has_on_command_output %}
|
||||
|
||||
import threading
|
||||
import time
|
||||
{% endif %}
|
||||
{%- set types = cookiecutter.extension_types.split(',') | map('trim') | list -%}
|
||||
{%- set has_obbject = 'obbject' in types or 'all' in types -%}
|
||||
"""{{ cookiecutter.obbject_name }} OBBject extensions."""
|
||||
{%- if has_obbject %}
|
||||
|
||||
from openbb_core.app.model.extension import Extension
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
{% if has_obbject %}
|
||||
|
||||
ext = Extension(
|
||||
name="to_string",
|
||||
description="An OBBject extension that converts the results to a string representation.",
|
||||
description="Serialize the results of an OBBject to a JSON string.",
|
||||
)
|
||||
|
||||
|
||||
@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")
|
||||
def to_string(obbject: OBBject) -> str:
|
||||
"""Return the results as a JSON string, read as ``obbject.to_string``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obbject : OBBject
|
||||
The command output.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The results serialized as JSON.
|
||||
"""
|
||||
return obbject.model_dump_json(
|
||||
exclude_none=True, exclude_unset=True, include={"results"}
|
||||
)
|
||||
|
||||
|
||||
class_ext = Extension(
|
||||
name="{{ cookiecutter.obbject_name }}",
|
||||
description="An OBBject extension with namespace."
|
||||
description="A namespace of methods on every OBBject.",
|
||||
)
|
||||
|
||||
|
||||
@class_ext.obbject_accessor
|
||||
class OBBjectExtension:
|
||||
"""OBBject Extension Template."""
|
||||
"""Methods available as ``obbject.{{ cookiecutter.obbject_name }}``."""
|
||||
|
||||
def __init__(self, obbject: OBBject):
|
||||
"""Initialize the extension."""
|
||||
"""Bind the extension to a command output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obbject : OBBject
|
||||
The command output.
|
||||
"""
|
||||
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
|
||||
{% endif %}
|
||||
{% if has_on_command_output %}
|
||||
def hello_world(self) -> str:
|
||||
"""Return a greeting that names the command's provider.
|
||||
|
||||
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,
|
||||
command_output_paths=["/{{cookiecutter.router_name}}/candles"],
|
||||
immutable=True,
|
||||
results_only=False,
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
"""Simulated on_command_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()
|
||||
{% endif %}
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The greeting.
|
||||
"""
|
||||
return f"Hello from the OBBject returned by {self._obbject.provider}!"
|
||||
{%- endif %}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""On-command-output plugin that processes results in a background thread."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from openbb_core.app.model.extension import Extension
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
nonblocking_plugin = Extension(
|
||||
name="nonblocking_plugin",
|
||||
description="Hand each /{{ cookiecutter.router_name }}/candles result to a background thread.",
|
||||
on_command_output=True,
|
||||
command_output_paths=["/{{ cookiecutter.router_name }}/candles"],
|
||||
immutable=True,
|
||||
results_only=False,
|
||||
)
|
||||
|
||||
|
||||
def process_in_background(serialized_obbject: dict) -> None:
|
||||
"""Rebuild the command output and process it off the request path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
serialized_obbject : dict
|
||||
The command output dumped with ``model_dump``.
|
||||
"""
|
||||
obbject = OBBject(**serialized_obbject)
|
||||
logger.info(
|
||||
"Processed %s rows from %s.", len(obbject.results or []), obbject.provider
|
||||
)
|
||||
|
||||
|
||||
@nonblocking_plugin.obbject_accessor
|
||||
def start_background_processing(obbject: OBBject) -> threading.Thread:
|
||||
"""Start processing the command output without blocking the response.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obbject : OBBject
|
||||
The command output.
|
||||
|
||||
Returns
|
||||
-------
|
||||
threading.Thread
|
||||
The started worker thread.
|
||||
"""
|
||||
worker = threading.Thread(
|
||||
target=process_in_background,
|
||||
args=(obbject.model_dump(),),
|
||||
name="nonblocking-plugin",
|
||||
daemon=True,
|
||||
)
|
||||
worker.start()
|
||||
return worker
|
||||
@@ -1 +1 @@
|
||||
"""Providers Module"""
|
||||
"""Providers module."""
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
"""{{ cookiecutter.package_name }} OpenBB Platform Provider."""
|
||||
{%- set provider_class = cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
"""{{ cookiecutter.project_name }} 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,
|
||||
}
|
||||
from {{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}.models.equity_historical import (
|
||||
{{ provider_class }}EquityHistoricalFetcher,
|
||||
)
|
||||
from {{ cookiecutter.package_name }}.providers.{{ cookiecutter.provider_name }}.models.example import (
|
||||
ExampleFetcher,
|
||||
)
|
||||
|
||||
{{ cookiecutter.provider_name }}_provider = Provider(
|
||||
name="{{ cookiecutter.provider_name }}",
|
||||
description="Data provider for {{ cookiecutter.project_name }}.",
|
||||
website="https://{{ cookiecutter.project_tag }}.com",
|
||||
fetcher_dict={
|
||||
"EquityHistorical": {{ provider_class }}EquityHistoricalFetcher,
|
||||
"Example": ExampleFetcher,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""{{ cookiecutter.provider_name }} models."""
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{%- set provider_class = cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
"""{{ provider_class }} Equity Historical model built on the standard model."""
|
||||
|
||||
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 {{ provider_class }}EquityHistoricalQueryParams(EquityHistoricalQueryParams):
|
||||
"""{{ provider_class }} Equity Historical query parameters."""
|
||||
|
||||
custom_param: str | None = Field(
|
||||
default=None, description="An optional provider-specific parameter."
|
||||
)
|
||||
|
||||
|
||||
class {{ provider_class }}EquityHistoricalData(EquityHistoricalData):
|
||||
"""{{ provider_class }} Equity Historical data."""
|
||||
|
||||
__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="An optional provider-specific field."
|
||||
)
|
||||
|
||||
@field_validator("custom_field", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _default_custom_field(cls, v: str | None) -> str:
|
||||
"""Replace a missing custom field with a default value."""
|
||||
return v or "Data validator replaced None."
|
||||
|
||||
|
||||
class {{ provider_class }}EquityHistoricalFetcher(
|
||||
Fetcher[
|
||||
{{ provider_class }}EquityHistoricalQueryParams,
|
||||
list[{{ provider_class }}EquityHistoricalData],
|
||||
]
|
||||
):
|
||||
"""{{ provider_class }} Equity Historical fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(
|
||||
params: dict[str, Any],
|
||||
) -> {{ provider_class }}EquityHistoricalQueryParams:
|
||||
"""Transform the query parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : dict[str, Any]
|
||||
The raw query parameters.
|
||||
|
||||
Returns
|
||||
-------
|
||||
{{ provider_class }}EquityHistoricalQueryParams
|
||||
The validated query.
|
||||
"""
|
||||
return {{ provider_class }}EquityHistoricalQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: {{ provider_class }}EquityHistoricalQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Return the raw rows for the query.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
query : {{ provider_class }}EquityHistoricalQueryParams
|
||||
The validated query.
|
||||
credentials : dict[str, str] | None
|
||||
The provider credentials.
|
||||
**kwargs : Any
|
||||
Additional keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
The raw rows.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"d": "2023-08-23",
|
||||
"o": 2,
|
||||
"h": 5,
|
||||
"l": 1,
|
||||
"c": 4,
|
||||
"v": 5,
|
||||
"f": query.custom_param,
|
||||
},
|
||||
{
|
||||
"d": "2023-08-24",
|
||||
"o": 4,
|
||||
"h": 7,
|
||||
"l": 3,
|
||||
"c": 6,
|
||||
"v": 10,
|
||||
"f": None,
|
||||
},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: {{ provider_class }}EquityHistoricalQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> list[{{ provider_class }}EquityHistoricalData]:
|
||||
"""Validate the raw rows into the data model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
query : {{ provider_class }}EquityHistoricalQueryParams
|
||||
The validated query.
|
||||
data : list[dict]
|
||||
The raw rows.
|
||||
**kwargs : Any
|
||||
Additional keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[{{ provider_class }}EquityHistoricalData]
|
||||
The validated rows.
|
||||
"""
|
||||
return [{{ provider_class }}EquityHistoricalData.model_validate(row) for row in data]
|
||||
@@ -1,22 +1,6 @@
|
||||
"""Example Data Integration.
|
||||
"""Example model with custom query parameters and data."""
|
||||
|
||||
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 typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
@@ -25,97 +9,114 @@ 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.
|
||||
"""
|
||||
"""Example query parameters."""
|
||||
|
||||
symbol: str = Field(description="Symbol to query.")
|
||||
|
||||
|
||||
class ExampleData(Data):
|
||||
"""Sample provider data.
|
||||
"""Example 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.
|
||||
"""
|
||||
__alias_dict__ = {
|
||||
"date": "d",
|
||||
"open": "o",
|
||||
"high": "h",
|
||||
"low": "l",
|
||||
"close": "c",
|
||||
"volume": "v",
|
||||
}
|
||||
|
||||
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")
|
||||
symbol: str = Field(description="Symbol of the row.")
|
||||
date: str = Field(description="Date of the row.")
|
||||
open: float = Field(description="Open price.")
|
||||
high: float = Field(description="High price.")
|
||||
low: float = Field(description="Low price.")
|
||||
close: float = Field(description="Close price.")
|
||||
volume: float = Field(description="Volume.")
|
||||
|
||||
|
||||
class ExampleFetcher(
|
||||
Fetcher[
|
||||
ExampleQueryParams,
|
||||
list[ExampleData],
|
||||
]
|
||||
):
|
||||
"""Example Fetcher class.
|
||||
|
||||
This class is responsible for the actual data retrieval.
|
||||
"""
|
||||
class ExampleFetcher(Fetcher[ExampleQueryParams, list[ExampleData]]):
|
||||
"""Example fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> ExampleQueryParams:
|
||||
"""Define example transform_query.
|
||||
"""Transform the query parameters.
|
||||
|
||||
Here we can pre-process the query parameters and add any extra parameters that
|
||||
will be used inside the extract_data method.
|
||||
Parameters
|
||||
----------
|
||||
params : dict[str, Any]
|
||||
The raw query parameters.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ExampleQueryParams
|
||||
The validated query.
|
||||
"""
|
||||
return ExampleQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
def extract_data(
|
||||
async def aextract_data(
|
||||
query: ExampleQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Define example extract_data.
|
||||
"""Return the raw rows for the query.
|
||||
|
||||
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.
|
||||
Parameters
|
||||
----------
|
||||
query : ExampleQueryParams
|
||||
The validated query.
|
||||
credentials : dict[str, str] | None
|
||||
The provider credentials.
|
||||
**kwargs : Any
|
||||
Additional keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
The raw rows.
|
||||
"""
|
||||
api_key = (
|
||||
credentials.get("{{cookiecutter.package_name}}_api_key")
|
||||
if credentials
|
||||
else ""
|
||||
)
|
||||
|
||||
# Here we mock an example_response for brevity.
|
||||
example_response = [
|
||||
return [
|
||||
{
|
||||
"symbol": query.symbol,
|
||||
"d": "2023-08-23",
|
||||
"o": 2,
|
||||
"h": 5,
|
||||
"l": 1,
|
||||
"c": 4,
|
||||
"v": 5,
|
||||
"d": "August 23, 2023",
|
||||
},
|
||||
{
|
||||
"symbol": query.symbol,
|
||||
"d": "2023-08-24",
|
||||
"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
|
||||
query: ExampleQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> list[ExampleData]:
|
||||
"""Define example transform_data.
|
||||
"""Validate the raw rows into the data model.
|
||||
|
||||
Right now, we're converting the data to fit our desired format.
|
||||
You can apply other transformations to it here.
|
||||
Parameters
|
||||
----------
|
||||
query : ExampleQueryParams
|
||||
The validated query.
|
||||
data : list[dict]
|
||||
The raw rows.
|
||||
**kwargs : Any
|
||||
Additional keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[ExampleData]
|
||||
The validated rows.
|
||||
"""
|
||||
return [ExampleData(**d) for d in data]
|
||||
return [ExampleData.model_validate(row) for row in data]
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"""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('_', ' ').title().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('_', ' ').title().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('_', ' ').title().replace(' ', '')}}EquityHistoricalFetcher(
|
||||
Fetcher[
|
||||
{{cookiecutter.provider_name.replace('_', ' ').title().replace(' ', '')}}EquityHistoricalQueryParams,
|
||||
list[{{cookiecutter.provider_name.replace('_', ' ').title().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('_', ' ').title().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('_', ' ').title().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('_', ' ').title().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('_', ' ').title().replace(' ', '')}}EquityHistoricalQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any
|
||||
) -> list[{{cookiecutter.provider_name.replace('_', ' ').title().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('_', ' ').title().replace(' ', '')}}EquityHistoricalData.model_validate(d) for d in data]
|
||||
@@ -1 +1 @@
|
||||
"""{{ cookiecutter.provider_name}} utilities module."""
|
||||
"""{{ cookiecutter.provider_name }} utilities."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""{{ cookiecutter.provider_name}} helper functions."""
|
||||
"""{{ cookiecutter.provider_name }} helper functions."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""{{ cookiecutter.project_name}} routers module."""
|
||||
"""{{ cookiecutter.project_name }} routers."""
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""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)]
|
||||
@@ -1,80 +1,93 @@
|
||||
"""{{cookiecutter.router_name}} router command example."""
|
||||
{% set types = cookiecutter.extension_types.split(',') | map('trim') | list %}
|
||||
{% set has_provider = 'provider' in types or 'all' in types %}
|
||||
# pylint: disable=unused-argument
|
||||
{% if has_provider %}
|
||||
{%- set types = cookiecutter.extension_types.split(',') | map('trim') | list -%}
|
||||
{%- set has_provider = 'provider' in types or 'all' in types -%}
|
||||
"""{{ cookiecutter.router_name }} router."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
{% if has_provider -%}
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
{% endif %}
|
||||
from openbb_core.app.model.example import APIEx, PythonEx
|
||||
{% endif -%}
|
||||
from openbb_core.app.model.example import APIEx
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
{% if has_provider %}
|
||||
from openbb_core.app.provider_interface import ExtraParams, ProviderChoices, StandardParams
|
||||
{% if has_provider -%}
|
||||
from openbb_core.app.provider_interface import (
|
||||
ExtraParams,
|
||||
ProviderChoices,
|
||||
StandardParams,
|
||||
)
|
||||
from openbb_core.app.query import Query
|
||||
{% endif %}
|
||||
{% endif -%}
|
||||
from openbb_core.app.router import Router
|
||||
from pydantic import BaseModel
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.helpers import amake_request
|
||||
from pydantic import Field
|
||||
|
||||
from {{cookiecutter.package_name}}.routers.depends import Session
|
||||
router = Router(prefix="", description="{{ cookiecutter.project_name }} commands.")
|
||||
|
||||
router = Router(prefix="")
|
||||
SYMBOL_INFO_URL = "https://www.cboe.com/education/tools/trade-optimizer/symbol-info"
|
||||
|
||||
|
||||
class QuoteQueryParams(QueryParams):
|
||||
"""Bid and ask prices to measure."""
|
||||
|
||||
bid: float = Field(description="Bid price.")
|
||||
ask: float = Field(description="Ask price.")
|
||||
flag: bool = Field(default=False, description="A flag echoed in the results.")
|
||||
|
||||
|
||||
@router.command(
|
||||
methods=["GET"],
|
||||
examples=[
|
||||
PythonEx(
|
||||
description="Here is an example for using this endpoint.",
|
||||
code=[
|
||||
"obb.{{ cookiecutter.router_name }}.get_example(symbol='AAPL')",
|
||||
]
|
||||
)
|
||||
]
|
||||
examples=[APIEx(parameters={"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"])
|
||||
async def get_example(symbol: str = "AAPL") -> OBBject[dict[str, Any]]:
|
||||
"""Get the Cboe symbol details for a ticker."""
|
||||
response = await amake_request(f"{SYMBOL_INFO_URL}?symbol={symbol}")
|
||||
if not isinstance(response, dict):
|
||||
raise OpenBBError(f"Unexpected response for {symbol}: {response}")
|
||||
return OBBject(results=response["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})
|
||||
{% if has_provider %}
|
||||
@router.command(
|
||||
methods=["POST"],
|
||||
examples=[APIEx(parameters={"bid": 1.0, "ask": 3.0})],
|
||||
)
|
||||
async def post_example(params: QuoteQueryParams) -> OBBject[dict[str, Any]]:
|
||||
"""Calculate the mid price and spread of a quote."""
|
||||
return OBBject(
|
||||
results={
|
||||
"mid": (params.bid + params.ask) / 2,
|
||||
"spread": params.ask - params.bid,
|
||||
"flag": params.flag,
|
||||
}
|
||||
)
|
||||
{%- if has_provider %}
|
||||
|
||||
|
||||
@router.command(model="Example")
|
||||
@router.command(
|
||||
model="Example",
|
||||
examples=[APIEx(parameters={"symbol": "AAPL", "provider": "{{ cookiecutter.provider_name }}"})],
|
||||
)
|
||||
async def model_example(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject[BaseModel]:
|
||||
"""Example Data."""
|
||||
) -> OBBject:
|
||||
"""Get example OHLCV data from a provider."""
|
||||
return await OBBject.from_query(Query(**locals()))
|
||||
|
||||
|
||||
@router.command(model="EquityHistorical")
|
||||
@router.command(
|
||||
model="EquityHistorical",
|
||||
examples=[APIEx(parameters={"symbol": "AAPL", "provider": "{{ cookiecutter.provider_name }}"})],
|
||||
)
|
||||
async def candles(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Example Data."""
|
||||
"""Get historical equity prices from a provider."""
|
||||
return await OBBject.from_query(Query(**locals()))
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
"""Views for the {{ cookiecutter.router_name }} Extension."""
|
||||
|
||||
# flake8: noqa: PLR0912
|
||||
# pylint: disable=import-outside-toplevel,too-few-public-methods
|
||||
{%- set views_class = cookiecutter.router_name.replace('_', ' ').title().replace(' ', '') -%}
|
||||
"""Charting views for the {{ cookiecutter.router_name }} router."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure # `openbb-charting` is Optional for the user.
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||
|
||||
|
||||
# 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."""
|
||||
class {{ views_class }}Views:
|
||||
"""Charts for {{ cookiecutter.router_name }} commands, one static method per route."""
|
||||
|
||||
@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.
|
||||
def {{ cookiecutter.router_name }}_candles(
|
||||
**kwargs: Any,
|
||||
) -> tuple["OpenBBFigure", dict[str, Any]]:
|
||||
"""Chart the daily high prices returned by ``/{{ cookiecutter.router_name }}/candles``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : Any
|
||||
The charting context, with the command's rows in ``obbject_item``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[OpenBBFigure, dict[str, Any]]
|
||||
The figure and its JSON-serializable content.
|
||||
"""
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||
|
||||
data = kwargs["obbject_item"] # This is where the data will always be.
|
||||
|
||||
print(data)
|
||||
|
||||
rows = kwargs["obbject_item"]
|
||||
fig = OpenBBFigure()
|
||||
|
||||
fig.add_bar(x=[d.date for d in data], y=[d.high for d in data])
|
||||
fig.add_bar(x=[row.date for row in rows], y=[row.high for row in rows])
|
||||
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
|
||||
|
||||
633
cookiecutter/poetry.lock
generated
vendored
633
cookiecutter/poetry.lock
generated
vendored
@@ -1,633 +0,0 @@
|
||||
# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "arrow"
|
||||
version = "1.4.0"
|
||||
description = "Better dates & times for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"},
|
||||
{file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.7.0"
|
||||
tzdata = {version = "*", markers = "python_version >= \"3.9\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"]
|
||||
test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2025.2)", "simplejson (==3.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "binaryornot"
|
||||
version = "0.6.0"
|
||||
description = "Ultra-lightweight pure Python package to check if a file is binary or text."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "binaryornot-0.6.0-py3-none-any.whl", hash = "sha256:900adfd5e1b821255ba7e63139b0396b14c88b9286e74e03b6f51e0200331337"},
|
||||
{file = "binaryornot-0.6.0.tar.gz", hash = "sha256:cc8d57cfa71d74ff8c28a7726734d53a851d02fad9e3a5581fb807f989f702f0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.5.20"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"},
|
||||
{file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"},
|
||||
{file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
|
||||
{file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.1"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"},
|
||||
{file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookiecutter"
|
||||
version = "2.7.1"
|
||||
description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cookiecutter-2.7.1-py3-none-any.whl", hash = "sha256:cee50defc1eaa7ad0071ee9b9893b746c1b3201b66bf4d3686d0f127c8ed6cf9"},
|
||||
{file = "cookiecutter-2.7.1.tar.gz", hash = "sha256:ca7bb7bc8c6ff441fbf53921b5537668000e38d56e28d763a1b73975c66c6138"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
arrow = "*"
|
||||
binaryornot = ">=0.4.4"
|
||||
click = ">=7.0,<9.0.0"
|
||||
Jinja2 = ">=2.7,<4.0.0"
|
||||
python-slugify = ">=4.0.0"
|
||||
pyyaml = ">=5.3.1"
|
||||
requests = ">=2.23.0"
|
||||
rich = "*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.17"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c"},
|
||||
{file = "idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
||||
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"},
|
||||
{file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mdurl = ">=0.1,<1.0"
|
||||
|
||||
[package.extras]
|
||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
|
||||
linkify = ["linkify-it-py (>=1,<3)"]
|
||||
plugins = ["mdit-py-plugins (>=0.5.0)"]
|
||||
profiling = ["gprof2dot"]
|
||||
rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
|
||||
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
|
||||
{file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
|
||||
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
description = "Markdown URL utilities"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
||||
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-slugify"
|
||||
version = "8.0.4"
|
||||
description = "A Python slugify application that also handles Unicode"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"},
|
||||
{file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
text-unidecode = ">=1.3"
|
||||
|
||||
[package.extras]
|
||||
unidecode = ["Unidecode (>=1.1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
|
||||
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2023.5.7"
|
||||
charset_normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.26,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.9.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
|
||||
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
markdown-it-py = ">=2.2.0"
|
||||
pygments = ">=2.13.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-unidecode"
|
||||
version = "1.3"
|
||||
description = "The most basic Text::Unidecode port"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
|
||||
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.2"
|
||||
description = "Provider of IANA time zone data"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
|
||||
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
|
||||
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4"
|
||||
content-hash = "91682122cf39ed4225e7166c2fdcf2f47ab587d2084d2f0ce4fa89dfb824acdf"
|
||||
92
cookiecutter/pyproject.toml
vendored
92
cookiecutter/pyproject.toml
vendored
@@ -1,24 +1,88 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "openbb-cookiecutter"
|
||||
version = "0.6.0"
|
||||
description = "Extensions template for the OpenBB Python Package."
|
||||
license = "Apache-2.0"
|
||||
authors = ["OpenBB Team <hello@openbb.co>"]
|
||||
packages = [{ include = "openbb_cookiecutter" }]
|
||||
authors = [{ name = "OpenBB Team", email = "hello@openbb.co" }]
|
||||
license = { text = "Apache-2.0" }
|
||||
readme = "README.md"
|
||||
homepage = "https://openbb.co"
|
||||
repository = "https://github.com/OpenBB-finance/openbb-cookiecutter"
|
||||
requires-python = ">=3.10,<4"
|
||||
dependencies = [
|
||||
"cookiecutter>=2.6.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4"
|
||||
cookiecutter = "^2.6.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
[project.scripts]
|
||||
openbb-cookiecutter = "openbb_cookiecutter.cli:main"
|
||||
|
||||
[tool.poetry.plugins."cookiecutter.templates"]
|
||||
[project.entry-points."cookiecutter.templates"]
|
||||
openbb = "openbb_cookiecutter"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://openbb.co"
|
||||
Repository = "https://github.com/OpenBB-finance/OpenBB"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["openbb-devtools>=2.0.1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling>=1.25"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build]
|
||||
exclude = [
|
||||
"**/__pycache__",
|
||||
"**/*.pyc",
|
||||
"**/.ruff_cache",
|
||||
"**/.pytest_cache",
|
||||
"**/.ipynb_checkpoints",
|
||||
"**/.DS_Store",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["openbb_cookiecutter"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["openbb_cookiecutter", "tests", "README.md", "pyproject.toml"]
|
||||
|
||||
[tool.uv.sources]
|
||||
openbb-devtools = { path = "../openbb_platform/extensions/devtools", editable = true }
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
fix = true
|
||||
extend-exclude = ["openbb_cookiecutter/template"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["D", "E", "W", "F", "S", "UP", "I", "PLC", "PLE", "PLR", "PLW", "SIM", "T20"]
|
||||
ignore = [
|
||||
"PLR2004", "PLR0913", "PLR0915", "PLR0917", "PLC0415",
|
||||
"E402", "E501",
|
||||
"D202", "D203", "D205", "D206", "D210", "D212", "D213", "D214",
|
||||
"D215", "D300", "D400", "D404", "D405", "D406", "D407", "D408",
|
||||
"D409", "D410", "D411", "D413", "D415", "D416", "D417",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"openbb_cookiecutter/cli.py" = ["T201"]
|
||||
"tests/**" = ["S101", "S603", "S607", "D"]
|
||||
"**/__init__.py" = ["F401"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
combine-as-imports = true
|
||||
force-wrap-aliases = true
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "numpy"
|
||||
|
||||
[tool.ty]
|
||||
[tool.ty.environment]
|
||||
python-version = "3.10"
|
||||
|
||||
[tool.ty.src]
|
||||
include = ["openbb_cookiecutter"]
|
||||
exclude = ["openbb_cookiecutter/template", "tests"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q --strict-markers"
|
||||
|
||||
1
cookiecutter/tests/__init__.py
Normal file
1
cookiecutter/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""openbb-cookiecutter tests."""
|
||||
50
cookiecutter/tests/test_cli.py
Normal file
50
cookiecutter/tests/test_cli.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Tests for the openbb-cookiecutter command line."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openbb_cookiecutter import cli
|
||||
|
||||
|
||||
class TestParseExtensionTypes:
|
||||
"""Extension types are validated."""
|
||||
|
||||
def test_valid(self):
|
||||
assert cli._parse_extension_types("router, provider") == ["router", "provider"]
|
||||
|
||||
@pytest.mark.parametrize("value", ["", "router,bogus"])
|
||||
def test_invalid(self, value):
|
||||
with pytest.raises(ValueError):
|
||||
cli._parse_extension_types(value)
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""The command generates a project."""
|
||||
|
||||
def test_no_input(self, tmp_path):
|
||||
code = cli.main(["--no-input", "-o", str(tmp_path), "-e", "router", "provider"])
|
||||
project = tmp_path / "extension-template"
|
||||
assert code == 0
|
||||
assert (project / "extension_template/routers/template.py").exists()
|
||||
assert (project / "extension_template/providers/template").is_dir()
|
||||
assert not (project / "extension_template/obbject").exists()
|
||||
|
||||
def test_extra_context_requires_key_value(self, tmp_path, capsys):
|
||||
code = cli.main(["--no-input", "-o", str(tmp_path), "--extra-context", "bad"])
|
||||
assert code == 1
|
||||
assert "KEY=VALUE" in capsys.readouterr().out
|
||||
|
||||
def test_generation_error_returns_nonzero(self, tmp_path, capsys):
|
||||
code = cli.main(
|
||||
[
|
||||
"--no-input",
|
||||
"-o",
|
||||
str(tmp_path),
|
||||
"--extra-context",
|
||||
"package_name=1-bad",
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
assert "Error:" in capsys.readouterr().err
|
||||
assert not Path(tmp_path / "extension-template").exists()
|
||||
213
cookiecutter/tests/test_template.py
Normal file
213
cookiecutter/tests/test_template.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""Tests for the project template."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from cookiecutter.exceptions import FailedHookException
|
||||
from cookiecutter.main import cookiecutter
|
||||
|
||||
from openbb_cookiecutter import get_template_path
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
PACKAGE = "extension_template"
|
||||
|
||||
|
||||
def generate(tmp_path: Path, extension_types: str, **context: str) -> Path:
|
||||
return Path(
|
||||
cookiecutter(
|
||||
str(get_template_path()),
|
||||
output_dir=str(tmp_path),
|
||||
no_input=True,
|
||||
extra_context={"extension_types": extension_types, **context},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def files(project: Path) -> set[str]:
|
||||
return {
|
||||
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
|
||||
}
|
||||
|
||||
|
||||
def entry_points(project: Path) -> dict:
|
||||
return tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8"))[
|
||||
"project"
|
||||
].get("entry-points", {})
|
||||
|
||||
|
||||
class TestGeneratedFiles:
|
||||
"""Each extension type generates only its own modules and tests."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extension_types", "expected"),
|
||||
[
|
||||
(
|
||||
"router",
|
||||
{
|
||||
f"{PACKAGE}/routers/template.py",
|
||||
"tests/test_router.py",
|
||||
},
|
||||
),
|
||||
(
|
||||
"provider",
|
||||
{
|
||||
f"{PACKAGE}/providers/template/models/example.py",
|
||||
f"{PACKAGE}/providers/template/models/equity_historical.py",
|
||||
"tests/test_provider.py",
|
||||
},
|
||||
),
|
||||
(
|
||||
"obbject",
|
||||
{
|
||||
f"{PACKAGE}/obbject/template_ext/__init__.py",
|
||||
"tests/test_obbject.py",
|
||||
},
|
||||
),
|
||||
(
|
||||
"on_command_output",
|
||||
{
|
||||
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
|
||||
"tests/test_on_command_output.py",
|
||||
},
|
||||
),
|
||||
(
|
||||
"charting",
|
||||
{
|
||||
f"{PACKAGE}/routers/template_views.py",
|
||||
"tests/test_views.py",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_single_type(self, tmp_path, extension_types, expected):
|
||||
generated = files(generate(tmp_path, extension_types))
|
||||
optional = {
|
||||
f"{PACKAGE}/routers/template.py",
|
||||
f"{PACKAGE}/routers/template_views.py",
|
||||
f"{PACKAGE}/providers/template/models/example.py",
|
||||
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
|
||||
"tests/test_router.py",
|
||||
"tests/test_views.py",
|
||||
"tests/test_provider.py",
|
||||
"tests/test_obbject.py",
|
||||
"tests/test_on_command_output.py",
|
||||
}
|
||||
assert expected <= generated
|
||||
assert not (optional - expected) & generated
|
||||
|
||||
def test_all_types(self, tmp_path):
|
||||
generated = files(generate(tmp_path, "all"))
|
||||
assert {
|
||||
"pyproject.toml",
|
||||
"README.md",
|
||||
".gitignore",
|
||||
f"{PACKAGE}/routers/template.py",
|
||||
f"{PACKAGE}/routers/template_views.py",
|
||||
f"{PACKAGE}/providers/template/__init__.py",
|
||||
f"{PACKAGE}/obbject/template_ext/__init__.py",
|
||||
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
|
||||
"tests/test_router.py",
|
||||
"tests/test_views.py",
|
||||
"tests/test_provider.py",
|
||||
"tests/test_obbject.py",
|
||||
"tests/test_on_command_output.py",
|
||||
} <= generated
|
||||
|
||||
def test_local_artifacts_are_not_copied(self, tmp_path):
|
||||
generated = files(generate(tmp_path, "all"))
|
||||
artifacts = (".ruff_cache", ".ipynb_checkpoints", "__pycache__", ".DS_Store")
|
||||
assert not [f for f in generated if any(a in f for a in artifacts)]
|
||||
|
||||
|
||||
class TestPyproject:
|
||||
"""The generated pyproject declares V5 packaging and entry points."""
|
||||
|
||||
def test_build_and_dependencies(self, tmp_path):
|
||||
pyproject = tomllib.loads(
|
||||
(generate(tmp_path, "all") / "pyproject.toml").read_text(encoding="utf-8")
|
||||
)
|
||||
assert pyproject["build-system"]["build-backend"] == "hatchling.build"
|
||||
assert pyproject["project"]["dependencies"] == ["openbb-core[pandas]>=2.0.0"]
|
||||
assert pyproject["project"]["optional-dependencies"] == {
|
||||
"charting": ["openbb-charting>=3.0.0"]
|
||||
}
|
||||
assert "openbb-devtools>=2.0.1" in pyproject["dependency-groups"]["dev"]
|
||||
|
||||
def test_entry_points_for_all_types(self, tmp_path):
|
||||
assert entry_points(generate(tmp_path, "all")) == {
|
||||
"openbb_core_extension": {
|
||||
"template": f"{PACKAGE}.routers.template:router",
|
||||
},
|
||||
"openbb_charting_extension": {
|
||||
"template": f"{PACKAGE}.routers.template_views:TemplateViews",
|
||||
},
|
||||
"openbb_provider_extension": {
|
||||
"template": f"{PACKAGE}.providers.template:template_provider",
|
||||
},
|
||||
"openbb_obbject_extension": {
|
||||
"to_string": f"{PACKAGE}.obbject.template_ext:ext",
|
||||
"template_ext": f"{PACKAGE}.obbject.template_ext:class_ext",
|
||||
"nonblocking_plugin": (
|
||||
f"{PACKAGE}.obbject.template_ext.on_command_output:nonblocking_plugin"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def test_entry_points_for_router_only(self, tmp_path):
|
||||
assert entry_points(generate(tmp_path, "router")) == {
|
||||
"openbb_core_extension": {
|
||||
"template": f"{PACKAGE}.routers.template:router",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestHooks:
|
||||
"""The post-generation hook validates names."""
|
||||
|
||||
def test_rejects_invalid_package_name(self, tmp_path):
|
||||
with pytest.raises(FailedHookException):
|
||||
generate(tmp_path, "router", package_name="1-bad")
|
||||
|
||||
def test_rejects_shared_provider_and_obbject_name(self, tmp_path):
|
||||
with pytest.raises(FailedHookException):
|
||||
generate(tmp_path, "all", obbject_name="template")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("ruff") is None, reason="ruff is not installed")
|
||||
class TestGeneratedCodeQuality:
|
||||
"""Generated projects pass their own lint and type checks."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extension_types",
|
||||
["router", "provider", "obbject", "on_command_output", "charting", "all"],
|
||||
)
|
||||
def test_ruff(self, tmp_path, extension_types):
|
||||
project = generate(tmp_path, extension_types)
|
||||
for command in (["format", "--check"], ["check", "--no-fix"]):
|
||||
result = subprocess.run(
|
||||
["ruff", *command, "--no-cache", "."],
|
||||
cwd=project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
|
||||
@pytest.mark.skipif(shutil.which("ty") is None, reason="ty is not installed")
|
||||
def test_ty(self, tmp_path):
|
||||
project = generate(tmp_path, "all")
|
||||
result = subprocess.run(
|
||||
["ty", "check", "--python", sys.executable, PACKAGE],
|
||||
cwd=project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
2143
cookiecutter/uv.lock
generated
vendored
Normal file
2143
cookiecutter/uv.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user