mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-04 00:12:29 +00:00
Merge and fixes
This commit is contained in:
9
CONTRIBUTING.md
vendored
9
CONTRIBUTING.md
vendored
@@ -13,7 +13,8 @@ Use your best judgment, and feel free to propose changes to this document in a p
|
||||
- [Data sources](#data-sources)
|
||||
- [View](#view)
|
||||
- [Controller](#controller)
|
||||
- [Add SDK endpoint](#add-sdk-endpoint)
|
||||
- [Add SDK endpoint (V3)](#add-sdk-endpoint-v3)
|
||||
- [Add SDK endpoint (V4)](#add-sdk-endpoint-v4)
|
||||
- [Add Unit Tests](#add-unit-tests)
|
||||
- [Open a Pull Request](#open-a-pull-request)
|
||||
- [Review Process](#review-process)
|
||||
@@ -452,7 +453,7 @@ The **import only occurs inside this menu call**, this is so that the loading ti
|
||||
|
||||
In addition, note the `self.load_class` which allows to not create a new `FundamentalAnalysisController` instance but re-load the previous created one. Unless the arguments `self.ticker`, `self.start` or `self.stock` have changed since. The `self.queue` list of commands is passed around as it contains the commands that the terminal must perform.
|
||||
|
||||
### Add SDK endpoint
|
||||
### Add SDK endpoint (V3)
|
||||
|
||||
In order to add a command to the SDK, follow these steps:
|
||||
|
||||
@@ -508,6 +509,10 @@ In order to add a command to the SDK, follow these steps:
|
||||
python generate_sdk.py sort
|
||||
```
|
||||
|
||||
### Add SDK endpoint (V4)
|
||||
|
||||
Refer to the documentation [here](openbb_sdk\CONTRIBUTING.md).
|
||||
|
||||
### Add Unit Tests
|
||||
|
||||
This is a vital part of the contribution process. We have a set of unit tests that are run on every Pull Request. These tests are located in the `tests` folder.
|
||||
|
||||
477
openbb_sdk/CONTRIBUTING.md
vendored
Normal file
477
openbb_sdk/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,477 @@
|
||||
# CONTRIBUTING - THIS IS A WORK IN PROGRESS
|
||||
|
||||
- [CONTRIBUTING - THIS IS A WORK IN PROGRESS](#contributing---this-is-a-work-in-progress)
|
||||
- [Get started contributing with a template](#get-started-contributing-with-a-template)
|
||||
- [Cookiecuter, a closer look](#cookiecuter-a-closer-look)
|
||||
- [Get your data](#get-your-data)
|
||||
- [SDK commands: query and output your data](#sdk-commands-query-and-output-your-data)
|
||||
- [Adding a new data point](#adding-a-new-data-point)
|
||||
- [Identify which type of data you want to add](#identify-which-type-of-data-you-want-to-add)
|
||||
- [What is the Standardization Framework?](#what-is-the-standardization-framework)
|
||||
- [Standardization Caveats](#standardization-caveats)
|
||||
- [Standard QueryParams Example](#standard-queryparams-example)
|
||||
- [Standard Data Example](#standard-data-example)
|
||||
- [Check if the standard model exists](#check-if-the-standard-model-exists)
|
||||
- [Refer to the API documentation and start developing](#refer-to-the-api-documentation-and-start-developing)
|
||||
- [How to use the standardization framework?](#how-to-use-the-standardization-framework)
|
||||
- [Query Parameters](#query-parameters)
|
||||
- [Data Output](#data-output)
|
||||
- [Build the Fetcher](#build-the-fetcher)
|
||||
- [Make the new provider visible to the SDK](#make-the-new-provider-visible-to-the-sdk)
|
||||
- [Using other extension as a dependency](#using-other-extension-as-a-dependency)
|
||||
- [Using our internal extension](#using-our-internal-extension)
|
||||
- [Adding an external extension](#adding-an-external-extension)
|
||||
- [The charting extension](#the-charting-extension)
|
||||
- [Add a visualization to an existing SDK command](#add-a-visualization-to-an-existing-sdk-command)
|
||||
- [Using the `to_chart` OBBject method](#using-the-to_chart-obbject-method)
|
||||
- [Environment and dependencies](#environment-and-dependencies)
|
||||
|
||||
## Get started contributing with a template
|
||||
|
||||
In order to get started contributing we have setup a Cookiecutter template that will help you get started.
|
||||
|
||||
Please refer to the [Cookiecutter template](https://github.com/OpenBB-finance/openbb-cookiecutter) and follow the instructions there.
|
||||
|
||||
This document will walk you through the steps of adding a new custom extension to the SDK. The high level steps are:
|
||||
|
||||
- Generate the extension structure
|
||||
- Install your dependencies
|
||||
- Install your new package
|
||||
- Use your extension (either from Python or the API interface)
|
||||
|
||||
## Cookiecuter, a closer look
|
||||
|
||||
The Cookiecutter template generates a set of files in which we can find instructions and explanations.
|
||||
|
||||
> Note that the code is functional, so you can just run it and start playing with it.
|
||||
|
||||
### Get your data
|
||||
|
||||
Either from a CSV file, local database or from an API endpoint, you'll need to get your data.
|
||||
|
||||
If you don't want to partake in the data standardization framework, you can simply write all the logic straight inside the router file. This is usually the case when you are adding unique data that isn't easily standardizable.
|
||||
|
||||
Saying that, we recommend following the standardization framework, as it will make your life easier in the long run and unlock a set of features that are only available to standardized data.
|
||||
|
||||
When standardizing, all data is defined using two different pydantic models:
|
||||
|
||||
1. Define the [query parameters](openbb_sdk\sdk\provider\openbb_provider\abstract\query_params.py) model.
|
||||
2. Define the resulting [data schema](openbb_sdk\sdk\provider\openbb_provider\abstract\data.py) model.
|
||||
|
||||
> The models can be entirely custom, or inherit from the OpenBB standardized models.
|
||||
> They enforce a safe and consistent data structure, validation and type checking.
|
||||
|
||||
We call this the ***Know-Your-Data*** principle.
|
||||
|
||||
After you've defined both models, you'll need to define a `Fetcher` class which contains three methods:
|
||||
|
||||
1. `transform_query` - transforms the query parameters to the format of the API endpoint.
|
||||
2. `extract_data` - makes the request to the API endpoint and returns the raw data.
|
||||
3. `transform_data` - transforms the raw data into the defined data model.
|
||||
|
||||
> Note that the `Fetcher` should inherit from the [`Fetcher`](openbb_sdk\sdk\provider\openbb_provider\abstract\`Fetcher`.py) class, which is a generic class that receives the query parameters and the data model as type parameters.
|
||||
|
||||
After finalizing your models you need to make them visible to the SDK. This is done by adding the `Fetcher` to the `__init__.py` file of the `<your_package_name>/<your_module_name>` folder as part of the [`Provider`](openbb_sdk\sdk\provider\openbb_provider\abstract\provider.py).
|
||||
|
||||
Any command using the `Fetcher` class you've just defined, will be calling the `transform_query`, `extract_data` and `transform_data` methods under the hood in order to get the data and output it do the end user.
|
||||
|
||||
If you're not sure what's a command and why is it even using the `Fetcher` class, follow along!
|
||||
|
||||
### SDK commands: query and output your data
|
||||
|
||||
The SDK will enable you to query and output your data in a very simple way.
|
||||
|
||||
> Any SDK endpoint will be available both from a Python interface and the API.
|
||||
|
||||
The command definition on the SDK follows [FastAPI](https://fastapi.tiangolo.com/) conventions, meaning that you'll be defining **endpoints** similar to like FastAPI.
|
||||
|
||||
The Cookiecutter template generates for you a `router.py` file with a set of examples that you can follow, namely:
|
||||
|
||||
- Perform a simple `GET` and `POST` request - without worrying on any custom data definition.
|
||||
- Using a custom data definition so you get your data the exact way you want it.
|
||||
|
||||
You can expect the following when using a `Fetcher`:
|
||||
|
||||
```python
|
||||
@router.command(model="Example")
|
||||
def model_example(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject[BaseModel]:
|
||||
"""Example Data."""
|
||||
return OBBject(results=Query(**locals()).execute())
|
||||
```
|
||||
|
||||
Let's break it down:
|
||||
|
||||
- `@router.command(...)` - this tells the SDK that this is a command that can be called.
|
||||
- `model="Example"` - this is the name of the data model that will be used to validate the data.
|
||||
- `cc: CommandContext` - this is a parameter that is passed to the command. It contains a set of user and system settings that maybe useful during the execution of the command - eg. api keys.
|
||||
- `provider_choices: ProviderChoices` - all the providers that implement the `Example` `Fetcher`.
|
||||
- `standard_params: StandardParams` - standardized parameters that are common to all providers that implement the `Example` `Fetcher`.
|
||||
- `extra_params: ExtraParams` - this is a parameter that is passed to the command and it contains the provider specific arguments.
|
||||
|
||||
You only need to change the `model` parameter to the name of the `Fetcher` dictionary key that you've defined in the `__init__.py` file of the `<your_package_name>/<your_module_name>` folder.
|
||||
|
||||
## Adding a new data point
|
||||
|
||||
In the above section, we've seen how to get started with a template.
|
||||
|
||||
In this section, we'll be adding a new data point to the SDK. We will add a new provider with an existing [standard data](openbb_sdk\sdk\provider\openbb_provider\standard_models) model.
|
||||
|
||||
### Identify which type of data you want to add
|
||||
|
||||
In this example, we'll be adding OHLC stock data that is used by the `stocks/load` command.
|
||||
|
||||
Note that, if no command exists for your data, we need to add one under the right router.
|
||||
Each router is categorized into different extensions (stocks, forex, crypto, etc.).
|
||||
|
||||
### What is the Standardization Framework?
|
||||
|
||||
The Standardization Framework is a set of tools and guidelines that enable the user to query and obtain data in a consistent way across several providers.
|
||||
|
||||
Each data model should inherit from a [standard data](openbb_sdk\sdk\provider\openbb_provider\standard_models) that is already defined in the SDK. This will unlock a set of perks that are only available to standardized data, namely:
|
||||
|
||||
- Can query and output data in a standardized way.
|
||||
- Can expect extensions that follow standardization to work out-of-the-box.
|
||||
- Can expect transparently defined schemas for the data that is returned by the API.
|
||||
- Can expect consistent data types and validation.
|
||||
- Will work seamlessly with other providers that implement the same standard model.
|
||||
|
||||
The standard models are defined under the `./sdk/core/provider/openbb_provider/standard_models/` directory.
|
||||
|
||||
They implement the [`QueryParams`](openbb_sdk\sdk\provider\openbb_provider\abstract\query_params.py) and [`Data`](openbb_sdk\sdk\provider\openbb_provider\abstract\data.py) models, which are the models that are used to query and output data, respectively. They are pydantic models under the hood, so you can leverage all the pydantic features such as validators.
|
||||
|
||||
#### Standardization Caveats
|
||||
|
||||
The standardization framework is a very powerful tool, but it has some caveats that you should be aware of:
|
||||
|
||||
- We standardize fields that are shared between two or more providers. If there is a third provider that doesn't share the same fields, we will declare it as an `Optional` field.
|
||||
- When mapping the column names from a provider-specific model to the standard model, the CamelCase to snake_case conversion is done automatically. If the column names are not the same, you'll need to manually map them. (e.g. `o` -> `open`)
|
||||
- The standard models are created and maintained by the OpenBB team. If you want to add a new field to a standard model, you'll need to open a PR to the SDK.
|
||||
|
||||
### Standard QueryParams Example
|
||||
|
||||
```python
|
||||
class StockEODQueryParams(QueryParams, BaseSymbol):
|
||||
"""Stock end of day Query."""
|
||||
|
||||
start_date: Optional[date] = Field(
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", ""), default=None
|
||||
)
|
||||
end_date: Optional[date] = Field(
|
||||
description=QUERY_DESCRIPTIONS.get("end_date", ""), default=None
|
||||
)
|
||||
```
|
||||
|
||||
The `QueryParams` is an abstract class that just tells us that we are dealing with query parameters. The `BaseSymbol` is a helper class that contains the `symbol` field and an
|
||||
upper case validator.
|
||||
|
||||
The SDK dynamically knows where the standard models begin in the inheritance tree, so you don't need to worry about it.
|
||||
|
||||
### Standard Data Example
|
||||
|
||||
```python
|
||||
class StockEODData(Data):
|
||||
"""Stock end of day price Data."""
|
||||
|
||||
date: datetime = Field(description=DATA_DESCRIPTIONS.get("date", ""))
|
||||
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
|
||||
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
|
||||
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
|
||||
close: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("close", ""))
|
||||
volume: float = Field(description=DATA_DESCRIPTIONS.get("volume", ""))
|
||||
vwap: Optional[PositiveFloat] = Field(description=DATA_DESCRIPTIONS.get("vwap", ""))
|
||||
```
|
||||
|
||||
The `Data` class is an abstract class that tells us the expected output data. Here we can see a `vwap` field that is `Optional`. This is because not all providers share this field while it is shared between two or more providers.
|
||||
|
||||
#### Check if the standard model exists
|
||||
|
||||
Given the fact that there's already an endpoint for OHLCV stock data, we can check if the standard exists.
|
||||
|
||||
In this case, it's `StockEOD` which can be found inside the `./sdk/core/provider/openbb_provider/standard_models/` directory.
|
||||
|
||||
If the standard model doesn't exist:
|
||||
|
||||
- you won't need to inherit from it in the next steps
|
||||
- all your provider-specific query parameters will be under the `**kwargs` in the python interface
|
||||
- it might not work out-of-the box with other extensions that follow standardization e.g. the `charting` extension
|
||||
|
||||
## Refer to the API documentation and start developing
|
||||
|
||||
You can start developing your extension by referring to the API documentation of the provider you are interested to integrate.
|
||||
|
||||
### How to use the standardization framework?
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
Query Parameters are the parameters that are passed to the API endpoint in order to make the request.
|
||||
|
||||
For the `StockEOD` example, this would look like the following:
|
||||
|
||||
```python
|
||||
|
||||
class <ProviderName>StockEODQueryParams(StockEODQueryParams):
|
||||
"""<ProviderName> Stock End of Day Query.
|
||||
|
||||
Source: https://www.<provider_name>.co/documentation/
|
||||
"""
|
||||
|
||||
# provider specific query parameters
|
||||
|
||||
```
|
||||
|
||||
> Note that, since `StockEODQueryParams` inherits from pydantic's `BaseModel`, so we can leverage validators to perform additional checks on the query parameters.
|
||||
|
||||
#### Data Output
|
||||
|
||||
The data output is the data that is returned by the API endpoint.
|
||||
For the `StockEOD` example, this would look like the following:
|
||||
|
||||
```python
|
||||
|
||||
class <ProviderName>StockEODData(StockEODData):
|
||||
"""<ProviderName> Stock End of Day Data.
|
||||
|
||||
Source: https://www.<provider_name>.co/documentation/
|
||||
"""
|
||||
|
||||
# provider specific data output
|
||||
|
||||
```
|
||||
|
||||
> Note that, since `StockEODData` inherits from pydantic's `BaseModel`, so we can leverage validators to perform additional checks on the output model. A very good example of this, would be to transform a string date into a datetime object.
|
||||
|
||||
#### Build the Fetcher
|
||||
|
||||
The `Fetcher` class is responsible for making the request to the API endpoint and providing the output.
|
||||
|
||||
It will receive the Query Parameters, and it will return the output while leveraging the Data model.
|
||||
|
||||
For the `StockEOD` example, this would look like the following:
|
||||
|
||||
```python
|
||||
|
||||
class <ProviderName>StockEODFetcher(
|
||||
Fetcher[
|
||||
<ProviderName>StockEODQueryParams,
|
||||
List[<ProviderName>StockEODData],
|
||||
]
|
||||
):
|
||||
"""Transform the query, extract and transform the data."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: Dict[str, Any]) -> <ProviderName>StockEODQueryParams:
|
||||
"""Transform the query parameters."""
|
||||
|
||||
return <ProviderName>StockEODQueryParams(**transformed_params)
|
||||
|
||||
@staticmethod
|
||||
def extract_data(
|
||||
query: <ProviderName>StockEODQueryParams,
|
||||
credentials: Optional[Dict[str, str]],
|
||||
**kwargs: Any,
|
||||
) -> dict:
|
||||
"""Return the raw data from the endpoint."""
|
||||
|
||||
obtained_data = my_request(query, credentials, **kwargs)
|
||||
|
||||
return obtained_data
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
data: dict,
|
||||
) -> List[<ProviderName>StockEODData]:
|
||||
"""Transform the data to the standard format."""
|
||||
|
||||
return [<ProviderName>StockEODData.parse_obj(**d) for d in data]
|
||||
|
||||
```
|
||||
|
||||
> Make sure that you're following the TET pattern when building a `Fetcher` - **Transform, Extract, Transform**.
|
||||
|
||||
### Make the new provider visible to the SDK
|
||||
|
||||
In order to make the new provider visible to the SDK, you'll need to add it to the `__init__.py` file of the `providers/<provider_name>/openbb_<provider_name>/` folder.
|
||||
|
||||
```python
|
||||
|
||||
"""<Provider Name> Provider module."""
|
||||
from openbb_provider.abstract.provider import Provider
|
||||
|
||||
from openbb_<provider_name>.models.stock_eod import <ProviderName>StockEODFetcher
|
||||
|
||||
<provider_name>_provider = Provider(
|
||||
name="<provider_name>",
|
||||
website="<URL to the provider website>",
|
||||
description="Provider description goes here",
|
||||
required_credentials=["api_key"],
|
||||
`Fetcher`_dict={
|
||||
"StockEOD": <ProviderName>StockEODFetcher,
|
||||
},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
If the provider does not require any credentials, you can simply omit it. On the other hand if it requires more than 2 items to authenticate, you can simply add a list of all the required items to the `required_credentials` list.
|
||||
|
||||
After running `pip install .` on `openbb_sdk/providers/<provider_name>` your provider should be ready for usage, both from the Python interface and the API.
|
||||
|
||||
## Using other extension as a dependency
|
||||
|
||||
We can use internal and external extensions with the custom developed extension and bundle it as a dependency.
|
||||
|
||||
### Using our internal extension
|
||||
|
||||
We will use the `openbb-qa` extension by utilizing its `summary` endpoint.
|
||||
|
||||
To create a `period_summary_example` endpoint we need to add the following to the `router.py` file:
|
||||
|
||||
```python
|
||||
from typing import List
|
||||
from openbb_provider.abstract.data import Data
|
||||
from openbb_core.app.utils import basemodel_to_df, df_to_basemodel
|
||||
from openbb_qa.qa_router import summary
|
||||
|
||||
|
||||
@router.command(methods=["POST"])
|
||||
def period_summary_example(
|
||||
data: List[Data],
|
||||
target: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
) -> OBBject[dict]:
|
||||
"""Example Data."""
|
||||
|
||||
df = basemodel_to_df(data)
|
||||
df = df[(df["date"] >= start_date) & (df["date"] <= end_date)]
|
||||
df = df.reset_index(drop=True)
|
||||
target_data = df_to_basemodel(df)
|
||||
results = summary(target_data, target=target)
|
||||
|
||||
return OBBject(results=results.results)
|
||||
```
|
||||
|
||||
Here we are using two vital utility functions - `basemodel_to_df` and `df_to_basemodel`. The function `basemodel_to_df` converts the data to a pandas dataframe and `df_to_basemodel` converts the dataframe back to the `Data` model.
|
||||
|
||||
### Adding an external extension
|
||||
|
||||
To add the `openbb-charting` charting extension as a dependency, you'll need to add it to the `pyproject.toml` file:
|
||||
|
||||
<!-- TODO: Change the version to match the stable release -->
|
||||
```toml
|
||||
[tool.poetry.dependencies]
|
||||
openbb-charting = "^0.0.0a1"
|
||||
```
|
||||
|
||||
Then execute the command `poetry install` in the root of your extension to install the new dependency.
|
||||
|
||||
### The charting extension
|
||||
|
||||
> In theory the same principles apply to any other extension.
|
||||
|
||||
#### Add a visualization to an existing SDK command
|
||||
|
||||
One should first ensure that the already implemented endpoint is available in the [charting router](openbb_sdk/extensions/charting/openbb_charting/charting_router.py).
|
||||
|
||||
To do so, you can run:
|
||||
`python openbb_sdk/extensions/charting/openbb_charting/builder.py` - which will read all the available endpoints and add them to the charting router.
|
||||
|
||||
Afterwards, you'll need to add the visualization to the [charting router](openbb_sdk/extensions/charting/openbb_charting/charting_router.py). The convention to match the endpoint with the respective charting function is the following:
|
||||
|
||||
- `stocks/load` -> `stocks_load`
|
||||
- `ta/ema` -> `ta_ema`
|
||||
|
||||
When you spot the charting function on the charting router file, you can add the visualization to it.
|
||||
|
||||
The implementation should leverage the already existing classes and methods to do so, namely:
|
||||
|
||||
- `OpenBBFigure`
|
||||
- `OpenBBFigureTable`
|
||||
- `PlotlyTA`
|
||||
|
||||
Note that the return of each charting function should respect the already defined return types: `Tuple[OpenBBFigure, Dict[str, Any]]`.
|
||||
|
||||
The returned tuple contains a `OpenBBFigure` that is an interactive plotly figure that can be used in a Python interpreter, and a `Dict[str, Any]` that contains the raw data leveraged by the API.
|
||||
|
||||
After you're done implementing the charting function, you can simply use either the Python interface or the API to get the chart. To do so, you'll only need to set the already available `chart` argument to `True`.
|
||||
|
||||
Refer to the charting extension [documentation](openbb_sdk/extensions/charting/README.md) for more information on usage.
|
||||
|
||||
#### Using the `to_chart` OBBject method
|
||||
|
||||
The `OBBject` is the custom OpenBB object that is returned by the SDK commands.
|
||||
It implements a set of `to_<something>` functions that enable the user to easily transform the data into a different format.
|
||||
|
||||
The `to_chart` function should be taken as an advanced feature, as it requires the user to have a good understanding of the charting extension and the `OpenBBFigure` class.
|
||||
|
||||
The user can use any number of `**kwargs` that will be passed to the `PlotlyTA` class in order to build custom visualizations with custom indicators and similar.
|
||||
|
||||
Refer to the [`to_chart` implementation](openbb_sdk/extensions/charting/openbb_charting/core/to_chart.py) for further details.
|
||||
|
||||
> Note that, this method will only work to some limited extent with data that is not standardized.
|
||||
> Also, it is designed only to handle time series data.
|
||||
|
||||
## Environment and dependencies
|
||||
|
||||
In order to contribute to the SDK you need to setup your environment in order to ensure a smooth development experience.
|
||||
|
||||
<details>
|
||||
<summary>Need help seting up Miniconda or Git?</summary>
|
||||
|
||||
Sometimes installing Miniconda or Git can be a bit tricky, so we've prepared a set of instructions to help you get started.
|
||||
|
||||
Please refer to [OpenBBTerminal docs](https://docs.openbb.co/terminal/installation/pypi) for more information.
|
||||
</details>
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:OpenBB-finance/OpenBBTerminal.git
|
||||
```
|
||||
|
||||
2. Create and activate a virtual environment:
|
||||
|
||||
> Supported python versions: python = ">=3.8,<3.12"
|
||||
|
||||
```bash
|
||||
conda create -n "obb-sdk" python=3.9.13
|
||||
conda activate obb-sdk
|
||||
```
|
||||
|
||||
3. Manage your environment with [Poetry](https://python-poetry.org/):
|
||||
|
||||
```bash
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
4. Install the `openbb-core`:
|
||||
|
||||
```bash
|
||||
cd OpenBBTerminal/openbb_sdk/sdk/core/
|
||||
poetry install
|
||||
```
|
||||
|
||||
5. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd OpenBBTerminal/openbb_sdk/extensions/stocks/ # or any other extension
|
||||
poetry install
|
||||
```
|
||||
|
||||
> When installing the dependencies using poetry we ensure that dependencies are being installed in editable mode, which is the most straightforward way to develop on top of the SDK.
|
||||
|
||||
<details>
|
||||
<summary>Install all dependencies in editable mode at once</summary>
|
||||
|
||||
For development purposes, one can install every available extension by running a custom shell script:
|
||||
|
||||
Navigate to `/OpenBBTerminal/openbb_sdk`
|
||||
|
||||
Run `./install_all.sh` for Linux/mac or `install_all.bat` for Windows.
|
||||
</details>
|
||||
|
||||
> In order to install any other custom extension or provider, you'd follow the exact same steps as above.
|
||||
@@ -72,7 +72,7 @@ class OpenBBFigureTable(OpenBBFigure):
|
||||
df_outgoing = df_outgoing.reset_index()
|
||||
|
||||
for col in df_outgoing.columns:
|
||||
if col == "":
|
||||
if not col:
|
||||
df_outgoing = df_outgoing.rename(columns={col: " "})
|
||||
|
||||
self._backend.send_table(
|
||||
|
||||
@@ -42,7 +42,7 @@ class CLASS_crypto(Container):
|
||||
provider: Optional[Literal["fmp", "polygon", "yfinance"]] = None,
|
||||
**kwargs
|
||||
) -> OBBject[List]:
|
||||
r"""Crypto EOD Price.
|
||||
"""Crypto EOD Price.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -118,11 +118,11 @@ class CLASS_crypto(Container):
|
||||
change : Optional[float]
|
||||
Change in the price of the symbol from the previous day. (provider: fmp)
|
||||
change_percent : Optional[float]
|
||||
Change \% in the price of the symbol. (provider: fmp)
|
||||
Change \\% in the price of the symbol. (provider: fmp)
|
||||
label : Optional[str]
|
||||
Human readable format of the date. (provider: fmp)
|
||||
change_over_time : Optional[float]
|
||||
Change \% in the price of the symbol over a period of time. (provider: fmp)
|
||||
Change \\% in the price of the symbol over a period of time. (provider: fmp)
|
||||
n : Optional[PositiveInt]
|
||||
Number of transactions for the symbol in the time period. (provider: polygon)
|
||||
""" # noqa: E501
|
||||
|
||||
@@ -335,7 +335,7 @@ class CLASS_economy(Container):
|
||||
provider: Optional[Literal["fmp", "polygon", "yfinance"]] = None,
|
||||
**kwargs
|
||||
) -> OBBject[List]:
|
||||
r"""Get OHLCV data for an index.
|
||||
"""Get OHLCV data for an index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -411,11 +411,11 @@ class CLASS_economy(Container):
|
||||
change : Optional[float]
|
||||
Change in the price of the symbol from the previous day. (provider: fmp)
|
||||
change_percent : Optional[float]
|
||||
Change \% in the price of the symbol. (provider: fmp)
|
||||
Change \\% in the price of the symbol. (provider: fmp)
|
||||
label : Optional[str]
|
||||
Human readable format of the date. (provider: fmp)
|
||||
change_over_time : Optional[float]
|
||||
Change \% in the price of the symbol over a period of time. (provider: fmp)
|
||||
Change \\% in the price of the symbol over a period of time. (provider: fmp)
|
||||
n : Optional[PositiveInt]
|
||||
Number of transactions for the symbol in the time period. (provider: polygon)
|
||||
""" # noqa: E501
|
||||
|
||||
@@ -43,7 +43,7 @@ class CLASS_forex(Container):
|
||||
provider: Optional[Literal["fmp", "polygon", "yfinance"]] = None,
|
||||
**kwargs
|
||||
) -> OBBject[List]:
|
||||
r"""Forex Intraday Price.
|
||||
"""Forex Intraday Price.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -117,11 +117,11 @@ class CLASS_forex(Container):
|
||||
change : Optional[float]
|
||||
Change in the price of the symbol from the previous day. (provider: fmp)
|
||||
change_percent : Optional[float]
|
||||
Change \% in the price of the symbol. (provider: fmp)
|
||||
Change \\% in the price of the symbol. (provider: fmp)
|
||||
label : Optional[str]
|
||||
Human readable format of the date. (provider: fmp)
|
||||
change_over_time : Optional[float]
|
||||
Change \% in the price of the symbol over a period of time. (provider: fmp)
|
||||
Change \\% in the price of the symbol over a period of time. (provider: fmp)
|
||||
n : Optional[PositiveInt]
|
||||
Number of transactions for the symbol in the time period. (provider: polygon)
|
||||
""" # noqa: E501
|
||||
|
||||
@@ -187,7 +187,7 @@ class CLASS_stocks(Container):
|
||||
provider: Optional[Literal["cboe", "fmp", "polygon", "yfinance"]] = None,
|
||||
**kwargs
|
||||
) -> OBBject[List]:
|
||||
r"""Load stock data for a specific ticker.
|
||||
"""Load stock data for a specific ticker.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -263,11 +263,11 @@ class CLASS_stocks(Container):
|
||||
change : Optional[float]
|
||||
Change in the price of the symbol from the previous day. (provider: fmp)
|
||||
change_percent : Optional[float]
|
||||
Change \% in the price of the symbol. (provider: fmp)
|
||||
Change \\% in the price of the symbol. (provider: fmp)
|
||||
label : Optional[str]
|
||||
Human readable format of the date. (provider: fmp)
|
||||
change_over_time : Optional[float]
|
||||
Change \% in the price of the symbol over a period of time. (provider: fmp)
|
||||
Change \\% in the price of the symbol over a period of time. (provider: fmp)
|
||||
n : Optional[PositiveInt]
|
||||
Number of transactions for the symbol in the time period. (provider: polygon)
|
||||
""" # noqa: E501
|
||||
|
||||
@@ -62,13 +62,13 @@ class CLASS_stocks_options(Container):
|
||||
Expiration date of the contract.
|
||||
strike : Optional[float]
|
||||
Strike price of the contract.
|
||||
optionType : Optional[str]
|
||||
option_type : Optional[str]
|
||||
Call or Put.
|
||||
bid : Optional[float]
|
||||
Bid price of the contract.
|
||||
ask : Optional[float]
|
||||
Ask price of the contract.
|
||||
openInterest : Optional[float]
|
||||
open_interest : Optional[float]
|
||||
Open interest on the contract.
|
||||
volume : Optional[float]
|
||||
Current trading volume on the contract.
|
||||
|
||||
@@ -24,19 +24,19 @@ class CboeOptionsChainsQueryParams(OptionsChainsQueryParams):
|
||||
class CboeOptionsChainsData(OptionsChainsData):
|
||||
"""CBOE Options Chains Data."""
|
||||
|
||||
contractSymbol: str = Field(
|
||||
contract_symbol: str = Field(
|
||||
description="Contract symbol for the option.",
|
||||
)
|
||||
dte: int = Field(
|
||||
description="Days to expiration for the option.",
|
||||
)
|
||||
bidSize: int = Field(
|
||||
bid_size: int = Field(
|
||||
description="Bid size for the option.",
|
||||
)
|
||||
askSize: int = Field(
|
||||
ask_size: int = Field(
|
||||
description="Ask size for the option.",
|
||||
)
|
||||
impliedVolatility: float = Field(
|
||||
implied_volatility: float = Field(
|
||||
description="Implied volatility of the option.",
|
||||
)
|
||||
delta: float = Field(
|
||||
@@ -66,22 +66,22 @@ class CboeOptionsChainsData(OptionsChainsData):
|
||||
low: float = Field(
|
||||
description="Low price of the option.",
|
||||
)
|
||||
lastTradePrice: float = Field(
|
||||
last_trade_price: float = Field(
|
||||
description="Last trade price of the option.",
|
||||
)
|
||||
tick: str = Field(
|
||||
description="Whether the last tick was up or down in price.",
|
||||
)
|
||||
previousClose: float = Field(
|
||||
previous_close: float = Field(
|
||||
description="Previous closing price of the option.",
|
||||
)
|
||||
change: float = Field(
|
||||
description="Change in price of the option.",
|
||||
)
|
||||
changePercent: float = Field(
|
||||
change_percent: float = Field(
|
||||
description="Change, in percent, of the option.",
|
||||
)
|
||||
lastTradeTimestamp: datetime = Field(
|
||||
last_trade_timestamp: datetime = Field(
|
||||
description="Last trade timestamp of the option.",
|
||||
)
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ class CboeStockSearchData(StockSearchData):
|
||||
"name": "Company Name",
|
||||
}
|
||||
|
||||
dpmName: Optional[str] = Field(
|
||||
dpm_name: Optional[str] = Field(
|
||||
description="Name of the primary market maker.",
|
||||
alias="DPM Name",
|
||||
)
|
||||
postStation: Optional[str] = Field(
|
||||
post_station: Optional[str] = Field(
|
||||
description="Post and station location on the CBOE trading floor.",
|
||||
alias="Post/Station",
|
||||
)
|
||||
|
||||
@@ -24,13 +24,6 @@ class FMPBalanceSheetGrowthQueryParams(BalanceSheetGrowthQueryParams):
|
||||
class FMPBalanceSheetGrowthData(BalanceSheetGrowthData):
|
||||
"""FMP Balance Sheet Growth Data."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic alias config using fields Dict."""
|
||||
|
||||
fields = {
|
||||
"growth_deferred_tax_liabilities_non_current": "growthDeferrredTaxLiabilitiesNonCurrent",
|
||||
}
|
||||
|
||||
@validator("date", pre=True, check_fields=False)
|
||||
def date_validate(cls, v): # pylint: disable=E0213
|
||||
"""Return the date as a datetime object."""
|
||||
|
||||
@@ -24,6 +24,16 @@ class FMPIncomeStatementGrowthQueryParams(IncomeStatementGrowthQueryParams):
|
||||
class FMPIncomeStatementGrowthData(IncomeStatementGrowthData):
|
||||
"""FMP Income Statement Growth Data."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic alias config using fields dict."""
|
||||
|
||||
fields = {
|
||||
"growth_ebitda": "growthEBITDA",
|
||||
"growth_ebitda_ratio": "growthEBITDARatio",
|
||||
"growth_eps": "growthEPS",
|
||||
"growth_eps_diluted": "growthEPSDiluted",
|
||||
}
|
||||
|
||||
@validator("date", pre=True, check_fields=False)
|
||||
def date_validate(cls, v): # pylint: disable=E0213
|
||||
"""Return the date as a datetime object."""
|
||||
|
||||
@@ -32,11 +32,11 @@ class FMPMajorIndicesEODQueryParams(MajorIndicesEODQueryParams):
|
||||
class FMPMajorIndicesEODData(MajorIndicesEODData):
|
||||
"""FMP Major Indices end of day Data."""
|
||||
|
||||
adjClose: Optional[float] = Field(
|
||||
adj_close: Optional[float] = Field(
|
||||
description="Adjusted Close Price of the symbol.",
|
||||
default=None,
|
||||
)
|
||||
unadjustedVolume: Optional[float] = Field(
|
||||
unadjusted_volume: Optional[float] = Field(
|
||||
description="Unadjusted volume of the symbol.",
|
||||
default=None,
|
||||
)
|
||||
@@ -44,26 +44,25 @@ class FMPMajorIndicesEODData(MajorIndicesEODData):
|
||||
description="Change in the price of the symbol from the previous day.",
|
||||
default=None,
|
||||
)
|
||||
changePercent: Optional[float] = Field(
|
||||
change_percent: Optional[float] = Field(
|
||||
description=r"Change \% in the price of the symbol.",
|
||||
default=None,
|
||||
)
|
||||
label: Optional[str] = Field(
|
||||
description="Human readable format of the date.", default=None
|
||||
)
|
||||
changeOverTime: Optional[float] = Field(
|
||||
change_over_time: Optional[float] = Field(
|
||||
description=r"Change \% in the price of the symbol over a period of time.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
@validator("date", pre=True)
|
||||
def date_validate( # pylint: disable=E0213
|
||||
cls, v, values: Dict[str, Any]
|
||||
) -> datetime:
|
||||
def date_validate(cls, v) -> datetime: # pylint: disable=E0213
|
||||
"""Return the date as a datetime object."""
|
||||
if values.get("changeOverTime", None) is not None:
|
||||
return datetime.strptime(v, "%Y-%m-%d")
|
||||
return datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
|
||||
# TODO: Check if this condition is necessary
|
||||
# if values.get("changeOverTime", None) is not None:
|
||||
# return datetime.strptime(v, "%Y-%m-%d")
|
||||
return datetime.strptime(v, "%Y-%m-%d")
|
||||
|
||||
|
||||
class FMPMajorIndicesEODFetcher(
|
||||
|
||||
@@ -28,9 +28,14 @@ class FMPPriceTargetQueryParams(PriceTargetQueryParams):
|
||||
class FMPPriceTargetData(PriceTargetData):
|
||||
"""FMP Price Target Data."""
|
||||
|
||||
newGrade: Optional[str] = Field(alias="new_grade", default=None)
|
||||
previousGrade: Optional[str] = Field(alias="previous_grade", default=None)
|
||||
gradingCompany: Optional[str] = Field(alias="grading_company", default=None)
|
||||
class Config:
|
||||
"""Pydantic alias config using fields dict."""
|
||||
|
||||
fields = {"news_url": "newsURL", "news_base_url": "newsBaseURL"}
|
||||
|
||||
new_grade: Optional[str]
|
||||
previous_grade: Optional[str]
|
||||
grading_company: Optional[str]
|
||||
|
||||
|
||||
class FMPPriceTargetFetcher(
|
||||
|
||||
@@ -22,14 +22,6 @@ class FMPStockInsiderTradingQueryParams(StockInsiderTradingQueryParams):
|
||||
class FMPStockInsiderTradingData(StockInsiderTradingData):
|
||||
"""FMP Stock Insider Trading Data."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic alias config using fields dict."""
|
||||
|
||||
fields = {
|
||||
"acquisition_or_disposition": "acquistionOrDisposition",
|
||||
"last_number_of_13f_shares": "lastNumberOf13FShares",
|
||||
}
|
||||
|
||||
|
||||
class FMPStockInsiderTradingFetcher(
|
||||
Fetcher[
|
||||
|
||||
@@ -28,8 +28,6 @@ class FMPStockQuoteData(StockQuoteData):
|
||||
"""Pydantic alias config using fields dict."""
|
||||
|
||||
fields = {
|
||||
"price_avg50": "priceAvg50",
|
||||
"price_avg200": "priceAvg200",
|
||||
"date": "timestamp",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
"""System dependency."""
|
||||
from copy import deepcopy
|
||||
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
|
||||
__system_service = SystemService()
|
||||
|
||||
|
||||
async def get_system_settings() -> SystemSettings:
|
||||
"""Get system settings."""
|
||||
global __system_service # noqa: PLW0602 # pylint: disable=global-variable-not-assigned
|
||||
return deepcopy(__system_service.system_settings)
|
||||
|
||||
|
||||
async def get_system_service() -> SystemService:
|
||||
"""Get system service."""
|
||||
global __system_service # noqa: PLW0602 # pylint: disable=global-variable-not-assigned
|
||||
return __system_service
|
||||
return SystemService().system_settings
|
||||
|
||||
@@ -16,7 +16,7 @@ SECRET_KEY = "a0657288545d1d2e991195841782ae2a22574a22954081db0c2888c5f5ddbecc"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 1440
|
||||
|
||||
__user_service: Optional[UserService] = None
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/account/token")
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
@@ -79,12 +79,8 @@ def create_jwt_token(
|
||||
|
||||
async def get_user_service() -> UserService:
|
||||
"""Get user service."""
|
||||
global __user_service # pylint: disable=global-statement # noqa: PLW0603
|
||||
|
||||
if __user_service is None:
|
||||
__user_service = UserService()
|
||||
|
||||
return __user_service
|
||||
return UserService()
|
||||
|
||||
|
||||
async def get_user(
|
||||
|
||||
@@ -166,6 +166,6 @@ def add_command_map(command_runner: CommandRunner, api_router: APIRouter) -> Non
|
||||
api_router.include_router(router=plugins_router.api_router)
|
||||
|
||||
|
||||
system_settings = SystemService.read_default_system_settings()
|
||||
system_settings = SystemService(logging_sub_app="api").system_settings
|
||||
command_runner_instance = CommandRunner(system_settings=system_settings)
|
||||
add_command_map(command_runner=command_runner_instance, api_router=router)
|
||||
|
||||
@@ -8,36 +8,39 @@ from openbb_core.app.model.charts.chart import Chart, ChartFormat
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
from openbb_core.app.service.user_service import UserService
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
POETRY_PLUGIN = "openbb_core_extension"
|
||||
|
||||
|
||||
class ChartingManagerError(Exception):
|
||||
class ChartingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ChartingManager(metaclass=SingletonMeta):
|
||||
class ChartingService(metaclass=SingletonMeta):
|
||||
"""
|
||||
Charting manager class.
|
||||
Charting service class.
|
||||
It is responsible for retrieving and executing the charting function, corresponding
|
||||
to a given route, from the user's preferred charting extension.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_settings : Optional[UserSettings]
|
||||
user_settings : UserSettings
|
||||
User settings.
|
||||
system_settings : SystemSettings
|
||||
System settings.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
_charting_extension : str
|
||||
Charting extension name, which is retrieved from the user preferences.
|
||||
_charting_extension_installed : bool
|
||||
Either charting extension is installed or not.
|
||||
|
||||
Raises
|
||||
------
|
||||
ChartingManagerError
|
||||
ChartingServiceError
|
||||
If charting extension is not installed.
|
||||
"""
|
||||
|
||||
@@ -46,14 +49,19 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
user_settings: Optional[UserSettings] = None,
|
||||
system_settings: Optional[SystemSettings] = None,
|
||||
) -> None:
|
||||
user_settings = user_settings or UserService().read_default_user_settings()
|
||||
system_settings = (
|
||||
system_settings or SystemService().read_default_system_settings()
|
||||
)
|
||||
# Although the __init__ method states that both the user_settings and the system_settings
|
||||
# are optional, they are actually required for the first initialization of the ChartingService.
|
||||
# This is because the ChartingService is a singleton and it is initialized only once.
|
||||
# We want to be able to call the ChartingService from other parts of the code without having
|
||||
# to pass the user_settings and the system_settings every time.
|
||||
if not user_settings or not system_settings:
|
||||
raise ChartingServiceError(
|
||||
"User settings and system settings must be provided."
|
||||
)
|
||||
|
||||
self._charting_settings = ChartingSettings(user_settings, system_settings)
|
||||
self._charting_extension = user_settings.preferences.charting_extension
|
||||
self._charting_extension_installed = self.check_charting_extension_installed(
|
||||
self._charting_extension_installed = self._check_charting_extension_installed(
|
||||
self._charting_extension
|
||||
)
|
||||
|
||||
@@ -70,7 +78,7 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_charting_extension_installed(
|
||||
def _check_charting_extension_installed(
|
||||
charting_extension: str, plugin: str = POETRY_PLUGIN
|
||||
) -> bool:
|
||||
"""
|
||||
@@ -104,7 +112,7 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
return import_module(entry_point.module)
|
||||
|
||||
@classmethod
|
||||
def get_chart_format(cls, extension_name: str) -> ChartFormat:
|
||||
def _get_chart_format(cls, extension_name: str) -> ChartFormat:
|
||||
"""
|
||||
Given an extension name, it returns the chart format.
|
||||
The module must contain the `CHART_FORMAT` attribute.
|
||||
@@ -113,7 +121,7 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
return getattr(module, "CHART_FORMAT")
|
||||
|
||||
@classmethod
|
||||
def get_chart_function(cls, extension_name: str, route: str) -> Callable:
|
||||
def _get_chart_function(cls, extension_name: str, route: str) -> Callable:
|
||||
"""
|
||||
Given an extension name and a route, it returns the chart function.
|
||||
The module must contain the given route.
|
||||
@@ -123,7 +131,7 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
return getattr(module, adjusted_route)
|
||||
|
||||
@staticmethod
|
||||
def handle_backend(charting_extension: str, charting_settings: ChartingSettings):
|
||||
def _handle_backend(charting_extension: str, charting_settings: ChartingSettings):
|
||||
"""
|
||||
Handles the backend of the given charting extension.
|
||||
This function that the module expose in its root (__init__.py) the following functions:
|
||||
@@ -162,17 +170,17 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
|
||||
Raises
|
||||
------
|
||||
ChartingManagerError
|
||||
ChartingServiceError
|
||||
If charting extension is not installed.
|
||||
Exception
|
||||
If the charting extension module does not contain the `to_chart` function.
|
||||
"""
|
||||
|
||||
if not self._charting_extension_installed:
|
||||
raise ChartingManagerError(
|
||||
raise ChartingServiceError(
|
||||
f"Charting extension `{self._charting_extension}` is not installed"
|
||||
)
|
||||
self.handle_backend(self._charting_extension, self._charting_settings)
|
||||
self._handle_backend(self._charting_extension, self._charting_settings)
|
||||
|
||||
# Dynamically import the charting module
|
||||
backend_module = import_module(self._charting_extension)
|
||||
@@ -186,7 +194,7 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
|
||||
return Chart(
|
||||
content=content,
|
||||
format=self.get_chart_format(self._charting_extension),
|
||||
format=self._get_chart_format(self._charting_extension),
|
||||
fig=fig,
|
||||
)
|
||||
|
||||
@@ -224,25 +232,25 @@ class ChartingManager(metaclass=SingletonMeta):
|
||||
"""
|
||||
self._charting_settings = ChartingSettings(user_settings, system_settings)
|
||||
self._charting_extension = user_settings.preferences.charting_extension
|
||||
self._charting_extension_installed = self.check_charting_extension_installed(
|
||||
self._charting_extension_installed = self._check_charting_extension_installed(
|
||||
self._charting_extension
|
||||
)
|
||||
|
||||
if not self._charting_extension_installed:
|
||||
raise ChartingManagerError(
|
||||
raise ChartingServiceError(
|
||||
f"Charting extension `{self._charting_extension}` is not installed"
|
||||
)
|
||||
|
||||
self.handle_backend(self._charting_extension, self._charting_settings)
|
||||
self._handle_backend(self._charting_extension, self._charting_settings)
|
||||
|
||||
kwargs["obbject_item"] = obbject_item
|
||||
kwargs["charting_settings"] = self._charting_settings
|
||||
|
||||
charting_function = self.get_chart_function(self._charting_extension, route)
|
||||
charting_function = self._get_chart_function(self._charting_extension, route)
|
||||
fig, content = charting_function(**kwargs)
|
||||
|
||||
return Chart(
|
||||
content=content,
|
||||
format=self.get_chart_format(self._charting_extension),
|
||||
format=self._get_chart_format(self._charting_extension),
|
||||
fig=fig,
|
||||
)
|
||||
@@ -9,8 +9,8 @@ from typing import Any, Callable, ContextManager, Dict, List, Optional, Tuple, U
|
||||
|
||||
from pydantic import BaseConfig, Extra, create_model
|
||||
|
||||
from openbb_core.app.charting_manager import ChartingManager
|
||||
from openbb_core.app.logs.logging_manager import LoggingManager
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.logs.logging_service import LoggingService
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.warning import cast_warning
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
@@ -230,9 +230,6 @@ class ParametersBuilder:
|
||||
|
||||
|
||||
class StaticCommandRunner:
|
||||
logging_manager: LoggingManager = LoggingManager()
|
||||
charting_manager: ChartingManager = ChartingManager()
|
||||
|
||||
@classmethod
|
||||
def __command(cls, func: Callable, kwargs: Dict[str, Any]) -> OBBject:
|
||||
"""Run a command and return the output"""
|
||||
@@ -264,7 +261,10 @@ class StaticCommandRunner:
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Create a chart from the command output."""
|
||||
obbject.chart = cls.charting_manager.chart(
|
||||
cs = ChartingService(
|
||||
user_settings=user_settings, system_settings=system_settings
|
||||
)
|
||||
obbject.chart = cs.chart(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
@@ -324,7 +324,10 @@ class StaticCommandRunner:
|
||||
except Exception as e:
|
||||
raise OpenBBError(e) from e
|
||||
finally:
|
||||
cls.logging_manager.log(
|
||||
ls = LoggingService(
|
||||
user_settings=user_settings, system_settings=system_settings
|
||||
)
|
||||
ls.log(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
@@ -335,13 +338,6 @@ class StaticCommandRunner:
|
||||
|
||||
return obbject
|
||||
|
||||
@classmethod
|
||||
def __update_managers_settings(
|
||||
cls, system_settings: SystemSettings, user_settings: UserSettings
|
||||
):
|
||||
cls.logging_manager.logging_settings = (system_settings, user_settings)
|
||||
cls.charting_manager.charting_settings = (system_settings, user_settings)
|
||||
|
||||
@classmethod
|
||||
def run(
|
||||
cls,
|
||||
@@ -356,10 +352,6 @@ class StaticCommandRunner:
|
||||
command_map = execution_context.command_map
|
||||
route = execution_context.route
|
||||
|
||||
cls.__update_managers_settings(
|
||||
execution_context.system_settings, execution_context.user_settings
|
||||
)
|
||||
|
||||
if func := command_map.get_command(route=route):
|
||||
obbject = cls.__execute_func(
|
||||
route=route,
|
||||
@@ -392,11 +384,16 @@ class CommandRunner:
|
||||
user_settings: Optional[UserSettings] = None,
|
||||
) -> None:
|
||||
self._command_map = command_map or CommandMap()
|
||||
self._system_settings = (
|
||||
system_settings or SystemService.read_default_system_settings()
|
||||
)
|
||||
self._system_settings = system_settings or SystemService().system_settings
|
||||
self._user_settings = user_settings or UserService.read_default_user_settings()
|
||||
|
||||
self._logging_service = LoggingService(
|
||||
system_settings=self._system_settings, user_settings=self._user_settings
|
||||
)
|
||||
self._charting_service = ChartingService(
|
||||
system_settings=self._system_settings, user_settings=self._user_settings
|
||||
)
|
||||
|
||||
@property
|
||||
def command_map(self) -> CommandMap:
|
||||
return self._command_map
|
||||
@@ -409,6 +406,10 @@ class CommandRunner:
|
||||
def user_settings(self) -> UserSettings:
|
||||
return self._user_settings
|
||||
|
||||
@user_settings.setter
|
||||
def user_settings(self, user_settings: UserSettings) -> None:
|
||||
self._user_settings = user_settings
|
||||
|
||||
def run(
|
||||
self,
|
||||
route: str,
|
||||
|
||||
@@ -111,6 +111,7 @@ class PosthogHandler(logging.Handler):
|
||||
|
||||
log_extra: Dict[str, Any] = {
|
||||
"appName": self._settings.app_name,
|
||||
"subAppName": self._settings.sub_app_name,
|
||||
"appId": self._settings.app_id,
|
||||
"sessionId": self._settings.session_id,
|
||||
"platform": self._settings.platform,
|
||||
|
||||
@@ -13,12 +13,10 @@ from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
from openbb_core.app.service.user_service import UserService
|
||||
from pydantic.json import pydantic_encoder
|
||||
|
||||
|
||||
class LoggingManager(metaclass=SingletonMeta):
|
||||
class LoggingService(metaclass=SingletonMeta):
|
||||
"""
|
||||
Logging Manager class responsible for managing logging settings and handling logs.
|
||||
|
||||
@@ -56,8 +54,8 @@ class LoggingManager(metaclass=SingletonMeta):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
system_settings: Optional[SystemSettings] = None,
|
||||
user_settings: Optional[UserSettings] = None,
|
||||
system_settings: SystemSettings,
|
||||
user_settings: UserSettings,
|
||||
) -> None:
|
||||
"""
|
||||
Logging Manager Constructor
|
||||
@@ -65,17 +63,13 @@ class LoggingManager(metaclass=SingletonMeta):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
system_settings : Optional[SystemSettings], optional
|
||||
system_settings : SystemSettings
|
||||
System Settings, by default None
|
||||
user_settings : Optional[UserSettings], optional
|
||||
user_settings : UserSettings
|
||||
User Settings, by default None
|
||||
"""
|
||||
self._user_settings = (
|
||||
user_settings or UserService().read_default_user_settings()
|
||||
)
|
||||
self._system_settings = (
|
||||
system_settings or SystemService().read_default_system_settings()
|
||||
)
|
||||
self._user_settings = user_settings
|
||||
self._system_settings = system_settings
|
||||
self._logging_settings = LoggingSettings(
|
||||
user_settings=self._user_settings,
|
||||
system_settings=self._system_settings,
|
||||
@@ -33,6 +33,7 @@ class LoggingSettings:
|
||||
|
||||
# System
|
||||
self.app_name: str = system_settings.logging_app_name
|
||||
self.sub_app_name: str = system_settings.logging_sub_app
|
||||
self.app_id: str = get_app_id(user_data_directory)
|
||||
self.session_id: str = get_session_id()
|
||||
self.frequency: str = system_settings.logging_frequency
|
||||
|
||||
@@ -5,7 +5,7 @@ import pandas as pd
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
from openbb_core.app.charting_manager import ChartingManager
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.tagged import Tagged
|
||||
from openbb_core.app.model.abstract.warning import Warning_
|
||||
@@ -146,14 +146,18 @@ class OBBject(GenericModel, Generic[T], Tagged):
|
||||
chart.fig
|
||||
The chart figure.
|
||||
"""
|
||||
cm = ChartingManager()
|
||||
|
||||
cs = ChartingService()
|
||||
kwargs["data"] = self.to_dataframe()
|
||||
|
||||
self.chart = cm.to_chart(**kwargs)
|
||||
self.chart = cs.to_chart(**kwargs)
|
||||
return self.chart.fig
|
||||
|
||||
def show(self):
|
||||
"""Display chart."""
|
||||
if not self.chart or not self.chart.fig:
|
||||
raise OpenBBError("Chart not found.")
|
||||
raise OpenBBError(
|
||||
"Chart not found. "
|
||||
"Please compute the chart first by using the `chart=True` argument."
|
||||
)
|
||||
self.chart.fig.show()
|
||||
|
||||
@@ -33,13 +33,13 @@ class SystemSettings(Tagged):
|
||||
system_settings_path: str = FrozenField(default=str(SYSTEM_SETTINGS_PATH))
|
||||
|
||||
# Logging section
|
||||
logging_app_name: str = FrozenField(default="gst")
|
||||
logging_app_name: Literal["sdk"] = FrozenField(default="sdk")
|
||||
logging_commit_hash: Optional[str] = FrozenField(default=None)
|
||||
logging_frequency: Literal["D", "H", "M", "S"] = FrozenField(default="H")
|
||||
logging_handlers: List[str] = FrozenField(default_factory=lambda: ["file"])
|
||||
logging_rolling_clock: bool = FrozenField(default=False)
|
||||
logging_verbosity: int = FrozenField(default=20)
|
||||
logging_sub_app: str = FrozenField(default="sdk")
|
||||
logging_sub_app: Literal["python", "api", "pro"] = FrozenField(default="python")
|
||||
logging_suppress: bool = FrozenField(default=False)
|
||||
log_collect: bool = FrozenField(default=True)
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from openbb_core.app.constants import SYSTEM_SETTINGS_PATH
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
|
||||
|
||||
class SystemService:
|
||||
class SystemService(metaclass=SingletonMeta):
|
||||
"""System service."""
|
||||
|
||||
SYSTEM_SETTINGS_PATH = SYSTEM_SETTINGS_PATH
|
||||
@@ -14,12 +16,34 @@ class SystemService:
|
||||
"log_collect",
|
||||
"test_mode",
|
||||
"headless",
|
||||
"dbms_uri",
|
||||
"logging_sub_app",
|
||||
}
|
||||
|
||||
PRO_VALIDATION_HASH = (
|
||||
"a35f9683d36f45fa6fac4f62d98c24974a3332e16ddf2832fcf8b119126b83db"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
**kwargs,
|
||||
):
|
||||
self._system_settings = self._read_default_system_settings(
|
||||
path=self.SYSTEM_SETTINGS_PATH, **kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_default_system_settings(
|
||||
cls, path: Optional[Path] = None
|
||||
def _compare_hash(cls, input_value, existing_hash: Optional[str] = None):
|
||||
existing_hash = existing_hash or cls.PRO_VALIDATION_HASH
|
||||
|
||||
hash_object = hashlib.sha256()
|
||||
hash_object.update(input_value.encode("utf-8"))
|
||||
hashed_input = hash_object.hexdigest()
|
||||
|
||||
return hashed_input == existing_hash
|
||||
|
||||
@classmethod
|
||||
def _read_default_system_settings(
|
||||
cls, path: Optional[Path] = None, **kwargs
|
||||
) -> SystemSettings:
|
||||
"""Read default system settings."""
|
||||
path = path or cls.SYSTEM_SETTINGS_PATH
|
||||
@@ -29,14 +53,22 @@ class SystemService:
|
||||
system_settings_json = file.read()
|
||||
|
||||
system_settings_dict = json.loads(system_settings_json)
|
||||
|
||||
S = system_settings_dict.copy()
|
||||
for field in S:
|
||||
if field not in cls.SYSTEM_SETTINGS_ALLOWED_FIELD_SET:
|
||||
del system_settings_dict[field]
|
||||
elif field == "logging_sub_app":
|
||||
if cls._compare_hash(system_settings_dict[field]):
|
||||
system_settings_dict[field] = "pro"
|
||||
kwargs.pop(field, None)
|
||||
else:
|
||||
del system_settings_dict[field]
|
||||
|
||||
system_settings_dict.update(kwargs)
|
||||
system_settings = SystemSettings.parse_obj(system_settings_dict)
|
||||
else:
|
||||
system_settings = SystemSettings()
|
||||
system_settings = SystemSettings.parse_obj(kwargs)
|
||||
|
||||
return system_settings
|
||||
|
||||
@@ -57,13 +89,6 @@ class SystemService:
|
||||
with path.open(mode="w") as file:
|
||||
file.write(system_settings_json)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Path = SYSTEM_SETTINGS_PATH,
|
||||
):
|
||||
self._path = path
|
||||
self._system_settings = self.read_default_system_settings()
|
||||
|
||||
@property
|
||||
def system_settings(self) -> SystemSettings:
|
||||
"""Get system settings."""
|
||||
@@ -76,6 +101,6 @@ class SystemService:
|
||||
|
||||
def refresh_system_settings(self) -> SystemSettings:
|
||||
"""Refresh system settings."""
|
||||
self._system_settings = self.read_default_system_settings()
|
||||
self._system_settings = self._read_default_system_settings()
|
||||
|
||||
return self._system_settings
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, MutableMapping, Optional
|
||||
|
||||
from openbb_core.app.constants import USER_SETTINGS_PATH
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.app.repository.abstract.access_token_repository import (
|
||||
AccessTokenRepository as AbstractAccessTokenRepository,
|
||||
@@ -19,13 +20,26 @@ from openbb_core.app.repository.in_memory.user_settings_repository import (
|
||||
)
|
||||
|
||||
|
||||
class UserService:
|
||||
class UserService(metaclass=SingletonMeta):
|
||||
"""User service."""
|
||||
|
||||
REPOSITORY_DIRECTORY = Path(__file__).parent.parent.parent.parent
|
||||
USER_SETTINGS_PATH = USER_SETTINGS_PATH
|
||||
USER_SETTINGS_ALLOWED_FIELD_SET = {"credentials", "preferences", "defaults"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
access_token_repository: Optional[AbstractAccessTokenRepository] = None,
|
||||
user_settings_repository: Optional[AbstractUserSettingsRepository] = None,
|
||||
default_user_settings: Optional[UserSettings] = None,
|
||||
):
|
||||
self._token_repository = self.build_token_repository(access_token_repository)
|
||||
self._user_settings_repository = self.build_user_settings_repository(
|
||||
user_settings_repository
|
||||
)
|
||||
self._default_user_settings = (
|
||||
default_user_settings or self.read_default_user_settings()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_token_repository(
|
||||
access_token_repository: Optional[AbstractAccessTokenRepository] = None,
|
||||
@@ -100,20 +114,6 @@ class UserService:
|
||||
result = reduce(recursive_merge, (result, d))
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
access_token_repository: Optional[AbstractAccessTokenRepository] = None,
|
||||
user_settings_repository: Optional[AbstractUserSettingsRepository] = None,
|
||||
default_user_settings: Optional[UserSettings] = None,
|
||||
):
|
||||
self._token_repository = self.build_token_repository(access_token_repository)
|
||||
self._user_settings_repository = self.build_user_settings_repository(
|
||||
user_settings_repository
|
||||
)
|
||||
self._default_user_settings = (
|
||||
default_user_settings or self.read_default_user_settings()
|
||||
)
|
||||
|
||||
@property
|
||||
def default_user_settings(self) -> UserSettings:
|
||||
"""Return default user settings."""
|
||||
|
||||
@@ -3,7 +3,6 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from openbb_core.app.command_runner import CommandRunner
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.hub.hub_session import HubSession
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
@@ -79,8 +78,8 @@ class Account:
|
||||
"""
|
||||
hs = self._create_hub_service(email, password, pat)
|
||||
incoming = hs.pull()
|
||||
updated = UserService.update_default(incoming)
|
||||
self._base_app._command_runner = CommandRunner(user_settings=updated)
|
||||
updated: UserSettings = UserService.update_default(incoming)
|
||||
self._base_app._command_runner.user_settings = updated
|
||||
if remember_me:
|
||||
Path(self._openbb_directory).mkdir(parents=False, exist_ok=True)
|
||||
session_file = Path(self._openbb_directory, ".sdk_hub_session.json")
|
||||
@@ -120,13 +119,15 @@ class Account:
|
||||
"""
|
||||
hub_session = self._base_app._command_runner.user_settings.profile.hub_session
|
||||
if not hub_session:
|
||||
self._base_app._command_runner = CommandRunner()
|
||||
self._base_app._command_runner.user_settings = (
|
||||
UserService.read_default_user_settings()
|
||||
)
|
||||
else:
|
||||
hs = HubService(hub_session)
|
||||
incoming = hs.pull()
|
||||
updated = UserService.update_default(incoming)
|
||||
updated: UserSettings = UserService.update_default(incoming)
|
||||
updated.id = self._base_app._command_runner.user_settings.id
|
||||
self._base_app._command_runner = CommandRunner(user_settings=updated)
|
||||
self._base_app._command_runner.user_settings = updated
|
||||
return self._base_app._command_runner.user_settings
|
||||
|
||||
def logout(self) -> UserSettings:
|
||||
@@ -148,5 +149,7 @@ class Account:
|
||||
if session_file.exists():
|
||||
session_file.unlink()
|
||||
|
||||
self._base_app._command_runner = CommandRunner()
|
||||
self._base_app._command_runner.user_settings = (
|
||||
UserService.read_default_user_settings()
|
||||
)
|
||||
return self._base_app._command_runner.user_settings
|
||||
|
||||
@@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from openbb_core.api.dependency.system import (
|
||||
SystemSettings,
|
||||
get_system_service,
|
||||
get_system_settings,
|
||||
)
|
||||
|
||||
@@ -13,22 +12,8 @@ from openbb_core.api.dependency.system import (
|
||||
def test_get_system_settings(mock_system_service):
|
||||
"""Test get_system_settings."""
|
||||
mock_system_settings = MagicMock(spec=SystemSettings)
|
||||
mock_system_service.system_settings = mock_system_settings
|
||||
mock_system_service.return_value.system_settings = mock_system_settings
|
||||
|
||||
with patch(
|
||||
"openbb_core.api.dependency.system.__system_service", new=mock_system_service
|
||||
):
|
||||
system_settings = asyncio.run(get_system_settings())
|
||||
system_settings = asyncio.run(get_system_settings())
|
||||
|
||||
assert system_settings == mock_system_settings
|
||||
|
||||
|
||||
@patch("openbb_core.api.dependency.system.SystemService")
|
||||
def test_get_system_service(mock_system_service):
|
||||
"""Test get_system_service."""
|
||||
with patch(
|
||||
"openbb_core.api.dependency.system.__system_service", new=mock_system_service
|
||||
):
|
||||
system_service = asyncio.run(get_system_service())
|
||||
|
||||
assert system_service == mock_system_service
|
||||
assert system_settings == mock_system_settings
|
||||
|
||||
@@ -61,8 +61,10 @@ def test_create_jwt_token():
|
||||
@patch("openbb_core.api.dependency.user.UserService")
|
||||
def test_get_user_service(mock_user_service):
|
||||
"""Test get_user_service."""
|
||||
with patch("openbb_core.api.dependency.user.__user_service", new=None):
|
||||
asyncio.run(get_user_service())
|
||||
|
||||
mock_user_service.return_value = MagicMock()
|
||||
|
||||
asyncio.run(get_user_service())
|
||||
|
||||
mock_user_service.assert_called_once_with()
|
||||
|
||||
@@ -74,7 +76,7 @@ def test_get_user(mock_user_service, mock_decode):
|
||||
mock_decode.return_value = {"sub": "user_id"}
|
||||
mock_user_settings = MagicMock(spec=UserSettings, profile=MagicMock(active=True))
|
||||
mock_user_service.user_settings_repository.read.return_value = mock_user_settings
|
||||
with patch("openbb_core.api.dependency.user.__user_service", new=mock_user_service):
|
||||
result = asyncio.run(get_user("jwt_token", mock_user_service))
|
||||
mock_user_service.return_value = mock_user_service
|
||||
result = asyncio.run(get_user("jwt_token", mock_user_service))
|
||||
|
||||
assert result == mock_user_settings
|
||||
|
||||
@@ -12,6 +12,7 @@ class MockLoggingSettings:
|
||||
def __init__(
|
||||
self,
|
||||
app_name,
|
||||
sub_app_name,
|
||||
user_logs_directory,
|
||||
session_id,
|
||||
frequency,
|
||||
@@ -22,6 +23,7 @@ class MockLoggingSettings:
|
||||
userid,
|
||||
):
|
||||
self.app_name = app_name
|
||||
self.sub_app_name = sub_app_name
|
||||
self.user_logs_directory = Path(user_logs_directory)
|
||||
self.session_id = session_id
|
||||
self.frequency = frequency
|
||||
@@ -34,6 +36,7 @@ class MockLoggingSettings:
|
||||
|
||||
logging_settings = MagicMock(spec=MockLoggingSettings)
|
||||
logging_settings.app_name = "TestApp"
|
||||
logging_settings.sub_app_name = "TestSubApp"
|
||||
logging_settings.user_logs_directory = MagicMock()
|
||||
logging_settings.user_logs_directory.absolute.return_value = Path(
|
||||
"/mocked/logs/directory"
|
||||
@@ -195,6 +198,7 @@ def test_log_to_dict(handler, log_info, expected_dict):
|
||||
),
|
||||
{
|
||||
"appName": "TestApp",
|
||||
"subAppName": "TestSubApp",
|
||||
"appId": "test123",
|
||||
"sessionId": "session123",
|
||||
"platform": "Windows",
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.logs.logging_manager import LoggingManager
|
||||
from openbb_core.app.logs.logging_service import LoggingService
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -22,7 +22,7 @@ class MockOBBject(BaseModel):
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def logging_manager():
|
||||
def logging_service():
|
||||
mock_system_settings = Mock()
|
||||
mock_system_settings = "mock_system_settings"
|
||||
mock_user_settings = Mock()
|
||||
@@ -31,16 +31,16 @@ def logging_manager():
|
||||
mock_log_startup = Mock()
|
||||
|
||||
with patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingSettings",
|
||||
"openbb_core.app.logs.logging_service.LoggingSettings",
|
||||
MockLoggingSettings,
|
||||
), patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingManager._setup_handlers",
|
||||
"openbb_core.app.logs.logging_service.LoggingService._setup_handlers",
|
||||
mock_setup_handlers,
|
||||
), patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingManager._log_startup",
|
||||
"openbb_core.app.logs.logging_service.LoggingService._log_startup",
|
||||
mock_log_startup,
|
||||
):
|
||||
logging_manager = LoggingManager(
|
||||
logging_service = LoggingService(
|
||||
system_settings=mock_system_settings,
|
||||
user_settings=mock_user_settings,
|
||||
)
|
||||
@@ -48,31 +48,31 @@ def logging_manager():
|
||||
assert mock_setup_handlers.assert_called_once
|
||||
assert mock_log_startup.assert_called_once
|
||||
|
||||
return logging_manager
|
||||
return logging_service
|
||||
|
||||
|
||||
def test_correctly_initialized(logging_manager):
|
||||
assert logging_manager
|
||||
def test_correctly_initialized(logging_service):
|
||||
assert logging_service
|
||||
|
||||
|
||||
def test_logging_settings_setter(logging_manager):
|
||||
def test_logging_settings_setter(logging_service):
|
||||
custom_user_settings = "custom_user_settings"
|
||||
custom_system_settings = "custom_system_settings"
|
||||
|
||||
with patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingSettings",
|
||||
"openbb_core.app.logs.logging_service.LoggingSettings",
|
||||
MockLoggingSettings,
|
||||
):
|
||||
logging_manager.logging_settings = (
|
||||
logging_service.logging_settings = (
|
||||
custom_system_settings,
|
||||
custom_user_settings,
|
||||
)
|
||||
|
||||
assert logging_manager.logging_settings.system_settings == "custom_system_settings"
|
||||
assert logging_manager.logging_settings.user_settings == "custom_user_settings"
|
||||
assert logging_service.logging_settings.system_settings == "custom_system_settings"
|
||||
assert logging_service.logging_settings.user_settings == "custom_user_settings"
|
||||
|
||||
|
||||
def test_log_startup(logging_manager):
|
||||
def test_log_startup(logging_service):
|
||||
with patch("logging.getLogger") as mock_get_logger:
|
||||
mock_info = mock_get_logger.return_value.info
|
||||
|
||||
@@ -80,13 +80,13 @@ def test_log_startup(logging_manager):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
logging_manager._user_settings = MagicMock(
|
||||
logging_service._user_settings = MagicMock(
|
||||
preferences="your_preferences",
|
||||
credentials=MockCredentials(username="username", password="password"),
|
||||
)
|
||||
logging_manager._system_settings = "your_system_settings"
|
||||
logging_service._system_settings = "your_system_settings"
|
||||
|
||||
logging_manager._log_startup()
|
||||
logging_service._log_startup()
|
||||
|
||||
expected_log_data = {
|
||||
"PREFERENCES": "your_preferences",
|
||||
@@ -130,17 +130,17 @@ def test_log_startup(logging_manager):
|
||||
],
|
||||
)
|
||||
def test_log(
|
||||
logging_manager, user_settings, system_settings, route, func, kwargs, exec_info
|
||||
logging_service, user_settings, system_settings, route, func, kwargs, exec_info
|
||||
):
|
||||
with patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingSettings",
|
||||
"openbb_core.app.logs.logging_service.LoggingSettings",
|
||||
MockLoggingSettings,
|
||||
), patch("logging.getLogger") as mock_get_logger:
|
||||
if route == "login":
|
||||
with patch(
|
||||
"openbb_core.app.logs.logging_manager.LoggingManager._log_startup"
|
||||
"openbb_core.app.logs.logging_service.LoggingService._log_startup"
|
||||
) as mock_log_startup:
|
||||
logging_manager.log(
|
||||
logging_service.log(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
@@ -157,7 +157,7 @@ def test_log(
|
||||
mock_callable = Mock()
|
||||
mock_callable.__name__ = func
|
||||
|
||||
logging_manager.log(
|
||||
logging_service.log(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
@@ -232,33 +232,10 @@ def test_to_dict(results, expected):
|
||||
assert result == expected
|
||||
|
||||
|
||||
@patch("openbb_core.app.charting_manager.ChartingManager.to_chart")
|
||||
@patch("openbb_core.app.model.obbject.OBBject.to_dataframe")
|
||||
def test_to_chart_with_existing_chart(mock_to_dataframe, mock_to_chart):
|
||||
"""Test helper."""
|
||||
# Arrange
|
||||
mock_instance = OBBject()
|
||||
mock_instance.chart = Chart(content={"existing_chart_data": "some_data"})
|
||||
|
||||
# mock the return value of ChartingManager.to_chart()
|
||||
mock_to_chart.return_value = Chart(
|
||||
content={"content": "some_new_content"}, fig={"fig": "some_mock_fig"}
|
||||
)
|
||||
|
||||
# Act
|
||||
result = mock_instance.to_chart()
|
||||
|
||||
# Assert
|
||||
assert result == {"fig": "some_mock_fig"}
|
||||
assert mock_instance.chart.content == {"content": "some_new_content"}
|
||||
|
||||
mock_to_dataframe.assert_called_once()
|
||||
|
||||
|
||||
@patch("openbb_core.app.model.obbject.OBBject.to_dataframe")
|
||||
@patch("openbb_core.app.model.obbject.ChartingManager")
|
||||
@patch("openbb_core.app.model.obbject.ChartingService")
|
||||
def test_to_chart_with_new_chart(
|
||||
mock_charting_manager,
|
||||
mock_charting_service,
|
||||
mock_to_dataframe,
|
||||
):
|
||||
"""Test helper."""
|
||||
@@ -274,8 +251,8 @@ def test_to_chart_with_new_chart(
|
||||
|
||||
# Arrange
|
||||
mock_instance = OBBject()
|
||||
mock_charting_manager_instance = mock_charting_manager.return_value
|
||||
mock_charting_manager_instance.to_chart.return_value = Chart(
|
||||
mock_charting_service_instance = mock_charting_service.return_value
|
||||
mock_charting_service_instance.to_chart.return_value = Chart(
|
||||
content={"content": "some_new_content"}, fig={"fig": "some_mock_fig"}
|
||||
)
|
||||
mock_to_dataframe.return_value = get_mock_dataframe()
|
||||
@@ -290,8 +267,8 @@ def test_to_chart_with_new_chart(
|
||||
# Ensure self.to_dataframe() was called
|
||||
mock_to_dataframe.assert_called_once()
|
||||
|
||||
# Ensure ChartingManager was called with the correct parameters
|
||||
mock_charting_manager_instance.to_chart.assert_called_once()
|
||||
# Ensure ChartingService was called with the correct parameters
|
||||
mock_charting_service_instance.to_chart.assert_called_once()
|
||||
|
||||
|
||||
def test_show_chart_exists():
|
||||
|
||||
@@ -18,14 +18,14 @@ def test_system_service_init(system_service):
|
||||
|
||||
def test_read_default_system_settings(system_service):
|
||||
"""Test read default system settings."""
|
||||
system_settings = system_service.read_default_system_settings()
|
||||
system_settings = system_service._read_default_system_settings()
|
||||
|
||||
assert system_settings
|
||||
|
||||
|
||||
def test_write_default_system_settings(system_service):
|
||||
"""Test write default system settings."""
|
||||
system_settings = system_service.read_default_system_settings()
|
||||
system_settings = system_service._read_default_system_settings()
|
||||
system_service.write_default_system_settings(system_settings=system_settings)
|
||||
|
||||
assert system_service
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Test charting manager."""
|
||||
import pytest
|
||||
from openbb_core.app.charting_manager import ChartingManager
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def setup_and_teardown():
|
||||
"""Prepare test context."""
|
||||
# Code to run before each test function
|
||||
yield # This is where the test function runs
|
||||
# Code to run after each test function
|
||||
ChartingManager._instances = {} # pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_charting_manager():
|
||||
"""Smoke test."""
|
||||
assert ChartingManager() is not None
|
||||
|
||||
|
||||
def test_charting_manager_singleton_prop():
|
||||
"""Smoke test."""
|
||||
cm_1 = ChartingManager()
|
||||
cm_2 = ChartingManager()
|
||||
|
||||
assert cm_1 is cm_2
|
||||
|
||||
|
||||
def test_charting_settings():
|
||||
"""Test helper."""
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
cm = ChartingManager(user_settings=user, system_settings=sys)
|
||||
|
||||
assert cm.charting_settings.plot_enable_pywry is False
|
||||
assert cm.charting_settings.test_mode is True
|
||||
|
||||
|
||||
def test_charting_settings_setter():
|
||||
"""Test helper."""
|
||||
cm = ChartingManager()
|
||||
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
cm.charting_settings = (sys, user)
|
||||
|
||||
assert cm.charting_settings.plot_enable_pywry is False
|
||||
assert cm.charting_settings.test_mode is True
|
||||
|
||||
|
||||
def test_check_charting_extension_installed():
|
||||
"""Test helper."""
|
||||
cm = ChartingManager()
|
||||
|
||||
try:
|
||||
import openbb_charting # pylint: disable=unused-import,import-outside-toplevel # noqa: F401,E501
|
||||
|
||||
assert cm.check_charting_extension_installed("charting") is True
|
||||
except ImportError:
|
||||
assert cm.check_charting_extension_installed("charting") is False
|
||||
assert cm.check_charting_extension_installed("openbb_core_extension_2") is False
|
||||
|
||||
63
openbb_sdk/sdk/core/tests/app/test_charting_service.py
Normal file
63
openbb_sdk/sdk/core/tests/app/test_charting_service.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def setup_and_teardown():
|
||||
# Code to run before each test function
|
||||
yield # This is where the test function runs
|
||||
# Code to run after each test function
|
||||
ChartingService._instances = {}
|
||||
|
||||
|
||||
def test_charting_service():
|
||||
sys = SystemSettings()
|
||||
user = UserSettings()
|
||||
|
||||
assert ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
|
||||
def test_charting_service_singleton_prop():
|
||||
sys = SystemSettings()
|
||||
user = UserSettings()
|
||||
|
||||
cm_1 = ChartingService(user_settings=user, system_settings=sys)
|
||||
cm_2 = ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
assert cm_1 is cm_2
|
||||
|
||||
|
||||
def test_charting_settings():
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
cm = ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
assert cm.charting_settings.plot_enable_pywry is False
|
||||
assert cm.charting_settings.test_mode is True
|
||||
|
||||
|
||||
def test_charting_settings_setter():
|
||||
sys = SystemSettings()
|
||||
user = UserSettings()
|
||||
cm = ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
cm.charting_settings = (sys, user)
|
||||
|
||||
assert cm.charting_settings.plot_enable_pywry is False
|
||||
assert cm.charting_settings.test_mode is True
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="function needs review")
|
||||
def test_check_charting_extension_installed():
|
||||
sys = SystemSettings()
|
||||
user = UserSettings()
|
||||
cm = ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
assert cm._check_charting_extension_installed("openbb_charting") is True
|
||||
assert cm._check_charting_extension_installed("openbb_core_extension_2") is False
|
||||
@@ -1,4 +1,5 @@
|
||||
from inspect import Parameter
|
||||
from typing import Dict
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -26,7 +27,9 @@ def execution_context():
|
||||
def mock_func():
|
||||
"""Set up mock function."""
|
||||
|
||||
def mock_func(a: int, b: int, c: float = 10.0, d: int = 5) -> None:
|
||||
def mock_func(
|
||||
a: int, b: int, c: float = 10.0, d: int = 5, provider_choices: Dict = {}
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
return mock_func
|
||||
@@ -138,30 +141,27 @@ def test_parameters_builder_merge_args_and_kwargs(
|
||||
{"cc": "existing_cc"},
|
||||
SystemSettings(),
|
||||
UserSettings(),
|
||||
{"cc": "existing_cc"},
|
||||
),
|
||||
({}, SystemSettings(), UserSettings(), {}),
|
||||
(
|
||||
{"other_arg": "value"},
|
||||
SystemSettings(),
|
||||
UserSettings(),
|
||||
{"other_arg": "value"},
|
||||
{"cc": "mock_cc"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parameters_builder_update_command_context(
|
||||
kwargs, system_settings, user_settings, expected_result
|
||||
):
|
||||
def other_mock_func(
|
||||
cc: CommandContext,
|
||||
a: int,
|
||||
b: int,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
result = ParametersBuilder.update_command_context(
|
||||
kwargs, system_settings, user_settings
|
||||
other_mock_func, kwargs, system_settings, user_settings
|
||||
)
|
||||
|
||||
if "cc" in expected_result:
|
||||
assert isinstance(result["cc"], CommandContext)
|
||||
assert result["cc"].system_settings == system_settings
|
||||
assert result["cc"].user_settings == user_settings
|
||||
else:
|
||||
assert result == expected_result
|
||||
assert isinstance(result["cc"], CommandContext)
|
||||
assert result["cc"].system_settings == system_settings
|
||||
assert result["cc"].user_settings == user_settings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -172,28 +172,21 @@ def test_parameters_builder_update_command_context(
|
||||
"route1",
|
||||
{"provider_choices": {"provider": None}},
|
||||
None,
|
||||
{"provider_choices": {"provider": "choice1"}},
|
||||
{"provider_choices": {"provider": None}},
|
||||
),
|
||||
(
|
||||
{"route1": ["choice1", "choice2"]},
|
||||
"route1",
|
||||
{},
|
||||
{"provider": "default_provider"},
|
||||
{"provider_choices": {"provider": "default_provider"}},
|
||||
),
|
||||
(
|
||||
{},
|
||||
"route2",
|
||||
{"provider_choices": {"provider": None}},
|
||||
None,
|
||||
{"provider_choices": {"provider": "provider1"}},
|
||||
{"provider_choices": {"provider": ["choice1", "choice2"]}},
|
||||
{"provider": "choice1"},
|
||||
{"provider_choices": {"provider": ["choice1", "choice2"]}},
|
||||
),
|
||||
(
|
||||
{},
|
||||
"route2",
|
||||
{},
|
||||
{"provider": "default_provider"},
|
||||
{"provider_choices": {"provider": "provider1"}},
|
||||
{},
|
||||
),
|
||||
(
|
||||
{},
|
||||
@@ -202,13 +195,6 @@ def test_parameters_builder_update_command_context(
|
||||
None,
|
||||
{"provider_choices": {"provider": "existing_provider"}},
|
||||
),
|
||||
(
|
||||
{"route4": []},
|
||||
"route4",
|
||||
{},
|
||||
None,
|
||||
{"provider_choices": {"provider": None}},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parameters_builder_update_provider_choices(
|
||||
@@ -219,7 +205,7 @@ def test_parameters_builder_update_provider_choices(
|
||||
**{"return_value.available_providers": ["provider1", "provider2"]}
|
||||
):
|
||||
result = ParametersBuilder.update_provider_choices(
|
||||
command_coverage, route, kwargs, route_default
|
||||
mock_func, command_coverage, route, kwargs, route_default
|
||||
)
|
||||
|
||||
assert result == expected_result
|
||||
@@ -234,7 +220,7 @@ def test_parameters_builder_validate_kwargs(mock_func):
|
||||
mock_func, {"a": 1, "b": "2", "c": 3.0, "d": 4.3}
|
||||
)
|
||||
|
||||
assert result == {"a": 1, "b": 2, "c": 3.0, "d": 4}
|
||||
assert result == {"a": 1, "b": 2, "c": 3.0, "d": 4, "provider_choices": {}}
|
||||
|
||||
|
||||
def test_parameters_builder_build(mock_func, execution_context):
|
||||
@@ -248,7 +234,11 @@ def test_parameters_builder_build(mock_func, execution_context):
|
||||
):
|
||||
result = ParametersBuilder.build(
|
||||
args=[1, 2],
|
||||
kwargs={"c": 3, "d": "4"},
|
||||
kwargs={
|
||||
"c": 3,
|
||||
"d": "4",
|
||||
"provider_choices": {"provider": ["provider1", "provider2"]},
|
||||
},
|
||||
func=mock_func,
|
||||
execution_context=execution_context,
|
||||
route="mock/route",
|
||||
@@ -259,7 +249,7 @@ def test_parameters_builder_build(mock_func, execution_context):
|
||||
"b": 2,
|
||||
"c": 3.0,
|
||||
"d": 4,
|
||||
"provider_choices": {"provider": "provider1"},
|
||||
"provider_choices": {"provider": ["provider1", "provider2"]},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""The OpenBB Standardized Data Model."""
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
from openbb_provider.utils.helpers import to_snake_case
|
||||
from openbb_provider.utils.helpers import to_camel_case, to_snake_case
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
@@ -13,15 +13,11 @@ class Data(BaseModel):
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the object."""
|
||||
return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items()])})"
|
||||
return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in super().dict().items()])})"
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
|
||||
extra = Extra.allow
|
||||
allow_population_by_field_name = True
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Initialize the model."""
|
||||
data = {to_snake_case(k): v for k, v in data.items()}
|
||||
super().__init__(**data)
|
||||
alias_generator = to_camel_case
|
||||
|
||||
@@ -18,8 +18,8 @@ class OptionsChainsData(Data):
|
||||
|
||||
expiration: datetime = Field(description="Expiration date of the contract.")
|
||||
strike: float = Field(description="Strike price of the contract.")
|
||||
optionType: str = Field(description="Call or Put.")
|
||||
option_type: str = Field(description="Call or Put.")
|
||||
bid: float = Field(description="Bid price of the contract.")
|
||||
ask: float = Field(description="Ask price of the contract.")
|
||||
openInterest: float = Field(description="Open interest on the contract.")
|
||||
open_interest: float = Field(description="Open interest on the contract.")
|
||||
volume: float = Field(description="Current trading volume on the contract.")
|
||||
|
||||
@@ -26,5 +26,9 @@ class PriceTargetData(Data, BaseSymbol):
|
||||
price_target: Optional[float] = Field(description="Price target.")
|
||||
adj_price_target: Optional[float] = Field(description="Adjusted price target.")
|
||||
price_when_posted: Optional[float] = Field(description="Price when posted.")
|
||||
news_publisher: str = Field(description="News publisher of the price target.")
|
||||
news_base_url: str = Field(description="News base URL of the price target.")
|
||||
news_publisher: Optional[str] = Field(
|
||||
description="News publisher of the price target."
|
||||
)
|
||||
news_base_url: Optional[str] = Field(
|
||||
description="News base URL of the price target."
|
||||
)
|
||||
|
||||
@@ -107,3 +107,21 @@ def to_snake_case(string: str) -> str:
|
||||
.replace(" ", "_")
|
||||
.replace("__", "_")
|
||||
)
|
||||
|
||||
|
||||
def to_camel_case(string: str):
|
||||
"""Convert a string to camel case. If all characters are lower case we return title
|
||||
version of the string.
|
||||
|
||||
Examples
|
||||
--------
|
||||
"close" -> "Close"
|
||||
"some_field" -> "someField"
|
||||
"""
|
||||
s_list = string.replace("-", " ").replace("_", " ").split()
|
||||
if len(string) == 0:
|
||||
return string
|
||||
result = s_list[0] + "".join(i.capitalize() for i in s_list[1:])
|
||||
if result.islower():
|
||||
return result.title()
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user