From 27970b424d96a9e0369c47d0586c78840cbcdd83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 10:31:05 +0100 Subject: [PATCH 01/16] Add webUI serving to api-server --- .gitignore | 1 + freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 28 +++++++++++++++++++++++++++ freqtrade/rpc/api_server/webserver.py | 4 ++++ requirements.txt | 1 + tests/rpc/test_rpc_apiserver.py | 2 +- 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/web_ui.py diff --git a/.gitignore b/.gitignore index f206fce66..4720ff5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ user_data/* user_data/notebooks/* freqtrade-plot.html freqtrade-profit-plot.html +freqtrade/rpc/api_server/ui/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py new file mode 100644 index 000000000..971f9e4de --- /dev/null +++ b/freqtrade/rpc/api_server/web_ui.py @@ -0,0 +1,28 @@ +from pathlib import Path + +from fastapi import APIRouter +from fastapi.exceptions import HTTPException +from starlette.responses import FileResponse + + +router_ui = APIRouter() + + +@router_ui.get('/favicon.ico', include_in_schema=False) +async def favicon(): + return FileResponse(Path(__file__).parent / 'ui/favicon.ico') + + +@router_ui.get('/{rest_of_path:path}', include_in_schema=False) +async def index_html(rest_of_path: str): + """ + Emulate path fallback to index.html. + """ + if rest_of_path.startswith('api') or rest_of_path.startswith('.'): + raise HTTPException(status_code=404, detail="Not Found") + uibase = Path(__file__).parent / 'ui' + if (uibase / rest_of_path).is_file(): + return FileResponse(uibase / rest_of_path) + + # Fall back to index.html, as indicated by vue router docs + return FileResponse(uibase / 'index.html') diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 9c0779274..f3eaa1ebc 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -57,12 +57,16 @@ class ApiServer(RPCHandler): from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public + from freqtrade.rpc.api_server.web_ui import router_ui + app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", dependencies=[Depends(http_basic_or_jwt_token)], ) app.include_router(router_login, prefix="/api/v1", tags=["auth"]) + # UI Router MUST be last! + app.include_router(router_ui, prefix='') app.add_middleware( CORSMiddleware, diff --git a/requirements.txt b/requirements.txt index c6cb3e445..5993fb3f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ sdnotify==0.3.2 fastapi==0.63.0 uvicorn==0.13.3 pyjwt==2.0.1 +aiofiles==0.6.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b050e5694..919481598 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -83,7 +83,7 @@ def assert_response(response, expected_code=200, needs_cors=True): def test_api_not_found(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/invalid_url") + rc = client_get(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json() == {"detail": "Not Found"} From a47616eed4648ad4b7331251c79b85354bba62d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 13:55:16 +0100 Subject: [PATCH 02/16] Add UI installation subcommand --- freqtrade/commands/__init__.py | 4 +- freqtrade/commands/arguments.py | 12 +++++- freqtrade/commands/deploy_commands.py | 54 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 21c5d6812..784b99bed 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -10,8 +10,8 @@ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, - start_new_strategy) +from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, + start_new_hyperopt, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a6c8a245f..b39c75640 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -167,8 +167,8 @@ class Arguments: from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_hyperopt, start_new_strategy, start_plot_dataframe, start_plot_profit, @@ -355,6 +355,14 @@ class Arguments: test_pairlist_cmd.set_defaults(func=start_test_pairlist) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) + # Add install-ui subcommand + install_ui_cmd = subparsers.add_parser( + 'install-ui', + help='Install FreqUI', + ) + install_ui_cmd.set_defaults(func=start_install_ui) + self._build_args(optionlist=[], parser=install_ui_cmd) + # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( 'plot-dataframe', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index a0105e140..b43ad5970 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -137,3 +137,57 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + +def clean_ui_subdir(directory: Path): + print(directory) + if directory.is_dir(): + logger.info("Removing UI directory content") + + for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root + if p.name == '.gitkeep': + continue + if p.is_file(): + p.unlink() + elif p.is_dir(): + p.rmdir() + + +def download_and_install_ui(dest_folder: Path): + import requests + from io import BytesIO + from zipfile import ZipFile + + base_url = 'https://api.github.com/repos/freqtrade/frequi/' + # Get base UI Repo path + + resp = requests.get(f"{base_url}releases") + resp.raise_for_status() + r = resp.json() + + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + + dl_url = r[0]['browser_download_url'] + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + print(destfile) + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + # Download a new version + download_and_install_ui(dest_folder) From 87ed2d750205d0e1b10a7c185f853cbb13a51d9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:06:06 +0100 Subject: [PATCH 03/16] Write some tests for UI Downloading --- freqtrade/commands/deploy_commands.py | 46 +++++++++---------- tests/commands/test_commands.py | 65 ++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b43ad5970..e951c962f 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -3,6 +3,8 @@ import sys from pathlib import Path from typing import Any, Dict +import requests + from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES @@ -140,9 +142,8 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: def clean_ui_subdir(directory: Path): - print(directory) if directory.is_dir(): - logger.info("Removing UI directory content") + logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root if p.name == '.gitkeep': @@ -153,11 +154,28 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path): - import requests +def download_and_install_ui(dest_folder: Path, dl_url: str): from io import BytesIO from zipfile import ZipFile + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -170,24 +188,6 @@ def download_and_install_ui(dest_folder: Path): r = resp.json() dl_url = r[0]['browser_download_url'] - logger.info(f"Downloading {dl_url}") - resp = requests.get(dl_url).content - with ZipFile(BytesIO(resp)) as zf: - for fn in zf.filelist: - with zf.open(fn) as x: - destfile = dest_folder / fn.filename - print(destfile) - if fn.is_dir(): - destfile.mkdir(exist_ok=True) - else: - destfile.write_bytes(x.read()) - - -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) # Download a new version - download_and_install_ui(dest_folder) + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index f8ecc8218..12d18b3a7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,16 +1,19 @@ +from io import BytesIO import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +from zipfile import ZipFile import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, start_list_timeframes, + start_new_hyperopt, start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -546,7 +549,7 @@ def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): start_new_hyperopt(get_args(args)) -def test_start_new_hyperopt_no_arg(mocker, caplog): +def test_start_new_hyperopt_no_arg(mocker): args = [ "new-hyperopt", ] @@ -555,6 +558,56 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): start_new_hyperopt(get_args(args)) +def test_start_install_ui(mocker): + clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + args = [ + "install-ui", + ] + start_install_ui(args) + assert clean_mock.call_count == 1 + + assert download_mock.call_count == 1 + + +def test_clean_ui_subdir(mocker, tmpdir, caplog): + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, True]) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_file", + side_effect=[False, True]) + rd_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.rmdir") + ul_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.unlink") + + mocker.patch("freqtrade.commands.deploy_commands.Path.glob", + return_value=[Path('test1'), Path('test2'), Path('.gitkeep')]) + folder = Path(tmpdir) / "uitests" + clean_ui_subdir(folder) + assert log_has("Removing UI directory content.", caplog) + assert rd_mock.call_count == 1 + assert ul_mock.call_count == 1 + + +def test_download_and_install_ui(mocker, tmpdir, caplog): + # Should be something "zip-like" + requests_mock = MagicMock() + file_like_object = BytesIO() + with ZipFile(file_like_object, mode='w') as zipfile: + for file in ('test1.txt', 'hello/', 'test2.txt'): + zipfile.writestr(file, file) + file_like_object.seek(0) + requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, False]) + mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") + wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" + download_and_install_ui(folder, 'http://whatever.xxx') + + assert mkdir_mock.call_count == 1 + assert wb_mock.call_count == 2 + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From a1a35115ad7a1c5610920c628ff82fe71b842c67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:46:59 +0100 Subject: [PATCH 04/16] Extract get_ui_download_url --- freqtrade/commands/deploy_commands.py | 17 +++++++++++------ tests/commands/test_commands.py | 10 +++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index e951c962f..174f1bb59 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -170,12 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def start_install_ui(args: Dict[str, Any]) -> None: - - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' - # First make sure the assets are removed. - clean_ui_subdir(dest_folder) - +def get_ui_download_url() -> str: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -188,6 +183,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: r = resp.json() dl_url = r[0]['browser_download_url'] + return dl_url + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + # First make sure the assets are removed. + clean_ui_subdir(dest_folder) + + dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 12d18b3a7..a39044c18 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -560,13 +560,14 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') args = [ "install-ui", ] start_install_ui(args) assert clean_mock.call_count == 1 - + assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 @@ -588,7 +589,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): def test_download_and_install_ui(mocker, tmpdir, caplog): - # Should be something "zip-like" + # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() with ZipFile(file_like_object, mode='w') as zipfile: @@ -596,13 +597,16 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): zipfile.writestr(file, file) file_like_object.seek(0) requests_mock.content = file_like_object.read() + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx') + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 From ddc99553bd422c250f1d9beba59fccd90211b999 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Jan 2021 14:52:51 +0100 Subject: [PATCH 05/16] Add test case for get_ui_download_url --- tests/commands/test_commands.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index a39044c18..ba45f1618 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -13,7 +13,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui +from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -588,7 +588,7 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): assert ul_mock.call_count == 1 -def test_download_and_install_ui(mocker, tmpdir, caplog): +def test_download_and_install_ui(mocker, tmpdir): # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() @@ -612,6 +612,18 @@ def test_download_and_install_ui(mocker, tmpdir, caplog): assert wb_mock.call_count == 2 +def test_get_ui_download_url(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{'assets_url': 'http://whatever.json'}], + [{'browser_download_url': 'http://download.zip'}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x = get_ui_download_url() + assert get_mock.call_count == 2 + assert x == 'http://download.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From f05f2c45e89dc9dd394bd1165ce3e7451562f3a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:15:27 +0100 Subject: [PATCH 06/16] Allow deleting of UI only --- freqtrade/commands/arguments.py | 4 +++- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/deploy_commands.py | 10 ++++++---- tests/commands/test_commands.py | 15 ++++++++++++++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b39c75640..c64c11a18 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -70,6 +70,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe"] +ARGS_INSTALL_UI = ["erase_ui_only"] + ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", @@ -361,7 +363,7 @@ class Arguments: help='Install FreqUI', ) install_ui_cmd.set_defaults(func=start_install_ui) - self._build_args(optionlist=[], parser=install_ui_cmd) + self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 668b4abf5..7dc85377d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -387,6 +387,12 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + "erase_ui_only": Arg( + '--erase', + help="Clean UI folder, don't download new version.", + action='store_true', + default=False, + ), # Templating options "template": Arg( '--template', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 174f1bb59..b9bda9c63 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -191,8 +191,10 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. clean_ui_subdir(dest_folder) + if args.get('erase_ui_only'): + logger.info("Erased UI directory content. Not downloading new version.") + else: + dl_url = get_ui_download_url() - dl_url = get_ui_download_url() - - # Download a new version - download_and_install_ui(dest_folder, dl_url) + # Download a new version + download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index ba45f1618..fdc2b204b 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -565,11 +565,24 @@ def test_start_install_ui(mocker): args = [ "install-ui", ] - start_install_ui(args) + start_install_ui(get_args(args)) assert clean_mock.call_count == 1 assert get_url_mock.call_count == 1 assert download_mock.call_count == 1 + clean_mock.reset_mock() + get_url_mock.reset_mock() + download_mock.reset_mock() + + args = [ + "install-ui", + "--erase", + ] + start_install_ui(get_args(args)) + assert clean_mock.call_count == 1 + assert get_url_mock.call_count == 0 + assert download_mock.call_count == 0 + def test_clean_ui_subdir(mocker, tmpdir, caplog): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", From e928d2991ddb4ff67aa2d52fda5caf78052e14b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jan 2021 10:27:15 +0100 Subject: [PATCH 07/16] Add fallback file --- freqtrade/commands/deploy_commands.py | 2 +- .../rpc/api_server/ui/fallback_file.html | 31 +++++++++++++++++++ freqtrade/rpc/api_server/web_ui.py | 5 ++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/fallback_file.html diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index b9bda9c63..8a8d2373e 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name == '.gitkeep': + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/freqtrade/rpc/api_server/ui/fallback_file.html b/freqtrade/rpc/api_server/ui/fallback_file.html new file mode 100644 index 000000000..7943530af --- /dev/null +++ b/freqtrade/rpc/api_server/ui/fallback_file.html @@ -0,0 +1,31 @@ + + + + + + Freqtrade UI + + + +
+

Freqtrade UI not installed.

+

Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.

+

You can then refresh this page and you should see the UI.

+
+ + diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 971f9e4de..6d397da56 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -24,5 +24,8 @@ async def index_html(rest_of_path: str): if (uibase / rest_of_path).is_file(): return FileResponse(uibase / rest_of_path) + index_file = uibase / 'index.html' + if not index_file.is_file(): + return FileResponse(uibase / 'fallback_file.html') # Fall back to index.html, as indicated by vue router docs - return FileResponse(uibase / 'index.html') + return FileResponse(index_file) From 35c2e2556ece3cc93634f4435b6044374794850d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 20:48:52 +0100 Subject: [PATCH 08/16] Document FreqUI usage --- docker-compose.yml | 5 ++++ docs/rest-api.md | 68 ++++++++++++++++++++++++++-------------------- mkdocs.yml | 2 +- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7094500b4..06cce4e8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,11 @@ services: container_name: freqtrade volumes: - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation + # before enabling this. + #ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade diff --git a/docs/rest-api.md b/docs/rest-api.md index 2c7142c61..e2b94f080 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -1,4 +1,19 @@ -# REST API Usage +# REST API & FreqUI + +## FreqUI + +Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. + +By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`. +This same command can also be used to update freqUI, should there be a new release. + +Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). + +!!! info "Alpha release" + FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). + +!!! Note "developers" + Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. ## Configuration @@ -23,9 +38,6 @@ Sample configuration: !!! Danger "Security warning" By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger "Password selection" - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. - You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: @@ -35,16 +47,22 @@ This should return the response: All other endpoints return sensitive info and require authentication and are therefore not available through a web browser. -To generate a secure password, either use a password manager, or use the below code snipped. +### Security + +To generate a secure password, best use a password manager, or use the below code. ``` python import secrets secrets.token_hex() ``` -!!! Hint +!!! Hint "JWT token" Use the same method to also generate a JWT secret key (`jwt_secret_key`). +!!! Danger "Password selection" + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. + Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. @@ -57,28 +75,20 @@ If you run your bot using docker, you'll need to have the bot listen to incoming }, ``` -Add the following to your docker command: +Uncomment the following from your docker-compose file: -``` bash - -p 127.0.0.1:8080:8080 -``` - -A complete sample-command may then look as follows: - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -p 127.0.0.1:8080:8080 \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +```yml + ports: + - "127.0.0.1:8080:8080" ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. -## Consuming the API + +## Rest API + +### Consuming the API You can consume the API by using the script `scripts/rest_client.py`. The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. @@ -89,7 +99,7 @@ python3 scripts/rest_client.py [optional parameters] By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. -### Minimalistic client config +#### Minimalistic client config ``` json { @@ -105,7 +115,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use python3 scripts/rest_client.py --config rest_config.json [optional parameters] ``` -## Available endpoints +### Available endpoints | Command | Description | |----------|-------------| @@ -264,12 +274,12 @@ whitelist ``` -## OpenAPI interface +### OpenAPI interface To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. -## Advanced API usage using JWT tokens +### Advanced API usage using JWT tokens !!! Note The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis. @@ -294,9 +304,9 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` -## CORS +### CORS -All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. diff --git a/mkdocs.yml b/mkdocs.yml index 4545e8d84..2d37ddba8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md - - REST API: rest-api.md + - REST API & FreqUI: rest-api.md - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md From 1df0aa875193fc64a1c9ec6d8aae9bd4aeda277b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jan 2021 21:08:15 +0100 Subject: [PATCH 09/16] Add ui installation to docker container builds --- Dockerfile | 4 +++- Dockerfile.armhf | 4 +++- docker-compose.yml | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 445f909b0..8d4f0ebe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ RUN pip install -e . --no-cache-dir \ - && mkdir /freqtrade/user_data/ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2c7d4538a..ec6aa72b8 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/docker-compose.yml b/docker-compose.yml index 06cce4e8e..1f63059f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,8 @@ services: # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation # before enabling this. - #ports: - # - "127.0.0.1:8080:8080" + # ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade From 2af1d2d6390357c267b0ab4a3ccb5e07e7086576 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:38:40 +0100 Subject: [PATCH 10/16] Extract last FreqUI version from api response --- freqtrade/commands/deploy_commands.py | 24 ++++++++++++++++-------- tests/commands/test_commands.py | 25 +++++++++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 8a8d2373e..44ddc1fdc 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Tuple import requests @@ -170,7 +170,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.write_bytes(x.read()) -def get_ui_download_url() -> str: +def get_ui_download_url() -> Tuple[str, str]: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -178,23 +178,31 @@ def get_ui_download_url() -> str: resp.raise_for_status() r = resp.json() - assets = r[0]['assets_url'] - resp = requests.get(assets) - r = resp.json() + latest_version = r[0]['name'] + assets = r[0].get('assets', []) + dl_url = '' + if assets and len(assets) > 0: + dl_url = assets[0]['browser_download_url'] - dl_url = r[0]['browser_download_url'] - return dl_url + # URL not found - try assets url + if not dl_url: + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + dl_url = r[0]['browser_download_url'] + + return dl_url, latest_version def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. + dl_url, latest_version = get_ui_download_url() clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): logger.info("Erased UI directory content. Not downloading new version.") else: - dl_url = get_ui_download_url() # Download a new version download_and_install_ui(dest_folder, dl_url) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index fdc2b204b..dcc0cd1d2 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,5 +1,5 @@ -from io import BytesIO import re +from io import BytesIO from pathlib import Path from unittest.mock import MagicMock, PropertyMock from zipfile import ZipFile @@ -13,7 +13,8 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) -from freqtrade.commands.deploy_commands import clean_ui_subdir, download_and_install_ui, get_ui_download_url +from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, + get_ui_download_url) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -628,15 +629,31 @@ def test_download_and_install_ui(mocker, tmpdir): def test_get_ui_download_url(mocker): response = MagicMock() response.json = MagicMock( - side_effect=[[{'assets_url': 'http://whatever.json'}], + side_effect=[[{'assets_url': 'http://whatever.json', 'name': '0.0.1'}], [{'browser_download_url': 'http://download.zip'}]]) get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=response) - x = get_ui_download_url() + x, last_version = get_ui_download_url() assert get_mock.call_count == 2 + assert last_version == '0.0.1' assert x == 'http://download.zip' +def test_get_ui_download_url_direct(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{ + 'assets_url': 'http://whatever.json', + 'name': '0.0.1', + 'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x, last_version = get_ui_download_url() + assert get_mock.call_count == 1 + assert last_version == '0.0.1' + assert x == 'http://download11.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) From 944d674eeb6d590685f1970c60210f80b93fc726 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:39:46 +0100 Subject: [PATCH 11/16] Store freqUI version and read it again --- freqtrade/commands/deploy_commands.py | 25 +++++++++++++++++++++---- tests/commands/test_commands.py | 10 ++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 44ddc1fdc..c4e958df6 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple import requests @@ -154,7 +154,16 @@ def clean_ui_subdir(directory: Path): p.rmdir() -def download_and_install_ui(dest_folder: Path, dl_url: str): +def read_ui_version(dest_folder: Path) -> Optional[str]: + file = dest_folder / '.uiversion' + if not file.is_file(): + return None + + with file.open('r') as f: + return f.read() + + +def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): from io import BytesIO from zipfile import ZipFile @@ -168,6 +177,8 @@ def download_and_install_ui(dest_folder: Path, dl_url: str): destfile.mkdir(exist_ok=True) else: destfile.write_bytes(x.read()) + with (dest_folder / '.uiversion').open('w') as f: + f.write(version) def get_ui_download_url() -> Tuple[str, str]: @@ -199,10 +210,16 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() - clean_ui_subdir(dest_folder) + + curr_version = read_ui_version(dest_folder) + if curr_version == latest_version and not args.get('erase_ui_only'): + logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + return + if args.get('erase_ui_only'): + clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: # Download a new version - download_and_install_ui(dest_folder, dl_url) + download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index dcc0cd1d2..b243df192 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -14,7 +14,7 @@ from freqtrade.commands import (start_convert_data, start_create_userdir, start_ start_new_hyperopt, start_new_strategy, start_show_trades, start_test_pairlist, start_trading) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, - get_ui_download_url) + get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -563,6 +563,7 @@ def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ "install-ui", ] @@ -616,15 +617,16 @@ def test_download_and_install_ui(mocker, tmpdir): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, False]) - mkdir_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.mkdir") wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") folder = Path(tmpdir) / "uitests_dl" - download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip') + folder.mkdir(exist_ok=True) + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') - assert mkdir_mock.call_count == 1 assert wb_mock.call_count == 2 + assert read_ui_version(folder) == '22' + def test_get_ui_download_url(mocker): response = MagicMock() From 7b3d99819f60f03a9464c2f67720ed7c83c17dc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:43:16 +0100 Subject: [PATCH 12/16] Fix bug with not cleaning UI folder --- freqtrade/commands/deploy_commands.py | 5 ++--- tests/commands/test_commands.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index c4e958df6..62da6abf3 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -213,13 +213,12 @@ def start_install_ui(args: Dict[str, Any]) -> None: curr_version = read_ui_version(dest_folder) if curr_version == latest_version and not args.get('erase_ui_only'): - logger.info(f"UI already uptodate, FreqUI Version {curr_version}.") + logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") return + clean_ui_subdir(dest_folder) if args.get('erase_ui_only'): - clean_ui_subdir(dest_folder) logger.info("Erased UI directory content. Not downloading new version.") else: - # Download a new version download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b243df192..0d2b9e394 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -561,7 +561,8 @@ def test_start_new_hyperopt_no_arg(mocker): def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') - get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url', + return_value=('https://example.com/whatever', '0.0.1')) download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) args = [ @@ -582,7 +583,7 @@ def test_start_install_ui(mocker): ] start_install_ui(get_args(args)) assert clean_mock.call_count == 1 - assert get_url_mock.call_count == 0 + assert get_url_mock.call_count == 1 assert download_mock.call_count == 0 From a87a885ccd3221fda93d3f1c3a37c754ba64403e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 14:54:58 +0100 Subject: [PATCH 13/16] Don't use Path object to return fileresponses --- freqtrade/rpc/api_server/web_ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 6d397da56..4876c9077 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -22,10 +22,10 @@ async def index_html(rest_of_path: str): raise HTTPException(status_code=404, detail="Not Found") uibase = Path(__file__).parent / 'ui' if (uibase / rest_of_path).is_file(): - return FileResponse(uibase / rest_of_path) + return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(uibase / 'fallback_file.html') + return FileResponse(str(uibase / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs - return FileResponse(index_file) + return FileResponse(str(index_file)) From 28be71806f000a0ba5a96307d721065ae42c76fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:00:13 +0100 Subject: [PATCH 14/16] Install html file as well --- MANIFEST.in | 2 ++ freqtrade/rpc/api_server/ui/.gitkeep | 0 freqtrade/rpc/api_server/ui/favicon.ico | Bin 0 -> 126794 bytes 3 files changed, 2 insertions(+) delete mode 100644 freqtrade/rpc/api_server/ui/.gitkeep create mode 100644 freqtrade/rpc/api_server/ui/favicon.ico diff --git a/MANIFEST.in b/MANIFEST.in index 2f59bcc7a..adbcd2e30 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include LICENSE include README.md recursive-include freqtrade *.py recursive-include freqtrade/templates/ *.j2 *.ipynb +include freqtrade/rpc/api_server/ui/fallback_file.html +include freqtrade/rpc/api_server/ui/favicon.ico diff --git a/freqtrade/rpc/api_server/ui/.gitkeep b/freqtrade/rpc/api_server/ui/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/freqtrade/rpc/api_server/ui/favicon.ico b/freqtrade/rpc/api_server/ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78c7e43b1e6a5756492a1d53813e1ec47f98efdf GIT binary patch literal 126794 zcmeHQ378ed5q|3m3mz=dctpe%QM{t!eWB}_D8?I2G%6YsyyA_T7;zWzict*lz$=0m zibq7nAR1u>BZwC&i6+swDg;pw5X2Ri?fn1j%+S-XyXSiE?JjSMZ<(2%?&|95dOh9M zRn@s%1^zwuOb-8Vn%iYzMQ;6EF1Ho_9>d?Diq>@8W>6#!eDJ{sJ;#n6d+hDE-#%#Q z(4m+8@|V9HdDKxyjooj*{U+_S(@t-0v&}X$dEI~i{U;uM^wDE}^{Zcvy!P5_2Q&Y; zapMmE=%bG|ED~uM+A`*V5TatrlqtJia>*sv9dpbv4{xx+2D8Bqk@j!njW=#Q@x&AF zyW)x~u9`V>=C)d)}>ExA{@zt5|u~Uw{4C=bd-naE!No%7E=z6Jv0I zW7)p@?)yB7tiWID?|Y$ebn4XUn}ZHIX#AURzPU||=44q{K@LoqFyX-6cH3=gp=7I= zcn&z=fM+L7nzZ*S*bP~eEFAdo!w)w(^2j3}FQ#l212^W*%TGS}Qpg(`gG?y zapFXA|NZxi>#n;_oO4x?!No(ld!g55LZrn_wFrjyX`jdpa1+PxnM>p`OZ7#VcRyvHAZynhGoo{V&G;D7)7-x~P#-VsMeq30Bs6N)$9cq4+86+R2+ z5d#Jc5FE>6(A{#;MHgKQYOrgWK}X{N`mO6e`|R@~=#JFqSht9C&N)Z?^Pm4LyZ|P^ z$lnToGjU`V`)Uue-5ss3%9-AI^X6^Pt5>i0qsnRj{{02!eF-G0_(lHlkAH|=cG)G; z?~U!$TT2y;l2{*o6xhcJJA<=Lb>b6np&w^VyVARNCUO&l1>Qiz=@(KL7mlwc2-& zl_zzLjg3F};SYcKMwpyZH#%a(h=K@NQ&W?(27CD7hsCY8-YRas`DSN54;`-f{qKJ- z{`ki~I(mdhAAMBtTmJ30-=?m2#flXIc8H>)q9RmgF?V|A@;kzg)cf>Z^ismlPg) z=pnJ@nrnvEfBf;sk0yWGXU~-%RVSTv(mk?F=C7{`u!eo-gX^*QKqy z?z)cLzW@IFiA46Tx84%auZGI&;K75hM*SJIF$eCs=bjTmg4I5X`<1=;7n!|FiA5G`?aOTA z71zlxz4TI4F%|#%*T2NByY5<48MM)>dxw3uSYs@8#bNtgBS(%r6?J7$A`Z}(F3heE zeG*wtsZ)xwrbhEhF3Y5gc7{=bwp%tj$v5bgzh9!RD*I~a*J0}q_&G<`2Mr%SJd&=` zR$sB-(VphM`|gV*uGCw=_uDG3urs)`?1iWV3}|R**wZE(TuaTLKR;5x{^?JD5?q7Z z@U@qBoI}%YI|_XK@yBBAwbwSbi~a>Kzx?tZCFriQt^#}Aui511x#ymX(w)xQ#fB&9 zJLM$7b5U9(w4Z0(SgHxrtg2^Tm61@Bsg*6@w)U_39)1R5*|KF$otz)b@3-H6d;eXS+{op^?zu;dF)_5AoC}1l z6<>e-b%yK7pa>kmJZu8c24uo}W*^R)H7m3a;mcC6{U`cSc|e;&d5`{@Yp(HLIam3e zM|09E_ljXh6~M21AU|yN;(PEUXvmjeED**_10S( zaoF(d-q9z}1I`g;eGz?9WRTzKaK%l7u2ir+VSrdJ#`Z#wyX?L9-rE#)XHY;6ARfxC zKxgE86#G8foY-*0eb<(oP$*kF>tdYW;03t5sE1k!)~j+ojU(J;NXIWAYcz85Dp64?OU|u|Q+wcN9N9+J@M0 z*xs$T-g=IGDs`Ol-@EU=>;FcUE?sJT$4o3o+kV*Eh~NG0cLKf(iRqDPrTst{&;~~? zqu#rEKqP07mII9a2pn2|@44rmp?d$YzB1wV(&=|y4B&ngV`6yqB)M`PjF@quqrG4|wHv+%KP#SD> zM$y$FR*-cq-Z~*a1jxhBT=bv-u&b5jIV(c0F$b?r-mt~dT`hWlX---Vi zb43N}Xt`&g)*JPidv9U(+SJwA@L@jF8}Bnn!2#G}{07CfeBOQc-PLZ-dq(_oZCMje zUAio*j1i>E6DMu+%{Lc_;U1tB@!(?n#K?8hFD49Z`Z~sv2fhqaaA5Df_cq3(;2OmS z9Ip^FJ+*8VT2{dhhbGk?<@D1}PfRO)xx!$`kRe9=@B_LxMK@(d!2#&1mV14xUa?1L z?H=0tsb!Kp7fV*T*T(VG1NwC(q0cMB`Hl_fZ|mh%ubXbV>3c}aARz}n{q)myP}Im@ z`jy(?(n~KjmbKwk-xW(%Sr7fPWw7b(Rl3yY-~RTuGOE0L?w45h&sJo1t>9=vc=p+6 zX{(}@H;e;$eW4~GUVyf&38yK&n6ip<0Rm*FvB^62*kc2@bPtoN$7!EBv&UmKNI>V(c$neZ`bj#=VdsjXf=WxWD`EI~iTxJ@wJLHZtw5 zyYBiGt{EiY0Cd!Mp_rDpS6_W~UO&hQ^szRTjhkLfS$+EHr+u<%BOaGFHGf!~Lz^xa zd&cJjUj_*{fEee`p_sP+!uFEj53em}!e>h_maO8eL0?XJ!?~-CjwtU`U09$3Ja8O0 z;W_u*b8o=A3=(hv`qSwsrsXa7)os9dQl_$T(~BXijQzG~(IUSI{C3mFATDicew?G} z3Eczya8`zm8&B-g4Zu!37swpe<*@XG@o5m3A^e{pn8={n3_` zx{1I2?QeO$|BT&cqao_MTW-0<#ukC~s<9rNOY~RNlR*Lw?7Z{NMp^yqU;k?D6P|m5 zDhu)_%c`}~gfAa^?6HFRDDrz^gUOR88+n3#mq`h_DJudF*kqMslMR0U^Pe00B(4k> zQ&wr)Ux>XmuT5cVPWozjdDW|XE*+9Hh|2++tnv&98~oxIzcBU*=!o@Bl~wLRB>mIT zOkb@+$tz>P+JN69Q~MDYl;?bAkbna=S>?RT28=<8Dhu)_%PPn7O*Yvi(O*qXjlg$S zz;8q#=5&C3{`S)BS$S>cyw?UiGtSGaUSaF?7o=s7fCIL*RajgH#x6sZ1^JU@wGehz zq>E=2z%MT#R*YYjmgcb7NE~-fb-)*h`s@r6Z~%KOQ&3FH+c4b>*DI#7anp+-tE7Q@ zz)O}a@#VD;zk_k(#`*YkH#YxB*b@u|T^S_c0CeS#pqN(XIBwd2zJR8(anp+>tE5A= z3#5-gT-wz9jCW`Q`VgpPyl3o1oQ?Yo5^&&(E3T+RF)eRTJMA=EmpH!jg_!@b{$k4N zC6`~kzY-%^YQ*Ys21}QjTJ6|o#-x}u#rt-G~*-qaULsoGnM_#I%80zkyG;J$Rm#orNicLjd-q%Yg3lMb9^z6A9(lO zcMqa}9~ik>8b%<_opDcq=RSwc(`@z{piBE3kkfHSWT-5}%|M+!_mI-YE~GEv^y$+> ztEVrP+$Q<{j5E%-8P8<}U|46y@dE$8LBoCCY_-)^ZzB#lzvYPl{wk-VppQ?z5A!kN zTn#ov#_}pY^_hAe`iL^tg#0IuxAc|b+}L}s+ShvTXjcp0oPwSA#xrNafO8nNP4Y9& z0ox1DWdq<>SF9V}M1zfdx#*&c5}jcS+*R=JH0rKs{}%=vdl;|OMqk`_^8BOJaji)! z&k_oouahtImoc>sHZ+{*mepB;(1#2&@k35WZ1PyXlAQA+*3uB1fwM@)Bj0mv5v9*) zizk;$d*9Zw%GiHlK-+4I{$F7HxE6dU8-S17_pl~6$`Tmi%LvATrHHe)Sr~KB2dmMq z5^;2v%H>V>;CWcL3EGA*?wJWk-1N4TRnBSHo-i0NV1Ti`*w1Z(Z+cI(yKDfu!F|!# z08g;C{Qz_J+bEmrJJ@NyU}FyW-wAQ3A6LuSo~geIo42xG8T09T-}_#GRvTX1yZZWi zUsSkOlE<&Qx;p2NdG6FeB$Fb-Gtei!RG2Q)K^vj`(E1T1o|I7YUhy& zu!%lSeaC0_tSV;7gFpZI&p$_E0AG}6`jE#L+<4=SoP!4Va_60Q4)mDd`bGrabN?Cq?&9T< zuZS1^sfv%!*em)BW6Br0cV1dly*=$;3pv(#^%wI>{S)BSY z5jj%j1?HMJAql(RVi=a>ya$VCy;=IztQ4MFR10?g-gA#zadB^v0q%Ly7GU@CIjvO_2*A9^N0+JeXrHMjXX82r&x{!n6>R=TGS0TVXFRsBI5Tp4ZSmFE4#c#|vr|F&PJ;#w z8iBf&>)MKQW8UT-58kB$#wo?wF%Nd{-hDn0`F#jmg%9O#ajHrzg>l~Ex@cpwTsLAT zzAqO6FS=mw?-B2Bw4CpuhaNgc&7;N#hpu#IjMK(_SC$WpcO~~N#%y8R*HP&y zi#~l1b$9X$#=PP87rMtU zZS}zC>&lpDV`qmhh;fnqw$kntHhJA;MEHiCBIn^fWbRw|G4q^t2XNNXuDJ2>@E-mu zx3|7s`M;|d4CIr`aScM=VVkL>#RXAQOS{%uvq?&O`97Xn?&%%2R+(SZ^O8R|uY0;QKUL&4(Ck z6e{QU>e{9U@L)aryo`tU$X^M)*nd=9@-uDlRKI3hztb+CHlB=)F5@ZnJ(pYe zz4qFU;A!5R2zD+fF)#SfIedV2&hb?|e4aUT=C)WHFH`f&&y0T^7^c(_Ox;9GUe~ptJnL`r|iSI0e-h%$@ys8f#=9z8&6?(H1RjT`OOfl z^;Q6(Pe1zm<;WEz$f!Gq3NfWd=}^IL>LHmn-gD1j{P^+v>&~-;-3aZhw1D5p8F=Bh zQ+Ll;k>I=By`lFU(-;ed-(eljLZ)$zkMrimlqpj}=~6TBtS#zt!|1{O%jB>;d`~|N z>{qXkduQB$tS*tuVV?9B-$en$f;}8_65TnvD{zJmox@fq9{kSc;NrKTXV0D=M8VHI zz)^v}SoREDUtzoqjPuZ;(v~F;zWnmbYJBGwBubMuf$39z%j+g6_MdiU-v z!hGjdIq;r2&M`(N^ZUweuunl-jU4PW2VniJ)lWmF zzJnjj-5|i3i!r~>j~KqP9A`oSv51bsH%}+00yZIje{zo(bCJMzcBh?onr^EAG**oo zHR@!1gT`R(ITv%4CHxkgefHTS(8oP&WxaQpKXUEw_a|icTXlWq<%Rq)Zi1iJbkA>4 z*ctcaFXp_|ky-KaOwuG{mnT49fBm%+zm)q>JU5fN`LH_W_smawTl$I7W{~?`NrAE3 zqkQ+d4}&_K@e8!%Xp6!4D)^nuqTU*I=regP```cmcdlszu*V*IOhQ7SjhILCY&0UW(?@uo8%u(0fky%6M1& z_Vak);c@p10(5%iyldE)>fz$J4*vhcD`&cLoy=J0Jo};$z_~Ez3fw264Aak>XBF~F z{SkDA&YtL;Idck8ofD(O7Druf_jcQD_nIuH9uAVyUS7m^;@*-EIDd0%eA60{V3if`J>?ZKqz3v3G&hLNcY~MTyJG%% zye6xk1ioJadYiob-j#DW{03viS7JN?wCN)chwaL^5A){DD~SA*3JL914m#+dNd2|L z4m-?Pym&Fs2TKI~`}cnm1p{?qkL4a4GOjn&wU~zkYp6Q4EPm6sdGEce+kb(qoSS&> zqh2vDXu{mBCV)tKz+Cz$71x<(p7}>9i}sc_s@!j7tTpJQ-^BjOtvF|VCvE1CMEEcD zLTr&nWDD?F_ske1VX?|eC5K`4z^@|GH=pg~_Z_~18{j?>a8DfLhdS4!-gA2sLd477 zLuXTm`~vgH8_~YN`iFbz*cZrKvtXWktXcrLJxw?>%&#FRHk_XV?`{ z_a7*O`Oq8Q`%+ywUq^oh_9j%ii7hvDR-t`q7V`VY|~Xo8z6j2f+E5$%Yd??K^TGOvhL3H{PkE1#`9Ds+_R==+UE% zYa+z^Jdo#|!G7ahuCsJ_p$mObr2}j3m(WLnG2JVR-}N*#zbE@%o>E;wV<+_YT`E1c zXYS?jyfW?wgvHA$w4Aa&j-&KLw9%-(<67dZv(DmJm>QrHxCTf9^}9NjJlwEZ+5vL9 zP|eS0`o@Ak6?uh_fbZC&Y8lAQ-*tK8q;YQuzcY32B1~s@Vr_q;N=F(W);P49@^bGR zYtuac1eH$pnQ>jX=H{BFn7}cVIs>eS0y3h~qkD$l=}YKA1AE!Xmk3}3)m0aZ!L`E- z;N*G&_C9lwm`5|lhdTn9r3amUfjBtdm9izig$?7w#3%nDZrHmzgsx3QATNHa!*zK} zna1@3Xb#nB$!+xRv1SlE?zp31oPeoQr#gD0G_p&(1K8*|dM)Z{sYmnbigU$S^hzQ9 zrC`rHSgjwndfcc8jF03ce?3UJE)|!rYU3+=2k@7CD~iV@w}aUyA+BS*a>dS(r4^E|zn1 z$QbvU{WT@-^LXCg)2L5^{&O7T(#@SacfFMCaf)*-ueKXw>Ibq;f%-ti&i+Kr$7kwe zRatNlOaEcd?m4wA{12#)DkyNDlXm<-8t7ZtN1ditM7@ce4`Xt3Tdc3ZyUvKkd$Jp^ zDL}VOUkAT$!CGZ3b<}D<#CgU(&mff#uvPH=F}OF2Jzeem6!kv>UTfFd9u2zR0^Y6Q zddSOD$SY|7J@5tk9OKk~)jALdC2#EwTh;+;zG6Oe9=FRbyS$>(yQ)58tX-DW4x0UN z(@i(!*>vKptaJE|>mk@aeTdsY-}4!I*QX$-otV!CnD4)e_kr@R�|f;&=3w!Tcg` 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 Date: Sun, 31 Jan 2021 15:27:00 +0100 Subject: [PATCH 15/16] Add test for UI methods --- freqtrade/commands/deploy_commands.py | 2 +- tests/rpc/test_rpc_apiserver.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 62da6abf3..797e9f87d 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('.gitkeep', 'fallback_file.html'): + if p.name in ('favicon.ico', 'fallback_file.html'): continue if p.is_file(): p.unlink() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 919481598..586af6dc1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -88,6 +88,21 @@ def test_api_not_found(botclient): assert rc.json() == {"detail": "Not Found"} +def test_api_ui_fallback(botclient): + ftbot, client = botclient + + rc = client_get(client, "/favicon.ico") + assert rc.status_code == 200 + + rc = client_get(client, "/fallback_file.html") + assert rc.status_code == 200 + assert '`freqtrade install-ui`' in rc.text + + # Forwarded to fallback_html or index.html (depending if it's installed or not) + rc = client_get(client, "/something") + assert rc.status_code == 200 + + def test_api_auth(): with pytest.raises(ValueError): create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") From 06e2bc94c3e111bcea180485018fd081da176104 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jan 2021 15:37:57 +0100 Subject: [PATCH 16/16] Deploy to subdirectory --- freqtrade/commands/deploy_commands.py | 5 +++-- freqtrade/rpc/api_server/ui/installed/.gitkeep | 0 freqtrade/rpc/api_server/web_ui.py | 4 ++-- tests/commands/test_commands.py | 3 +++ 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 freqtrade/rpc/api_server/ui/installed/.gitkeep diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 797e9f87d..5ba3db9f9 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -146,7 +146,7 @@ def clean_ui_subdir(directory: Path): logger.info("Removing UI directory content.") for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root - if p.name in ('favicon.ico', 'fallback_file.html'): + if p.name in ('.gitkeep', 'fallback_file.html'): continue if p.is_file(): p.unlink() @@ -169,6 +169,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): logger.info(f"Downloading {dl_url}") resp = requests.get(dl_url).content + dest_folder.mkdir(parents=True, exist_ok=True) with ZipFile(BytesIO(resp)) as zf: for fn in zf.filelist: with zf.open(fn) as x: @@ -207,7 +208,7 @@ def get_ui_download_url() -> Tuple[str, str]: def start_install_ui(args: Dict[str, Any]) -> None: - dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui' + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' # First make sure the assets are removed. dl_url, latest_version = get_ui_download_url() diff --git a/freqtrade/rpc/api_server/ui/installed/.gitkeep b/freqtrade/rpc/api_server/ui/installed/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 4876c9077..6d7e77953 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -20,12 +20,12 @@ async def index_html(rest_of_path: str): """ if rest_of_path.startswith('api') or rest_of_path.startswith('.'): raise HTTPException(status_code=404, detail="Not Found") - uibase = Path(__file__).parent / 'ui' + uibase = Path(__file__).parent / 'ui/installed/' if (uibase / rest_of_path).is_file(): return FileResponse(str(uibase / rest_of_path)) index_file = uibase / 'index.html' if not index_file.is_file(): - return FileResponse(str(uibase / 'fallback_file.html')) + return FileResponse(str(uibase.parent / 'fallback_file.html')) # Fall back to index.html, as indicated by vue router docs return FileResponse(str(index_file)) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 0d2b9e394..cec0b168e 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -622,6 +622,9 @@ def test_download_and_install_ui(mocker, tmpdir): folder = Path(tmpdir) / "uitests_dl" folder.mkdir(exist_ok=True) + + assert read_ui_version(folder) is None + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') assert wb_mock.call_count == 2