Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: support new config type account-id #500

Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7d5c883
feat: support `account-id` config
Romazes Sep 18, 2024
0b4ef98
refactor: reuse interactive logic of `input-account-id`
Romazes Sep 18, 2024
0b05cfe
refactor: update account_ids in AuthConfiguration
Romazes Sep 18, 2024
ec72de2
test:feat: Auth0Client
Romazes Sep 19, 2024
31428be
feat: support Prompt in AccountIdsConfiguration
Romazes Sep 19, 2024
977b7af
feat: handle accountIds and provide different option to user
Romazes Sep 19, 2024
cc9c5fb
refactor: handle of accountIds in json_module
Romazes Sep 19, 2024
345e539
refactor: validate len api_accounts_ids
Romazes Sep 19, 2024
19bac8a
feat: handle when api return None of api_account_ids
Romazes Sep 19, 2024
70c2b96
feat: skip validation of choices (not provided)
Romazes Sep 20, 2024
eaec7f6
feat: filter dict config by regex condition
Romazes Sep 20, 2024
b0b0195
refactor: update of api_account_ids
Romazes Sep 20, 2024
d5288de
remove: not used class AccountIdsConfiguration
Romazes Sep 20, 2024
886868a
remove: missed import class
Romazes Sep 20, 2024
f9560fb
remove: not used import
Romazes Sep 20, 2024
fd68fc0
test:refactor: test_auth0_client
Romazes Sep 20, 2024
a6b4037
feat: new config of TredeStation brokerage in README
Romazes Sep 20, 2024
fe93144
refactor: change spacing
Romazes Sep 20, 2024
ae82cfc
feat: class Accounts in auth0_client
Romazes Sep 23, 2024
e7d212f
feat: skip choice type without choices
Romazes Sep 23, 2024
0bec06f
feat: new object account in Auth0 model
Romazes Oct 1, 2024
2390fa0
refactor: QCAuth0Authorization model
Romazes Oct 1, 2024
be9414a
test:feat: add alpaca configuration test
Romazes Oct 1, 2024
326e616
remove: trade-station-account-type parameter (deprecated)
Romazes Oct 1, 2024
8fcd385
feat: add filter_dependency flag
Romazes Oct 1, 2024
e1294cc
refactor: remove optional in trade-station-account-id
Romazes Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ Options:
The Open FIGI API key to use for mapping options
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--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
Expand Down Expand Up @@ -433,10 +433,10 @@ Options:
--bybit-api-secret TEXT Your Bybit API secret
--bybit-vip-level [VIP0|VIP1|VIP2|VIP3|VIP4|VIP5|SupremeVIP|Pro1|Pro2|Pro3|Pro4|Pro5]
Your Bybit VIP Level
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--polygon-api-key TEXT Your Polygon.io API Key
Expand Down Expand Up @@ -912,10 +912,10 @@ Options:
The Open FIGI API key to use for mapping options
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--dataset TEXT The name of the dataset to download non-interactively
Expand Down Expand Up @@ -1390,10 +1390,10 @@ Options:
Your Bybit VIP Level
--bybit-use-testnet [live|paper]
Whether the testnet should be used
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--ib-enable-delayed-streaming-data BOOLEAN
Expand Down Expand Up @@ -1803,10 +1803,10 @@ Options:
The Open FIGI API key to use for mapping options
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--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)
Expand Down Expand Up @@ -1975,10 +1975,10 @@ Options:
The Open FIGI API key to use for mapping options
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-account-id TEXT
The TradeStation account Id (Optional).
--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
--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
Expand Down
28 changes: 27 additions & 1 deletion lean/models/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lean/models/click_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions lean/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
35 changes: 30 additions & 5 deletions lean/models/json_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
76 changes: 76 additions & 0 deletions tests/components/api/test_auth0_client.py
Original file line number Diff line number Diff line change
@@ -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
Loading