Merge and fixes

This commit is contained in:
Igor Radovanovic
2023-09-04 12:16:47 +02:00
42 changed files with 843 additions and 366 deletions

9
CONTRIBUTING.md vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,8 +28,6 @@ class FMPStockQuoteData(StockQuoteData):
"""Pydantic alias config using fields dict."""
fields = {
"price_avg50": "priceAvg50",
"price_avg200": "priceAvg200",
"date": "timestamp",
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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