From a7236c110b0d54efacc4851567b6c83269e3d59a Mon Sep 17 00:00:00 2001 From: Roman Yavnikov <45608740+Romazes@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:20:20 +0300 Subject: [PATCH] Feature: support new config type `account-id` (#500) * feat: support `account-id` config * refactor: reuse interactive logic of `input-account-id` * refactor: update account_ids in AuthConfiguration feat: skip account ids if it is not provided * test:feat: Auth0Client * feat: support Prompt in AccountIdsConfiguration * feat: handle accountIds and provide different option to user * refactor: handle of accountIds in json_module * refactor: validate len api_accounts_ids refactor: use elif instead of if * feat: handle when api return None of api_account_ids * feat: skip validation of choices (not provided) * feat: filter dict config by regex condition * refactor: update of api_account_ids * remove: not used class AccountIdsConfiguration * remove: missed import class * remove: not used import * test:refactor: test_auth0_client * feat: new config of TredeStation brokerage in README * refactor: change spacing * feat: class Accounts in auth0_client * feat: skip choice type without choices * feat: new object account in Auth0 model refactor: parsing of new object Auth0 test:refactor: use new mock object of Auth0 * refactor: QCAuth0Authorization model * test:feat: add alpaca configuration test * remove: trade-station-account-type parameter (deprecated) * feat: add filter_dependency flag * refactor: remove optional in trade-station-account-id --- README.md | 48 +++++++------- lean/models/api.py | 28 ++++++++- lean/models/click_options.py | 3 + lean/models/configuration.py | 6 ++ lean/models/json_module.py | 35 +++++++++-- tests/components/api/test_auth0_client.py | 76 +++++++++++++++++++++++ 6 files changed, 166 insertions(+), 30 deletions(-) create mode 100644 tests/components/api/test_auth0_client.py diff --git a/README.md b/README.md index 5629fa17..86ecd8d5 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,6 @@ Options: Your ThetaData subscription price plan --terminal-link-connection-type [DAPI|SAPI] Terminal Link Connection Type [DAPI, SAPI] - --terminal-link-server-auth-id TEXT - The Auth ID of the TerminalLink server --terminal-link-environment [Production|Beta] The environment to run in --terminal-link-server-host TEXT @@ -208,12 +206,14 @@ Options: The port of the TerminalLink server --terminal-link-openfigi-api-key TEXT The Open FIGI API key to use for mapping options + --terminal-link-server-auth-id TEXT + The Auth ID of the TerminalLink server --bybit-api-key TEXT Your Bybit API key --bybit-api-secret TEXT Your Bybit API secret --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --download-data Update the Lean configuration file to download data from the QuantConnect API, alias @@ -435,8 +435,8 @@ Options: Your Bybit VIP Level --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --polygon-api-key TEXT Your Polygon.io API Key @@ -900,8 +900,6 @@ Options: Your ThetaData subscription price plan --terminal-link-connection-type [DAPI|SAPI] Terminal Link Connection Type [DAPI, SAPI] - --terminal-link-server-auth-id TEXT - The Auth ID of the TerminalLink server --terminal-link-environment [Production|Beta] The environment to run in --terminal-link-server-host TEXT @@ -910,12 +908,14 @@ Options: The port of the TerminalLink server --terminal-link-openfigi-api-key TEXT The Open FIGI API key to use for mapping options + --terminal-link-server-auth-id TEXT + The Auth ID of the TerminalLink server --bybit-api-key TEXT Your Bybit API key --bybit-api-secret TEXT Your Bybit API secret --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --dataset TEXT The name of the dataset to download non-interactively @@ -1341,8 +1341,6 @@ Options: commodities on MCX --terminal-link-connection-type [DAPI|SAPI] Terminal Link Connection Type [DAPI, SAPI] - --terminal-link-server-auth-id TEXT - The Auth ID of the TerminalLink server --terminal-link-environment [Production|Beta] The environment to run in --terminal-link-server-host TEXT @@ -1356,20 +1354,22 @@ Options: --terminal-link-emsx-team TEXT The EMSX team to receive order events from (Optional). --terminal-link-openfigi-api-key TEXT The Open FIGI API key to use for mapping options + --terminal-link-server-auth-id TEXT + The Auth ID of the TerminalLink server --tt-user-name TEXT Your Trading Technologies username --tt-session-password TEXT Your Trading Technologies session password --tt-account-name TEXT Your Trading Technologies account name --tt-rest-app-key TEXT Your Trading Technologies REST app key --tt-rest-app-secret TEXT Your Trading Technologies REST app secret --tt-rest-environment TEXT The REST environment to run in + --tt-order-routing-sender-comp-id TEXT + The order routing sender comp id to use --tt-market-data-sender-comp-id TEXT The market data sender comp id to use --tt-market-data-target-comp-id TEXT The market data target comp id to use --tt-market-data-host TEXT The host of the market data server --tt-market-data-port TEXT The port of the market data server - --tt-order-routing-sender-comp-id TEXT - The order routing sender comp id to use --tt-order-routing-target-comp-id TEXT The order routing target comp id to use --tt-order-routing-host TEXT The host of the order routing server @@ -1392,8 +1392,8 @@ Options: Whether the testnet should be used --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --ib-enable-delayed-streaming-data BOOLEAN @@ -1791,8 +1791,6 @@ Options: Your ThetaData subscription price plan --terminal-link-connection-type [DAPI|SAPI] Terminal Link Connection Type [DAPI, SAPI] - --terminal-link-server-auth-id TEXT - The Auth ID of the TerminalLink server --terminal-link-environment [Production|Beta] The environment to run in --terminal-link-server-host TEXT @@ -1801,12 +1799,14 @@ Options: The port of the TerminalLink server --terminal-link-openfigi-api-key TEXT The Open FIGI API key to use for mapping options + --terminal-link-server-auth-id TEXT + The Auth ID of the TerminalLink server --bybit-api-key TEXT Your Bybit API key --bybit-api-secret TEXT Your Bybit API secret --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json) @@ -1963,8 +1963,6 @@ Options: Your ThetaData subscription price plan --terminal-link-connection-type [DAPI|SAPI] Terminal Link Connection Type [DAPI, SAPI] - --terminal-link-server-auth-id TEXT - The Auth ID of the TerminalLink server --terminal-link-environment [Production|Beta] The environment to run in --terminal-link-server-host TEXT @@ -1973,12 +1971,14 @@ Options: The port of the TerminalLink server --terminal-link-openfigi-api-key TEXT The Open FIGI API key to use for mapping options + --terminal-link-server-auth-id TEXT + The Auth ID of the TerminalLink server --bybit-api-key TEXT Your Bybit API key --bybit-api-secret TEXT Your Bybit API secret --trade-station-environment [live|paper] Whether Live or Paper environment should be used - --trade-station-account-type [Cash|Margin|Futures|DVP] - Specifies the type of account on TradeStation + --trade-station-account-id TEXT + The TradeStation account Id --alpaca-environment [live|paper] Whether Live or Paper environment should be used --download-data Update the Lean configuration file to download data from the QuantConnect API, alias diff --git a/lean/models/api.py b/lean/models/api.py index 14851de3..99a69509 100644 --- a/lean/models/api.py +++ b/lean/models/api.py @@ -21,8 +21,34 @@ # The models in this module are all parts of responses from the QuantConnect API # The keys of properties are not changed, so they don't obey the rest of the project's naming conventions + class QCAuth0Authorization(WrappedBaseModel): - authorization: Optional[Dict[str, str]] + authorization: Optional[Dict[str, Any]] + + def get_account_ids(self) -> List[str]: + """ + Retrieves a list of account IDs from the list of Account objects. + + This method returns only the 'id' values from each account in the 'accounts' list. + If there are no accounts, it returns an empty list. + + Returns: + List[str]: A list of account IDs. + """ + accounts = self.authorization.get('accounts', []) + return [account["id"] for account in accounts] if accounts else [] + + def get_authorization_config_without_account(self) -> Dict[str, str]: + """ + Returns the authorization data without the 'accounts' key. + + Iterates through the 'authorization' dictionary and excludes the 'accounts' entry. + + Returns: + Dict[str, str]: Authorization details excluding 'accounts'. + """ + return {key: value for key, value in self.authorization.items() if key != 'accounts'} + class ProjectEncryptionKey(WrappedBaseModel): id: str diff --git a/lean/models/click_options.py b/lean/models/click_options.py index 15d95349..65526334 100644 --- a/lean/models/click_options.py +++ b/lean/models/click_options.py @@ -56,6 +56,9 @@ def get_click_option_type(configuration: Configuration): if configuration._input_method == "confirm": return bool elif configuration._input_method == "choice": + # Skip validation if no predefined choices in config and user provided input manually + if not configuration._choices: + return str return Choice(configuration._choices, case_sensitive=False) elif configuration._input_method == "prompt": return configuration.get_input_type() diff --git a/lean/models/configuration.py b/lean/models/configuration.py index 867abb29..b3cd23d7 100644 --- a/lean/models/configuration.py +++ b/lean/models/configuration.py @@ -96,10 +96,12 @@ def __init__(self, config_json_object): self._is_required_from_user = False self._save_persistently_in_lean = False self._log_message: str = "" + self.has_filter_dependency: bool = False if "log-message" in config_json_object.keys(): self._log_message = config_json_object["log-message"] if "filters" in config_json_object.keys(): self._filter = Filter(config_json_object["filters"]) + self.has_filter_dependency = Filter.has_conditions else: self._filter = Filter([]) self._input_default = config_json_object["input-default"] if "input-default" in config_json_object else None @@ -137,6 +139,10 @@ def __init__(self, filter_conditions): self._conditions: List[BaseCondition] = [BaseCondition.factory( condition["condition"]) for condition in filter_conditions] + @property + def has_conditions(self) -> bool: + """Returns True if there are any conditions, False otherwise.""" + return bool(self._conditions) class InfoConfiguration(Configuration): """Configuration class used for informational configurations. diff --git a/lean/models/json_module.py b/lean/models/json_module.py index 8b18f4ef..c07d1777 100644 --- a/lean/models/json_module.py +++ b/lean/models/json_module.py @@ -22,7 +22,7 @@ from lean.constants import MODULE_TYPE, MODULE_PLATFORM, MODULE_CLI_PLATFORM from lean.container import container from lean.models.configuration import BrokerageEnvConfiguration, Configuration, InternalInputUserInput, \ - PathParameterUserInput, AuthConfiguration + PathParameterUserInput, AuthConfiguration, ChoiceUserInput from copy import copy from abc import ABC @@ -60,13 +60,17 @@ def get_id(self): def sort_configs(self) -> List[Configuration]: sorted_configs = [] + filter_configs = [] brokerage_configs = [] for config in self._lean_configs: if isinstance(config, BrokerageEnvConfiguration): brokerage_configs.append(config) else: - sorted_configs.append(config) - return brokerage_configs + sorted_configs + if config.has_filter_dependency: + filter_configs.append(config) + else: + sorted_configs.append(config) + return brokerage_configs + sorted_configs + filter_configs def get_name(self) -> str: """Returns the user-friendly name which users can identify this object by. @@ -86,7 +90,11 @@ def _check_if_config_passes_filters(self, config: Configuration, all_for_platfor # skip, we want all configurations that match type and platform, for help continue target_value = self.get_config_value_from_name(condition._dependent_config_id) - if not target_value or not condition.check(target_value): + if not target_value: + return False + elif isinstance(target_value, dict): + return all(condition.check(value) for value in target_value.values()) + elif not condition.check(target_value): return False return True @@ -207,10 +215,27 @@ def config_build(self, _logged_messages.add(log_message) if type(configuration) is InternalInputUserInput: continue + if isinstance(configuration, ChoiceUserInput) and len(configuration._choices) == 0: + logger.debug(f"skipping configuration '{configuration._id}': no choices available.") + continue elif isinstance(configuration, AuthConfiguration): auth_authorizations = get_authorization(container.api_client.auth0, self._display_name.lower(), logger) logger.debug(f'auth: {auth_authorizations}') - configuration._value = auth_authorizations.authorization + configuration._value = auth_authorizations.get_authorization_config_without_account() + for inner_config in self._lean_configs: + if any(condition._dependent_config_id == configuration._id for condition in + inner_config._filter._conditions): + api_account_ids = auth_authorizations.get_account_ids() + config_dash = inner_config._id.replace('-', '_') + inner_config._choices = api_account_ids + if user_provided_options and config_dash in user_provided_options: + user_provide_account_id = user_provided_options[config_dash] + if (api_account_ids and len(api_account_ids) > 0 and + not any(account_id.lower() == user_provide_account_id.lower() + for account_id in api_account_ids)): + raise ValueError(f"The provided account id '{user_provide_account_id}' is not valid, " + f"available: {api_account_ids}") + break continue property_name = self.convert_lean_key_to_variable(configuration._id) diff --git a/tests/components/api/test_auth0_client.py b/tests/components/api/test_auth0_client.py new file mode 100644 index 00000000..5495747a --- /dev/null +++ b/tests/components/api/test_auth0_client.py @@ -0,0 +1,76 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import responses +from unittest import mock +from lean.constants import API_BASE_URL +from lean.components.api.api_client import APIClient +from lean.components.util.http_client import HTTPClient + + +@responses.activate +def test_auth0client_trade_station() -> None: + api_clint = APIClient(mock.Mock(), HTTPClient(mock.Mock()), user_id="123", api_token="abc") + + responses.add( + responses.POST, + f"{API_BASE_URL}live/auth0/read", + json={ + "authorization": { + "trade-station-client-id": "123", + "trade-station-refresh-token": "456", + "accounts": [ + {"id": "11223344", "name": "11223344 | Margin | USD"}, + {"id": "55667788", "name": "55667788 | Futures | USD"} + ] + }, + "success": "true"}, + status=200 + ) + + brokerage_id = "TestBrokerage" + + result = api_clint.auth0.read(brokerage_id) + + assert result + assert result.authorization + assert len(result.authorization) > 0 + assert len(result.get_authorization_config_without_account()) > 0 + assert len(result.get_account_ids()) > 0 + + +@responses.activate +def test_auth0client_alpaca() -> None: + api_clint = APIClient(mock.Mock(), HTTPClient(mock.Mock()), user_id="123", api_token="abc") + + responses.add( + responses.POST, + f"{API_BASE_URL}live/auth0/read", + json={ + "authorization": { + "alpaca-access-token": "XXXX-XXX-XXX-XXX-XXXXX-XX", + "accounts": [{"id": "XXXX-XXX-XXX-XXX-XXXXX-XX", "name": " |USD"}] + }, + "success": "true"}, + status=200 + ) + + brokerage_id = "TestBrokerage" + + result = api_clint.auth0.read(brokerage_id) + + assert result + assert result.authorization + assert len(result.authorization) > 0 + assert len(result.get_authorization_config_without_account()) > 0 + assert len(result.get_account_ids()) > 0